-
Notifications
You must be signed in to change notification settings - Fork 241
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Pipelines folder management commands (#788)
* Pipeline folder management commands * Add unit tests for pipeline folder commands * Add recording tests
- Loading branch information
Showing
8 changed files
with
2,370 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
azure-devops/azext_devops/dev/pipelines/pipeline_folders.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
# -------------------------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for license information. | ||
# -------------------------------------------------------------------------------------------- | ||
|
||
from knack.log import get_logger | ||
from knack.util import CLIError | ||
from azext_devops.dev.common.services import resolve_instance_and_project, get_build_client | ||
|
||
logger = get_logger(__name__) | ||
|
||
|
||
def pipeline_folder_create(path, description=None, organization=None, | ||
project=None, detect=None): | ||
""" Create a folder. | ||
:param path: Full path of the folder. | ||
:type path: str | ||
:param description: Description of the folder. | ||
:type description: str | ||
:param project: Name or ID of the team project. | ||
:type project: str | ||
:param detect: Automatically detect organization and project. Default is "on". | ||
:type detect: str | ||
""" | ||
organization, project = resolve_instance_and_project( | ||
detect=detect, organization=organization, project=project) | ||
client = get_build_client(organization) | ||
from azext_devops.devops_sdk.v5_0.build.models import Folder | ||
folder = Folder() | ||
folder.description = description | ||
folder.path = path | ||
new_folder = client.create_folder(folder=folder, path=path, project=project) | ||
return new_folder | ||
|
||
|
||
def pipeline_folder_delete(path, organization=None, project=None, detect=None): | ||
""" Delete a folder. | ||
:param path: Full path of the folder. | ||
:type path: str | ||
:param project: Name or ID of the team project. | ||
:type project: str | ||
:param detect: Automatically detect organization and project. Default is "on". | ||
:type detect: str | ||
""" | ||
organization, project = resolve_instance_and_project( | ||
detect=detect, organization=organization, project=project) | ||
client = get_build_client(organization) | ||
return client.delete_folder(path=path, project=project) | ||
|
||
|
||
def pipeline_folder_list(path=None, query_order=None, organization=None, project=None, detect=None): | ||
""" List all folders. | ||
:param path: Full path of the folder. | ||
:type path: str | ||
:param query_order: Order in which folders are returned. | ||
:type query_order: str | ||
:param project: Name or ID of the team project. | ||
:type project: str | ||
:param detect: Automatically detect organization and project. Default is "on". | ||
:type detect: str | ||
""" | ||
organization, project = resolve_instance_and_project( | ||
detect=detect, organization=organization, project=project) | ||
client = get_build_client(organization) | ||
if query_order: | ||
if query_order.lower() == 'asc': | ||
query_order = 'folderAscending' | ||
elif query_order.lower() == 'desc': | ||
query_order = 'folderDescending' | ||
return client.get_folders(path=path, query_order=query_order, project=project) | ||
|
||
|
||
def pipeline_folder_update(path, new_path=None, new_description=None, | ||
organization=None, project=None, detect=None): | ||
""" Update a folder name or description. | ||
:param path: Full path of the folder. | ||
:type path: str | ||
:param new_path: New full path of the folder. | ||
:type new_path: str | ||
:param new_description: New description of the folder. | ||
:type new_description: str | ||
:param project: Name or ID of the team project. | ||
:type project: str | ||
:param detect: Automatically detect organization and project. Default is "on". | ||
:type detect: str | ||
""" | ||
if not new_path and not new_description: | ||
raise CLIError('Either --new-path or --new-description should be specified.') | ||
organization, project = resolve_instance_and_project( | ||
detect=detect, organization=organization, project=project) | ||
client = get_build_client(organization) | ||
folders = client.get_folders(path=path, project=project, query_order='folderAscending') | ||
folder_to_update = None | ||
# find matching folder if present | ||
for folder in folders: | ||
if folder.path.strip('\\') == path.strip('\\'): | ||
folder_to_update = folder | ||
break | ||
if not folder_to_update: | ||
raise CLIError('Cannot find folder with path {}. Update operation failed.'.format(path)) | ||
if new_description: | ||
folder_to_update.description = new_description | ||
if new_path: | ||
folder_to_update.path = new_path | ||
return client.update_folder(path=path, folder=folder_to_update, project=project) |
104 changes: 104 additions & 0 deletions
104
azure-devops/azext_devops/test/pipelines/test_pipeline_folders.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# -------------------------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for license information. | ||
# -------------------------------------------------------------------------------------------- | ||
|
||
import unittest | ||
|
||
try: | ||
# Attempt to load mock (works on Python 3.3 and above) | ||
from unittest.mock import patch | ||
except ImportError: | ||
# Attempt to load mock (works on Python version below 3.3) | ||
from mock import patch | ||
|
||
from azext_devops.dev.common.services import clear_connection_cache | ||
from azext_devops.test.utils.authentication import AuthenticatedTests | ||
from azext_devops.dev.pipelines.pipeline_folders import (pipeline_folder_create, | ||
pipeline_folder_delete, | ||
pipeline_folder_list, | ||
pipeline_folder_update) | ||
from azext_devops.devops_sdk.v5_0.build.build_client import BuildClient | ||
from azext_devops.devops_sdk.v5_0.build.models import Folder | ||
|
||
|
||
class TestPipelinesFoldersMethods(AuthenticatedTests): | ||
|
||
_TEST_DEVOPS_ORGANIZATION = 'https://someorganization.visualstudio.com' | ||
_TEST_DEVOPS_PROJECT = 'MyProject' | ||
|
||
def setUp(self): | ||
self.authentication_setup() | ||
self.authenticate() | ||
|
||
self.get_client_patcher = patch('azext_devops.devops_sdk.connection.Connection.get_client') | ||
#start the patchers | ||
self.mock_get_client = self.get_client_patcher.start() | ||
# Set return values which will be same across tests | ||
self.mock_get_client.return_value = BuildClient(base_url=self._TEST_DEVOPS_ORGANIZATION) | ||
#clear connection cache before running each test | ||
clear_connection_cache() | ||
|
||
def tearDown(self): | ||
patch.stopall() | ||
|
||
def test_folder_create(self): | ||
with patch('azext_devops.devops_sdk.v5_0.build.build_client.BuildClient.create_folder') as mock_create_folder: | ||
# Creating folder | ||
new_folder = pipeline_folder_create(path='test', description='test description', | ||
project=self._TEST_DEVOPS_PROJECT, organization=self._TEST_DEVOPS_ORGANIZATION) | ||
folder = Folder() | ||
folder.description = 'test description' | ||
folder.path = 'test' | ||
# assert | ||
mock_create_folder.assert_called_once_with( | ||
folder=folder, path='test', project=self._TEST_DEVOPS_PROJECT) | ||
|
||
def test_folder_delete(self): | ||
with patch('azext_devops.devops_sdk.v5_0.build.build_client.BuildClient.delete_folder') as mock_delete_folder: | ||
# deleting folder | ||
pipeline_folder_delete( | ||
path='test', project=self._TEST_DEVOPS_PROJECT, organization=self._TEST_DEVOPS_ORGANIZATION) | ||
# assert | ||
mock_delete_folder.assert_called_once_with( | ||
path='test', project=self._TEST_DEVOPS_PROJECT) | ||
|
||
def test_folder_list(self): | ||
with patch('azext_devops.devops_sdk.v5_0.build.build_client.BuildClient.get_folders') as mock_list_folders: | ||
# listing folders | ||
folders = pipeline_folder_list( | ||
path='test', query_order='asc', project=self._TEST_DEVOPS_PROJECT, organization=self._TEST_DEVOPS_ORGANIZATION) | ||
# assert | ||
mock_list_folders.assert_called_once_with( | ||
path='test', query_order='folderAscending', project=self._TEST_DEVOPS_PROJECT) | ||
|
||
def test_folder_update(self): | ||
with patch('azext_devops.devops_sdk.v5_0.build.build_client.BuildClient.update_folder') as mock_update_folders: | ||
with patch('azext_devops.devops_sdk.v5_0.build.build_client.BuildClient.get_folders') as mock_list_folders: | ||
folder = Folder() | ||
folder.description ='hello world' | ||
folder.path = 'test' | ||
mock_list_folders.return_value = [folder] | ||
|
||
update_folder = Folder() | ||
update_folder.description ='hello world updated' | ||
update_folder.path = 'test123' | ||
|
||
updated_folder = pipeline_folder_update( | ||
path='test', new_path='test123', new_description='hello world updated', | ||
project=self._TEST_DEVOPS_PROJECT, organization=self._TEST_DEVOPS_ORGANIZATION) | ||
|
||
# assert | ||
mock_list_folders.assert_called_once_with( | ||
path='test', project=self._TEST_DEVOPS_PROJECT, query_order='folderAscending') | ||
mock_update_folders.assert_called_once_with( | ||
folder=mock_list_folders.return_value[0], path='test', project=self._TEST_DEVOPS_PROJECT) | ||
self.assertEqual(mock_list_folders.return_value[0].path, 'test123') | ||
self.assertEqual(mock_list_folders.return_value[0].description, 'hello world updated') | ||
|
||
|
||
def test_folder_update_with_invalid_input(self): | ||
with self.assertRaises(Exception) as exc: | ||
response = pipeline_folder_update( | ||
path='test', project=self._TEST_DEVOPS_PROJECT, organization=self._TEST_DEVOPS_ORGANIZATION) | ||
self.assertEqual(str(exc.exception),r'Either --new-path or --new-description should be specified.') |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# -------------------------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for license information. | ||
# -------------------------------------------------------------------------------------------- | ||
|
||
import os | ||
import unittest | ||
|
||
from azure_devtools.scenario_tests import AllowLargeResponse | ||
from .utilities.helper import DevopsScenarioTest, disable_telemetry, set_authentication, get_test_org_from_env_variable | ||
|
||
DEVOPS_CLI_TEST_ORGANIZATION = get_test_org_from_env_variable() or 'Https://dev.azure.com/azuredevopsclitest' | ||
|
||
class PipelinesFolderTests(DevopsScenarioTest): | ||
@AllowLargeResponse(size_kb=3072) | ||
@disable_telemetry | ||
@set_authentication | ||
def test_pipeline_folders(self): | ||
random_project_name = self.create_random_name(prefix='pipelinesFolderTest', length=25) | ||
self.cmd('az devops configure --defaults organization=' + DEVOPS_CLI_TEST_ORGANIZATION + ' project=' + random_project_name) | ||
created_project_id = None | ||
|
||
try: | ||
create_project_command = 'az devops project create --name ' + random_project_name + ' --output json --detect false' | ||
project_create_output = self.cmd(create_project_command).get_output_in_json() | ||
created_project_id = project_create_output["id"] | ||
|
||
# create a pipeline | ||
FOLDER_NAME = '\\TestFolder' | ||
FOLDER_DESCRIPTION = 'Test folder' | ||
folder_create_command = 'az pipelines folder create --path {} --description "{}" \ | ||
--detect false --output json'.format(FOLDER_NAME, FOLDER_DESCRIPTION) | ||
folder_create_output = self.cmd(folder_create_command).get_output_in_json() | ||
assert folder_create_output['path'] == FOLDER_NAME | ||
assert folder_create_output['description'] == FOLDER_DESCRIPTION | ||
|
||
UPDATED_FOLDER_DESCRIPTION = 'New test folder' | ||
UPDATED_FOLDER_PATH = '\\Testing' + FOLDER_NAME | ||
folder_update_command = 'az pipelines folder update --path {} --new-path "{}" --new-description "{}" \ | ||
--detect false --output json'.format(FOLDER_NAME, UPDATED_FOLDER_PATH, UPDATED_FOLDER_DESCRIPTION) | ||
folder_update_output = self.cmd(folder_update_command).get_output_in_json() | ||
assert folder_update_output['path'] == UPDATED_FOLDER_PATH | ||
assert folder_update_output['description'] == UPDATED_FOLDER_DESCRIPTION | ||
|
||
folder_list_command = 'az pipelines folder list --detect false --output json --query-order asc' | ||
folder_list_output = self.cmd(folder_list_command).get_output_in_json() | ||
assert len(folder_list_output) == 3 # root folder + testing/test123 nested folders | ||
assert folder_list_output[0]['path'] == '\\' # root folder | ||
assert folder_list_output[0]['description'] == None | ||
assert folder_list_output[1]['path'] == '\\Testing' | ||
assert folder_list_output[1]['description'] == None | ||
assert folder_list_output[2]['path'] == UPDATED_FOLDER_PATH | ||
assert folder_list_output[2]['description'] == UPDATED_FOLDER_DESCRIPTION | ||
|
||
folder_delete_command = 'az pipelines folder delete --path "{}" --detect false --output json -y'.format(UPDATED_FOLDER_PATH) | ||
folder_delete_output = self.cmd(folder_delete_command) | ||
# verify deletion | ||
folder_list_output = self.cmd(folder_list_command).get_output_in_json() | ||
assert len(folder_list_output) == 2 | ||
assert folder_list_output[0]['path'] == '\\' # root folder | ||
assert folder_list_output[0]['description'] == None | ||
assert folder_list_output[1]['path'] == '\\Testing' | ||
assert folder_list_output[1]['description'] == None | ||
|
||
finally: | ||
if created_project_id is not None: | ||
delete_project_command = 'az devops project delete --id ' + created_project_id + ' --output json --detect false -y' | ||
self.cmd(delete_project_command) |