Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion localstack/services/apigateway/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class RestApiContainer:
documentation_parts: Dict[str, DocumentationPart]
# not used yet, still in moto
gateway_responses: Dict[str, GatewayResponse]
# not used yet, still in moto
# maps Model name -> Model
models: Dict[str, Model]
# maps ResourceId of a Resource to its children ResourceIds
resource_children: Dict[str, List[str]]
Expand Down
34 changes: 32 additions & 2 deletions localstack/services/apigateway/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,19 @@ def update_method(
remove_empty_attributes_from_method(response)
return response

def delete_method(
self, context: RequestContext, rest_api_id: String, resource_id: String, http_method: String
) -> None:
moto_backend = apigw_models.apigateway_backends[context.account_id][context.region]
moto_rest_api: MotoRestAPI = moto_backend.apis.get(rest_api_id)
if not moto_rest_api or not (moto_resource := moto_rest_api.resources.get(resource_id)):
raise NotFoundException("Invalid Resource identifier specified")

if not (moto_resource.resource_methods.get(http_method)):
raise NotFoundException("Invalid Method identifier specified")

call_moto(context)

# method responses

@handler("UpdateMethodResponse", expand=False)
Expand Down Expand Up @@ -1323,11 +1336,18 @@ def delete_model(
self, context: RequestContext, rest_api_id: String, model_name: String
) -> None:
store = get_apigateway_store(account_id=context.account_id, region=context.region)
if rest_api_id not in store.rest_apis or not (
store.rest_apis[rest_api_id].models.pop(model_name, None)

if (
rest_api_id not in store.rest_apis
or model_name not in store.rest_apis[rest_api_id].models
):
raise NotFoundException(f"Invalid model name specified: {model_name}")

moto_rest_api = get_moto_rest_api(context, rest_api_id)
validate_model_in_use(moto_rest_api, model_name)

store.rest_apis[rest_api_id].models.pop(model_name, None)


# ---------------
# UTIL FUNCTIONS
Expand Down Expand Up @@ -1392,6 +1412,16 @@ def is_variable_path(path_part: str) -> bool:
return path_part.startswith("{") and path_part.endswith("}")


def validate_model_in_use(moto_rest_api: MotoRestAPI, model_name: str) -> None:
for resource in moto_rest_api.resources.values():
for method in resource.resource_methods.values():
if model_name in set(method.request_models.values()):
path = f"{resource.get_path()}/{method.http_method}"
raise ConflictException(
f"Cannot delete model '{model_name}', is referenced in method request: {path}"
)


def create_custom_context(
context: RequestContext, action: str, parameters: ServiceRequest
) -> RequestContext:
Expand Down
128 changes: 128 additions & 0 deletions tests/integration/apigateway/test_apigateway_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,14 @@ def test_method_lifecycle(
)
snapshot.match("del-base-method-response", del_base_method_response)

with pytest.raises(ClientError) as e:
apigateway_client.get_method(restApiId=api_id, resourceId=root_id, httpMethod="ANY")
snapshot.match("get-deleted-method-response", e.value.response)

with pytest.raises(ClientError) as e:
apigateway_client.delete_method(restApiId=api_id, resourceId=root_id, httpMethod="ANY")
snapshot.match("delete-deleted-method-response", e.value.response)

@pytest.mark.aws_validated
def test_method_request_parameters(
self,
Expand Down Expand Up @@ -835,6 +843,126 @@ def test_method_request_parameters(

snapshot.match("req-params-same-name", e.value.response)

@pytest.mark.aws_validated
@pytest.mark.skip_snapshot_verify(
paths=[
"$.delete-model-used-by-2-method.Error.Message",
"$.delete-model-used-by-2-method.message", # we can't guarantee the last method will be the same as AWS
]
)
def test_put_method_model(
self,
apigateway_client,
apigw_create_rest_api,
snapshot,
):
response = apigw_create_rest_api(
name=f"test-api-{short_uid()}", description="testing resource method model"
)
api_id = response["id"]
root_rest_api_resource = apigateway_client.get_resources(restApiId=api_id)
root_id = root_rest_api_resource["items"][0]["id"]

create_model = apigateway_client.create_model(
name="MySchema",
restApiId=api_id,
contentType="application/json",
description="",
schema=json.dumps({"title": "MySchema", "type": "object"}),
)
snapshot.match("create-model", create_model)

create_model_2 = apigateway_client.create_model(
name="MySchemaTwo",
restApiId=api_id,
contentType="application/json",
description="",
schema=json.dumps({"title": "MySchemaTwo", "type": "object"}),
)
snapshot.match("create-model-2", create_model_2)

put_method_response = apigateway_client.put_method(
restApiId=api_id,
resourceId=root_id,
httpMethod="ANY",
authorizationType="NONE",
requestModels={"application/json": "MySchema"},
)
snapshot.match("put-method-request-models", put_method_response)

with pytest.raises(ClientError) as e:
apigateway_client.delete_model(restApiId=api_id, modelName="MySchema")
snapshot.match("delete-model-used", e.value.response)

patch_operations = [
{"op": "replace", "path": "/requestModels/application~1json", "value": "MySchemaTwo"},
]

update_method_model = apigateway_client.update_method(
restApiId=api_id,
resourceId=root_id,
httpMethod="ANY",
patchOperations=patch_operations,
)
snapshot.match("update-method-model", update_method_model)

delete_model = apigateway_client.delete_model(restApiId=api_id, modelName="MySchema")
snapshot.match("delete-model-unused", delete_model)

with pytest.raises(ClientError) as e:
apigateway_client.delete_model(restApiId=api_id, modelName="MySchemaTwo")
snapshot.match("delete-model-used-2", e.value.response)

# create a subresource using MySchemaTwo
resource = apigateway_client.create_resource(
restApiId=api_id, parentId=root_id, pathPart="test"
)
put_method_response = apigateway_client.put_method(
restApiId=api_id,
resourceId=resource["id"],
httpMethod="ANY",
authorizationType="NONE",
requestModels={"application/json": "MySchemaTwo"},
)
snapshot.match("put-method-2-request-models", put_method_response)

# assert that the error raised gives the path of the subresource
with pytest.raises(ClientError) as e:
apigateway_client.delete_model(restApiId=api_id, modelName="MySchemaTwo")
snapshot.match("delete-model-used-by-2-method", e.value.response)

patch_operations = [
{"op": "remove", "path": "/requestModels/application~1json", "value": "MySchemaTwo"},
]

# remove the Model from the subresource
update_method_model = apigateway_client.update_method(
restApiId=api_id,
resourceId=resource["id"],
httpMethod="ANY",
patchOperations=patch_operations,
)
snapshot.match("update-method-model-2", update_method_model)

if is_aws_cloud():
# just to be sure the change is properly set in AWS
time.sleep(3)

# assert that the error raised gives the path of the resource now
with pytest.raises(ClientError) as e:
apigateway_client.delete_model(restApiId=api_id, modelName="MySchemaTwo")
snapshot.match("delete-model-used-by-method-1", e.value.response)

# delete the Method using MySchemaTwo
delete_method = apigateway_client.delete_method(
restApiId=api_id, resourceId=root_id, httpMethod="ANY"
)
snapshot.match("delete-method-using-model-2", delete_method)

# assert we can now delete MySchemaTwo
delete_model = apigateway_client.delete_model(restApiId=api_id, modelName="MySchemaTwo")
snapshot.match("delete-model-unused-2", delete_model)

@pytest.mark.aws_validated
def test_put_method_validation(
self,
Expand Down
162 changes: 161 additions & 1 deletion tests/integration/apigateway/test_apigateway_api.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -1175,7 +1175,7 @@
}
},
"tests/integration/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_method_lifecycle": {
"recorded-date": "24-02-2023, 19:17:18",
"recorded-date": "15-03-2023, 12:06:26",
"recorded-content": {
"put-base-method-response": {
"apiKeyRequired": false,
Expand All @@ -1200,6 +1200,28 @@
"HTTPHeaders": {},
"HTTPStatusCode": 204
}
},
"get-deleted-method-response": {
"Error": {
"Code": "NotFoundException",
"Message": "Invalid Method identifier specified"
},
"message": "Invalid Method identifier specified",
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 404
}
},
"delete-deleted-method-response": {
"Error": {
"Code": "NotFoundException",
"Message": "Invalid Method identifier specified"
},
"message": "Invalid Method identifier specified",
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 404
}
}
}
},
Expand Down Expand Up @@ -1855,5 +1877,143 @@
}
}
}
},
"tests/integration/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_put_method_model": {
"recorded-date": "15-03-2023, 12:12:59",
"recorded-content": {
"create-model": {
"contentType": "application/json",
"id": "<id:1>",
"name": "<name:1>",
"schema": {
"title": "<name:1>",
"type": "object"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 201
}
},
"create-model-2": {
"contentType": "application/json",
"id": "<id:2>",
"name": "<name:1>Two",
"schema": {
"title": "<name:1>Two",
"type": "object"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 201
}
},
"put-method-request-models": {
"apiKeyRequired": false,
"authorizationType": "NONE",
"httpMethod": "ANY",
"requestModels": {
"application/json": "<name:1>"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 201
}
},
"delete-model-used": {
"Error": {
"Code": "ConflictException",
"Message": "Cannot delete model '<name:1>', is referenced in method request: //ANY"
},
"message": "Cannot delete model '<name:1>', is referenced in method request: //ANY",
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 409
}
},
"update-method-model": {
"apiKeyRequired": false,
"authorizationType": "NONE",
"httpMethod": "ANY",
"requestModels": {
"application/json": "<name:1>Two"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"delete-model-unused": {
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 202
}
},
"delete-model-used-2": {
"Error": {
"Code": "ConflictException",
"Message": "Cannot delete model '<name:1>Two', is referenced in method request: //ANY"
},
"message": "Cannot delete model '<name:1>Two', is referenced in method request: //ANY",
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 409
}
},
"put-method-2-request-models": {
"apiKeyRequired": false,
"authorizationType": "NONE",
"httpMethod": "ANY",
"requestModels": {
"application/json": "<name:1>Two"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 201
}
},
"delete-model-used-by-2-method": {
"Error": {
"Code": "ConflictException",
"Message": "Cannot delete model '<name:1>Two', is referenced in method request: /test/ANY"
},
"message": "Cannot delete model '<name:1>Two', is referenced in method request: /test/ANY",
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 409
}
},
"update-method-model-2": {
"apiKeyRequired": false,
"authorizationType": "NONE",
"httpMethod": "ANY",
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"delete-model-used-by-method-1": {
"Error": {
"Code": "ConflictException",
"Message": "Cannot delete model '<name:1>Two', is referenced in method request: //ANY"
},
"message": "Cannot delete model '<name:1>Two', is referenced in method request: //ANY",
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 409
}
},
"delete-method-using-model-2": {
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 204
}
},
"delete-model-unused-2": {
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 202
}
}
}
}
}