From 7ac56fcb0af78daa4d5b7e6c5f1e08942410e864 Mon Sep 17 00:00:00 2001 From: Ishita Mehta <43136080+ishitam8@users.noreply.github.com> Date: Fri, 14 Jun 2019 11:59:14 +0530 Subject: [PATCH] Boards Iterations and Areas Unit tests and Backlog/Default team iteration commands (#663) * Fix python versions in Mac machines * UTs for Project iterations * UTs * UTs for Project Area commands * UTs for Team area commands * Unwanted import * Bug bash fix: Change from relative to absolute path * Absolute path in iterations * Add commands to configure default iteration and backlog iteration * Change help text for update commands * Minor help text changes * Changes in default area * Hanlde empty backlog iteration ID error and adding a troubleshotting help page * Fix UTs * Pylint fixes * Flake fixes * Additional tests for Backlog/default and list work items command * Renaming test helper file --- .../azext_devops/dev/boards/_format.py | 52 +++- azure-devops/azext_devops/dev/boards/_help.py | 27 ++ azure-devops/azext_devops/dev/boards/area.py | 25 +- .../azext_devops/dev/boards/arguments.py | 14 +- .../azext_devops/dev/boards/boards_helper.py | 18 ++ .../azext_devops/dev/boards/commands.py | 16 +- .../azext_devops/dev/boards/iteration.py | 143 +++++++++-- .../azext_devops/test/boards/test_area.py | 237 ++++++++++++++++++ .../test/boards/test_boards_helper.py | 27 ++ .../test/boards/test_iteration.py | 236 +++++++++++++++++ .../azext_devops/test/utils/helper.py | 3 + doc/troubleshooting_common_errors.md | 10 + 12 files changed, 766 insertions(+), 42 deletions(-) create mode 100644 azure-devops/azext_devops/dev/boards/boards_helper.py create mode 100644 azure-devops/azext_devops/test/boards/test_area.py create mode 100644 azure-devops/azext_devops/test/boards/test_boards_helper.py create mode 100644 azure-devops/azext_devops/test/boards/test_iteration.py create mode 100644 doc/troubleshooting_common_errors.md diff --git a/azure-devops/azext_devops/dev/boards/_format.py b/azure-devops/azext_devops/dev/boards/_format.py index 74c5a64c..da1cd529 100644 --- a/azure-devops/azext_devops/dev/boards/_format.py +++ b/azure-devops/azext_devops/dev/boards/_format.py @@ -133,18 +133,54 @@ def _transform_team_iteration_row(row): table_row = OrderedDict() table_row['ID'] = row['id'] table_row['Name'] = row['name'] - if row['attributes']['startDate'] is None: - table_row['Start Date'] = '' - else: - table_row['Start Date'] = row['attributes']['startDate'] - if row['attributes']['finishDate'] is None: - table_row['Finish Date'] = '' - else: - table_row['Finish Date'] = row['attributes']['finishDate'] + if row['attributes']: + if row['attributes']['startDate'] is None: + table_row['Start Date'] = '' + else: + table_row['Start Date'] = row['attributes']['startDate'] + if row['attributes']['finishDate'] is None: + table_row['Finish Date'] = '' + else: + table_row['Finish Date'] = row['attributes']['finishDate'] + if 'timeFrame' in row['attributes']: + table_row['Time Frame'] = row['attributes']['timeFrame'] table_row['Path'] = row['path'] return table_row +def transform_work_item_team_iteration_work_items(result): + table_output = [] + for item in result['workItemRelations']: + table_output.append(_transform_team_iteration_work_item_row(item)) + return table_output + + +def _transform_team_iteration_work_item_row(row): + table_row = OrderedDict() + if row['source']: + table_row['Source'] = row['source']['id'] + if row['target']: + table_row['Target'] = row['target']['id'] + table_row['Relation Type'] = row['rel'] + return table_row + + +def transform_work_item_team_default_iteration_table_output(result): + table_output = [] + table_row = OrderedDict() + if result['defaultIteration']: + table_row = _transform_team_iteration_row(result['defaultIteration']) + table_row['Default Iteration Macro'] = result['defaultIterationMacro'] + table_output.append(table_row) + return table_output + + +def transform_work_item_team_backlog_iteration_table_output(result): + table_output = [] + table_output.append(_transform_team_iteration_row(result['backlogIteration'])) + return table_output + + def transform_work_item_project_classification_nodes_table_output(response): table_op = [] table_op = transform_work_item_project_classification_nodes_table_output_recursive(response, table_op) diff --git a/azure-devops/azext_devops/dev/boards/_help.py b/azure-devops/azext_devops/dev/boards/_help.py index 267e8542..31b89f62 100644 --- a/azure-devops/azext_devops/dev/boards/_help.py +++ b/azure-devops/azext_devops/dev/boards/_help.py @@ -43,6 +43,11 @@ def load_boards_help(): long-summary: """ + helps['boards iteration project update'] = """ + type: command + long-summary: Move iteration or update iteration details like name AND/OR start-date and finish-date. + """ + helps['boards area'] = """ type: group short-summary: Manage area paths. @@ -55,12 +60,34 @@ def load_boards_help(): long-summary: """ + helps['boards area project update'] = """ + type: command + long-summary: Move area or update area name. + """ + helps['boards area team'] = """ type: group short-summary: Manage areas for a team. long-summary: """ + helps['boards area team update'] = """ + type: command + long-summary: Update any area to include/exclude sub areas OR Set already added area as default. + """ + + helps['boards area team add'] = """ + type: command + long-summary: Every team needs to have a default area configured which can't be empty. + Hence, you need to pass --set-as-default while adding first area to your team. + You can later configure any other area which already added to team as default + by using `az boards area team update -h` command. + examples: + - name: Add area to a team. + text: | + az boards area team --team 'ContosoTeam' --path '\\ContosoProject\\MyProjectAreaName' + """ + helps['boards work-item relation'] = """ type: group short-summary: Manage work item relations. diff --git a/azure-devops/azext_devops/dev/boards/area.py b/azure-devops/azext_devops/dev/boards/area.py index 9cf7b5d4..8405131b 100644 --- a/azure-devops/azext_devops/dev/boards/area.py +++ b/azure-devops/azext_devops/dev/boards/area.py @@ -11,19 +11,21 @@ from azext_devops.dev.common.services import (resolve_instance_and_project, get_work_item_tracking_client, get_work_client) - +from .boards_helper import resolve_classification_node_path _STRUCTURE_GROUP_AREA = 'areas' def get_project_areas(depth=1, path=None, organization=None, project=None, detect=None): """List areas for a project. - :param depth: Depth of child nodes to be fetched. + :param depth: Depth of child nodes to be fetched. Example: --depth 3 :type depth: int """ organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project) client = get_work_item_tracking_client(organization) + if path: + path = resolve_classification_node_path(client, path, project, _STRUCTURE_GROUP_AREA) list_of_areas = client.get_classification_node(project=project, structure_group=_STRUCTURE_GROUP_AREA, depth=depth, path=path) @@ -37,6 +39,7 @@ def delete_project_area(path, organization=None, project=None, detect=None): organization=organization, project=project) client = get_work_item_tracking_client(organization) + path = resolve_classification_node_path(client, path, project, _STRUCTURE_GROUP_AREA) response = client.delete_classification_node(project=project, structure_group=_STRUCTURE_GROUP_AREA, path=path) @@ -52,6 +55,8 @@ def create_project_area(name, path=None, organization=None, project=None, detect organization=organization, project=project) client = get_work_item_tracking_client(organization) + if path: + path = resolve_classification_node_path(client, path, project, _STRUCTURE_GROUP_AREA) classification_node_object = WorkItemClassificationNode() classification_node_object.name = name response = client.create_or_update_classification_node(project=project, @@ -76,8 +81,8 @@ def get_project_area(id, organization=None, project=None, detect=None): # pylin return response -def update_project_area(path=None, name=None, child_id=None, organization=None, project=None, detect=None): - """Move area or update area name. +def update_project_area(path, name=None, child_id=None, organization=None, project=None, detect=None): + """Update area. :param name: New name of the area. :type: str :param child_id: Move an existing area and add as child node for this area. @@ -89,6 +94,7 @@ def update_project_area(path=None, name=None, child_id=None, organization=None, organization=organization, project=project) client = get_work_item_tracking_client(organization) + path = resolve_classification_node_path(client, path, project, _STRUCTURE_GROUP_AREA) if child_id: move_classification_node_object = WorkItemClassificationNode() move_classification_node_object.id = child_id @@ -123,14 +129,13 @@ def get_team_areas(team, organization=None, project=None, detect=None): def add_team_area(path, team, set_as_default=False, include_sub_areas=None, organization=None, project=None, detect=None): """Add area to a team. - :param set_as_default: Set this area path as default area for this team. + :param set_as_default: Set this area path as default area for this team. Default: False :type set_as_default: bool """ organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project) client = get_work_client(organization) - team_context = TeamContext(project=project, team=team) get_response = client.get_team_field_values(team_context=team_context) patch_doc = TeamFieldValuesPatch() @@ -159,9 +164,13 @@ def remove_team_area(path, team, organization=None, project=None, detect=None): if get_response.default_value == path: raise CLIError('You are trying to remove the default area for this team. ' 'Please change the default area node and then try this command again.') + area_found = False for entry in get_response.values: if path == entry.value[:]: + area_found = True get_response.values.remove(entry) + if not area_found: + raise CLIError('Path is not added to team area list.') patch_doc = TeamFieldValuesPatch() patch_doc.values = get_response.values patch_doc.default_value = get_response.default_value @@ -171,8 +180,8 @@ def remove_team_area(path, team, organization=None, project=None, detect=None): def update_team_area(path, team, include_sub_areas=None, set_as_default=False, organization=None, project=None, detect=None): - """Update any area to include/exclude sub areas OR Set already added area as default. - :param default_area:set_as_default: Set as default team area path. + """Update team area. + :param set_as_default: Set as default team area path. Default: False :type set_as_default: bool """ if include_sub_areas is None and set_as_default is False: diff --git a/azure-devops/azext_devops/dev/boards/arguments.py b/azure-devops/azext_devops/dev/boards/arguments.py index 00c886af..f08be08b 100644 --- a/azure-devops/azext_devops/dev/boards/arguments.py +++ b/azure-devops/azext_devops/dev/boards/arguments.py @@ -36,7 +36,8 @@ def load_work_arguments(self, _): Multiple values can be passed comma separated. Example: 1,2 ') with self.argument_context('boards iteration project') as context: - context.argument('path', help='Iteration path.') + context.argument('path', help='Absolute path of an iteration. ' + 'Example:' + r'\ProjectName\Iteration\IterationName') context.argument('start_date', help='Start date of the iteration. Example : "2019-06-03"') context.argument('finish_date', @@ -46,15 +47,20 @@ def load_work_arguments(self, _): context.argument('id', type=int) with self.argument_context('boards iteration project create') as context: - context.argument('path', help='Iteration path. Creates an iteration at root level if --path is not specified.') + context.argument('path', help='Absolute path of an iteration. ' + 'Creates an iteration at root level if --path is not specified. ' + 'Example:' + r'\ProjectName\Iteration\IterationName.') with self.argument_context('boards area') as context: - context.argument('path', help='Area path.') + context.argument('path', help='Absolute path of an area. Example:' + r'\ProjectName\Area\AreaName') with self.argument_context('boards area project create') as context: - context.argument('path', help='Area path. Creates an area at root level if --path is not specified.') + context.argument('path', help='Absolute path of an area. ' + 'Creates an area at root level if --path is not specified. ' + 'Example:' + r'\ProjectName\Area\AreaName.') with self.argument_context('boards area team') as context: context.argument('team', help='The name or id of the team.') context.argument('include_sub_areas', arg_type=get_three_state_flag(), help='Include child nodes of this area.') + context.argument('path', help='Area path. Example:' + r'\ProjectName\AreaName') diff --git a/azure-devops/azext_devops/dev/boards/boards_helper.py b/azure-devops/azext_devops/dev/boards/boards_helper.py new file mode 100644 index 00000000..5ca571c5 --- /dev/null +++ b/azure-devops/azext_devops/dev/boards/boards_helper.py @@ -0,0 +1,18 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack.util import CLIError + + +def resolve_classification_node_path(client, path, project, structure_group): + get_root_node = client.get_root_nodes(project=project, depth=0) + root_node_path = None + for entry in get_root_node: + if entry.structure_type == structure_group[:-1]: + root_node_path = entry.additional_properties['path'] + if root_node_path and path.lower().startswith(root_node_path.lower()): + updated_path = path[len(root_node_path):] + return updated_path + raise CLIError("--path parameter is expected to be absolute path.") diff --git a/azure-devops/azext_devops/dev/boards/commands.py b/azure-devops/azext_devops/dev/boards/commands.py index efa7468a..75052ecd 100644 --- a/azure-devops/azext_devops/dev/boards/commands.py +++ b/azure-devops/azext_devops/dev/boards/commands.py @@ -12,6 +12,9 @@ transform_work_item_relations, transform_work_item_team_iterations_table_output, transform_work_item_team_iteration_table_output, + transform_work_item_team_iteration_work_items, + transform_work_item_team_default_iteration_table_output, + transform_work_item_team_backlog_iteration_table_output, transform_work_item_project_classification_nodes_table_output, transform_work_item_project_classification_node_table_output, transform_work_item_team_areas_table_output) @@ -62,7 +65,16 @@ def load_work_commands(self, _): with self.command_group('boards iteration team', command_type=workProjectAndTeamIterationOps) as g: # team iteration commands g.command('list', 'get_team_iterations', table_transformer=transform_work_item_team_iterations_table_output) - g.command('show', 'get_team_iteration', table_transformer=transform_work_item_team_iteration_table_output) + g.command('list-work-items', 'list_iteration_work_items', + table_transformer=transform_work_item_team_iteration_work_items) + g.command('set-default-iteration', 'set_default_iteration', + table_transformer=transform_work_item_team_default_iteration_table_output) + g.command('set-backlog-iteration', 'set_backlog_iteration', + table_transformer=transform_work_item_team_backlog_iteration_table_output) + g.command('show-default-iteration', 'show_default_iteration', + table_transformer=transform_work_item_team_default_iteration_table_output) + g.command('show-backlog-iteration', 'show_backlog_iteration', + table_transformer=transform_work_item_team_backlog_iteration_table_output) g.command('remove', 'delete_team_iteration', table_transformer=transform_work_item_team_iteration_table_output) g.command('add', 'post_team_iteration', table_transformer=transform_work_item_team_iteration_table_output) @@ -75,7 +87,7 @@ def load_work_commands(self, _): g.command('delete', 'delete_project_iteration', confirmation='Are you sure you want to delete this iteration?') g.command('show', 'get_project_iteration', - table_transformer=transform_work_item_project_classification_nodes_table_output) + table_transformer=transform_work_item_project_classification_node_table_output) g.command('create', 'create_project_iteration', table_transformer=transform_work_item_project_classification_nodes_table_output) diff --git a/azure-devops/azext_devops/dev/boards/iteration.py b/azure-devops/azext_devops/dev/boards/iteration.py index 97b75a6e..01a7c4ea 100644 --- a/azure-devops/azext_devops/dev/boards/iteration.py +++ b/azure-devops/azext_devops/dev/boards/iteration.py @@ -4,25 +4,35 @@ # -------------------------------------------------------------------------------------------- from knack.util import CLIError +from knack.log import get_logger +from azext_devops.devops_sdk.exceptions import AzureDevOpsServiceError from azext_devops.devops_sdk.v5_0.work_item_tracking.models import WorkItemClassificationNode from azext_devops.devops_sdk.v5_0.work.models import (TeamContext, - TeamSettingsIteration) + TeamSettingsIteration, + TeamSettingsPatch) from azext_devops.dev.common.arguments import convert_date_only_string_to_iso8601 from azext_devops.dev.common.services import (resolve_instance_and_project, get_work_item_tracking_client, get_work_client) +from azext_devops.dev.common.uuid import EMPTY_UUID +from .boards_helper import resolve_classification_node_path + +logger = get_logger(__name__) + _STRUCTURE_GROUP_ITERATION = 'iterations' def get_project_iterations(depth=1, path=None, organization=None, project=None, detect=None): """List iterations for a project. - :param depth: Depth of child nodes to be fetched. + :param depth: Depth of child nodes to be fetched. Example: --depth 3. :type depth: int """ organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project) client = get_work_item_tracking_client(organization) + if path: + path = resolve_classification_node_path(client, path, project, _STRUCTURE_GROUP_ITERATION) list_of_iterations = client.get_classification_node(project=project, structure_group=_STRUCTURE_GROUP_ITERATION, depth=depth, path=path) @@ -31,7 +41,7 @@ def get_project_iterations(depth=1, path=None, organization=None, project=None, def update_project_iteration(path, child_id=None, name=None, start_date=None, finish_date=None, organization=None, project=None, detect=None): - """Move iteration or update iteration details like name AND/OR start-date and finish-date. + """Update project iteration. :param name: New name of the iteration. :type: str :param child_id: Move an existing iteration and add as child node for this iteration. @@ -43,6 +53,7 @@ def update_project_iteration(path, child_id=None, name=None, start_date=None, organization=organization, project=project) client = get_work_item_tracking_client(organization) + path = resolve_classification_node_path(client, path, project, _STRUCTURE_GROUP_ITERATION) if child_id: move_classification_node_object = WorkItemClassificationNode() move_classification_node_object.id = child_id @@ -81,6 +92,7 @@ def delete_project_iteration(path, organization=None, project=None, detect=None) organization=organization, project=project) client = get_work_item_tracking_client(organization) + path = resolve_classification_node_path(client, path, project, _STRUCTURE_GROUP_ITERATION) response = client.delete_classification_node(project=project, structure_group=_STRUCTURE_GROUP_ITERATION, path=path) @@ -108,14 +120,14 @@ def create_project_iteration(name, path=None, start_date=None, finish_date=None, :param name: Name of the iteration. :type: str """ - import pdb - pdb.set_trace() if start_date is None and finish_date is None and name is None: raise CLIError('At least one of --start-date , --finish-date or --name arguments is required.') organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project) client = get_work_item_tracking_client(organization) + if path: + path = resolve_classification_node_path(client, path, project, _STRUCTURE_GROUP_ITERATION) classification_node_object = WorkItemClassificationNode() if ((start_date and not finish_date) or (not start_date and finish_date)): raise CLIError('You must specify both start and finish dates or neither date') @@ -155,24 +167,22 @@ def get_team_iterations(team, timeframe=None, organization=None, project=None, d return list_of_iterations -def get_team_iteration(id, team, organization=None, project=None, detect=None): # pylint: disable=redefined-builtin - """ Get iteration details for a team. +def delete_team_iteration(id, team, organization=None, project=None, detect=None): # pylint: disable=redefined-builtin + """ Remove iteration from a team. :param id: Identifier of the iteration. :type: str :param team: Name or ID of the team. :type: str """ - organization, project = resolve_instance_and_project(detect=detect, - organization=organization, - project=project) + organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project) client = get_work_client(organization) team_context = TeamContext(project=project, team=team) - team_iteration = client.get_team_iteration(team_context=team_context, id=id) + team_iteration = client.delete_team_iteration(team_context=team_context, id=id) return team_iteration -def delete_team_iteration(id, team, organization=None, project=None, detect=None): # pylint: disable=redefined-builtin - """ Remove iteration from a team. +def post_team_iteration(id, team, organization=None, project=None, detect=None): # pylint: disable=redefined-builtin + """Add iteration to a team. :param id: Identifier of the iteration. :type: str :param team: Name or ID of the team. @@ -181,12 +191,16 @@ def delete_team_iteration(id, team, organization=None, project=None, detect=None organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project) client = get_work_client(organization) team_context = TeamContext(project=project, team=team) - team_iteration = client.delete_team_iteration(team_context=team_context, id=id) - return team_iteration + team_setting_iteration = TeamSettingsIteration(id=id) + try: + team_iteration = client.post_team_iteration(iteration=team_setting_iteration, team_context=team_context) + return team_iteration + except AzureDevOpsServiceError as ex: + _handle_empty_backlog_iteration_id(ex=ex, client=client, team_context=team_context) -def post_team_iteration(id, team, organization=None, project=None, detect=None): # pylint: disable=redefined-builtin - """Add iteration to a team. +def list_iteration_work_items(id, team, organization=None, project=None, detect=None): # pylint: disable=redefined-builtin + """List work-items for an iteration. :param id: Identifier of the iteration. :type: str :param team: Name or ID of the team. @@ -195,6 +209,95 @@ def post_team_iteration(id, team, organization=None, project=None, detect=None): organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project) client = get_work_client(organization) team_context = TeamContext(project=project, team=team) - team_setting_iteration = TeamSettingsIteration(id=id) - team_iteration = client.post_team_iteration(iteration=team_setting_iteration, team_context=team_context) - return team_iteration + work_items = client.get_iteration_work_items(iteration_id=id, team_context=team_context) + wit_client = get_work_item_tracking_client(organization) + relation_types = wit_client.get_relation_types() + work_items = _fill_friendly_name_for_relations_in_iteration_work_items(relation_types_from_service=relation_types, + iteration_work_items=work_items) + return work_items + + +def set_default_iteration(team, id=None, default_iteration_macro=None, organization=None, project=None, detect=None): # pylint: disable=redefined-builtin + """Set default iteration for a team. + :param id: Identifier of the iteration which needs to be set as default. + :type: str + :param team: Name or ID of the team. + :type: str + :param default_iteration_macro: Default iteration macro. Example: @CurrentIteration. + :type: str + """ + if default_iteration_macro is None and id is None: + raise CLIError('Either --id or --default-iteration-macro is required.') + organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project) + client = get_work_client(organization) + team_context = TeamContext(project=project, team=team) + patch_object = TeamSettingsPatch() + if id: + patch_object.default_iteration = id + if default_iteration_macro: + patch_object.default_iteration_macro = default_iteration_macro + team_iteration_setting = client.update_team_settings(team_settings_patch=patch_object, team_context=team_context) + return team_iteration_setting + + +def set_backlog_iteration(team, id, organization=None, project=None, detect=None): # pylint: disable=redefined-builtin + """Set backlog iteration for a team. + :param id: Identifier of the iteration which needs to be set as backlog iteration. + :type: str + :param team: Name or ID of the team. + :type: str + """ + organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project) + client = get_work_client(organization) + team_context = TeamContext(project=project, team=team) + patch_object = TeamSettingsPatch() + patch_object.backlog_iteration = id + team_iteration_setting = client.update_team_settings(team_settings_patch=patch_object, team_context=team_context) + return team_iteration_setting + + +def show_default_iteration(team, organization=None, project=None, detect=None): + """Show default iteration for a team. + :param team: Name or ID of the team. + :type: str + """ + organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project) + client = get_work_client(organization) + team_context = TeamContext(project=project, team=team) + team_iteration_setting = client.get_team_settings(team_context=team_context) + return team_iteration_setting + + +def show_backlog_iteration(team, organization=None, project=None, detect=None): + """Show backlog iteration for a team. + :param team: Name or ID of the team. + :type: str + """ + organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project) + client = get_work_client(organization) + team_context = TeamContext(project=project, team=team) + team_iteration_setting = client.get_team_settings(team_context=team_context) + return team_iteration_setting + + +def _fill_friendly_name_for_relations_in_iteration_work_items(relation_types_from_service, iteration_work_items): + if not iteration_work_items.work_item_relations: + return iteration_work_items + for relation in iteration_work_items.work_item_relations: + for relation_type_from_service in relation_types_from_service: + if relation_type_from_service.reference_name == relation.rel: + relation.rel = relation_type_from_service.name + return iteration_work_items + + +def _handle_empty_backlog_iteration_id(ex, client, team_context): + logger.debug(ex, exc_info=True) + exception_message_str = r'The guid specified for parameter rootIterationId must not be Guid.Empty.' + if exception_message_str in ex.message: + # Check if backlog iteration ID is empty + backlog_setting = client.get_team_settings(team_context=team_context) + if backlog_setting.backlog_iteration.id == EMPTY_UUID: + raise CLIError('No backlog iteration has been selected for your team. ' + 'Before you can select iterations for your team to participate in, ' + 'you must first specify a backlog iteration.') + raise CLIError(ex) diff --git a/azure-devops/azext_devops/test/boards/test_area.py b/azure-devops/azext_devops/test/boards/test_area.py new file mode 100644 index 00000000..bcfacfc4 --- /dev/null +++ b/azure-devops/azext_devops/test/boards/test_area.py @@ -0,0 +1,237 @@ +# -------------------------------------------------------------------------------------------- +# 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.devops_sdk.v5_0.work.models import (TeamFieldValue, + TeamFieldValuesPatch) +from azext_devops.dev.common.services import clear_connection_cache +from azext_devops.test.utils.authentication import AuthenticatedTests +from azext_devops.test.utils.helper import get_client_mock_helper +from azext_devops.dev.boards.area import (get_project_areas, + get_project_area, + delete_project_area, + create_project_area, + update_project_area, + get_team_areas, + add_team_area, + remove_team_area, + update_team_area) +from .test_boards_helper import (get_root_nodes_response, + TEST_PROJECT_NAME, + PATH_SEPARATOR, + TEST_DEVOPS_ORGANIZATION, + TEST_TEAM_NAME, + WORK_CLIENT_LOCATION, + WORK_ITEM_TRACKING_CLIENT_LOCATION) + +class TestBoardsAreaMethods(AuthenticatedTests): + _STRUCTURE_GROUP = 'areas' + _ROOT_AREA_NAME = 'root_area' + _ROOT_AREA_PATH = PATH_SEPARATOR + TEST_PROJECT_NAME + PATH_SEPARATOR + 'Area' + PATH_SEPARATOR + _ROOT_AREA_NAME + _CHILD_AREA_NAME = 'child_area' + _CHILD_AREA_PATH ='\\' + _ROOT_AREA_PATH + '\\' + _CHILD_AREA_NAME + _NEW_AREA_NAME = 'root_area_renamed' + _AREA_ID = 1 + _AREA_IDENTIFIER = 'some-guid' + + def setUp(self): + self.authentication_setup() + self.authenticate() + self.get_classification_nodes_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'get_classification_nodes') + self.get_classification_node_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'get_classification_node') + self.get_root_nodes_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'get_root_nodes') + self.delete_classification_node_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'delete_classification_node') + self.create_update_classification_node_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'create_or_update_classification_node') + self.update_classification_node_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'update_classification_node') + self.get_team_field_values_patcher = patch(WORK_CLIENT_LOCATION + 'get_team_field_values') + self.update_team_field_values_patcher = patch(WORK_CLIENT_LOCATION + 'update_team_field_values') + + self.get_client = patch('azext_devops.devops_sdk.connection.Connection.get_client', new=get_client_mock_helper) + + self.mock_get_client = self.get_client.start() + self.mock_get_classification_node = self.get_classification_node_patcher.start() + self.mock_get_classification_nodes = self.get_classification_nodes_patcher.start() + self.mock_get_root_nodes = self.get_root_nodes_patcher.start() + self.mock_delete_classification_node = self.delete_classification_node_patcher.start() + self.mock_create_update_classification_node = self.create_update_classification_node_patcher.start() + self.mock_update_classification_node = self.update_classification_node_patcher.start() + self.mock_get_team_field_values = self.get_team_field_values_patcher.start() + self.mock_update_team_field_values = self.update_team_field_values_patcher.start() + + self.mock_get_root_nodes.return_value = get_root_nodes_response() + #clear connection cache before running each test + clear_connection_cache() + + def tearDown(self): + patch.stopall() + + def test_list_project_areas(self): + response = get_project_areas(depth=1,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_get_classification_node.assert_called_once() + list_project_areas_param = self.mock_get_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, list_project_areas_param['project'], str(list_project_areas_param)) + self.assertEqual(self._STRUCTURE_GROUP, list_project_areas_param['structure_group'], str(list_project_areas_param)) + self.assertEqual(1, list_project_areas_param['depth'], str(list_project_areas_param)) + + def test_list_project_area_with_depth_and_path(self): + response = get_project_areas(depth=3,path=self._ROOT_AREA_PATH,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_get_classification_node.assert_called_once() + list_project_areas_param = self.mock_get_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, list_project_areas_param['project'], str(list_project_areas_param)) + self.assertEqual(self._STRUCTURE_GROUP, list_project_areas_param['structure_group'], str(list_project_areas_param)) + self.assertEqual(3, list_project_areas_param['depth'], str(list_project_areas_param)) + self.assertEqual('\\root_area', list_project_areas_param['path'], str(list_project_areas_param)) + + def test_show_project_area(self): + area_ids_list = [] + area_ids_list.append(1) + response = get_project_area(id=self._AREA_ID,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_get_classification_nodes.assert_called_once() + show_project_area_param = self.mock_get_classification_nodes.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, show_project_area_param['project'], str(show_project_area_param)) + self.assertEqual(area_ids_list, show_project_area_param['ids'], str(show_project_area_param)) + + def test_delete_project_area(self): + response = delete_project_area(path=self._ROOT_AREA_PATH,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_delete_classification_node.assert_called_once() + delete_project_area_param = self.mock_delete_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, delete_project_area_param['project'], str(delete_project_area_param)) + self.assertEqual(self._STRUCTURE_GROUP, delete_project_area_param['structure_group'], str(delete_project_area_param)) + self.assertEqual('\\root_area', delete_project_area_param['path'], str(delete_project_area_param)) + + + def test_create_project_area(self): + response = create_project_area(name=self._ROOT_AREA_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_create_update_classification_node.assert_called_once() + create_project_area_param = self.mock_create_update_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, create_project_area_param['project'], str(create_project_area_param)) + self.assertEqual(self._STRUCTURE_GROUP, create_project_area_param['structure_group'], str(create_project_area_param)) + self.assertEqual(self._ROOT_AREA_NAME, create_project_area_param['posted_node'].name, str(create_project_area_param)) + + def test_create_project_area_with_path(self): + response = create_project_area(name=self._CHILD_AREA_NAME,path=self._ROOT_AREA_PATH,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_create_update_classification_node.assert_called_once() + create_project_area_param = self.mock_create_update_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, create_project_area_param['project'], str(create_project_area_param)) + self.assertEqual(self._STRUCTURE_GROUP, create_project_area_param['structure_group'], str(create_project_area_param)) + self.assertEqual(self._CHILD_AREA_NAME, create_project_area_param['posted_node'].name, str(create_project_area_param)) + self.assertEqual('\\root_area', create_project_area_param['path'], str(create_project_area_param)) + + def test_update_project_area(self): + response = update_project_area(path=self._ROOT_AREA_PATH, name=self._NEW_AREA_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_get_classification_node.assert_called_once() + self.mock_update_classification_node.assert_called_once() + update_project_area_param = self.mock_update_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, update_project_area_param['project'], str(update_project_area_param)) + self.assertEqual(self._STRUCTURE_GROUP, update_project_area_param['structure_group'], str(update_project_area_param)) + self.assertEqual(self._NEW_AREA_NAME, update_project_area_param['posted_node'].name, str(update_project_area_param)) + self.assertEqual('\\root_area', update_project_area_param['path'], str(update_project_area_param)) + + def test_move_project_area(self): + child_area_id = '2' + response = update_project_area(path=self._ROOT_AREA_PATH, name=self._NEW_AREA_NAME, child_id=child_area_id,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_create_update_classification_node.assert_called_once() + self.mock_get_classification_node.assert_called_once() + + self.mock_update_classification_node.assert_called_once() + update_project_area_param = self.mock_update_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, update_project_area_param['project'], str(update_project_area_param)) + self.assertEqual(self._STRUCTURE_GROUP, update_project_area_param['structure_group'], str(update_project_area_param)) + self.assertEqual(self._NEW_AREA_NAME, update_project_area_param['posted_node'].name, str(update_project_area_param)) + self.assertEqual('\\root_area', update_project_area_param['path'], str(update_project_area_param)) + + create_project_area_param = self.mock_create_update_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, create_project_area_param['project'], str(create_project_area_param)) + self.assertEqual(self._STRUCTURE_GROUP, create_project_area_param['structure_group'], str(create_project_area_param)) + self.assertEqual(child_area_id, create_project_area_param['posted_node'].id, str(create_project_area_param)) + self.assertEqual('\\root_area', create_project_area_param['path'], str(create_project_area_param)) + + def test_get_team_areas(self): + response = get_team_areas(team=TEST_TEAM_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + self.mock_get_team_field_values.assert_called_once() + list_team_areas_param = self.mock_get_team_field_values.call_args_list[0][1] + self.assertEqual(TEST_TEAM_NAME, list_team_areas_param['team_context'].team, str(list_team_areas_param)) + self.assertEqual(TEST_PROJECT_NAME, list_team_areas_param['team_context'].project, str(list_team_areas_param)) + + def test_update_team_area(self): + self.mock_get_team_field_values.return_value = self._prepare_team_field_values_patch_object(path=self._CHILD_AREA_PATH, include_children=False, is_default=False) + self.mock_update_team_field_values.return_value = self._prepare_team_field_values_patch_object(path=self._CHILD_AREA_PATH, include_children=True, is_default=True) + response = update_team_area(path=self._CHILD_AREA_PATH, set_as_default=True, include_sub_areas=True, team=TEST_TEAM_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + self.mock_get_team_field_values.assert_called_once() + update_team_area_param = self.mock_update_team_field_values.call_args_list[0][1] + self.assertEqual(TEST_TEAM_NAME, update_team_area_param['team_context'].team, str(update_team_area_param)) + self.assertEqual(TEST_PROJECT_NAME, update_team_area_param['team_context'].project, str(update_team_area_param)) + area_path_include_children= False + area_path_found = False + area_path_is_default = False + for entry in response.values: + if self._CHILD_AREA_PATH == entry.value: + area_path_found = True + area_path_include_children = ( entry.include_children is True ) + if response.default_value == self._CHILD_AREA_PATH: + area_path_is_default = True + self.assertEqual(area_path_found, True) + self.assertEqual(area_path_is_default, True) + self.assertEqual(area_path_include_children, True) + + def test_remove_team_area(self): + self.mock_get_team_field_values.return_value = self._prepare_team_field_values_patch_object(path=self._CHILD_AREA_PATH, include_children=False, is_default=False) + response = remove_team_area(path=self._CHILD_AREA_PATH,team=TEST_TEAM_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + self.mock_get_team_field_values.assert_called_once() + self.mock_update_team_field_values.assert_called_once() + remove_team_area_param = self.mock_update_team_field_values.call_args_list[0][1] + self.assertEqual(TEST_TEAM_NAME, remove_team_area_param['team_context'].team, str(remove_team_area_param)) + self.assertEqual(TEST_PROJECT_NAME, remove_team_area_param['team_context'].project, str(remove_team_area_param)) + updated_team_field_values = remove_team_area_param['patch'].values + for entry in updated_team_field_values: + self.assertNotEqual(self._CHILD_AREA_PATH, entry.value , str(remove_team_area_param)) + + def test_add_team_area(self): + self.mock_update_team_field_values.return_value = self._prepare_team_field_values_patch_object(path=self._CHILD_AREA_PATH, include_children=True, is_default=False) + response = add_team_area(path=self._CHILD_AREA_PATH,team=TEST_TEAM_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + self.mock_get_team_field_values.assert_called_once() + self.mock_update_team_field_values.assert_called_once() + add_team_area_param = self.mock_update_team_field_values.call_args_list[0][1] + self.assertEqual(TEST_TEAM_NAME, add_team_area_param['team_context'].team, str(add_team_area_param)) + self.assertEqual(TEST_PROJECT_NAME, add_team_area_param['team_context'].project, str(add_team_area_param)) + area_path_found = False + for entry in response.values: + if self._CHILD_AREA_PATH == entry.value: + area_path_found = True + self.assertEqual(area_path_found, True) + + + def _prepare_team_field_values_patch_object(self, path, include_children=False, is_default=True): + patch_obj = TeamFieldValuesPatch() + patch_obj.values = [] + # add root node + team_field_value = TeamFieldValue(include_children=False, value=self._ROOT_AREA_PATH) + patch_obj.values.append(team_field_value) + patch_obj.default_value = self._ROOT_AREA_PATH + # add child node + team_field_value = TeamFieldValue(include_children=include_children, value=path) + if is_default: + patch_obj.default_value = path + patch_obj.values.append(team_field_value) + team_field_value = TeamFieldValue(include_children=False, value=self._ROOT_AREA_PATH) + patch_obj.values.append(team_field_value) + return patch_obj + diff --git a/azure-devops/azext_devops/test/boards/test_boards_helper.py b/azure-devops/azext_devops/test/boards/test_boards_helper.py new file mode 100644 index 00000000..254357a2 --- /dev/null +++ b/azure-devops/azext_devops/test/boards/test_boards_helper.py @@ -0,0 +1,27 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azext_devops.devops_sdk.v5_0.work_item_tracking.models import WorkItemClassificationNode + +PATH_SEPARATOR = '\\' +TEST_DEVOPS_ORGANIZATION = 'https://someorganization.visualstudio.com' +TEST_PROJECT_NAME = 'sample_project' +WORK_ITEM_TRACKING_CLIENT_LOCATION = 'azext_devops.devops_sdk.v5_0.work_item_tracking.work_item_tracking_client.WorkItemTrackingClient.' +WORK_CLIENT_LOCATION = 'azext_devops.devops_sdk.v5_0.work.work_client.WorkClient.' +TEST_TEAM_NAME = 'sample_team' + +def get_root_nodes_response(): + root_nodes_list = [] + classification_node1 = WorkItemClassificationNode() + classification_node1.structure_type = 'area' + classification_node1.name = TEST_PROJECT_NAME + classification_node1.additional_properties['path'] = PATH_SEPARATOR + TEST_PROJECT_NAME + PATH_SEPARATOR + 'Area' + root_nodes_list.append(classification_node1) + classification_node2 = WorkItemClassificationNode() + classification_node2.structure_type = 'iteration' + classification_node2.name = TEST_PROJECT_NAME + classification_node2.additional_properties['path'] = PATH_SEPARATOR + TEST_PROJECT_NAME + PATH_SEPARATOR + 'Iteration' + root_nodes_list.append(classification_node2) + return root_nodes_list diff --git a/azure-devops/azext_devops/test/boards/test_iteration.py b/azure-devops/azext_devops/test/boards/test_iteration.py new file mode 100644 index 00000000..6581b31a --- /dev/null +++ b/azure-devops/azext_devops/test/boards/test_iteration.py @@ -0,0 +1,236 @@ +# -------------------------------------------------------------------------------------------- +# 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.test.utils.helper import get_client_mock_helper +from azext_devops.dev.boards.iteration import (get_project_iterations, + get_project_iteration, + delete_project_iteration, + create_project_iteration, + update_project_iteration, + get_team_iterations, + post_team_iteration, + delete_team_iteration, + show_default_iteration, + show_backlog_iteration, + set_backlog_iteration, + set_default_iteration, + list_iteration_work_items) +from .test_boards_helper import (get_root_nodes_response, + TEST_PROJECT_NAME, + PATH_SEPARATOR, + TEST_DEVOPS_ORGANIZATION, + TEST_TEAM_NAME, + WORK_CLIENT_LOCATION, + WORK_ITEM_TRACKING_CLIENT_LOCATION) +class TestBoardsIterationMethods(AuthenticatedTests): + + _STRUCTURE_GROUP = 'iterations' + _ROOT_ITERATION_NAME = 'root_iteration' + _ROOT_ITERATION_PATH = PATH_SEPARATOR + TEST_PROJECT_NAME + PATH_SEPARATOR + 'Iteration' + PATH_SEPARATOR + _ROOT_ITERATION_NAME + _CHILD_ITERATION_NAME = 'child_iteration' + _NEW_ITERATION_NAME = 'root_iteration_renamed' + _ITERATION_ID = 1 + _TEAM = 'sample_team' + _ITERATION_IDENTIFIER = 'some-guid' + + def setUp(self): + self.authentication_setup() + self.authenticate() + self.get_classification_nodes_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'get_classification_nodes') + self.get_classification_node_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'get_classification_node') + self.get_root_nodes_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'get_root_nodes') + self.delete_classification_node_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'delete_classification_node') + self.create_update_classification_node_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'create_or_update_classification_node') + self.update_classification_node_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'update_classification_node') + self.get_relation_types_patcher = patch(WORK_ITEM_TRACKING_CLIENT_LOCATION + 'get_relation_types') + self.get_team_iterations_patcher = patch(WORK_CLIENT_LOCATION + 'get_team_iterations') + self.get_team_settings_patcher = patch(WORK_CLIENT_LOCATION + 'get_team_settings') + self.update_team_settings_patcher = patch(WORK_CLIENT_LOCATION + 'update_team_settings') + self.delete_team_iteration_patcher = patch(WORK_CLIENT_LOCATION + 'delete_team_iteration') + self.post_team_iteration_patcher = patch(WORK_CLIENT_LOCATION + 'post_team_iteration') + self.list_team_iteration_work_items_patcher = patch(WORK_CLIENT_LOCATION + 'get_iteration_work_items') + self.get_client = patch('azext_devops.devops_sdk.connection.Connection.get_client', new=get_client_mock_helper) + + self.mock_get_client = self.get_client.start() + self.mock_get_classification_node = self.get_classification_node_patcher.start() + self.mock_get_classification_nodes = self.get_classification_nodes_patcher.start() + self.mock_delete_classification_node = self.delete_classification_node_patcher.start() + self.mock_get_root_nodes = self.get_root_nodes_patcher.start() + self.mock_create_update_classification_node = self.create_update_classification_node_patcher.start() + self.mock_update_classification_node = self.update_classification_node_patcher.start() + self.mock_get_team_iterations = self.get_team_iterations_patcher.start() + self.mock_delete_team_iteration = self.delete_team_iteration_patcher.start() + self.mock_post_team_iteration = self.post_team_iteration_patcher.start() + self.mock_get_team_settings = self.get_team_settings_patcher.start() + self.mock_update_team_settings = self.update_team_settings_patcher.start() + self.mock_list_team_iteration_work_items = self.list_team_iteration_work_items_patcher.start() + self.mock_get_relation_types = self.get_relation_types_patcher.start() + + self.mock_get_root_nodes.return_value = get_root_nodes_response() + #clear connection cache before running each test + clear_connection_cache() + + def tearDown(self): + patch.stopall() + + def test_list_project_iteration(self): + response = get_project_iterations(depth=1,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_get_classification_node.assert_called_once() + list_project_iterations_param = self.mock_get_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, list_project_iterations_param['project'], str(list_project_iterations_param)) + self.assertEqual(self._STRUCTURE_GROUP, list_project_iterations_param['structure_group'], str(list_project_iterations_param)) + self.assertEqual(1, list_project_iterations_param['depth'], str(list_project_iterations_param)) + + def test_list_project_iteration_with_depth_and_path(self): + response = get_project_iterations(depth=3,path=self._ROOT_ITERATION_PATH,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_get_classification_node.assert_called_once() + list_project_iterations_param = self.mock_get_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, list_project_iterations_param['project'], str(list_project_iterations_param)) + self.assertEqual(self._STRUCTURE_GROUP, list_project_iterations_param['structure_group'], str(list_project_iterations_param)) + self.assertEqual(3, list_project_iterations_param['depth'], str(list_project_iterations_param)) + self.assertEqual('\\root_iteration', list_project_iterations_param['path'], str(list_project_iterations_param)) + + def test_show_project_iteration(self): + iteration_ids_list = [] + iteration_ids_list.append(1) + response = get_project_iteration(id=self._ITERATION_ID,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_get_classification_nodes.assert_called_once() + show_project_iteration_param = self.mock_get_classification_nodes.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, show_project_iteration_param['project'], str(show_project_iteration_param)) + self.assertEqual(iteration_ids_list, show_project_iteration_param['ids'], str(show_project_iteration_param)) + + def test_delete_project_iteration(self): + response = delete_project_iteration(path=self._ROOT_ITERATION_PATH,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_delete_classification_node.assert_called_once() + delete_project_iteration_param = self.mock_delete_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, delete_project_iteration_param['project'], str(delete_project_iteration_param)) + self.assertEqual(self._STRUCTURE_GROUP, delete_project_iteration_param['structure_group'], str(delete_project_iteration_param)) + self.assertEqual('\\root_iteration', delete_project_iteration_param['path'], str(delete_project_iteration_param)) + + def test_create_project_iteration(self): + response = create_project_iteration(name=self._ROOT_ITERATION_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_create_update_classification_node.assert_called_once() + create_project_iteration_param = self.mock_create_update_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, create_project_iteration_param['project'], str(create_project_iteration_param)) + self.assertEqual(self._STRUCTURE_GROUP, create_project_iteration_param['structure_group'], str(create_project_iteration_param)) + self.assertEqual('root_iteration', create_project_iteration_param['posted_node'].name, str(create_project_iteration_param)) + + def test_create_project_iteration_with_path(self): + response = create_project_iteration(name=self._CHILD_ITERATION_NAME,path=self._ROOT_ITERATION_PATH,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_create_update_classification_node.assert_called_once() + create_project_iteration_param = self.mock_create_update_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, create_project_iteration_param['project'], str(create_project_iteration_param)) + self.assertEqual(self._STRUCTURE_GROUP, create_project_iteration_param['structure_group'], str(create_project_iteration_param)) + self.assertEqual(self._CHILD_ITERATION_NAME, create_project_iteration_param['posted_node'].name, str(create_project_iteration_param)) + self.assertEqual('\\root_iteration', create_project_iteration_param['path'], str(create_project_iteration_param)) + + def test_update_project_iteration(self): + response = update_project_iteration(path=self._ROOT_ITERATION_PATH, name=self._NEW_ITERATION_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_get_classification_node.assert_called_once() + self.mock_update_classification_node.assert_called_once() + update_project_iteration_param = self.mock_update_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, update_project_iteration_param['project'], str(update_project_iteration_param)) + self.assertEqual(self._STRUCTURE_GROUP, update_project_iteration_param['structure_group'], str(update_project_iteration_param)) + self.assertEqual(self._NEW_ITERATION_NAME, update_project_iteration_param['posted_node'].name, str(update_project_iteration_param)) + self.assertEqual('\\root_iteration', update_project_iteration_param['path'], str(update_project_iteration_param)) + + def test_move_project_iteration(self): + child_iteration_id = '2' + response = update_project_iteration(path=self._ROOT_ITERATION_PATH, name=self._NEW_ITERATION_NAME, child_id=child_iteration_id,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + #assert + self.mock_create_update_classification_node.assert_called_once() + self.mock_get_classification_node.assert_called_once() + + self.mock_update_classification_node.assert_called_once() + update_project_iteration_param = self.mock_update_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, update_project_iteration_param['project'], str(update_project_iteration_param)) + self.assertEqual(self._STRUCTURE_GROUP, update_project_iteration_param['structure_group'], str(update_project_iteration_param)) + self.assertEqual(self._NEW_ITERATION_NAME, update_project_iteration_param['posted_node'].name, str(update_project_iteration_param)) + self.assertEqual('\\root_iteration', update_project_iteration_param['path'], str(update_project_iteration_param)) + + create_project_iteration_param = self.mock_create_update_classification_node.call_args_list[0][1] + self.assertEqual(TEST_PROJECT_NAME, create_project_iteration_param['project'], str(create_project_iteration_param)) + self.assertEqual(self._STRUCTURE_GROUP, create_project_iteration_param['structure_group'], str(create_project_iteration_param)) + self.assertEqual(child_iteration_id, create_project_iteration_param['posted_node'].id, str(create_project_iteration_param)) + self.assertEqual('\\root_iteration', create_project_iteration_param['path'], str(create_project_iteration_param)) + + def test_get_team_iterations(self): + response = get_team_iterations(team=TEST_TEAM_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + self.mock_get_team_iterations.assert_called_once() + list_team_iterations_param = self.mock_get_team_iterations.call_args_list[0][1] + self.assertEqual(TEST_TEAM_NAME, list_team_iterations_param['team_context'].team, str(list_team_iterations_param)) + self.assertEqual(TEST_PROJECT_NAME, list_team_iterations_param['team_context'].project, str(list_team_iterations_param)) + + def test_remove_team_iteration(self): + response = delete_team_iteration(id=self._ITERATION_IDENTIFIER,team=TEST_TEAM_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + self.mock_delete_team_iteration.assert_called_once() + remove_team_iteration_param = self.mock_delete_team_iteration.call_args_list[0][1] + self.assertEqual(TEST_TEAM_NAME, remove_team_iteration_param['team_context'].team, str(remove_team_iteration_param)) + self.assertEqual(TEST_PROJECT_NAME, remove_team_iteration_param['team_context'].project, str(remove_team_iteration_param)) + self.assertEqual(self._ITERATION_IDENTIFIER, remove_team_iteration_param['id'], str(remove_team_iteration_param)) + + def test_add_team_iteration(self): + response = post_team_iteration(id=self._ITERATION_IDENTIFIER,team=TEST_TEAM_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + self.mock_post_team_iteration.assert_called_once() + create_team_iteration_param = self.mock_post_team_iteration.call_args_list[0][1] + self.assertEqual(TEST_TEAM_NAME, create_team_iteration_param['team_context'].team, str(create_team_iteration_param)) + self.assertEqual(TEST_PROJECT_NAME, create_team_iteration_param['team_context'].project, str(create_team_iteration_param)) + self.assertEqual(self._ITERATION_IDENTIFIER, create_team_iteration_param['iteration'].id, str(create_team_iteration_param)) + + def test_show_team_default_iteration(self): + response = show_default_iteration(team=TEST_TEAM_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + self.mock_get_team_settings.assert_called_once() + show_team_default_iteration_param = self.mock_get_team_settings.call_args_list[0][1] + self.assertEqual(TEST_TEAM_NAME, show_team_default_iteration_param['team_context'].team, str(show_team_default_iteration_param)) + self.assertEqual(TEST_PROJECT_NAME, show_team_default_iteration_param['team_context'].project, str(show_team_default_iteration_param)) + + def test_show_team_backlog_iteration(self): + response = show_backlog_iteration(team=TEST_TEAM_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + self.mock_get_team_settings.assert_called_once() + show_backlog_iterationn_param = self.mock_get_team_settings.call_args_list[0][1] + self.assertEqual(TEST_TEAM_NAME, show_backlog_iterationn_param['team_context'].team, str(show_backlog_iterationn_param)) + self.assertEqual(TEST_PROJECT_NAME, show_backlog_iterationn_param['team_context'].project, str(show_backlog_iterationn_param)) + + def test_list_team_iteration_work_items(self): + response = list_iteration_work_items(id=self._ITERATION_IDENTIFIER,team=TEST_TEAM_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + self.mock_list_team_iteration_work_items.assert_called_once() + self.mock_get_relation_types.assert_called_once() + list_team_iteration_work_items_param = self.mock_list_team_iteration_work_items.call_args_list[0][1] + self.assertEqual(TEST_TEAM_NAME, list_team_iteration_work_items_param['team_context'].team, str(list_team_iteration_work_items_param)) + self.assertEqual(TEST_PROJECT_NAME, list_team_iteration_work_items_param['team_context'].project, str(list_team_iteration_work_items_param)) + + def test_set_team_backlog_iteration(self): + response = set_backlog_iteration(id=self._ITERATION_IDENTIFIER,team=TEST_TEAM_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + self.mock_update_team_settings.assert_called_once() + set_backlog_iterationn_param = self.mock_update_team_settings.call_args_list[0][1] + self.assertEqual(TEST_TEAM_NAME, set_backlog_iterationn_param['team_context'].team, str(set_backlog_iterationn_param)) + self.assertEqual(TEST_PROJECT_NAME, set_backlog_iterationn_param['team_context'].project, str(set_backlog_iterationn_param)) + self.assertEqual(self._ITERATION_IDENTIFIER, set_backlog_iterationn_param['team_settings_patch'].backlog_iteration, str(set_backlog_iterationn_param)) + + def test_set_team_default_iteration(self): + response = set_default_iteration(id=self._ITERATION_IDENTIFIER,team=TEST_TEAM_NAME,project=TEST_PROJECT_NAME,organization=TEST_DEVOPS_ORGANIZATION) + self.mock_update_team_settings.assert_called_once() + set_team_default_iteration_param = self.mock_update_team_settings.call_args_list[0][1] + self.assertEqual(TEST_TEAM_NAME, set_team_default_iteration_param['team_context'].team, str(set_team_default_iteration_param)) + self.assertEqual(TEST_PROJECT_NAME, set_team_default_iteration_param['team_context'].project, str(set_team_default_iteration_param)) + self.assertEqual(self._ITERATION_IDENTIFIER, set_team_default_iteration_param['team_settings_patch'].default_iteration, str(set_team_default_iteration_param)) diff --git a/azure-devops/azext_devops/test/utils/helper.py b/azure-devops/azext_devops/test/utils/helper.py index bf821f8a..c18350ce 100644 --- a/azure-devops/azext_devops/test/utils/helper.py +++ b/azure-devops/azext_devops/test/utils/helper.py @@ -18,6 +18,7 @@ def get_client_mock_helper(_self_dummy, client_type): from azext_devops.devops_sdk.v5_0.operations.operations_client import OperationsClient from azext_devops.devops_sdk.v5_0.task_agent.task_agent_client import TaskAgentClient from azext_devops.devops_sdk.v5_0.work_item_tracking.work_item_tracking_client import WorkItemTrackingClient + from azext_devops.devops_sdk.v5_0.work.work_client import WorkClient from azext_devops.devops_sdk.v5_0.settings.settings_client import SettingsClient from azext_devops.devops_sdk.v5_0.identity.identity_client import IdentityClient from azext_devops.devops_sdk.v5_0.member_entitlement_management.member_entitlement_management_client import ( @@ -43,6 +44,8 @@ def get_client_mock_helper(_self_dummy, client_type): base_url=TEST_DEVOPS_ORG_URL), vsts+'v5_0.work_item_tracking.work_item_tracking_client.WorkItemTrackingClient': WorkItemTrackingClient( base_url=TEST_DEVOPS_ORG_URL), + vsts+'v5_0.work.work_client.WorkClient': WorkClient( + base_url=TEST_DEVOPS_ORG_URL), vsts+'v5_0.settings.settings_client.SettingsClient': SettingsClient( base_url=TEST_DEVOPS_ORG_URL), vsts+'v5_0.identity.identity_client.IdentityClient': IdentityClient( diff --git a/doc/troubleshooting_common_errors.md b/doc/troubleshooting_common_errors.md new file mode 100644 index 00000000..9ca73929 --- /dev/null +++ b/doc/troubleshooting_common_errors.md @@ -0,0 +1,10 @@ +# Troubleshooting common errors in Azure DevOps CLI + +## List of common errors faced while using Boards - Iterations and Area commands + +| Command group | Error | Scenario | Fix/Workaround | +|----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Team level iterations| `No backlog iteration has been selected for your team. Before you can select iterations for your team to participate in, you must first specify a backlog iteration.`| Empty Backlog iteration ID : If you are getting empty GUID error while adding an existing project iteration to an team, then probably you don't have any Backlog iteration configured for your project.| Before setting up iterations for your team, you need to set some backlog iteration for your team. You can use `az boards iteration team set-backlog-iteration --team --id ` | +| Team level iterations| `VS1530019: Cannot find iteration with id 'b1e33737-e943-4e93-b7ff-e3f1cbaddb7c'. The iteration might have been deleted, or it might not be selected for your team.` | Cannot find iteration: When you are trying to add iterations to team, but accidentally trying to add backlog iteration. | You can only add child iterations of backlog iteration to your team. | +| Team level Areas | `DefaultValue` | Default area path value not set: If you are trying to add your first area to a team without passing the flag --set-as-default. | There is no default area set for your team. While you add your first area path to your team , it needs to be pass with `--set-as-default` flag, so that some default area is set. If you wish, you can change this default area path later with `az boards area team update` command. Refer help for `az boards area team add -h`| +| Team level Areas | `TF400499: You have not set your team field.` | Adding/removing/updating area to team | Be cautious while working with team areas, --path parameter for team area must be `\ProjectName\RootAreaName\ChildArea1` and not `\ProjectName\Area\RootAreaName\ChildArea1`. Team area commands don't expect 'Area' keyword to be passed in the --path parameter. | \ No newline at end of file