API Gateway: put_rest_api and import_rest_api (#5140)
This commit is contained in:
parent
ecfd6d6065
commit
8bbd242d75
@ -22,7 +22,7 @@
|
||||
|
||||
## apigateway
|
||||
<details>
|
||||
<summary>62% implemented</summary>
|
||||
<summary>65% implemented</summary>
|
||||
|
||||
- [X] create_api_key
|
||||
- [X] create_authorizer
|
||||
@ -93,7 +93,7 @@
|
||||
- [X] get_request_validator
|
||||
- [X] get_request_validators
|
||||
- [X] get_resource
|
||||
- [ ] get_resources
|
||||
- [X] get_resources
|
||||
- [X] get_rest_api
|
||||
- [ ] get_rest_apis
|
||||
- [ ] get_sdk
|
||||
@ -111,13 +111,13 @@
|
||||
- [X] get_vpc_links
|
||||
- [ ] import_api_keys
|
||||
- [ ] import_documentation_parts
|
||||
- [ ] import_rest_api
|
||||
- [X] import_rest_api
|
||||
- [X] put_gateway_response
|
||||
- [X] put_integration
|
||||
- [X] put_integration_response
|
||||
- [X] put_method
|
||||
- [X] put_method_response
|
||||
- [ ] put_rest_api
|
||||
- [X] put_rest_api
|
||||
- [ ] tag_resource
|
||||
- [ ] test_invoke_authorizer
|
||||
- [ ] test_invoke_method
|
||||
|
@ -100,7 +100,7 @@ apigateway
|
||||
- [X] get_request_validator
|
||||
- [X] get_request_validators
|
||||
- [X] get_resource
|
||||
- [ ] get_resources
|
||||
- [X] get_resources
|
||||
- [X] get_rest_api
|
||||
- [ ] get_rest_apis
|
||||
- [ ] get_sdk
|
||||
@ -122,13 +122,21 @@ apigateway
|
||||
|
||||
- [ ] import_api_keys
|
||||
- [ ] import_documentation_parts
|
||||
- [ ] import_rest_api
|
||||
- [X] import_rest_api
|
||||
|
||||
Only a subset of the OpenAPI spec 3.x is currently implemented.
|
||||
|
||||
|
||||
- [X] put_gateway_response
|
||||
- [X] put_integration
|
||||
- [X] put_integration_response
|
||||
- [X] put_method
|
||||
- [X] put_method_response
|
||||
- [ ] put_rest_api
|
||||
- [X] put_rest_api
|
||||
|
||||
Only a subset of the OpenAPI spec 3.x is currently implemented.
|
||||
|
||||
|
||||
- [ ] tag_resource
|
||||
- [ ] test_invoke_authorizer
|
||||
- [ ] test_invoke_method
|
||||
|
@ -50,6 +50,25 @@ class IntegrationMethodNotDefined(BadRequestException):
|
||||
super().__init__("Enumeration value for HttpMethod must be non-empty")
|
||||
|
||||
|
||||
class InvalidOpenAPIDocumentException(BadRequestException):
|
||||
def __init__(self, cause):
|
||||
super().__init__(
|
||||
f"Failed to parse the uploaded OpenAPI document due to: {cause.message}"
|
||||
)
|
||||
|
||||
|
||||
class InvalidOpenApiDocVersionException(BadRequestException):
|
||||
def __init__(self):
|
||||
super().__init__("Only OpenAPI 3.x.x are currently supported")
|
||||
|
||||
|
||||
class InvalidOpenApiModeException(BadRequestException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'Enumeration value of OpenAPI import mode must be "overwrite" or "merge"',
|
||||
)
|
||||
|
||||
|
||||
class InvalidResourcePathException(BadRequestException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
@ -233,6 +252,13 @@ class BasePathNotFoundException(NotFoundException):
|
||||
super().__init__("Invalid base path mapping identifier specified")
|
||||
|
||||
|
||||
class ResourceIdNotFoundException(NotFoundException):
|
||||
code = 404
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("Invalid resource identifier specified")
|
||||
|
||||
|
||||
class VpcLinkNotFound(NotFoundException):
|
||||
code = 404
|
||||
|
||||
|
@ -6,10 +6,13 @@ import re
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
|
||||
from openapi_spec_validator import validate_spec
|
||||
import time
|
||||
|
||||
from urllib.parse import urlparse
|
||||
import responses
|
||||
|
||||
from openapi_spec_validator.exceptions import OpenAPIValidationError
|
||||
from moto.core import get_account_id, BaseBackend, BaseModel, CloudFormationModel
|
||||
from .utils import create_id, to_path
|
||||
from moto.core.utils import path_url, BackendDict
|
||||
@ -27,9 +30,13 @@ from .exceptions import (
|
||||
InvalidArn,
|
||||
InvalidIntegrationArn,
|
||||
InvalidHttpEndpoint,
|
||||
InvalidOpenAPIDocumentException,
|
||||
InvalidOpenApiDocVersionException,
|
||||
InvalidOpenApiModeException,
|
||||
InvalidResourcePathException,
|
||||
AuthorizerNotFoundException,
|
||||
StageNotFoundException,
|
||||
ResourceIdNotFoundException,
|
||||
RoleNotSpecified,
|
||||
NoIntegrationDefined,
|
||||
NoIntegrationResponseDefined,
|
||||
@ -184,7 +191,7 @@ class Method(CloudFormationModel, dict):
|
||||
requestValidatorId=kwargs.get("request_validator_id"),
|
||||
)
|
||||
)
|
||||
self.method_responses = {}
|
||||
self["methodResponses"] = {}
|
||||
|
||||
@staticmethod
|
||||
def cloudformation_name_type():
|
||||
@ -229,14 +236,14 @@ class Method(CloudFormationModel, dict):
|
||||
method_response = MethodResponse(
|
||||
response_code, response_models, response_parameters
|
||||
)
|
||||
self.method_responses[response_code] = method_response
|
||||
self["methodResponses"][response_code] = method_response
|
||||
return method_response
|
||||
|
||||
def get_response(self, response_code):
|
||||
return self.method_responses.get(response_code)
|
||||
return self["methodResponses"].get(response_code)
|
||||
|
||||
def delete_response(self, response_code):
|
||||
return self.method_responses.pop(response_code, None)
|
||||
return self["methodResponses"].pop(response_code, None)
|
||||
|
||||
|
||||
class Resource(CloudFormationModel):
|
||||
@ -288,7 +295,7 @@ class Resource(CloudFormationModel):
|
||||
backend = apigateway_backends[region_name]
|
||||
if parent == api_id:
|
||||
# A Root path (/) is automatically created. Any new paths should use this as their parent
|
||||
resources = backend.list_resources(function_id=api_id)
|
||||
resources = backend.get_resources(function_id=api_id)
|
||||
root_id = [resource for resource in resources if resource.path_part == "/"][
|
||||
0
|
||||
].id
|
||||
@ -789,7 +796,7 @@ class RestAPI(CloudFormationModel):
|
||||
self.resources = {}
|
||||
self.models = {}
|
||||
self.request_validators = {}
|
||||
self.add_child("/") # Add default child
|
||||
self.default = self.add_child("/") # Add default child
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.id)
|
||||
@ -811,8 +818,6 @@ class RestAPI(CloudFormationModel):
|
||||
}
|
||||
|
||||
def apply_patch_operations(self, patch_operations):
|
||||
def to_path(prop):
|
||||
return "/" + prop
|
||||
|
||||
for op in patch_operations:
|
||||
path = op[self.OPERATION_PATH]
|
||||
@ -1270,12 +1275,80 @@ class APIGatewayBackend(BaseBackend):
|
||||
self.apis[api_id] = rest_api
|
||||
return rest_api
|
||||
|
||||
def import_rest_api(self, api_doc, fail_on_warnings):
|
||||
"""
|
||||
Only a subset of the OpenAPI spec 3.x is currently implemented.
|
||||
"""
|
||||
if fail_on_warnings:
|
||||
try:
|
||||
validate_spec(api_doc)
|
||||
except OpenAPIValidationError as e:
|
||||
raise InvalidOpenAPIDocumentException(e)
|
||||
name = api_doc["info"]["title"]
|
||||
description = api_doc["info"]["description"]
|
||||
api = self.create_rest_api(name=name, description=description)
|
||||
self.put_rest_api(api.id, api_doc, fail_on_warnings=fail_on_warnings)
|
||||
return api
|
||||
|
||||
def get_rest_api(self, function_id):
|
||||
rest_api = self.apis.get(function_id)
|
||||
if rest_api is None:
|
||||
raise RestAPINotFound()
|
||||
return rest_api
|
||||
|
||||
def put_rest_api(self, function_id, api_doc, mode="merge", fail_on_warnings=False):
|
||||
"""
|
||||
Only a subset of the OpenAPI spec 3.x is currently implemented.
|
||||
"""
|
||||
if mode not in ["merge", "overwrite"]:
|
||||
raise InvalidOpenApiModeException()
|
||||
|
||||
if api_doc.get("swagger") is not None or (
|
||||
api_doc.get("openapi") is not None and api_doc["openapi"][0] != "3"
|
||||
):
|
||||
raise InvalidOpenApiDocVersionException()
|
||||
|
||||
if fail_on_warnings:
|
||||
try:
|
||||
validate_spec(api_doc)
|
||||
except OpenAPIValidationError as e:
|
||||
raise InvalidOpenAPIDocumentException(e)
|
||||
|
||||
if mode == "overwrite":
|
||||
api = self.get_rest_api(function_id)
|
||||
api.resources = {}
|
||||
api.default = api.add_child("/") # Add default child
|
||||
|
||||
for (path, resource_doc) in sorted(
|
||||
api_doc["paths"].items(), key=lambda x: x[0]
|
||||
):
|
||||
parent_path_part = path[0 : path.rfind("/")] or "/"
|
||||
parent_resource_id = (
|
||||
self.apis[function_id].get_resource_for_path(parent_path_part).id
|
||||
)
|
||||
resource = self.create_resource(
|
||||
function_id=function_id,
|
||||
parent_resource_id=parent_resource_id,
|
||||
path_part=path[path.rfind("/") + 1 :],
|
||||
)
|
||||
|
||||
for (method_type, method_doc) in resource_doc.items():
|
||||
method_type = method_type.upper()
|
||||
if method_doc.get("x-amazon-apigateway-integration") is None:
|
||||
self.put_method(function_id, resource.id, method_type, None)
|
||||
method_responses = method_doc.get("responses", {}).items()
|
||||
for (response_code, _) in method_responses:
|
||||
self.put_method_response(
|
||||
function_id,
|
||||
resource.id,
|
||||
method_type,
|
||||
response_code,
|
||||
response_models=None,
|
||||
response_parameters=None,
|
||||
)
|
||||
|
||||
return self.get_rest_api(function_id)
|
||||
|
||||
def update_rest_api(self, function_id, patch_operations):
|
||||
rest_api = self.apis.get(function_id)
|
||||
if rest_api is None:
|
||||
@ -1290,19 +1363,24 @@ class APIGatewayBackend(BaseBackend):
|
||||
rest_api = self.apis.pop(function_id)
|
||||
return rest_api
|
||||
|
||||
def list_resources(self, function_id):
|
||||
def get_resources(self, function_id):
|
||||
api = self.get_rest_api(function_id)
|
||||
return api.resources.values()
|
||||
|
||||
def get_resource(self, function_id, resource_id):
|
||||
api = self.get_rest_api(function_id)
|
||||
if resource_id not in api.resources:
|
||||
raise ResourceIdNotFoundException
|
||||
resource = api.resources[resource_id]
|
||||
return resource
|
||||
|
||||
def create_resource(self, function_id, parent_resource_id, path_part):
|
||||
api = self.get_rest_api(function_id)
|
||||
if not path_part:
|
||||
# We're attempting to create the default resource, which already exists.
|
||||
return api.default
|
||||
if not re.match("^\\{?[a-zA-Z0-9._-]+\\+?\\}?$", path_part):
|
||||
raise InvalidResourcePathException()
|
||||
api = self.get_rest_api(function_id)
|
||||
child = api.add_child(path=path_part, parent_id=parent_resource_id)
|
||||
return child
|
||||
|
||||
@ -1578,7 +1656,7 @@ class APIGatewayBackend(BaseBackend):
|
||||
api = self.get_rest_api(function_id)
|
||||
methods = [
|
||||
list(res.resource_methods.values())
|
||||
for res in self.list_resources(function_id)
|
||||
for res in self.get_resources(function_id)
|
||||
]
|
||||
methods = [m for sublist in methods for m in sublist]
|
||||
if not any(methods):
|
||||
|
@ -4,6 +4,7 @@ from urllib.parse import unquote
|
||||
from moto.utilities.utils import merge_multiple_dicts
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import apigateway_backends
|
||||
from .utils import deserialize_body
|
||||
from .exceptions import InvalidRequestInput
|
||||
|
||||
API_KEY_SOURCES = ["AUTHORIZER", "HEADER"]
|
||||
@ -56,8 +57,16 @@ class APIGatewayResponse(BaseResponse):
|
||||
apis = self.backend.list_apis()
|
||||
return 200, {}, json.dumps({"item": [api.to_dict() for api in apis]})
|
||||
elif self.method == "POST":
|
||||
api_doc = deserialize_body(self.body)
|
||||
if api_doc:
|
||||
fail_on_warnings = self._get_bool_param("failonwarnings")
|
||||
rest_api = self.backend.import_rest_api(api_doc, fail_on_warnings)
|
||||
|
||||
return 200, {}, json.dumps(rest_api.to_dict())
|
||||
|
||||
name = self._get_param("name")
|
||||
description = self._get_param("description")
|
||||
|
||||
api_key_source = self._get_param("apiKeySource")
|
||||
endpoint_configuration = self._get_param("endpointConfiguration")
|
||||
tags = self._get_param("tags")
|
||||
@ -82,6 +91,7 @@ class APIGatewayResponse(BaseResponse):
|
||||
policy=policy,
|
||||
minimum_compression_size=minimum_compression_size,
|
||||
)
|
||||
|
||||
return 200, {}, json.dumps(rest_api.to_dict())
|
||||
|
||||
def __validte_rest_patch_operations(self, patch_operations):
|
||||
@ -99,6 +109,15 @@ class APIGatewayResponse(BaseResponse):
|
||||
rest_api = self.backend.get_rest_api(function_id)
|
||||
elif self.method == "DELETE":
|
||||
rest_api = self.backend.delete_rest_api(function_id)
|
||||
elif self.method == "PUT":
|
||||
mode = self._get_param("mode", "merge")
|
||||
fail_on_warnings = self._get_bool_param("failonwarnings", False)
|
||||
|
||||
api_doc = deserialize_body(self.body)
|
||||
|
||||
rest_api = self.backend.put_rest_api(
|
||||
function_id, api_doc, mode, fail_on_warnings
|
||||
)
|
||||
elif self.method == "PATCH":
|
||||
patch_operations = self._get_param("patchOperations")
|
||||
response = self.__validte_rest_patch_operations(patch_operations)
|
||||
@ -113,7 +132,7 @@ class APIGatewayResponse(BaseResponse):
|
||||
function_id = self.path.replace("/restapis/", "", 1).split("/")[0]
|
||||
|
||||
if self.method == "GET":
|
||||
resources = self.backend.list_resources(function_id)
|
||||
resources = self.backend.get_resources(function_id)
|
||||
return (
|
||||
200,
|
||||
{},
|
||||
|
@ -1,5 +1,7 @@
|
||||
import random
|
||||
import string
|
||||
import json
|
||||
import yaml
|
||||
|
||||
|
||||
def create_id():
|
||||
@ -8,5 +10,17 @@ def create_id():
|
||||
return "".join(str(random.choice(chars)) for x in range(size))
|
||||
|
||||
|
||||
def deserialize_body(body):
|
||||
try:
|
||||
api_doc = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
api_doc = yaml.safe_load(body)
|
||||
|
||||
if "openapi" in api_doc or "swagger" in api_doc:
|
||||
return api_doc
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def to_path(prop):
|
||||
return "/" + prop
|
||||
|
4
setup.py
4
setup.py
@ -54,6 +54,7 @@ _dep_aws_xray_sdk = "aws-xray-sdk!=0.96,>=0.93"
|
||||
_dep_idna = "idna<4,>=2.5"
|
||||
_dep_cfn_lint = "cfn-lint>=0.4.0"
|
||||
_dep_sshpubkeys = "sshpubkeys>=3.1.0"
|
||||
_dep_openapi = "openapi-spec-validator>=0.2.8"
|
||||
_dep_pyparsing = "pyparsing>=3.0.0"
|
||||
_setuptools = "setuptools"
|
||||
|
||||
@ -69,6 +70,7 @@ all_extra_deps = [
|
||||
_dep_cfn_lint,
|
||||
_dep_sshpubkeys,
|
||||
_dep_pyparsing,
|
||||
_dep_openapi,
|
||||
_setuptools,
|
||||
]
|
||||
all_server_deps = all_extra_deps + ["flask", "flask-cors"]
|
||||
@ -82,7 +84,7 @@ for service_name in [
|
||||
extras_per_service[service_name] = []
|
||||
extras_per_service.update(
|
||||
{
|
||||
"apigateway": [_dep_PyYAML, _dep_python_jose, _dep_python_jose_ecdsa_pin],
|
||||
"apigateway": [_dep_PyYAML, _dep_python_jose, _dep_python_jose_ecdsa_pin, _dep_openapi],
|
||||
"apigatewayv2": [_dep_PyYAML],
|
||||
"appsync": [_dep_graphql],
|
||||
"awslambda": [_dep_docker],
|
||||
|
102
tests/test_apigateway/resources/test_api.json
Normal file
102
tests/test_apigateway/resources/test_api.json
Normal file
@ -0,0 +1,102 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"description": "description from JSON file",
|
||||
"version": "1",
|
||||
"title": "doc"
|
||||
},
|
||||
"paths": {
|
||||
"/": {
|
||||
"get": {
|
||||
"description": "Method description.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "200 response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/test": {
|
||||
"post": {
|
||||
"description": "Method description.",
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "201 response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-amazon-apigateway-documentation": {
|
||||
"version": "1.0.3",
|
||||
"documentationParts": [
|
||||
{
|
||||
"location": {
|
||||
"type": "API"
|
||||
},
|
||||
"properties": {
|
||||
"description": "API description",
|
||||
"info": {
|
||||
"description": "API info description 4",
|
||||
"version": "API info version 3"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"type": "METHOD",
|
||||
"method": "GET"
|
||||
},
|
||||
"properties": {
|
||||
"description": "Method description."
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"type": "MODEL",
|
||||
"name": "Empty"
|
||||
},
|
||||
"properties": {
|
||||
"title": "Empty Schema"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"type": "RESPONSE",
|
||||
"method": "GET",
|
||||
"statusCode": "200"
|
||||
},
|
||||
"properties": {
|
||||
"description": "200 response"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Empty": {
|
||||
"type": "object",
|
||||
"title": "Empty Schema"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
60
tests/test_apigateway/resources/test_api.yaml
Normal file
60
tests/test_apigateway/resources/test_api.yaml
Normal file
@ -0,0 +1,60 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
description: description
|
||||
version: '1'
|
||||
title: doc
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
description: Method description.
|
||||
responses:
|
||||
'200':
|
||||
description: 200 response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Empty'
|
||||
/test:
|
||||
post:
|
||||
description: Method description.
|
||||
responses:
|
||||
'200':
|
||||
description: 200 response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Empty'
|
||||
|
||||
x-amazon-apigateway-documentation:
|
||||
version: 1.0.3
|
||||
documentationParts:
|
||||
- location:
|
||||
type: API
|
||||
properties:
|
||||
description: API description
|
||||
info:
|
||||
description: API info description 4
|
||||
version: API info version 3
|
||||
- location:
|
||||
type: METHOD
|
||||
method: GET
|
||||
properties:
|
||||
description: Method description.
|
||||
- location:
|
||||
type: MODEL
|
||||
name: Empty
|
||||
properties:
|
||||
title: Empty Schema
|
||||
- location:
|
||||
type: RESPONSE
|
||||
method: GET
|
||||
statusCode: '200'
|
||||
properties:
|
||||
description: 200 response
|
||||
servers:
|
||||
- url: /
|
||||
components:
|
||||
schemas:
|
||||
Empty:
|
||||
type: object
|
||||
title: Empty Schema
|
102
tests/test_apigateway/resources/test_api_invalid.json
Normal file
102
tests/test_apigateway/resources/test_api_invalid.json
Normal file
@ -0,0 +1,102 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"description": "description",
|
||||
"version": "1",
|
||||
"title": "doc"
|
||||
},
|
||||
"paaths": {
|
||||
"/": {
|
||||
"get": {
|
||||
"description": "Method description.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "200 response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/test": {
|
||||
"post": {
|
||||
"description": "Method description.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "200 response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-amazon-apigateway-documentation": {
|
||||
"version": "1.0.3",
|
||||
"documentationParts": [
|
||||
{
|
||||
"location": {
|
||||
"type": "API"
|
||||
},
|
||||
"properties": {
|
||||
"description": "API description",
|
||||
"info": {
|
||||
"description": "API info description 4",
|
||||
"version": "API info version 3"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"type": "METHOD",
|
||||
"method": "GET"
|
||||
},
|
||||
"properties": {
|
||||
"description": "Method description."
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"type": "MODEL",
|
||||
"name": "Empty"
|
||||
},
|
||||
"properties": {
|
||||
"title": "Empty Schema"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"type": "RESPONSE",
|
||||
"method": "GET",
|
||||
"statusCode": "200"
|
||||
},
|
||||
"properties": {
|
||||
"description": "200 response"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Empty": {
|
||||
"type": "object",
|
||||
"title": "Empty Schema"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
102
tests/test_apigateway/resources/test_api_invalid_version.json
Normal file
102
tests/test_apigateway/resources/test_api_invalid_version.json
Normal file
@ -0,0 +1,102 @@
|
||||
{
|
||||
"openapi": "2.0.0",
|
||||
"info": {
|
||||
"description": "description",
|
||||
"version": "1",
|
||||
"title": "doc"
|
||||
},
|
||||
"paths": {
|
||||
"/": {
|
||||
"get": {
|
||||
"description": "Method description.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "200 response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/test": {
|
||||
"post": {
|
||||
"description": "Method description.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "200 response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-amazon-apigateway-documentation": {
|
||||
"version": "1.0.3",
|
||||
"documentationParts": [
|
||||
{
|
||||
"location": {
|
||||
"type": "API"
|
||||
},
|
||||
"properties": {
|
||||
"description": "API description",
|
||||
"info": {
|
||||
"description": "API info description 4",
|
||||
"version": "API info version 3"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"type": "METHOD",
|
||||
"method": "GET"
|
||||
},
|
||||
"properties": {
|
||||
"description": "Method description."
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"type": "MODEL",
|
||||
"name": "Empty"
|
||||
},
|
||||
"properties": {
|
||||
"title": "Empty Schema"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"type": "RESPONSE",
|
||||
"method": "GET",
|
||||
"statusCode": "200"
|
||||
},
|
||||
"properties": {
|
||||
"description": "200 response"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Empty": {
|
||||
"type": "object",
|
||||
"title": "Empty Schema"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -367,6 +367,7 @@ def test_create_method():
|
||||
"httpMethod": "GET",
|
||||
"authorizationType": "none",
|
||||
"apiKeyRequired": False,
|
||||
"methodResponses": {},
|
||||
"ResponseMetadata": {"HTTPStatusCode": 200},
|
||||
}
|
||||
)
|
||||
@ -401,6 +402,7 @@ def test_create_method_apikeyrequired():
|
||||
"httpMethod": "GET",
|
||||
"authorizationType": "none",
|
||||
"apiKeyRequired": True,
|
||||
"methodResponses": {},
|
||||
"ResponseMetadata": {"HTTPStatusCode": 200},
|
||||
}
|
||||
)
|
||||
@ -452,6 +454,19 @@ def test_create_method_response():
|
||||
response.should.equal({"ResponseMetadata": {"HTTPStatusCode": 200}})
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_get_method_unknown_resource_id():
|
||||
client = boto3.client("apigateway", region_name="us-west-2")
|
||||
response = client.create_rest_api(name="my_api", description="this is my api")
|
||||
api_id = response["id"]
|
||||
|
||||
with pytest.raises(ClientError) as ex:
|
||||
client.get_method(restApiId=api_id, resourceId="sth", httpMethod="GET")
|
||||
err = ex.value.response["Error"]
|
||||
err["Code"].should.equal("NotFoundException")
|
||||
err["Message"].should.equal("Invalid resource identifier specified")
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_delete_method():
|
||||
client = boto3.client("apigateway", region_name="us-west-2")
|
||||
|
70
tests/test_apigateway/test_apigateway_importrestapi.py
Normal file
70
tests/test_apigateway/test_apigateway_importrestapi.py
Normal file
@ -0,0 +1,70 @@
|
||||
import boto3
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
from moto import mock_apigateway
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_import_rest_api__api_is_created():
|
||||
client = boto3.client("apigateway", region_name="us-west-2")
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(path + "/resources/test_api.json", "rb") as api_json:
|
||||
response = client.import_rest_api(body=api_json.read())
|
||||
|
||||
response.should.have.key("id")
|
||||
response.should.have.key("name").which.should.equal("doc")
|
||||
response.should.have.key("description").which.should.equal(
|
||||
"description from JSON file"
|
||||
)
|
||||
|
||||
response = client.get_rest_api(restApiId=response["id"])
|
||||
response.should.have.key("name").which.should.equal("doc")
|
||||
response.should.have.key("description").which.should.equal(
|
||||
"description from JSON file"
|
||||
)
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_import_rest_api__invalid_api_creates_nothing():
|
||||
client = boto3.client("apigateway", region_name="us-west-2")
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(path + "/resources/test_api_invalid.json", "rb") as api_json:
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.import_rest_api(body=api_json.read(), failOnWarnings=True)
|
||||
err = exc.value.response["Error"]
|
||||
err["Code"].should.equal("BadRequestException")
|
||||
err["Message"].should.equal(
|
||||
"Failed to parse the uploaded OpenAPI document due to: 'paths' is a required property"
|
||||
)
|
||||
|
||||
client.get_rest_apis().should.have.key("items").length_of(0)
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_import_rest_api__methods_are_created():
|
||||
client = boto3.client("apigateway", region_name="us-east-1")
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(path + "/resources/test_api.json", "rb") as api_json:
|
||||
resp = client.import_rest_api(body=api_json.read())
|
||||
api_id = resp["id"]
|
||||
|
||||
resources = client.get_resources(restApiId=api_id)
|
||||
root_id = [res for res in resources["items"] if res["path"] == "/"][0]["id"]
|
||||
|
||||
# We have a GET-method
|
||||
resp = client.get_method(restApiId=api_id, resourceId=root_id, httpMethod="GET")
|
||||
resp["methodResponses"].should.equal({"200": {"statusCode": "200"}})
|
||||
|
||||
# We have a POST on /test
|
||||
test_path_id = [res for res in resources["items"] if res["path"] == "/test"][0][
|
||||
"id"
|
||||
]
|
||||
resp = client.get_method(
|
||||
restApiId=api_id, resourceId=test_path_id, httpMethod="POST"
|
||||
)
|
||||
resp["methodResponses"].should.equal({"201": {"statusCode": "201"}})
|
217
tests/test_apigateway/test_apigateway_putrestapi.py
Normal file
217
tests/test_apigateway/test_apigateway_putrestapi.py
Normal file
@ -0,0 +1,217 @@
|
||||
import boto3
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
from moto import mock_apigateway
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_put_rest_api__api_details_are_persisted():
|
||||
client = boto3.client("apigateway", region_name="us-west-2")
|
||||
|
||||
response = client.create_rest_api(name="my_api", description="this is my api")
|
||||
api_id = response["id"]
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(path + "/resources/test_api.json", "rb") as api_json:
|
||||
response = client.put_rest_api(
|
||||
restApiId=api_id,
|
||||
mode="overwrite",
|
||||
failOnWarnings=True,
|
||||
body=api_json.read(),
|
||||
)
|
||||
|
||||
response.should.have.key("id").which.should.equal(api_id)
|
||||
response.should.have.key("name").which.should.equal("my_api")
|
||||
response.should.have.key("description").which.should.equal("this is my api")
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_put_rest_api__methods_are_created():
|
||||
client = boto3.client("apigateway", region_name="us-east-2")
|
||||
|
||||
response = client.create_rest_api(name="my_api", description="this is my api")
|
||||
api_id = response["id"]
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(path + "/resources/test_api.json", "rb") as api_json:
|
||||
client.put_rest_api(restApiId=api_id, body=api_json.read())
|
||||
|
||||
resources = client.get_resources(restApiId=api_id)
|
||||
root_id = [res for res in resources["items"] if res["path"] == "/"][0]["id"]
|
||||
|
||||
# We have a GET-method
|
||||
resp = client.get_method(restApiId=api_id, resourceId=root_id, httpMethod="GET")
|
||||
resp["methodResponses"].should.equal({"200": {"statusCode": "200"}})
|
||||
|
||||
# We have a POST on /test
|
||||
test_path_id = [res for res in resources["items"] if res["path"] == "/test"][0][
|
||||
"id"
|
||||
]
|
||||
resp = client.get_method(
|
||||
restApiId=api_id, resourceId=test_path_id, httpMethod="POST"
|
||||
)
|
||||
resp["methodResponses"].should.equal({"201": {"statusCode": "201"}})
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_put_rest_api__existing_methods_are_overwritten():
|
||||
client = boto3.client("apigateway", region_name="us-east-2")
|
||||
|
||||
response = client.create_rest_api(name="my_api", description="this is my api")
|
||||
api_id = response["id"]
|
||||
|
||||
resources = client.get_resources(restApiId=api_id)
|
||||
root_id = [resource for resource in resources["items"] if resource["path"] == "/"][
|
||||
0
|
||||
]["id"]
|
||||
|
||||
client.put_method(
|
||||
restApiId=api_id,
|
||||
resourceId=root_id,
|
||||
httpMethod="POST",
|
||||
authorizationType="none",
|
||||
)
|
||||
|
||||
response = client.get_method(
|
||||
restApiId=api_id, resourceId=root_id, httpMethod="POST"
|
||||
)
|
||||
response.should.have.key("httpMethod").equals("POST")
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(path + "/resources/test_api.json", "rb") as api_json:
|
||||
client.put_rest_api(
|
||||
restApiId=api_id,
|
||||
mode="overwrite",
|
||||
failOnWarnings=True,
|
||||
body=api_json.read(),
|
||||
)
|
||||
|
||||
# Since we chose mode=overwrite, the root_id is different
|
||||
resources = client.get_resources(restApiId=api_id)
|
||||
new_root_id = [
|
||||
resource for resource in resources["items"] if resource["path"] == "/"
|
||||
][0]["id"]
|
||||
|
||||
new_root_id.shouldnt.equal(root_id)
|
||||
|
||||
# Our POST-method should be gone
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.get_method(restApiId=api_id, resourceId=new_root_id, httpMethod="POST")
|
||||
err = exc.value.response["Error"]
|
||||
err["Code"].should.equal("NotFoundException")
|
||||
|
||||
# We just have a GET-method, as defined in the JSON
|
||||
client.get_method(restApiId=api_id, resourceId=new_root_id, httpMethod="GET")
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_put_rest_api__existing_methods_still_exist():
|
||||
client = boto3.client("apigateway", region_name="us-east-2")
|
||||
|
||||
response = client.create_rest_api(name="my_api", description="this is my api")
|
||||
api_id = response["id"]
|
||||
|
||||
resources = client.get_resources(restApiId=api_id)
|
||||
root_id = [resource for resource in resources["items"] if resource["path"] == "/"][
|
||||
0
|
||||
]["id"]
|
||||
|
||||
client.put_method(
|
||||
restApiId=api_id,
|
||||
resourceId=root_id,
|
||||
httpMethod="POST",
|
||||
authorizationType="none",
|
||||
)
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(path + "/resources/test_api.json", "rb") as api_json:
|
||||
client.put_rest_api(
|
||||
restApiId=api_id,
|
||||
mode="merge",
|
||||
failOnWarnings=True,
|
||||
body=api_json.read(),
|
||||
)
|
||||
|
||||
response = client.get_method(
|
||||
restApiId=api_id, resourceId=root_id, httpMethod="POST"
|
||||
)
|
||||
response.should.have.key("httpMethod").equals("POST")
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_put_rest_api__fail_on_invalid_spec():
|
||||
client = boto3.client("apigateway", region_name="us-east-2")
|
||||
|
||||
response = client.create_rest_api(name="my_api", description="this is my api")
|
||||
api_id = response["id"]
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(path + "/resources/test_api_invalid.json", "rb") as api_json:
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.put_rest_api(
|
||||
restApiId=api_id, failOnWarnings=True, body=api_json.read()
|
||||
)
|
||||
err = exc.value.response["Error"]
|
||||
err["Code"].should.equal("BadRequestException")
|
||||
err["Message"].should.equal(
|
||||
"Failed to parse the uploaded OpenAPI document due to: 'paths' is a required property"
|
||||
)
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_put_rest_api__fail_on_invalid_version():
|
||||
client = boto3.client("apigateway", region_name="us-east-2")
|
||||
|
||||
response = client.create_rest_api(name="my_api", description="this is my api")
|
||||
api_id = response["id"]
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(path + "/resources/test_api_invalid_version.json", "rb") as api_json:
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.put_rest_api(
|
||||
restApiId=api_id, failOnWarnings=True, body=api_json.read()
|
||||
)
|
||||
err = exc.value.response["Error"]
|
||||
err["Code"].should.equal("BadRequestException")
|
||||
err["Message"].should.equal("Only OpenAPI 3.x.x are currently supported")
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_put_rest_api__fail_on_invalid_mode():
|
||||
client = boto3.client("apigateway", region_name="us-east-2")
|
||||
|
||||
response = client.create_rest_api(name="my_api", description="this is my api")
|
||||
api_id = response["id"]
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(path + "/resources/test_api.json", "rb") as api_json:
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.put_rest_api(restApiId=api_id, mode="unknown", body=api_json.read())
|
||||
err = exc.value.response["Error"]
|
||||
err["Code"].should.equal("BadRequestException")
|
||||
err["Message"].should.equal(
|
||||
'Enumeration value of OpenAPI import mode must be "overwrite" or "merge"'
|
||||
)
|
||||
|
||||
|
||||
@mock_apigateway
|
||||
def test_put_rest_api__as_yaml():
|
||||
client = boto3.client("apigateway", region_name="us-west-2")
|
||||
|
||||
response = client.create_rest_api(name="my_api", description="this is my api")
|
||||
api_id = response["id"]
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(path + "/resources/test_api.yaml", "rb") as api_yaml:
|
||||
response = client.put_rest_api(
|
||||
restApiId=api_id,
|
||||
mode="overwrite",
|
||||
failOnWarnings=True,
|
||||
body=api_yaml.read(),
|
||||
)
|
||||
|
||||
response.should.have.key("id").which.should.equal(api_id)
|
||||
response.should.have.key("name").which.should.equal("my_api")
|
||||
response.should.have.key("description").which.should.equal("this is my api")
|
Loading…
Reference in New Issue
Block a user