From a820aada42e21a880d88debc81604a7cd6a56695 Mon Sep 17 00:00:00 2001 From: Jovan Zivanov Date: Thu, 26 Dec 2019 14:23:53 +0100 Subject: [PATCH] add codecommit create, get and delete repository --- IMPLEMENTATION_COVERAGE.md | 6 +- moto/__init__.py | 1 + moto/codecommit/__init__.py | 4 + moto/codecommit/exceptions.py | 32 +++++ moto/codecommit/models.py | 63 +++++++++ moto/codecommit/responses.py | 46 ++++++ moto/codecommit/urls.py | 6 + tests/test_codecommit/test_codecommit.py | 170 +++++++++++++++++++++++ 8 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 moto/codecommit/__init__.py create mode 100644 moto/codecommit/exceptions.py create mode 100644 moto/codecommit/models.py create mode 100644 moto/codecommit/responses.py create mode 100644 moto/codecommit/urls.py create mode 100644 tests/test_codecommit/test_codecommit.py diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 5e6ef1c9..245cf470 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -1240,14 +1240,14 @@ - [ ] create_commit - [ ] create_pull_request - [ ] create_pull_request_approval_rule -- [ ] create_repository +- [X] create_repository - [ ] create_unreferenced_merge_commit - [ ] delete_approval_rule_template - [ ] delete_branch - [ ] delete_comment_content - [ ] delete_file - [ ] delete_pull_request_approval_rule -- [ ] delete_repository +- [X] delete_repository - [ ] describe_merge_conflicts - [ ] describe_pull_request_events - [ ] disassociate_approval_rule_template_from_repository @@ -1268,7 +1268,7 @@ - [ ] get_pull_request - [ ] get_pull_request_approval_states - [ ] get_pull_request_override_state -- [ ] get_repository +- [X] get_repository - [ ] get_repository_triggers - [ ] list_approval_rule_templates - [ ] list_associated_approval_rule_templates_for_repository diff --git a/moto/__init__.py b/moto/__init__.py index a9f1bb8b..db79c59f 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -9,6 +9,7 @@ from .batch import mock_batch # noqa from .cloudformation import mock_cloudformation # noqa from .cloudformation import mock_cloudformation_deprecated # noqa from .cloudwatch import mock_cloudwatch, mock_cloudwatch_deprecated # noqa +from .codecommit import mock_codecommit from .codepipeline import mock_codepipeline # noqa from .cognitoidentity import mock_cognitoidentity # noqa from .cognitoidentity import mock_cognitoidentity_deprecated # noqa diff --git a/moto/codecommit/__init__.py b/moto/codecommit/__init__.py new file mode 100644 index 00000000..c1da043e --- /dev/null +++ b/moto/codecommit/__init__.py @@ -0,0 +1,4 @@ +from .models import codecommit_backends +from ..core.models import base_decorator + +mock_codecommit = base_decorator(codecommit_backends) \ No newline at end of file diff --git a/moto/codecommit/exceptions.py b/moto/codecommit/exceptions.py new file mode 100644 index 00000000..2cb3f7fa --- /dev/null +++ b/moto/codecommit/exceptions.py @@ -0,0 +1,32 @@ +from moto.core.exceptions import JsonRESTError + + +class RepositoryNameExistsException(JsonRESTError): + code = 400 + + def __init__(self, repository_name): + super(RepositoryNameExistsException, self).__init__( + "RepositoryNameExistsException", "Repository named {0} already exists".format(repository_name) + ) + + +class RepositoryDoesNotExistException(JsonRESTError): + code = 400 + + def __init__(self, repository_name): + super(RepositoryDoesNotExistException, self).__init__( + "RepositoryDoesNotExistException", "{0} does not exist".format(repository_name) + ) + + +class InvalidRepositoryNameException(JsonRESTError): + code = 400 + + def __init__(self): + super(InvalidRepositoryNameException, self).__init__( + "InvalidRepositoryNameException", "The repository name is not valid. Repository names can be any valid " + "combination of letters, numbers, " + "periods, underscores, and dashes between 1 and 100 characters in " + "length. Names are case sensitive. " + "For more information, see Limits in the AWS CodeCommit User Guide. " + ) diff --git a/moto/codecommit/models.py b/moto/codecommit/models.py new file mode 100644 index 00000000..e691507b --- /dev/null +++ b/moto/codecommit/models.py @@ -0,0 +1,63 @@ +from boto3 import Session +from moto.core import BaseBackend, BaseModel +from moto.core.utils import iso_8601_datetime_with_milliseconds +from datetime import datetime +from moto.iam.models import ACCOUNT_ID +from .exceptions import RepositoryDoesNotExistException, RepositoryNameExistsException +import uuid + + +class CodeCommit(BaseModel): + def __init__(self, region, repository_description, repository_name): + current_date = iso_8601_datetime_with_milliseconds(datetime.utcnow()) + self.repository_metadata = dict() + self.repository_metadata["repositoryName"] = repository_name + self.repository_metadata["cloneUrlSsh"] = "ssh://git-codecommit.{0}.amazonaws.com/v1/repos/{1}".format( + region, repository_name + ) + self.repository_metadata["cloneUrlHttp"] = "https://git-codecommit.{0}.amazonaws.com/v1/repos/{1}".format( + region, repository_name + ) + self.repository_metadata["creationDate"] = current_date + self.repository_metadata["lastModifiedDate"] = current_date + self.repository_metadata["repositoryDescription"] = repository_description + self.repository_metadata["repositoryId"] = str(uuid.uuid4()) + self.repository_metadata["Arn"] = "arn:aws:codecommit:{0}:{1}:{2}".format( + region, ACCOUNT_ID, repository_name + ) + self.repository_metadata["accountId"] = ACCOUNT_ID + + +class CodeCommitBackend(BaseBackend): + def __init__(self): + self.repositories = {} + + def create_repository(self, region, repository_name, repository_description): + repository = self.repositories.get(repository_name) + if repository: + raise RepositoryNameExistsException(repository_name) + + self.repositories[repository_name] = CodeCommit(region, repository_description, repository_name) + + return self.repositories[repository_name].repository_metadata + + def get_repository(self, repository_name): + repository = self.repositories.get(repository_name) + if not repository: + raise RepositoryDoesNotExistException(repository_name) + + return repository.repository_metadata + + def delete_repository(self, repository_name): + repository = self.repositories.get(repository_name) + + if repository: + self.repositories.pop(repository_name) + return repository.repository_metadata.get("repositoryId") + + return None + + +codecommit_backends = {} +for region in Session().get_available_regions("codecommit"): + codecommit_backends[region] = CodeCommitBackend() diff --git a/moto/codecommit/responses.py b/moto/codecommit/responses.py new file mode 100644 index 00000000..1ee177f3 --- /dev/null +++ b/moto/codecommit/responses.py @@ -0,0 +1,46 @@ +import json +import re + +from moto.core.responses import BaseResponse +from .models import codecommit_backends +from .exceptions import InvalidRepositoryNameException + + +class CodeCommitResponse(BaseResponse): + @property + def codecommit_backend(self): + return codecommit_backends[self.region] + + def create_repository(self): + if not self._is_repository_name_valid(self._get_param("repositoryName")): + raise InvalidRepositoryNameException() + + repository_metadata = self.codecommit_backend.create_repository( + self.region, self._get_param("repositoryName"), self._get_param("repositoryDescription") + ) + + return json.dumps({"repositoryMetadata": repository_metadata}) + + def get_repository(self): + if not self._is_repository_name_valid(self._get_param("repositoryName")): + raise InvalidRepositoryNameException() + + repository_metadata = self.codecommit_backend.get_repository(self._get_param("repositoryName")) + + return json.dumps({"repositoryMetadata": repository_metadata}) + + def delete_repository(self): + if not self._is_repository_name_valid(self._get_param("repositoryName")): + raise InvalidRepositoryNameException() + + repository_id = self.codecommit_backend.delete_repository(self._get_param("repositoryName")) + + if repository_id: + return json.dumps({"repositoryId": repository_id}) + + return json.dumps({}) + + def _is_repository_name_valid(self, repository_name): + name_regex = re.compile(r"[\w\.-]+") + result = name_regex.fullmatch(repository_name) + return result diff --git a/moto/codecommit/urls.py b/moto/codecommit/urls.py new file mode 100644 index 00000000..1e3cdb1b --- /dev/null +++ b/moto/codecommit/urls.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +from .responses import CodeCommitResponse + +url_bases = ["https?://codecommit.(.+).amazonaws.com"] + +url_paths = {"{0}/$": CodeCommitResponse.dispatch} diff --git a/tests/test_codecommit/test_codecommit.py b/tests/test_codecommit/test_codecommit.py new file mode 100644 index 00000000..c62c0127 --- /dev/null +++ b/tests/test_codecommit/test_codecommit.py @@ -0,0 +1,170 @@ +import boto3 + +import sure # noqa +from moto import mock_codecommit +from moto.iam.models import ACCOUNT_ID +from botocore.exceptions import ClientError +from nose.tools import assert_raises + + +@mock_codecommit +def test_create_repository(): + client = boto3.client("codecommit", region_name="eu-central-1") + response = client.create_repository( + repositoryName='repository_one', + repositoryDescription='description repo one' + ) + + response.should_not.be.none + response["repositoryMetadata"].should_not.be.none + response["repositoryMetadata"]["creationDate"].should_not.be.none + response["repositoryMetadata"]["lastModifiedDate"].should_not.be.none + response["repositoryMetadata"]["repositoryId"].should_not.be.empty + response["repositoryMetadata"]["repositoryName"].should.equal("repository_one") + response["repositoryMetadata"]["repositoryDescription"].should.equal('description repo one') + response["repositoryMetadata"]["cloneUrlSsh"].should.equal("ssh://git-codecommit.{0}.amazonaws.com/v1/repos/{1}". + format("eu-central-1", 'repository_one')) + response["repositoryMetadata"]["cloneUrlHttp"].should.equal("https://git-codecommit.{0}.amazonaws.com/v1/repos/{1}". + format("eu-central-1", 'repository_one')) + response["repositoryMetadata"]["Arn"].should.equal("arn:aws:codecommit:{0}:{1}:{2}".format( + "eu-central-1", ACCOUNT_ID, 'repository_one' + )) + response["repositoryMetadata"]["accountId"].should.equal(ACCOUNT_ID) + + response = client.create_repository( + repositoryName='repository_two' + ) + + response.should_not.be.none + response.get("repositoryMetadata").should_not.be.none + response.get("repositoryMetadata").get("repositoryName").should.equal("repository_two") + response.get("repositoryMetadata").get("repositoryDescription").should.be.none + + with assert_raises(ClientError) as e: + client.create_repository( + repositoryName='repository_two' + ) + ex = e.exception + ex.operation_name.should.equal("CreateRepository") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("RepositoryNameExistsException") + ex.response["Error"]["Message"].should.equal("Repository named {0} already exists".format("repository_two")) + + +@mock_codecommit +def test_get_repository(): + client = boto3.client("codecommit", region_name="eu-central-1") + + repository_name = 'repository_one' + + client.create_repository( + repositoryName=repository_name, + repositoryDescription='description repo one' + ) + + response = client.get_repository( + repositoryName=repository_name + ) + + response.should_not.be.none + response.get("repositoryMetadata").should_not.be.none + response.get("repositoryMetadata").get("creationDate").should_not.be.none + response.get("repositoryMetadata").get("lastModifiedDate").should_not.be.none + response.get("repositoryMetadata").get("repositoryId").should_not.be.empty + response.get("repositoryMetadata").get("repositoryName").should.equal(repository_name) + response.get("repositoryMetadata").get("repositoryDescription").should.equal('description repo one') + response.get("repositoryMetadata").get("cloneUrlSsh") \ + .should.equal("ssh://git-codecommit.{0}.amazonaws.com/v1/repos/{1}".format("eu-central-1", 'repository_one')) + response.get("repositoryMetadata").get("cloneUrlHttp") \ + .should.equal("https://git-codecommit.{0}.amazonaws.com/v1/repos/{1}".format("eu-central-1", 'repository_one')) + response.get("repositoryMetadata").get("Arn") \ + .should.equal("arn:aws:codecommit:{0}:{1}:{2}".format("eu-central-1", ACCOUNT_ID, 'repository_one' + )) + response.get("repositoryMetadata").get("accountId").should.equal(ACCOUNT_ID) + + client = boto3.client("codecommit", region_name="us-east-1") + + with assert_raises(ClientError) as e: + client.get_repository( + repositoryName=repository_name + ) + ex = e.exception + ex.operation_name.should.equal("GetRepository") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("RepositoryDoesNotExistException") + ex.response["Error"]["Message"].should.equal("{0} does not exist".format(repository_name)) + + +@mock_codecommit +def test_invalid_repository_name(): + client = boto3.client("codecommit", region_name="eu-central-1") + + with assert_raises(ClientError) as e: + client.create_repository( + repositoryName='repository_one-@#@' + ) + ex = e.exception + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidRepositoryNameException") + ex.response["Error"]["Message"].should.equal("The repository name is not valid. Repository names can be any valid " + "combination of letters, numbers, " + "periods, underscores, and dashes between 1 and 100 characters in " + "length. Names are case sensitive. " + "For more information, see Limits in the AWS CodeCommit User Guide. ") + with assert_raises(ClientError) as e: + client.create_repository( + repositoryName='!_repository_one' + ) + + with assert_raises(ClientError) as e: + client.create_repository( + repositoryName='_rep@ository_one' + ) + + with assert_raises(ClientError) as e: + client.get_repository( + repositoryName='_rep@ository_one' + ) + + +@mock_codecommit +def test_delete_repository(): + client = boto3.client("codecommit", region_name="us-east-1") + + response = client.create_repository( + repositoryName='repository_one' + ) + + repository_id_create = response.get("repositoryMetadata").get("repositoryId") + + response = client.delete_repository( + repositoryName='repository_one' + ) + + response.get('repositoryId').should_not.be.none + repository_id_create.should.equal(response.get("repositoryId")) + + response = client.delete_repository( + repositoryName='unknown_repository' + ) + + response.get('repositoryId').should.be.none + + +@mock_codecommit +def test_delete_repository_invalid_repository_name(): + client = boto3.client("codecommit", region_name="us-east-1") + + with assert_raises(ClientError) as e: + client.delete_repository( + repositoryName='_rep@ository_one' + ) + ex = e.exception + ex.operation_name.should.equal("DeleteRepository") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidRepositoryNameException") + ex.response["Error"]["Message"].should.equal("The repository name is not valid. Repository names can be any valid " + "combination of letters, numbers, " + "periods, underscores, and dashes between 1 and 100 characters in " + "length. Names are case sensitive. " + "For more information, see Limits in the AWS CodeCommit User Guide. ") \ No newline at end of file