API Gateway: put_rest_api and import_rest_api (#5140)

This commit is contained in:
Bert Blommers 2022-05-16 15:13:23 +00:00 committed by GitHub
parent ecfd6d6065
commit 8bbd242d75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 835 additions and 20 deletions

View File

@ -22,7 +22,7 @@
## apigateway ## apigateway
<details> <details>
<summary>62% implemented</summary> <summary>65% implemented</summary>
- [X] create_api_key - [X] create_api_key
- [X] create_authorizer - [X] create_authorizer
@ -93,7 +93,7 @@
- [X] get_request_validator - [X] get_request_validator
- [X] get_request_validators - [X] get_request_validators
- [X] get_resource - [X] get_resource
- [ ] get_resources - [X] get_resources
- [X] get_rest_api - [X] get_rest_api
- [ ] get_rest_apis - [ ] get_rest_apis
- [ ] get_sdk - [ ] get_sdk
@ -111,13 +111,13 @@
- [X] get_vpc_links - [X] get_vpc_links
- [ ] import_api_keys - [ ] import_api_keys
- [ ] import_documentation_parts - [ ] import_documentation_parts
- [ ] import_rest_api - [X] import_rest_api
- [X] put_gateway_response - [X] put_gateway_response
- [X] put_integration - [X] put_integration
- [X] put_integration_response - [X] put_integration_response
- [X] put_method - [X] put_method
- [X] put_method_response - [X] put_method_response
- [ ] put_rest_api - [X] put_rest_api
- [ ] tag_resource - [ ] tag_resource
- [ ] test_invoke_authorizer - [ ] test_invoke_authorizer
- [ ] test_invoke_method - [ ] test_invoke_method

View File

@ -100,7 +100,7 @@ apigateway
- [X] get_request_validator - [X] get_request_validator
- [X] get_request_validators - [X] get_request_validators
- [X] get_resource - [X] get_resource
- [ ] get_resources - [X] get_resources
- [X] get_rest_api - [X] get_rest_api
- [ ] get_rest_apis - [ ] get_rest_apis
- [ ] get_sdk - [ ] get_sdk
@ -122,13 +122,21 @@ apigateway
- [ ] import_api_keys - [ ] import_api_keys
- [ ] import_documentation_parts - [ ] 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_gateway_response
- [X] put_integration - [X] put_integration
- [X] put_integration_response - [X] put_integration_response
- [X] put_method - [X] put_method
- [X] put_method_response - [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 - [ ] tag_resource
- [ ] test_invoke_authorizer - [ ] test_invoke_authorizer
- [ ] test_invoke_method - [ ] test_invoke_method

View File

@ -50,6 +50,25 @@ class IntegrationMethodNotDefined(BadRequestException):
super().__init__("Enumeration value for HttpMethod must be non-empty") 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): class InvalidResourcePathException(BadRequestException):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
@ -233,6 +252,13 @@ class BasePathNotFoundException(NotFoundException):
super().__init__("Invalid base path mapping identifier specified") 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): class VpcLinkNotFound(NotFoundException):
code = 404 code = 404

View File

@ -6,10 +6,13 @@ import re
from collections import defaultdict from collections import defaultdict
from copy import copy from copy import copy
from openapi_spec_validator import validate_spec
import time import time
from urllib.parse import urlparse from urllib.parse import urlparse
import responses import responses
from openapi_spec_validator.exceptions import OpenAPIValidationError
from moto.core import get_account_id, BaseBackend, BaseModel, CloudFormationModel from moto.core import get_account_id, BaseBackend, BaseModel, CloudFormationModel
from .utils import create_id, to_path from .utils import create_id, to_path
from moto.core.utils import path_url, BackendDict from moto.core.utils import path_url, BackendDict
@ -27,9 +30,13 @@ from .exceptions import (
InvalidArn, InvalidArn,
InvalidIntegrationArn, InvalidIntegrationArn,
InvalidHttpEndpoint, InvalidHttpEndpoint,
InvalidOpenAPIDocumentException,
InvalidOpenApiDocVersionException,
InvalidOpenApiModeException,
InvalidResourcePathException, InvalidResourcePathException,
AuthorizerNotFoundException, AuthorizerNotFoundException,
StageNotFoundException, StageNotFoundException,
ResourceIdNotFoundException,
RoleNotSpecified, RoleNotSpecified,
NoIntegrationDefined, NoIntegrationDefined,
NoIntegrationResponseDefined, NoIntegrationResponseDefined,
@ -184,7 +191,7 @@ class Method(CloudFormationModel, dict):
requestValidatorId=kwargs.get("request_validator_id"), requestValidatorId=kwargs.get("request_validator_id"),
) )
) )
self.method_responses = {} self["methodResponses"] = {}
@staticmethod @staticmethod
def cloudformation_name_type(): def cloudformation_name_type():
@ -229,14 +236,14 @@ class Method(CloudFormationModel, dict):
method_response = MethodResponse( method_response = MethodResponse(
response_code, response_models, response_parameters response_code, response_models, response_parameters
) )
self.method_responses[response_code] = method_response self["methodResponses"][response_code] = method_response
return method_response return method_response
def get_response(self, response_code): 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): def delete_response(self, response_code):
return self.method_responses.pop(response_code, None) return self["methodResponses"].pop(response_code, None)
class Resource(CloudFormationModel): class Resource(CloudFormationModel):
@ -288,7 +295,7 @@ class Resource(CloudFormationModel):
backend = apigateway_backends[region_name] backend = apigateway_backends[region_name]
if parent == api_id: if parent == api_id:
# A Root path (/) is automatically created. Any new paths should use this as their parent # 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 == "/"][ root_id = [resource for resource in resources if resource.path_part == "/"][
0 0
].id ].id
@ -789,7 +796,7 @@ class RestAPI(CloudFormationModel):
self.resources = {} self.resources = {}
self.models = {} self.models = {}
self.request_validators = {} self.request_validators = {}
self.add_child("/") # Add default child self.default = self.add_child("/") # Add default child
def __repr__(self): def __repr__(self):
return str(self.id) return str(self.id)
@ -811,8 +818,6 @@ class RestAPI(CloudFormationModel):
} }
def apply_patch_operations(self, patch_operations): def apply_patch_operations(self, patch_operations):
def to_path(prop):
return "/" + prop
for op in patch_operations: for op in patch_operations:
path = op[self.OPERATION_PATH] path = op[self.OPERATION_PATH]
@ -1270,12 +1275,80 @@ class APIGatewayBackend(BaseBackend):
self.apis[api_id] = rest_api self.apis[api_id] = rest_api
return 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): def get_rest_api(self, function_id):
rest_api = self.apis.get(function_id) rest_api = self.apis.get(function_id)
if rest_api is None: if rest_api is None:
raise RestAPINotFound() raise RestAPINotFound()
return rest_api 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): def update_rest_api(self, function_id, patch_operations):
rest_api = self.apis.get(function_id) rest_api = self.apis.get(function_id)
if rest_api is None: if rest_api is None:
@ -1290,19 +1363,24 @@ class APIGatewayBackend(BaseBackend):
rest_api = self.apis.pop(function_id) rest_api = self.apis.pop(function_id)
return rest_api return rest_api
def list_resources(self, function_id): def get_resources(self, function_id):
api = self.get_rest_api(function_id) api = self.get_rest_api(function_id)
return api.resources.values() return api.resources.values()
def get_resource(self, function_id, resource_id): def get_resource(self, function_id, resource_id):
api = self.get_rest_api(function_id) api = self.get_rest_api(function_id)
if resource_id not in api.resources:
raise ResourceIdNotFoundException
resource = api.resources[resource_id] resource = api.resources[resource_id]
return resource return resource
def create_resource(self, function_id, parent_resource_id, path_part): 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): if not re.match("^\\{?[a-zA-Z0-9._-]+\\+?\\}?$", path_part):
raise InvalidResourcePathException() raise InvalidResourcePathException()
api = self.get_rest_api(function_id)
child = api.add_child(path=path_part, parent_id=parent_resource_id) child = api.add_child(path=path_part, parent_id=parent_resource_id)
return child return child
@ -1578,7 +1656,7 @@ class APIGatewayBackend(BaseBackend):
api = self.get_rest_api(function_id) api = self.get_rest_api(function_id)
methods = [ methods = [
list(res.resource_methods.values()) 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] methods = [m for sublist in methods for m in sublist]
if not any(methods): if not any(methods):

View File

@ -4,6 +4,7 @@ from urllib.parse import unquote
from moto.utilities.utils import merge_multiple_dicts from moto.utilities.utils import merge_multiple_dicts
from moto.core.responses import BaseResponse from moto.core.responses import BaseResponse
from .models import apigateway_backends from .models import apigateway_backends
from .utils import deserialize_body
from .exceptions import InvalidRequestInput from .exceptions import InvalidRequestInput
API_KEY_SOURCES = ["AUTHORIZER", "HEADER"] API_KEY_SOURCES = ["AUTHORIZER", "HEADER"]
@ -56,8 +57,16 @@ class APIGatewayResponse(BaseResponse):
apis = self.backend.list_apis() apis = self.backend.list_apis()
return 200, {}, json.dumps({"item": [api.to_dict() for api in apis]}) return 200, {}, json.dumps({"item": [api.to_dict() for api in apis]})
elif self.method == "POST": 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") name = self._get_param("name")
description = self._get_param("description") description = self._get_param("description")
api_key_source = self._get_param("apiKeySource") api_key_source = self._get_param("apiKeySource")
endpoint_configuration = self._get_param("endpointConfiguration") endpoint_configuration = self._get_param("endpointConfiguration")
tags = self._get_param("tags") tags = self._get_param("tags")
@ -82,6 +91,7 @@ class APIGatewayResponse(BaseResponse):
policy=policy, policy=policy,
minimum_compression_size=minimum_compression_size, minimum_compression_size=minimum_compression_size,
) )
return 200, {}, json.dumps(rest_api.to_dict()) return 200, {}, json.dumps(rest_api.to_dict())
def __validte_rest_patch_operations(self, patch_operations): def __validte_rest_patch_operations(self, patch_operations):
@ -99,6 +109,15 @@ class APIGatewayResponse(BaseResponse):
rest_api = self.backend.get_rest_api(function_id) rest_api = self.backend.get_rest_api(function_id)
elif self.method == "DELETE": elif self.method == "DELETE":
rest_api = self.backend.delete_rest_api(function_id) 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": elif self.method == "PATCH":
patch_operations = self._get_param("patchOperations") patch_operations = self._get_param("patchOperations")
response = self.__validte_rest_patch_operations(patch_operations) response = self.__validte_rest_patch_operations(patch_operations)
@ -113,7 +132,7 @@ class APIGatewayResponse(BaseResponse):
function_id = self.path.replace("/restapis/", "", 1).split("/")[0] function_id = self.path.replace("/restapis/", "", 1).split("/")[0]
if self.method == "GET": if self.method == "GET":
resources = self.backend.list_resources(function_id) resources = self.backend.get_resources(function_id)
return ( return (
200, 200,
{}, {},

View File

@ -1,5 +1,7 @@
import random import random
import string import string
import json
import yaml
def create_id(): def create_id():
@ -8,5 +10,17 @@ def create_id():
return "".join(str(random.choice(chars)) for x in range(size)) 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): def to_path(prop):
return "/" + prop return "/" + prop

View File

@ -54,6 +54,7 @@ _dep_aws_xray_sdk = "aws-xray-sdk!=0.96,>=0.93"
_dep_idna = "idna<4,>=2.5" _dep_idna = "idna<4,>=2.5"
_dep_cfn_lint = "cfn-lint>=0.4.0" _dep_cfn_lint = "cfn-lint>=0.4.0"
_dep_sshpubkeys = "sshpubkeys>=3.1.0" _dep_sshpubkeys = "sshpubkeys>=3.1.0"
_dep_openapi = "openapi-spec-validator>=0.2.8"
_dep_pyparsing = "pyparsing>=3.0.0" _dep_pyparsing = "pyparsing>=3.0.0"
_setuptools = "setuptools" _setuptools = "setuptools"
@ -69,6 +70,7 @@ all_extra_deps = [
_dep_cfn_lint, _dep_cfn_lint,
_dep_sshpubkeys, _dep_sshpubkeys,
_dep_pyparsing, _dep_pyparsing,
_dep_openapi,
_setuptools, _setuptools,
] ]
all_server_deps = all_extra_deps + ["flask", "flask-cors"] 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[service_name] = []
extras_per_service.update( 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], "apigatewayv2": [_dep_PyYAML],
"appsync": [_dep_graphql], "appsync": [_dep_graphql],
"awslambda": [_dep_docker], "awslambda": [_dep_docker],

View 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"
}
}
}
}

View 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

View 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"
}
}
}
}

View 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"
}
}
}
}

View File

@ -367,6 +367,7 @@ def test_create_method():
"httpMethod": "GET", "httpMethod": "GET",
"authorizationType": "none", "authorizationType": "none",
"apiKeyRequired": False, "apiKeyRequired": False,
"methodResponses": {},
"ResponseMetadata": {"HTTPStatusCode": 200}, "ResponseMetadata": {"HTTPStatusCode": 200},
} }
) )
@ -401,6 +402,7 @@ def test_create_method_apikeyrequired():
"httpMethod": "GET", "httpMethod": "GET",
"authorizationType": "none", "authorizationType": "none",
"apiKeyRequired": True, "apiKeyRequired": True,
"methodResponses": {},
"ResponseMetadata": {"HTTPStatusCode": 200}, "ResponseMetadata": {"HTTPStatusCode": 200},
} }
) )
@ -452,6 +454,19 @@ def test_create_method_response():
response.should.equal({"ResponseMetadata": {"HTTPStatusCode": 200}}) 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 @mock_apigateway
def test_delete_method(): def test_delete_method():
client = boto3.client("apigateway", region_name="us-west-2") client = boto3.client("apigateway", region_name="us-west-2")

View 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"}})

View 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")