diff --git a/azure-devops/azext_devops/dev/common/const.py b/azure-devops/azext_devops/dev/common/const.py index 430698a0..64e953f8 100644 --- a/azure-devops/azext_devops/dev/common/const.py +++ b/azure-devops/azext_devops/dev/common/const.py @@ -24,3 +24,29 @@ DEVOPS_TEAM_PROJECT_DEFAULT = 'project' PAT_ENV_VARIABLE_NAME = CLI_ENV_VARIABLE_PREFIX + 'PAT' AUTH_TOKEN_ENV_VARIABLE_NAME = CLI_ENV_VARIABLE_PREFIX + 'AUTH_TOKEN' + +# policy constants + +APPROVER_COUNT_POLICY = 'ApproverCountPolicy' +APPROVER_COUNT_POLICY_ID = 'fa4e907d-c16b-4a4c-9dfa-4906e5d171dd' + +BUILD_POLICY = 'BuildPolicy' +BUILD_POLICY_ID = '0609b952-1397-4640-95ec-e00a01b2c241' + +COMMENT_REQUIREMENTS_POLICY = 'CommentRequirementsPolicy' +COMMENT_REQUIREMENTS_POLICY_ID = 'c6a1889d-b943-4856-b76f-9e46bb6b0df2' + +MERGE_STRATEGY_POLICY = 'MergeStrategyPolicy' +MERGE_STRATEGY_POLICY_ID = 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab' + +FILE_SIZE_POLICY = 'FileSizePolicy' +FILE_SIZE_POLICY_ID = '2e26e725-8201-4edd-8bf5-978563c34a80' + +WORKITEM_LINKING_POLICY = 'WorkItemLinkingPolicy' +WORKITEM_LINKING_POLICY_ID = '40e92b44-2fe1-4dd6-b3d8-74a9c21d0c6e' + +REQUIRED_REVIEWER_POLICY = 'RequiredReviewersPolicy' +REQUIRED_REVIEWER_POLICY_ID = 'fd2167ab-b0be-447a-8ec8-39368250530e' + +REPO_POLICY_TYPE = [APPROVER_COUNT_POLICY, BUILD_POLICY, COMMENT_REQUIREMENTS_POLICY, MERGE_STRATEGY_POLICY, + FILE_SIZE_POLICY, WORKITEM_LINKING_POLICY, REQUIRED_REVIEWER_POLICY] diff --git a/azure-devops/azext_devops/dev/repos/_format.py b/azure-devops/azext_devops/dev/repos/_format.py index 7c363d17..67d5b045 100644 --- a/azure-devops/azext_devops/dev/repos/_format.py +++ b/azure-devops/azext_devops/dev/repos/_format.py @@ -12,6 +12,37 @@ _WORK_ITEM_TITLE_TRUNCATION_LENGTH = 70 +def transform_repo_policies_table_output(result): + table_output = [] + for item in result: + table_output.append(_transform_repo_policy_request_row(item)) + return table_output + + +def transform_repo_policy_table_output(result): + table_output = [_transform_repo_policy_request_row(result)] + return table_output + + +def _transform_repo_policy_request_row(row): + table_row = OrderedDict() + table_row['ID'] = row['id'] + table_row['Name'] = _get_policy_display_name(row) + table_row['Is Blocking'] = row['isBlocking'] + table_row['Is Enabled'] = row['isEnabled'] + # this will break if policy is applied across repo but that is not possible via UI at least now + table_row['Repository Id'] = row['settings']['scope'][0]['repositoryId'] + table_row['Branch'] = row['settings']['scope'][0]['refName'] + return table_row + + +def _get_policy_display_name(row): + if 'displayName' in row['settings']: + return row['settings']['displayName'] + + return row['type']['displayName'] + + def transform_pull_requests_table_output(result): table_output = [] for item in result: diff --git a/azure-devops/azext_devops/dev/repos/_help.py b/azure-devops/azext_devops/dev/repos/_help.py index 47ed0048..ec014d94 100644 --- a/azure-devops/azext_devops/dev/repos/_help.py +++ b/azure-devops/azext_devops/dev/repos/_help.py @@ -13,6 +13,12 @@ def load_repos_help(): long-summary: """ + helps['repos policies'] = """ + type: group + short-summary: Manage Azure Repos branch policies. + long-summary: + """ + helps['repos pr'] = """ type: group short-summary: Manage pull requests. diff --git a/azure-devops/azext_devops/dev/repos/arguments.py b/azure-devops/azext_devops/dev/repos/arguments.py index 5adfebc5..0a4bb4d2 100644 --- a/azure-devops/azext_devops/dev/repos/arguments.py +++ b/azure-devops/azext_devops/dev/repos/arguments.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- from knack.arguments import enum_choice_list +from azext_devops.dev.common.const import REPO_POLICY_TYPE # CUSTOM CHOICE LISTS _ON_OFF_SWITCH_VALUES = ['on', 'off'] @@ -18,6 +19,9 @@ def load_code_arguments(self, _): context.argument('reviewers', nargs='*') context.argument('detect', **enum_choice_list(_ON_OFF_SWITCH_VALUES)) + with self.argument_context('repos policies create') as context: + context.argument('policy_type', **enum_choice_list(REPO_POLICY_TYPE)) + with self.argument_context('repos pr') as context: context.argument('description', type=str, options_list=('--description', '-d'), nargs='*') context.argument('repository', options_list=('--repository', '-r')) diff --git a/azure-devops/azext_devops/dev/repos/commands.py b/azure-devops/azext_devops/dev/repos/commands.py index e344a3f9..37418ea2 100644 --- a/azure-devops/azext_devops/dev/repos/commands.py +++ b/azure-devops/azext_devops/dev/repos/commands.py @@ -13,7 +13,9 @@ transform_policies_table_output, transform_policy_table_output, transform_work_items_table_output, - transform_repo_import_table_output) + transform_repo_import_table_output, + transform_repo_policy_table_output, + transform_repo_policies_table_output) reposPullRequestOps = CliCommandType( @@ -28,6 +30,10 @@ operations_tmpl='azext_devops.dev.repos.import_request#{}' ) +policyOps = CliCommandType( + operations_tmpl='azext_devops.dev.repos.policy#{}' +) + def load_code_commands(self, _): with self.command_group('repos', command_type=reposRepositoryOps) as g: @@ -37,6 +43,14 @@ def load_code_commands(self, _): g.command('list', 'list_repos', table_transformer=transform_repos_table_output) g.command('show', 'show_repo', table_transformer=transform_repo_table_output) + with self.command_group('repos policies', command_type=policyOps) as g: + # repository/ branch policies + g.command('create', 'create_policy', table_transformer=transform_repo_policy_table_output) + g.command('list', 'list_policy', table_transformer=transform_repo_policies_table_output) + g.command('show', 'get_policy', table_transformer=transform_repo_policy_table_output) + g.command('update', 'update_policy', table_transformer=transform_repo_policy_table_output) + g.command('delete', 'delete_policy', confirmation='Are you sure you want to delete this policy?') + with self.command_group('repos pr', command_type=reposPullRequestOps) as g: # basic pr commands g.command('create', 'create_pull_request', table_transformer=transform_pull_request_table_output) diff --git a/azure-devops/azext_devops/dev/repos/policy.py b/azure-devops/azext_devops/dev/repos/policy.py new file mode 100644 index 00000000..59987c4b --- /dev/null +++ b/azure-devops/azext_devops/dev/repos/policy.py @@ -0,0 +1,393 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=line-too-long +# This file a lot of these are intentional because that makes grouping variables easy + +from knack.util import CLIError +from knack.log import get_logger +from vsts.exceptions import VstsServiceError +from vsts.policy.v4_0.models.policy_configuration import PolicyConfiguration + +from azext_devops.dev.common.services import (get_policy_client, resolve_instance_and_project) +from azext_devops.dev.common.const import (APPROVER_COUNT_POLICY, + APPROVER_COUNT_POLICY_ID, + BUILD_POLICY, + BUILD_POLICY_ID, + COMMENT_REQUIREMENTS_POLICY, + COMMENT_REQUIREMENTS_POLICY_ID, + MERGE_STRATEGY_POLICY, + MERGE_STRATEGY_POLICY_ID, + FILE_SIZE_POLICY, + FILE_SIZE_POLICY_ID, + WORKITEM_LINKING_POLICY, + WORKITEM_LINKING_POLICY_ID, + REQUIRED_REVIEWER_POLICY, + REQUIRED_REVIEWER_POLICY_ID) +from azext_devops.dev.common.identities import resolve_identity_as_id + +logger = get_logger(__name__) + + +def list_policy(organization=None, project=None, detect=None): + """ + :param organization: Azure Devops organization URL. Example: https://dev.azure.com/MyOrganizationName/ + :type organization: str + :param project: Name or ID of the project. + :type project: str + :param detect: Automatically detect organization. Default is "on". + :type detect: str + :rtype: [PolicyConfiguration] + """ + try: + organization, project = resolve_instance_and_project( + detect=detect, organization=organization, project=project) + policy_client = get_policy_client(organization) + return policy_client.get_policy_configurations(project=project) + except VstsServiceError as ex: + raise CLIError(ex) + + +def get_policy(id, organization=None, project=None, detect=None): # pylint: disable=redefined-builtin + """ + :param id: ID of the policy. + :type id: int + :param organization: Azure Devops organization URL. Example: https://dev.azure.com/MyOrganizationName/ + :type organization: str + :param project: Name or ID of the project. + :type project: str + :param detect: Automatically detect organization. Default is "on". + :type detect: str + :rtype: :class:` ` + """ + try: + organization, project = resolve_instance_and_project( + detect=detect, organization=organization, project=project) + policy_client = get_policy_client(organization) + return policy_client.get_policy_configuration(project=project, configuration_id=id) + except VstsServiceError as ex: + raise CLIError(ex) + + +def delete_policy(id, organization=None, project=None, detect=None): # pylint: disable=redefined-builtin + """ + :param id: ID of the policy. + :type id: int + :param organization: Azure Devops organization URL. Example: https://dev.azure.com/MyOrganizationName/ + :type organization: str + :param project: Name or ID of the project. + :type project: str + :param detect: Automatically detect organization. Default is "on". + :type detect: str + """ + try: + organization, project = resolve_instance_and_project( + detect=detect, organization=organization, project=project) + policy_client = get_policy_client(organization) + return policy_client.delete_policy_configuration(project=project, configuration_id=id) + except VstsServiceError as ex: + raise CLIError(ex) + + +# pylint: disable=too-many-locals +def create_policy(policy_configuration=None, + repository_id=None, branch=None, + isBlocking=False, isEnabled=False, + policy_type=None, + minimumApproverCount=None, creatorVoteCounts=None, allowDownvotes=None, resetOnSourcePush=None, + useSquashMerge=None, + buildDefinitionId=None, queueOnSourceUpdateOnly=None, manualQueueOnly=None, displayName=None, validDuration=None, + maximumGitBlobSizeInBytes=None, useUncompressedSize=None, + optionalReviewerIds=None, requiredReviewerIds=None, message=None, + organization=None, project=None, detect=None): + """ + :param policy_configuration: File path of file containing policy configuration to create in a serialized form. + please use / backslash when typing in directory path. + Only --project and --organization param are needed when passing this. + :type policy_configuration: string + + :param repository_id: Id (UUID) of the repository on which to apply the policy + :type repository_id: string + :param branch: Branch on which this policy should be applied + :type branch: string + :param isBlocking: Whether the policy should be blocking or not. Default value is false. Accepted values are true and false. + :type isBlocking: bool + :param isEnabled: Whether the policy is enabled or not. By default the value is true. Accepted values and true and false. + :type isEnabled: bool + :param policy_type: Type of policy you want to create + :type policy_type: string + + :param optionalReviewerIds: Optional Reviewers (List of email ids seperated with ';'). Required if policy type is RequiredReviewersPolicy. + :type optionalReviewerIds: string + :param requiredReviewerIds: Required Reviewers (List of email ids seperated with ';'). Required if policy type is RequiredReviewersPolicy. + :type requiredReviewerIds: string + :param message: Message. Required if policy type is RequiredReviewersPolicy. + :type message: string + + :param minimumApproverCount: Minimum approver count. Required if policy type is ApproverCountPolicy. + :type minimumApproverCount: int + :param creatorVoteCounts: Whether the creator's vote count counts or not. Required if policy type is ApproverCountPolicy + :type creatorVoteCounts: bool + :param allowDownvotes: Whether to allow downvotes or not. Required if policy type is ApproverCountPolicy. + :type allowDownvotes: bool + :param resetOnSourcePush: Whether to reset source on push. Required if policy type is ApproverCountPolicy. + :type resetOnSourcePush: bool + + :param buildDefinitionId: Build Definition Id. Required if policy type is Buildpolicy. + :type buildDefinitionId: int + :param queueOnSourceUpdateOnly: Queue Only on source update. Required if policy type is Buildpolicy. + :type queueOnSourceUpdateOnly: bool + :param manualQueueOnly : Whether to allow only manual queue of builds. Required if policy type is Buildpolicy. + :type manualQueueOnly : bool + :param displayName : Display Name. Required if policy type is Buildpolicy. + :type displayName : string + :param validDuration : Policy validity duration (in hours). Required if policy type is Buildpolicy. + :type validDuration : double + + :param useSquashMerge: Whether to squash merge always. Required if policy type is MergeStrategyPolicy + :type useSquashMerge: bool + + :param maximumGitBlobSizeInBytes: Maximum Git Blob Size In Bytes. Required if policy type is FileSizePolicy + :type maximumGitBlobSizeInBytes: long + :param useUncompressedSize: Whether to use uncompressed size. Required if policy type is FileSizePolicy + :type useUncompressedSize: bool + + :param organization: Azure Devops organization URL. Example: https://dev.azure.com/MyOrganizationName/ + :type organization: str + :param project: Name or ID of the project. + :type project: str + :param detect: Automatically detect organization. Default is "on". + :type detect: str + :rtype: :class:` ` + """ + try: + organization, project = resolve_instance_and_project( + detect=detect, organization=organization, project=project) + policy_client = get_policy_client(organization) + + policyConfigurationToCreate = generateConfigurationObject( + policy_configuration, + repository_id, branch, + policy_type, + isBlocking, isEnabled, + minimumApproverCount, creatorVoteCounts, allowDownvotes, resetOnSourcePush, + useSquashMerge, + buildDefinitionId, queueOnSourceUpdateOnly, manualQueueOnly, displayName, validDuration, + maximumGitBlobSizeInBytes, useUncompressedSize, + optionalReviewerIds, requiredReviewerIds, message, + organization) + + return policy_client.create_policy_configuration(configuration=policyConfigurationToCreate, project=project) + except VstsServiceError as ex: + raise CLIError(ex) + + +# pylint: disable=too-many-locals +def update_policy(policy_id, + policy_configuration=None, + repository_id=None, branch=None, + isBlocking=False, isEnabled=False, + policy_type=None, + minimumApproverCount=None, creatorVoteCounts=None, allowDownvotes=None, resetOnSourcePush=None, + useSquashMerge=None, + buildDefinitionId=None, queueOnSourceUpdateOnly=None, manualQueueOnly=None, displayName=None, validDuration=None, + maximumGitBlobSizeInBytes=None, useUncompressedSize=None, + optionalReviewerIds=None, requiredReviewerIds=None, message=None, + organization=None, project=None, detect=None): + """ + :param repository_id: Id (UUID) of the repository on which to apply the policy to. + :type repository_id: string + :param branch: Branch on which this policy should be applied + :type branch: string + :param policy_id: ID of the policy which needs to be updated + :type policy_id: int + :param isBlocking: Whether the policy should be blocking or not. Default value is false. Accepted values are true and false. + :type isBlocking: bool + :param isEnabled: Whether the policy is enabled or not. By default the value is true. Accepted values and true and false. + :type isEnabled: bool + :param policy_type: Type of policy you want to create + :type policy_type: string + + :param optionalReviewerIds: Optional Reviewers (List of email ids seperated with ';'). Required if policy type is RequiredReviewersPolicy. + :type optionalReviewerIds: string + :param requiredReviewerIds: Required Reviewers (List of email ids seperated with ';'). Required if policy type is RequiredReviewersPolicy. + :type requiredReviewerIds: string + :param message: Message. Required if policy type is RequiredReviewersPolicy. + :type message: string + + :param minimumApproverCount: Minimum approver count. Required if policy type is ApproverCountPolicy. + :type minimumApproverCount: int + :param creatorVoteCounts: Whether the creator's vote count counts or not. Required if policy type is ApproverCountPolicy + :type creatorVoteCounts: bool + :param allowDownvotes: Whether to allow downvotes or not. Required if policy type is ApproverCountPolicy. + :type allowDownvotes: bool + :param resetOnSourcePush: Whether to reset source on push. Required if policy type is ApproverCountPolicy. + :type resetOnSourcePush: bool + + :param buildDefinitionId: Build Definition Id. Required if policy type is Buildpolicy. + :type buildDefinitionId: int + :param queueOnSourceUpdateOnly: Queue Only on source update. Required if policy type is Buildpolicy. + :type queueOnSourceUpdateOnly: bool + :param manualQueueOnly : Whether to allow only manual queue of builds. Required if policy type is Buildpolicy. + :type manualQueueOnly : bool + :param displayName : Display Name. Required if policy type is Buildpolicy. + :type displayName : string + :param validDuration : Policy validity duration (in hours). Required if policy type is Buildpolicy. + :type validDuration : double + + :param useSquashMerge: Whether to squash merge always. Required if policy type is MergeStrategyPolicy + :type useSquashMerge: bool + + :param maximumGitBlobSizeInBytes: Maximum Git Blob Size In Bytes. Required if policy type is FileSizePolicy + :type maximumGitBlobSizeInBytes: long + :param useUncompressedSize: Whether to use uncompressed size. Required if policy type is FileSizePolicy + :type useUncompressedSize: bool + + :param organization: Azure Devops organization URL. Example: https://dev.azure.com/MyOrganizationName/ + :type organization: str + :param project: Name or ID of the project. + :type project: str + :param detect: Automatically detect organization. Default is "on". + :type detect: str + :rtype: :class:` ` + """ + try: + organization, project = resolve_instance_and_project( + detect=detect, organization=organization, project=project) + policy_client = get_policy_client(organization) + + policyConfigurationToCreate = generateConfigurationObject( + policy_configuration, + repository_id, branch, + policy_type, + isBlocking, isEnabled, + minimumApproverCount, creatorVoteCounts, allowDownvotes, resetOnSourcePush, + useSquashMerge, + buildDefinitionId, queueOnSourceUpdateOnly, manualQueueOnly, displayName, validDuration, + maximumGitBlobSizeInBytes, useUncompressedSize, + optionalReviewerIds, requiredReviewerIds, message, + organization) + + return policy_client.update_policy_configuration( + configuration=policyConfigurationToCreate, + project=project, + configuration_id=policy_id) + except VstsServiceError as ex: + raise CLIError(ex) + + +# pylint: disable=too-many-locals +def generateConfigurationObject(policy_configuration=None, + repository_id=None, branch=None, + policy_type=None, + isBlocking=False, isEnabled=False, + minimumApproverCount=None, creatorVoteCounts=None, allowDownvotes=None, resetOnSourcePush=None, + useSquashMerge=None, + buildDefinitionId=None, queueOnSourceUpdateOnly=None, manualQueueOnly=None, displayName=None, validDuration=None, + maximumGitBlobSizeInBytes=None, useUncompressedSize=None, + optionalReviewerIds=None, requiredReviewerIds=None, message=None, + organization=None): + if policy_configuration is None and policy_type is None: + raise CLIError('Either --policy-configuration or --policy-type must be passed') + + if policy_configuration is not None: + with open(policy_configuration) as f: + import json + return json.load(f) + + # these 2 will be filled by respective types + paramNameArray = [] + paramArray = [] + policytypeId = '' + + if policy_type == APPROVER_COUNT_POLICY: + policytypeId = APPROVER_COUNT_POLICY_ID + paramArray = [minimumApproverCount, creatorVoteCounts, allowDownvotes, resetOnSourcePush] + paramNameArray = ['minimumApproverCount', 'creatorVoteCounts', 'allowDownvotes', 'resetOnSourcePush'] + + elif policy_type == BUILD_POLICY: + policytypeId = BUILD_POLICY_ID + paramArray = [buildDefinitionId, queueOnSourceUpdateOnly, manualQueueOnly, displayName, validDuration] + paramNameArray = ['buildDefinitionId', 'queueOnSourceUpdateOnly', 'manualQueueOnly', 'displayName', 'validDuration'] + + elif policy_type == COMMENT_REQUIREMENTS_POLICY: + policytypeId = COMMENT_REQUIREMENTS_POLICY_ID + # this particular policy does not need any other parameter + + elif policy_type == MERGE_STRATEGY_POLICY: + policytypeId = MERGE_STRATEGY_POLICY_ID + paramArray = [useSquashMerge] + paramNameArray = ['useSquashMerge'] + + elif policy_type == FILE_SIZE_POLICY: + policytypeId = FILE_SIZE_POLICY_ID + paramArray = [maximumGitBlobSizeInBytes, useUncompressedSize] + paramNameArray = ['maximumGitBlobSizeInBytes', 'useUncompressedSize'] + + elif policy_type == WORKITEM_LINKING_POLICY: + policytypeId = WORKITEM_LINKING_POLICY_ID + # this particular policy does not need any other parameter + + elif policy_type == REQUIRED_REVIEWER_POLICY: + policytypeId = REQUIRED_REVIEWER_POLICY_ID + optionalReviewerIds = resolveIdentityMailsToIds(optionalReviewerIds, organization) + requiredReviewerIds = resolveIdentityMailsToIds(requiredReviewerIds, organization) + # special handling for this policy + if optionalReviewerIds and (not requiredReviewerIds): + requiredReviewerIds = [] + if requiredReviewerIds and (not optionalReviewerIds): + optionalReviewerIds = [] + paramArray = [optionalReviewerIds, requiredReviewerIds, message] + paramNameArray = ['optionalReviewerIds', 'requiredReviewerIds', 'message'] + + # check if we have value in all the required params or not + raiseErrorIfRequiredParamMissing(paramArray, paramNameArray, policy_type) + + policyConfiguration = PolicyConfiguration(is_blocking=isBlocking, is_enabled=isEnabled) + scope = [ + { + 'repositoryId': repository_id, + 'refName': branch, + 'matchKind': 'exact' + } + ] + + policyConfiguration.settings = { + 'scope': scope + } + + policyConfiguration.type = { + 'id': policytypeId + } + + index = 0 + for param in paramNameArray: + policyConfiguration.settings[param] = paramArray[index] + index = index + 1 + + return policyConfiguration + + +def resolveIdentityMailsToIds(mailList, organization): + logger.debug('mail list %s ', mailList) + if not mailList or (not mailList.strip()): + return None + + idList = [] + for mail in mailList.split(';'): + mailStripped = mail.strip() + logger.debug('trying to resolve %s', mailStripped) + identityId = resolve_identity_as_id(mailStripped, organization) + logger.debug('got id as %s', identityId) + idList.append(identityId) + + return idList + + +def raiseErrorIfRequiredParamMissing(paramArray, paramNameArray, policyName): + if not paramNameArray: + return + if any(v is None for v in paramArray): + raise CLIError('{} are required for {}'.format('--' + ' --'.join(paramNameArray), policyName)) diff --git a/azure-devops/azext_devops/test/repos/test_policy.py b/azure-devops/azext_devops/test/repos/test_policy.py new file mode 100644 index 00000000..bc924e5c --- /dev/null +++ b/azure-devops/azext_devops/test/repos/test_policy.py @@ -0,0 +1,166 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import unittest +from knack.util import CLIError +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.repos.policy import * +from azext_devops.dev.common.services import clear_connection_cache +from vsts.policy.v4_0.policy_client import PolicyClient + + +class TestUuidMethods(unittest.TestCase): + + _TEST_DEVOPS_ORGANIZATION = 'https://someorg.visualstudio.com' + _TEST_DEVOPS_PROJECT = 'sample project' + _TEST_PAT_TOKEN = 'lwghjbj67fghokrgxsytghg75nk2ssguljk7a78qpcg2ttygviyt' + _TEST_REPOSITORY_ID = 'b4da517c-0398-42dc-b2a8-0d3f240757f9' + + def setUp(self): + self.get_client = patch('vsts.vss_connection.VssConnection.get_client') + self.get_policies_patcher = patch('vsts.policy.v4_0.policy_client.PolicyClient.get_policy_configurations') + self.get_policy_patcher = patch('vsts.policy.v4_0.policy_client.PolicyClient.get_policy_configuration') + self.delete_policy_patcher = patch('vsts.policy.v4_0.policy_client.PolicyClient.delete_policy_configuration') + self.create_policy_patcher = patch('vsts.policy.v4_0.policy_client.PolicyClient.create_policy_configuration') + self.update_policy_patcher = patch('vsts.policy.v4_0.policy_client.PolicyClient.update_policy_configuration') + + self.mock_get_client = self.get_client.start() + self.mock_get_policies = self.get_policies_patcher.start() + self.mock_get_policy = self.get_policy_patcher.start() + self.mock_delete_policy = self.delete_policy_patcher.start() + self.mock_create_policy = self.create_policy_patcher.start() + self.mock_update_policy = self.update_policy_patcher.start() + + self.mock_get_client.return_value = PolicyClient(base_url=self._TEST_DEVOPS_ORGANIZATION) + + clear_connection_cache() + + def tearDown(self): + self.get_client.stop() + self.get_policies_patcher.stop() + self.get_policy_patcher.stop() + self.delete_policy_patcher.stop() + self.create_policy_patcher.stop() + self.update_policy_patcher.stop() + + def test_list_policy(self): + list_policy(organization = self._TEST_DEVOPS_ORGANIZATION, + project = self._TEST_DEVOPS_PROJECT, + detect='off') + + #assert + self.mock_get_policies.assert_called_once_with(project=self._TEST_DEVOPS_PROJECT) + + def test_get_policy(self): + get_policy(id = 121, + organization = self._TEST_DEVOPS_ORGANIZATION, + project = self._TEST_DEVOPS_PROJECT, + detect='off') + + #assert + self.mock_get_policy.assert_called_once_with(project=self._TEST_DEVOPS_PROJECT, configuration_id=121) + + def test_delete_policy(self): + delete_policy(id = 121, + organization = self._TEST_DEVOPS_ORGANIZATION, + project = self._TEST_DEVOPS_PROJECT, + detect='off') + + #assert + self.mock_delete_policy.assert_called_once_with(project=self._TEST_DEVOPS_PROJECT, configuration_id=121) + + def test_create_policy_argument_missing_message(self): + try: + create_policy(repository_id=self._TEST_REPOSITORY_ID, branch='master', + policy_type='ApproverCountPolicy', + minimumApproverCount=2, + organization = self._TEST_DEVOPS_ORGANIZATION, + project = self._TEST_DEVOPS_PROJECT, + detect='off') + self.fail('create should have thrown CLIError') + except CLIError as ex: + #assert + self.assertEqual(str(ex), + '--minimumApproverCount --creatorVoteCounts --allowDownvotes --resetOnSourcePush are required for ApproverCountPolicy') + + def test_create_policy_policy_type_and_configuration_missing(self): + try: + create_policy(organization = self._TEST_DEVOPS_ORGANIZATION, + project = self._TEST_DEVOPS_PROJECT, + detect='off') + self.fail('create should have thrown CLIError') + except CLIError as ex: + #assert + self.assertEqual(str(ex), + 'Either --policy-configuration or --policy-type must be passed') + + def test_create_policy_scope(self): + create_policy(repository_id=self._TEST_REPOSITORY_ID, branch='master', + policy_type='ApproverCountPolicy', + minimumApproverCount=2, creatorVoteCounts= True, allowDownvotes= False, resetOnSourcePush= True, + organization = self._TEST_DEVOPS_ORGANIZATION, + project = self._TEST_DEVOPS_PROJECT, + detect='off') + + self.mock_create_policy.assert_called_once() + create_policy_object = self.mock_create_policy.call_args_list[0][1] + self.assertEqual(self._TEST_DEVOPS_PROJECT, create_policy_object['project'], str(create_policy_object)) + scope = create_policy_object['configuration'].settings['scope'][0] # 0 because we set only only scope from CLI + self.assertEqual(scope['repositoryId'], self._TEST_REPOSITORY_ID) + self.assertEqual(scope['refName'], 'master') + self.assertEqual(scope['matchKind'], 'exact') + + def test_create_policy_setting_creation(self): + create_policy(repository_id=self._TEST_REPOSITORY_ID, branch='master', + policy_type='ApproverCountPolicy', + minimumApproverCount=2, creatorVoteCounts= True, allowDownvotes= False, resetOnSourcePush= True, + organization = self._TEST_DEVOPS_ORGANIZATION, + project = self._TEST_DEVOPS_PROJECT, + detect='off') + + self.mock_create_policy.assert_called_once() + create_policy_object = self.mock_create_policy.call_args_list[0][1] + self.assertEqual(self._TEST_DEVOPS_PROJECT, create_policy_object['project'], str(create_policy_object)) + settings = create_policy_object['configuration'].settings # 0 because we set only only scope from CLI + self.assertEqual(settings['minimumApproverCount'], 2) + self.assertEqual(settings['creatorVoteCounts'], True) + self.assertEqual(settings['allowDownvotes'], False) + self.assertEqual(settings['resetOnSourcePush'], True) + + def test_create_policy_id_assignment(self): + create_policy(repository_id=self._TEST_REPOSITORY_ID, branch='master', + policy_type='ApproverCountPolicy', + minimumApproverCount=2, creatorVoteCounts= True, allowDownvotes= False, resetOnSourcePush= True, + organization = self._TEST_DEVOPS_ORGANIZATION, + project = self._TEST_DEVOPS_PROJECT, + detect='off') + + self.mock_create_policy.assert_called_once() + create_policy_object = self.mock_create_policy.call_args_list[0][1] + self.assertEqual(self._TEST_DEVOPS_PROJECT, create_policy_object['project'], str(create_policy_object)) + policy_type_id = create_policy_object['configuration'].type['id'] # 0 because we set only only scope from CLI + self.assertEqual(policy_type_id, 'fa4e907d-c16b-4a4c-9dfa-4906e5d171dd') + + def test_update_policy(self): + update_policy(repository_id=self._TEST_REPOSITORY_ID, branch='master', + policy_id = 121, + policy_type='ApproverCountPolicy', + minimumApproverCount=2, creatorVoteCounts= True, allowDownvotes= False, resetOnSourcePush= True, + organization = self._TEST_DEVOPS_ORGANIZATION, + project = self._TEST_DEVOPS_PROJECT, + detect='off') + + self.mock_update_policy.assert_called_once() + update_policy_object = self.mock_update_policy.call_args_list[0][1] + self.assertEqual(update_policy_object['configuration_id'], 121) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file