diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index bb4a30d8f..a863d483d 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 7925d1c7e..44b25f41e 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 # noqa from .codepipeline import mock_codepipeline # noqa from .cognitoidentity import mock_cognitoidentity # noqa from .cognitoidentity import mock_cognitoidentity_deprecated # noqa diff --git a/moto/backends.py b/moto/backends.py index 81790bbaf..a358b8fd2 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -8,6 +8,7 @@ from moto.awslambda import lambda_backends from moto.batch import batch_backends from moto.cloudformation import cloudformation_backends from moto.cloudwatch import cloudwatch_backends +from moto.codecommit import codecommit_backends from moto.codepipeline import codepipeline_backends from moto.cognitoidentity import cognitoidentity_backends from moto.cognitoidp import cognitoidp_backends @@ -62,6 +63,7 @@ BACKENDS = { "batch": batch_backends, "cloudformation": cloudformation_backends, "cloudwatch": cloudwatch_backends, + "codecommit": codecommit_backends, "codepipeline": codepipeline_backends, "cognito-identity": cognitoidentity_backends, "cognito-idp": cognitoidp_backends, diff --git a/moto/codecommit/__init__.py b/moto/codecommit/__init__.py new file mode 100644 index 000000000..6c5a8f5ad --- /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) diff --git a/moto/codecommit/exceptions.py b/moto/codecommit/exceptions.py new file mode 100644 index 000000000..136af50f1 --- /dev/null +++ b/moto/codecommit/exceptions.py @@ -0,0 +1,35 @@ +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 000000000..6a4e82ad2 --- /dev/null +++ b/moto/codecommit/models.py @@ -0,0 +1,69 @@ +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 000000000..3c6fdc5ea --- /dev/null +++ b/moto/codecommit/responses.py @@ -0,0 +1,57 @@ +import json +import re + +from moto.core.responses import BaseResponse +from .models import codecommit_backends +from .exceptions import InvalidRepositoryNameException + + +def _is_repository_name_valid(repository_name): + name_regex = re.compile(r"[\w\.-]+") + result = name_regex.split(repository_name) + if len(result) > 0: + for match in result: + if len(match) > 0: + return False + return True + + +class CodeCommitResponse(BaseResponse): + @property + def codecommit_backend(self): + return codecommit_backends[self.region] + + def create_repository(self): + if not _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 _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 _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({}) diff --git a/moto/codecommit/urls.py b/moto/codecommit/urls.py new file mode 100644 index 000000000..1e3cdb1b4 --- /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 000000000..6e916f20a --- /dev/null +++ b/tests/test_codecommit/test_codecommit.py @@ -0,0 +1,222 @@ +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) + + +@mock_codecommit +def test_create_repository_without_description(): + client = boto3.client("codecommit", region_name="eu-central-1") + + 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 + 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"]["cloneUrlSsh"].should.equal( + "ssh://git-codecommit.{0}.amazonaws.com/v1/repos/{1}".format( + "eu-central-1", "repository_two" + ) + ) + response["repositoryMetadata"]["cloneUrlHttp"].should.equal( + "https://git-codecommit.{0}.amazonaws.com/v1/repos/{1}".format( + "eu-central-1", "repository_two" + ) + ) + response["repositoryMetadata"]["Arn"].should.equal( + "arn:aws:codecommit:{0}:{1}:{2}".format( + "eu-central-1", ACCOUNT_ID, "repository_two" + ) + ) + response["repositoryMetadata"]["accountId"].should.equal(ACCOUNT_ID) + + +@mock_codecommit +def test_create_repository_repository_name_exists(): + client = boto3.client("codecommit", region_name="eu-central-1") + + client.create_repository(repositoryName="repository_two") + + with assert_raises(ClientError) as e: + client.create_repository( + repositoryName="repository_two", + repositoryDescription="description repo 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_create_repository_invalid_repository_name(): + client = boto3.client("codecommit", region_name="eu-central-1") + + with assert_raises(ClientError) as e: + client.create_repository(repositoryName="in_123_valid_@#$_characters") + ex = e.exception + ex.operation_name.should.equal("CreateRepository") + 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. " + ) + + +@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_get_repository_invalid_repository_name(): + client = boto3.client("codecommit", region_name="eu-central-1") + + with assert_raises(ClientError) as e: + client.get_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. " + ) + + +@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. " + )