diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 846008ffb..5bcf2a376 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -175,13 +175,13 @@ ## apigatewayv2
-58% implemented +69% implemented - [X] create_api -- [ ] create_api_mapping +- [X] create_api_mapping - [X] create_authorizer - [ ] create_deployment -- [ ] create_domain_name +- [X] create_domain_name - [X] create_integration - [X] create_integration_response - [X] create_model @@ -191,11 +191,11 @@ - [X] create_vpc_link - [ ] delete_access_log_settings - [X] delete_api -- [ ] delete_api_mapping +- [X] delete_api_mapping - [X] delete_authorizer - [X] delete_cors_configuration - [ ] delete_deployment -- [ ] delete_domain_name +- [X] delete_domain_name - [X] delete_integration - [X] delete_integration_response - [X] delete_model @@ -207,15 +207,15 @@ - [X] delete_vpc_link - [ ] export_api - [X] get_api -- [ ] get_api_mapping -- [ ] get_api_mappings +- [X] get_api_mapping +- [X] get_api_mappings - [X] get_apis - [X] get_authorizer - [ ] get_authorizers - [ ] get_deployment - [ ] get_deployments -- [ ] get_domain_name -- [ ] get_domain_names +- [X] get_domain_name +- [X] get_domain_names - [X] get_integration - [X] get_integration_response - [X] get_integration_responses diff --git a/docs/docs/services/apigatewayv2.rst b/docs/docs/services/apigatewayv2.rst index c769ff5d4..452254ce5 100644 --- a/docs/docs/services/apigatewayv2.rst +++ b/docs/docs/services/apigatewayv2.rst @@ -33,10 +33,10 @@ apigatewayv2 CredentialsArn, RouteKey, Tags, Target -- [ ] create_api_mapping +- [X] create_api_mapping - [X] create_authorizer - [ ] create_deployment -- [ ] create_domain_name +- [X] create_domain_name - [X] create_integration - [X] create_integration_response - [X] create_model @@ -50,11 +50,11 @@ apigatewayv2 - [X] create_vpc_link - [ ] delete_access_log_settings - [X] delete_api -- [ ] delete_api_mapping +- [X] delete_api_mapping - [X] delete_authorizer - [X] delete_cors_configuration - [ ] delete_deployment -- [ ] delete_domain_name +- [X] delete_domain_name - [X] delete_integration - [X] delete_integration_response - [X] delete_model @@ -66,8 +66,8 @@ apigatewayv2 - [X] delete_vpc_link - [ ] export_api - [X] get_api -- [ ] get_api_mapping -- [ ] get_api_mappings +- [X] get_api_mapping +- [X] get_api_mappings - [X] get_apis Pagination is not yet implemented @@ -77,8 +77,12 @@ apigatewayv2 - [ ] get_authorizers - [ ] get_deployment - [ ] get_deployments -- [ ] get_domain_name -- [ ] get_domain_names +- [X] get_domain_name +- [X] get_domain_names + + Pagination is not yet implemented + + - [X] get_integration - [X] get_integration_response - [X] get_integration_responses diff --git a/moto/apigatewayv2/exceptions.py b/moto/apigatewayv2/exceptions.py index 73f8a213a..1a63b65a9 100644 --- a/moto/apigatewayv2/exceptions.py +++ b/moto/apigatewayv2/exceptions.py @@ -93,3 +93,33 @@ class UnknownProtocol(APIGatewayV2Error): "BadRequestException", "Invalid protocol specified. Must be one of [HTTP, WEBSOCKET]", ) + + +class DomainNameNotFound(APIGatewayV2Error): + code = 404 + + def __init__(self) -> None: + super().__init__( + "NotFoundException", + "The domain name resource specified in the request was not found.", + ) + + +class DomainNameAlreadyExists(APIGatewayV2Error): + code = 409 + + def __init__(self) -> None: + super().__init__( + "ConflictException", + "The domain name resource already exists.", + ) + + +class ApiMappingNotFound(APIGatewayV2Error): + code = 404 + + def __init__(self) -> None: + super().__init__( + "NotFoundException", + "The api mapping resource specified in the request was not found.", + ) diff --git a/moto/apigatewayv2/models.py b/moto/apigatewayv2/models.py index bfbc6a8c3..7842e5a2b 100644 --- a/moto/apigatewayv2/models.py +++ b/moto/apigatewayv2/models.py @@ -1,7 +1,8 @@ """ApiGatewayV2Backend class with methods for supported APIs.""" +import hashlib import string import yaml -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from moto.core import BaseBackend, BackendDict, BaseModel from moto.core.utils import unix_time @@ -9,6 +10,7 @@ from moto.moto_api._internal import mock_random as random from moto.utilities.tagging_service import TaggingService from .exceptions import ( + ApiMappingNotFound, ApiNotFound, AuthorizerNotFound, BadRequestException, @@ -18,6 +20,8 @@ from .exceptions import ( IntegrationResponseNotFound, RouteNotFound, VpcLinkNotFound, + DomainNameNotFound, + DomainNameAlreadyExists, ) @@ -1011,6 +1015,55 @@ class VpcLink(BaseModel): } +class DomainName(BaseModel): + def __init__( + self, + domain_name: str, + domain_name_configurations: List[Dict[str, str]], + mutual_tls_authentication: Dict[str, str], + tags: Dict[str, str], + ): + self.api_mapping_selection_expression = "$request.basepath" + self.domain_name = domain_name + self.domain_name_configurations = domain_name_configurations + self.mutual_tls_authentication = mutual_tls_authentication + self.tags = tags + + def to_json(self) -> Dict[str, Any]: + return { + "apiMappingSelectionExpression": self.api_mapping_selection_expression, + "domainName": self.domain_name, + "domainNameConfigurations": self.domain_name_configurations, + "mutualTlsAuthentication": self.mutual_tls_authentication, + "tags": self.tags, + } + + +class ApiMapping(BaseModel): + def __init__( + self, + api_id: str, + api_mapping_key: str, + api_mapping_id: str, + domain_name: str, + stage: str, + ) -> None: + self.api_id = api_id + self.api_mapping_key = api_mapping_key + self.api_mapping_id = api_mapping_id + self.domain_name = domain_name + self.stage = stage + + def to_json(self) -> Dict[str, Any]: + return { + "apiId": self.api_id, + "apiMappingId": self.api_mapping_id, + "apiMappingKey": self.api_mapping_key, + "domainName": self.domain_name, + "stage": self.stage, + } + + class ApiGatewayV2Backend(BaseBackend): """Implementation of ApiGatewayV2 APIs.""" @@ -1018,6 +1071,8 @@ class ApiGatewayV2Backend(BaseBackend): super().__init__(region_name, account_id) self.apis: Dict[str, Api] = dict() self.vpc_links: Dict[str, VpcLink] = dict() + self.domain_names: Dict[str, DomainName] = dict() + self.api_mappings: Dict[str, ApiMapping] = dict() self.tagger = TaggingService() def create_api( @@ -1537,5 +1592,114 @@ class ApiGatewayV2Backend(BaseBackend): vpc_link.update(name) return vpc_link + def create_domain_name( + self, + domain_name: str, + domain_name_configurations: List[Dict[str, str]], + mutual_tls_authentication: Dict[str, str], + tags: Dict[str, str], + ) -> DomainName: + + if domain_name in self.domain_names.keys(): + raise DomainNameAlreadyExists + + domain = DomainName( + domain_name=domain_name, + domain_name_configurations=domain_name_configurations, + mutual_tls_authentication=mutual_tls_authentication, + tags=tags, + ) + self.domain_names[domain.domain_name] = domain + return domain + + def get_domain_name(self, domain_name: Union[str, None]) -> DomainName: + if domain_name is None or domain_name not in self.domain_names: + raise DomainNameNotFound + return self.domain_names[domain_name] + + def get_domain_names(self) -> List[DomainName]: + """ + Pagination is not yet implemented + """ + return list(self.domain_names.values()) + + def delete_domain_name(self, domain_name: str) -> None: + if domain_name not in self.domain_names.keys(): + raise DomainNameNotFound + + for mapping_id, mapping in self.api_mappings.items(): + if mapping.domain_name == domain_name: + del self.api_mappings[mapping_id] + + del self.domain_names[domain_name] + + def _generate_api_maping_id( + self, api_mapping_key: str, stage: str, domain_name: str + ) -> str: + return str( + hashlib.sha256( + f"{stage} {domain_name}/{api_mapping_key}".encode("utf-8") + ).hexdigest() + )[:5] + + def create_api_mapping( + self, api_id: str, api_mapping_key: str, domain_name: str, stage: str + ) -> ApiMapping: + if domain_name not in self.domain_names.keys(): + raise DomainNameNotFound + + if api_id not in self.apis.keys(): + raise ApiNotFound("The resource specified in the request was not found.") + + if api_mapping_key.startswith("/") or "//" in api_mapping_key: + raise BadRequestException( + "API mapping key should not start with a '/' or have consecutive '/'s." + ) + + if api_mapping_key.endswith("/"): + raise BadRequestException("API mapping key should not end with a '/'.") + + api_mapping_id = self._generate_api_maping_id( + api_mapping_key=api_mapping_key, stage=stage, domain_name=domain_name + ) + + mapping = ApiMapping( + domain_name=domain_name, + api_id=api_id, + api_mapping_key=api_mapping_key, + api_mapping_id=api_mapping_id, + stage=stage, + ) + + self.api_mappings[api_mapping_id] = mapping + return mapping + + def get_api_mapping(self, api_mapping_id: str, domain_name: str) -> ApiMapping: + if domain_name not in self.domain_names.keys(): + raise DomainNameNotFound + + if api_mapping_id not in self.api_mappings.keys(): + raise ApiMappingNotFound + + return self.api_mappings[api_mapping_id] + + def get_api_mappings(self, domain_name: str) -> List[ApiMapping]: + domain_mappings = [] + for mapping in self.api_mappings.values(): + if mapping.domain_name == domain_name: + domain_mappings.append(mapping) + return domain_mappings + + def delete_api_mapping(self, api_mapping_id: str, domain_name: str) -> None: + if api_mapping_id not in self.api_mappings.keys(): + raise ApiMappingNotFound + + if self.api_mappings[api_mapping_id].domain_name != domain_name: + raise BadRequestException( + f"given domain name {domain_name} does not match with mapping definition of mapping {api_mapping_id}" + ) + + del self.api_mappings[api_mapping_id] + apigatewayv2_backends = BackendDict(ApiGatewayV2Backend, "apigatewayv2") diff --git a/moto/apigatewayv2/responses.py b/moto/apigatewayv2/responses.py index 403eaad18..91ac11669 100644 --- a/moto/apigatewayv2/responses.py +++ b/moto/apigatewayv2/responses.py @@ -180,6 +180,38 @@ class ApiGatewayV2Response(BaseResponse): if request.method == "POST": return self.create_vpc_link() + def domain_names(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE: # type: ignore[return] + self.setup_class(request, full_url, headers) + + if request.method == "GET": + return self.get_domain_names() + if request.method == "POST": + return self.create_domain_name() + + def domain_name(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE: # type: ignore[return] + self.setup_class(request, full_url, headers) + + if request.method == "GET": + return self.get_domain_name() + if request.method == "DELETE": + return self.delete_domain_name() + + def api_mappings(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE: # type: ignore[return] + self.setup_class(request, full_url, headers) + + if request.method == "GET": + return self.get_api_mappings() + if request.method == "POST": + return self.create_api_mapping() + + def api_mapping(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE: # type: ignore[return] + self.setup_class(request, full_url, headers) + + if request.method == "GET": + return self.get_api_mapping() + if request.method == "DELETE": + return self.delete_api_mapping() + def create_api(self) -> TYPE_RESPONSE: params = json.loads(self.body) @@ -751,3 +783,74 @@ class ApiGatewayV2Response(BaseResponse): vpc_link = self.apigatewayv2_backend.update_vpc_link(vpc_link_id, name=name) return 200, {}, json.dumps(vpc_link.to_json()) + + def create_domain_name(self) -> TYPE_RESPONSE: + params = json.loads(self.body) + domain_name = params.get("domainName") + domain_name_configurations = params.get("domainNameConfigurations", [{}]) + mutual_tls_authentication = params.get("mutualTlsAuthentication", {}) + tags = params.get("tags", {}) + domain_name = self.apigatewayv2_backend.create_domain_name( + domain_name=domain_name, + domain_name_configurations=domain_name_configurations, + mutual_tls_authentication=mutual_tls_authentication, + tags=tags, + ) + return 201, {}, json.dumps(domain_name.to_json()) + + def get_domain_name(self) -> TYPE_RESPONSE: + domain_name_param = self.path.split("/")[-1] + domain_name = self.apigatewayv2_backend.get_domain_name( + domain_name=domain_name_param + ) + return 200, {}, json.dumps(domain_name.to_json()) + + def get_domain_names(self) -> TYPE_RESPONSE: + domain_names = self.apigatewayv2_backend.get_domain_names() + list_of_dict = [domain_name.to_json() for domain_name in domain_names] + return 200, {}, json.dumps(dict(items=list_of_dict)) + + def create_api_mapping(self) -> TYPE_RESPONSE: + domain_name = self.path.split("/")[-2] + params = json.loads(self.body) + api_id = params.get("apiId") + api_mapping_key = params.get("apiMappingKey", "") + stage = params.get("stage") + mapping = self.apigatewayv2_backend.create_api_mapping( + api_id=api_id, + api_mapping_key=api_mapping_key, + domain_name=domain_name, + stage=stage, + ) + return 201, {}, json.dumps(mapping.to_json()) + + def get_api_mapping(self) -> TYPE_RESPONSE: + api_mapping_id = self.path.split("/")[-1] + domain_name = self.path.split("/")[-3] + mapping = self.apigatewayv2_backend.get_api_mapping( + api_mapping_id=api_mapping_id, + domain_name=domain_name, + ) + return 200, {}, json.dumps(mapping.to_json()) + + def get_api_mappings(self) -> TYPE_RESPONSE: + domain_name = self.path.split("/")[-2] + mappings = self.apigatewayv2_backend.get_api_mappings(domain_name=domain_name) + list_of_dict = [mapping.to_json() for mapping in mappings] + return 200, {}, json.dumps(dict(items=list_of_dict)) + + def delete_domain_name(self) -> TYPE_RESPONSE: + domain_name = self.path.split("/")[-1] + self.apigatewayv2_backend.delete_domain_name( + domain_name=domain_name, + ) + return 204, {}, "" + + def delete_api_mapping(self) -> TYPE_RESPONSE: + api_mapping_id = self.path.split("/")[-1] + domain_name = self.path.split("/")[-3] + self.apigatewayv2_backend.delete_api_mapping( + api_mapping_id=api_mapping_id, + domain_name=domain_name, + ) + return 204, {}, "" diff --git a/moto/apigatewayv2/urls.py b/moto/apigatewayv2/urls.py index bf66afe44..b737112e8 100644 --- a/moto/apigatewayv2/urls.py +++ b/moto/apigatewayv2/urls.py @@ -31,4 +31,8 @@ url_paths = { "{0}/v2/tags/(?P[^/]+)/vpclinks/(?P[^/]+)$": response_v2.tags, "{0}/v2/vpclinks$": response_v2.vpc_links, "{0}/v2/vpclinks/(?P[^/]+)$": response_v2.vpc_link, + "{0}/v2/domainnames$": response_v2.domain_names, + "{0}/v2/domainnames/(?P[^/]+)$": response_v2.domain_name, + "{0}/v2/domainnames/(?P[^/]+)/apimappings$": response_v2.api_mappings, + "{0}/v2/domainnames/(?P[^/]+)/apimappings/(?P[^/]+)$": response_v2.api_mapping, } diff --git a/tests/test_apigatewayv2/test_apigatewayv2_domains.py b/tests/test_apigatewayv2/test_apigatewayv2_domains.py new file mode 100644 index 000000000..61d70b00f --- /dev/null +++ b/tests/test_apigatewayv2/test_apigatewayv2_domains.py @@ -0,0 +1,96 @@ +import boto3 +import sure # noqa # pylint: disable=unused-import +import pytest +import botocore.exceptions +from moto import mock_apigatewayv2 + + +@mock_apigatewayv2 +def test_create_domain_name(): + client = boto3.client("apigatewayv2", region_name="us-east-1") + domain_name = "dev" + tags = {"tag": "it"} + expected_keys = [ + "DomainName", + "ApiMappingSelectionExpression", + "DomainNameConfigurations", + "MutualTlsAuthentication", + "Tags", + ] + + post_resp = client.create_domain_name(DomainName=domain_name, Tags=tags) + get_resp = client.get_domain_name(DomainName=domain_name) + + # check post response has all keys + for key in expected_keys: + post_resp.should.have.key(key) + + # check get response has all keys + for key in expected_keys: + get_resp.should.have.key(key) + + # check that values are equal for post and get of same resource + for key in expected_keys: + post_resp.get(key).should.equal(get_resp.get(key)) + + # ensure known values are set correct in post response + post_resp.get("DomainName").should.equal(domain_name) + post_resp.get("Tags").should.equal(tags) + + +@mock_apigatewayv2 +def test_create_domain_name_already_exists(): + client = boto3.client("apigatewayv2", region_name="us-east-1") + client.create_domain_name(DomainName="exists.io") + + with pytest.raises(botocore.exceptions.ClientError) as exc: + client.create_domain_name(DomainName="exists.io") + + err = exc.value.response["Error"] + err["Code"].should.equal("ConflictException") + err["Message"].should.equal("The domain name resource already exists.") + + +@mock_apigatewayv2 +def test_get_domain_names(): + client = boto3.client("apigatewayv2", region_name="us-east-1") + dev_domain = client.create_domain_name(DomainName="dev.service.io") + prod_domain = client.create_domain_name(DomainName="prod.service.io") + + # sanity check responses + dev_domain.should.have.key("DomainName").equals("dev.service.io") + prod_domain.should.have.key("DomainName").equals("prod.service.io") + + # make comparable + del dev_domain["ResponseMetadata"] + del prod_domain["ResponseMetadata"] + + get_resp = client.get_domain_names() + get_resp.should.have.key("Items") + get_resp.get("Items").should.contain(dev_domain) + get_resp.get("Items").should.contain(prod_domain) + + +@mock_apigatewayv2 +def test_delete_domain_name(): + client = boto3.client("apigatewayv2", region_name="ap-southeast-1") + post_resp = client.create_domain_name(DomainName="dev.service.io") + client.delete_domain_name(DomainName="dev.service.io") + get_resp = client.get_domain_names() + + del post_resp["ResponseMetadata"] + get_resp.should.have.key("Items") + get_resp.get("Items").should_not.contain(post_resp) + + +@mock_apigatewayv2 +def test_delete_domain_name_dne(): + client = boto3.client("apigatewayv2", region_name="ap-southeast-1") + with pytest.raises(botocore.exceptions.ClientError) as exc: + client.delete_domain_name(DomainName="dne.io") + + err = exc.value.response["Error"] + err["Code"].should.equal("NotFoundException") + err["Message"].should.equal( + "The domain name resource specified in the request was not found." + ) diff --git a/tests/test_apigatewayv2/test_apigatewayv2_mappings.py b/tests/test_apigatewayv2/test_apigatewayv2_mappings.py new file mode 100644 index 000000000..58bc9f23e --- /dev/null +++ b/tests/test_apigatewayv2/test_apigatewayv2_mappings.py @@ -0,0 +1,195 @@ +import boto3 +import sure # noqa # pylint: disable=unused-import +import pytest +import botocore.exceptions +from moto import mock_apigatewayv2 + + +@mock_apigatewayv2 +def test_create_api_mapping(): + client = boto3.client("apigatewayv2", region_name="us-east-1") + api = client.create_api(Name="test-api", ProtocolType="HTTP") + dev_domain = client.create_domain_name(DomainName="dev.service.io") + expected_keys = ["ApiId", "ApiMappingId", "ApiMappingKey", "Stage"] + + post_resp = client.create_api_mapping( + DomainName=dev_domain["DomainName"], + ApiMappingKey="v1/api", + Stage="$default", + ApiId=api["ApiId"], + ) + + get_resp = client.get_api_mapping( + DomainName="dev.service.io", ApiMappingId=post_resp["ApiMappingId"] + ) + + # check post response has all expected keys + for key in expected_keys: + post_resp.should.have.key(key) + + # check get response has all expected keys + for key in expected_keys: + get_resp.should.have.key(key) + + # check that values are equal for post and get of same resource + for key in expected_keys: + post_resp.get(key).should.equal(get_resp.get(key)) + + # ensure known values are set correct in post response + post_resp.get("ApiId").should_not.equal(None) + post_resp.get("ApiMappingId").should_not.equal(None) + post_resp.get("ApiMappingKey").should.equal("v1/api") + post_resp.get("Stage").should.equal("$default") + + +@mock_apigatewayv2 +def test_create_api_mapping_missing_api(): + client = boto3.client("apigatewayv2", region_name="us-east-1") + dev_domain = client.create_domain_name(DomainName="dev.service.io") + with pytest.raises(botocore.exceptions.ClientError) as exc: + client.create_api_mapping( + DomainName=dev_domain["DomainName"], + Stage="$default", + ApiId="123dne", + ) + + err = exc.value.response["Error"] + err["Code"].should.equal("NotFoundException") + err["Message"].should.equal( + "Invalid API identifier specified The resource specified in the request was not found." + ) + + +@mock_apigatewayv2 +def test_create_api_mapping_missing_domain(): + client = boto3.client("apigatewayv2", region_name="us-east-1") + api = client.create_api(Name="test-api", ProtocolType="HTTP") + + with pytest.raises(botocore.exceptions.ClientError) as exc: + client.create_api_mapping( + DomainName="domain.dne", + Stage="$default", + ApiId=api["ApiId"], + ) + + err = exc.value.response["Error"] + err["Code"].should.equal("NotFoundException") + err["Message"].should.equal( + "The domain name resource specified in the request was not found." + ) + + +@mock_apigatewayv2 +def test_create_api_mapping_bad_mapping_keys(): + client = boto3.client("apigatewayv2", region_name="us-east-1") + api = client.create_api(Name="test-api", ProtocolType="HTTP") + dev_domain = client.create_domain_name(DomainName="dev.service.io") + bad_keys = { + "/api": "API mapping key should not start with a '/' or have consecutive '/'s.", + "v1//api": "API mapping key should not start with a '/' or have consecutive '/'s.", + "api/": "API mapping key should not end with a '/'.", + } + + for bad_key, message in bad_keys.items(): + with pytest.raises(botocore.exceptions.ClientError) as exc: + client.create_api_mapping( + DomainName=dev_domain["DomainName"], + ApiMappingKey=bad_key, + Stage="$default", + ApiId=api["ApiId"], + ) + + err = exc.value.response["Error"] + err["Code"].should.equal("BadRequestException") + err["Message"].should.equal(message) + + +@mock_apigatewayv2 +def test_get_api_mappings(): + client = boto3.client("apigatewayv2", region_name="eu-west-1") + api = client.create_api(Name="test-api", ProtocolType="HTTP") + included_domain = client.create_domain_name(DomainName="dev.service.io") + excluded_domain = client.create_domain_name(DomainName="hr.service.io") + + v1_mapping = client.create_api_mapping( + DomainName=included_domain["DomainName"], + ApiMappingKey="v1/api", + Stage="$default", + ApiId=api["ApiId"], + ) + + v2_mapping = client.create_api_mapping( + DomainName=included_domain["DomainName"], + ApiMappingKey="v2/api", + Stage="$default", + ApiId=api["ApiId"], + ) + + hr_mapping = client.create_api_mapping( + DomainName=excluded_domain["DomainName"], + ApiMappingKey="hr/api", + Stage="$default", + ApiId=api["ApiId"], + ) + + # sanity check responses + v1_mapping.should.have.key("ApiMappingKey").equals("v1/api") + v2_mapping.should.have.key("ApiMappingKey").equals("v2/api") + hr_mapping.should.have.key("ApiMappingKey").equals("hr/api") + + # make comparable + del v1_mapping["ResponseMetadata"] + del v2_mapping["ResponseMetadata"] + del hr_mapping["ResponseMetadata"] + + get_resp = client.get_api_mappings(DomainName=included_domain["DomainName"]) + get_resp.should.have.key("Items") + get_resp.get("Items").should.contain(v1_mapping) + get_resp.get("Items").should.contain(v2_mapping) + get_resp.get("Items").should_not.contain(hr_mapping) + + +@mock_apigatewayv2 +def test_delete_api_mapping(): + client = boto3.client("apigatewayv2", region_name="eu-west-1") + api = client.create_api(Name="test-api", ProtocolType="HTTP") + api_domain = client.create_domain_name(DomainName="dev.service.io") + + v1_mapping = client.create_api_mapping( + DomainName=api_domain["DomainName"], + ApiMappingKey="v1/api", + Stage="$default", + ApiId=api["ApiId"], + ) + + del v1_mapping["ResponseMetadata"] + + get_resp = client.get_api_mappings(DomainName=api_domain["DomainName"]) + get_resp.should.have.key("Items") + get_resp.get("Items").should.contain(v1_mapping) + + client.delete_api_mapping( + DomainName=api_domain["DomainName"], ApiMappingId=v1_mapping["ApiMappingId"] + ) + + get_resp = client.get_api_mappings(DomainName=api_domain["DomainName"]) + get_resp.should.have.key("Items") + get_resp.get("Items").should_not.contain(v1_mapping) + + +@mock_apigatewayv2 +def test_delete_api_mapping_dne(): + client = boto3.client("apigatewayv2", region_name="eu-west-1") + client.create_api(Name="test-api", ProtocolType="HTTP") + api_domain = client.create_domain_name(DomainName="dev.service.io") + + with pytest.raises(botocore.exceptions.ClientError) as exc: + client.delete_api_mapping( + DomainName=api_domain["DomainName"], ApiMappingId="123dne" + ) + + err = exc.value.response["Error"] + err["Code"].should.equal("NotFoundException") + err["Message"].should.equal( + "The api mapping resource specified in the request was not found." + )