From 9bd6f0a725b5fea685e8b470155495193e643fc8 Mon Sep 17 00:00:00 2001 From: Aidan Fewster Date: Wed, 11 Jul 2018 17:17:58 +0100 Subject: [PATCH] APIGateway: Added usage plan keys API --- moto/apigateway/exceptions.py | 8 +++ moto/apigateway/models.py | 40 +++++++++++++- moto/apigateway/responses.py | 33 +++++++++++- moto/apigateway/urls.py | 4 +- tests/test_apigateway/test_apigateway.py | 52 ++++++++++++++++++ tests/test_apigateway/test_server.py | 68 +++++++++++++++++------- 6 files changed, 184 insertions(+), 21 deletions(-) diff --git a/moto/apigateway/exceptions.py b/moto/apigateway/exceptions.py index d4cf8d1c7..62fa24392 100644 --- a/moto/apigateway/exceptions.py +++ b/moto/apigateway/exceptions.py @@ -8,3 +8,11 @@ class StageNotFoundException(RESTError): def __init__(self): super(StageNotFoundException, self).__init__( "NotFoundException", "Invalid stage identifier specified") + + +class ApiKeyNotFoundException(RESTError): + code = 404 + + def __init__(self): + super(ApiKeyNotFoundException, self).__init__( + "NotFoundException", "Invalid API Key identifier specified") diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index d29a7669f..4094c7a69 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -10,7 +10,7 @@ from boto3.session import Session import responses from moto.core import BaseBackend, BaseModel from .utils import create_id -from .exceptions import StageNotFoundException +from .exceptions import StageNotFoundException, ApiKeyNotFoundException STAGE_URL = "https://{api_id}.execute-api.{region_name}.amazonaws.com/{stage_name}" @@ -322,6 +322,16 @@ class UsagePlan(BaseModel, dict): self['quota'] = quota +class UsagePlanKey(BaseModel, dict): + + def __init__(self, id, type, name, value): + super(UsagePlanKey, self).__init__() + self['id'] = id + self['name'] = name + self['type'] = type + self['value'] = value + + class RestAPI(BaseModel): def __init__(self, id, region_name, name, description): @@ -422,6 +432,7 @@ class APIGatewayBackend(BaseBackend): self.apis = {} self.keys = {} self.usage_plans = {} + self.usage_plan_keys = {} self.region_name = region_name def reset(self): @@ -605,6 +616,33 @@ class APIGatewayBackend(BaseBackend): self.usage_plans.pop(usage_plan_id) return {} + def create_usage_plan_key(self, usage_plan_id, payload): + if usage_plan_id not in self.usage_plan_keys: + self.usage_plan_keys[usage_plan_id] = {} + + key_id = payload["keyId"] + if key_id not in self.keys: + raise ApiKeyNotFoundException() + + api_key = self.keys[key_id] + + usage_plan_key = UsagePlanKey(id=key_id, type=payload["keyType"], name=api_key["name"], value=api_key["value"]) + self.usage_plan_keys[usage_plan_id][usage_plan_key['id']] = usage_plan_key + return usage_plan_key + + def get_usage_plan_keys(self, usage_plan_id): + if usage_plan_id not in self.usage_plan_keys: + return [] + + return list(self.usage_plan_keys[usage_plan_id].values()) + + def get_usage_plan_key(self, usage_plan_id, key_id): + return self.usage_plan_keys[usage_plan_id][key_id] + + def delete_usage_plan_key(self, usage_plan_id, key_id): + self.usage_plan_keys[usage_plan_id].pop(key_id) + return {} + apigateway_backends = {} for region_name in Session().get_available_regions('apigateway'): diff --git a/moto/apigateway/responses.py b/moto/apigateway/responses.py index 1d119baf7..7364ae2cb 100644 --- a/moto/apigateway/responses.py +++ b/moto/apigateway/responses.py @@ -4,7 +4,7 @@ import json from moto.core.responses import BaseResponse from .models import apigateway_backends -from .exceptions import StageNotFoundException +from .exceptions import StageNotFoundException, ApiKeyNotFoundException class APIGatewayResponse(BaseResponse): @@ -270,3 +270,34 @@ class APIGatewayResponse(BaseResponse): elif self.method == 'DELETE': usage_plan_response = self.backend.delete_usage_plan(usage_plan) return 200, {}, json.dumps(usage_plan_response) + + def usage_plan_keys(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + + url_path_parts = self.path.split("/") + usage_plan_id = url_path_parts[2] + + if self.method == 'POST': + try: + usage_plan_response = self.backend.create_usage_plan_key(usage_plan_id, json.loads(self.body)) + except ApiKeyNotFoundException as error: + return error.code, {}, '{{"message":"{0}","code":"{1}"}}'.format(error.message, error.error_type) + + elif self.method == 'GET': + usage_plans_response = self.backend.get_usage_plan_keys(usage_plan_id) + return 200, {}, json.dumps({"item": usage_plans_response}) + + return 200, {}, json.dumps(usage_plan_response) + + def usage_plan_key_individual(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + + url_path_parts = self.path.split("/") + usage_plan_id = url_path_parts[2] + key_id = url_path_parts[4] + + if self.method == 'GET': + usage_plan_response = self.backend.get_usage_plan_key(usage_plan_id, key_id) + elif self.method == 'DELETE': + usage_plan_response = self.backend.delete_usage_plan_key(usage_plan_id, key_id) + return 200, {}, json.dumps(usage_plan_response) diff --git a/moto/apigateway/urls.py b/moto/apigateway/urls.py index a2f4ace9a..5c6d372fa 100644 --- a/moto/apigateway/urls.py +++ b/moto/apigateway/urls.py @@ -21,5 +21,7 @@ url_paths = { '{0}/apikeys$': APIGatewayResponse().apikeys, '{0}/apikeys/(?P[^/]+)': APIGatewayResponse().apikey_individual, '{0}/usageplans$': APIGatewayResponse().usage_plans, - '{0}/usageplans/(?P[^/]+)': APIGatewayResponse().usage_plan_individual, + '{0}/usageplans/(?P[^/]+)/?$': APIGatewayResponse().usage_plan_individual, + '{0}/usageplans/(?P[^/]+)/keys$': APIGatewayResponse().usage_plan_keys, + '{0}/usageplans/(?P[^/]+)/keys/(?P[^/]+)/?$': APIGatewayResponse().usage_plan_key_individual, } diff --git a/tests/test_apigateway/test_apigateway.py b/tests/test_apigateway/test_apigateway.py index ea57c43f4..8a2c4370d 100644 --- a/tests/test_apigateway/test_apigateway.py +++ b/tests/test_apigateway/test_apigateway.py @@ -1032,3 +1032,55 @@ def test_usage_plans(): response = client.get_usage_plans() len(response['items']).should.equal(1) + +@mock_apigateway +def test_usage_plan_keys(): + region_name = 'us-west-2' + usage_plan_id = 'test_usage_plan_id' + client = boto3.client('apigateway', region_name=region_name) + usage_plan_id = "test" + + # Create an API key so we can use it + key_name = 'test-api-key' + response = client.create_api_key(name=key_name) + key_id = response["id"] + key_value = response["value"] + + # Get current plan keys (expect none) + response = client.get_usage_plan_keys(usagePlanId=usage_plan_id) + len(response['items']).should.equal(0) + + # Create usage plan key + key_type = 'API_KEY' + payload = {'usagePlanId': usage_plan_id, 'keyId': key_id, 'keyType': key_type } + response = client.create_usage_plan_key(**payload) + usage_plan_key_id = response["id"] + + # Get current plan keys (expect 1) + response = client.get_usage_plan_keys(usagePlanId=usage_plan_id) + len(response['items']).should.equal(1) + + # Get a single usage plan key and check it matches the created one + usage_plan_key = client.get_usage_plan_key(usagePlanId=usage_plan_id, keyId=usage_plan_key_id) + usage_plan_key['name'].should.equal(key_name) + usage_plan_key['id'].should.equal(key_id) + usage_plan_key['type'].should.equal(key_type) + usage_plan_key['value'].should.equal(key_value) + + # Delete usage plan key + client.delete_usage_plan_key(usagePlanId=usage_plan_id, keyId=key_id) + + # Get current plan keys (expect none) + response = client.get_usage_plan_keys(usagePlanId=usage_plan_id) + len(response['items']).should.equal(0) + +@mock_apigateway +def test_create_usage_plan_key_non_existent_api_key(): + region_name = 'us-west-2' + usage_plan_id = 'test_usage_plan_id' + client = boto3.client('apigateway', region_name=region_name) + usage_plan_id = "test" + + # Attempt to create a usage plan key for a API key that doesn't exists + payload = {'usagePlanId': usage_plan_id, 'keyId': 'non-existent', 'keyType': 'API_KEY' } + client.create_usage_plan_key.when.called_with(**payload).should.throw(ClientError) diff --git a/tests/test_apigateway/test_server.py b/tests/test_apigateway/test_server.py index b76a39e53..953d942cc 100644 --- a/tests/test_apigateway/test_server.py +++ b/tests/test_apigateway/test_server.py @@ -20,40 +20,72 @@ def test_usage_plans_apis(): backend = server.create_backend_app('apigateway') test_client = backend.test_client() - ''' - List usage plans (expect empty) - ''' + # List usage plans (expect empty) res = test_client.get('/usageplans') json.loads(res.data)["item"].should.have.length_of(0) - ''' - Create usage plan - ''' + # Create usage plan res = test_client.post('/usageplans', data=json.dumps({'name': 'test'})) created_plan = json.loads(res.data) created_plan['name'].should.equal('test') - ''' - List usage plans (expect 1 plan) - ''' + # List usage plans (expect 1 plan) res = test_client.get('/usageplans') json.loads(res.data)["item"].should.have.length_of(1) - ''' - Get single usage plan - ''' + # Get single usage plan res = test_client.get('/usageplans/{0}'.format(created_plan["id"])) fetched_plan = json.loads(res.data) fetched_plan.should.equal(created_plan) - ''' - Delete usage plan - ''' + # Delete usage plan res = test_client.delete('/usageplans/{0}'.format(created_plan["id"])) res.data.should.equal(b'{}') - ''' - List usage plans (expect empty again) - ''' + # List usage plans (expect empty again) res = test_client.get('/usageplans') json.loads(res.data)["item"].should.have.length_of(0) + +def test_usage_plans_keys(): + backend = server.create_backend_app('apigateway') + test_client = backend.test_client() + usage_plan_id = 'test_plan_id' + + # Create API key to be used in tests + res = test_client.post('/apikeys', data=json.dumps({'name': 'test'})) + created_api_key = json.loads(res.data) + + # List usage plans keys (expect empty) + res = test_client.get('/usageplans/{0}/keys'.format(usage_plan_id)) + json.loads(res.data)["item"].should.have.length_of(0) + + # Create usage plan key + res = test_client.post('/usageplans/{0}/keys'.format(usage_plan_id), data=json.dumps({'keyId': created_api_key["id"], 'keyType': 'API_KEY'})) + created_usage_plan_key = json.loads(res.data) + + # List usage plans keys (expect 1 key) + res = test_client.get('/usageplans/{0}/keys'.format(usage_plan_id)) + json.loads(res.data)["item"].should.have.length_of(1) + + # Get single usage plan key + res = test_client.get('/usageplans/{0}/keys/{1}'.format(usage_plan_id, created_api_key["id"])) + fetched_plan_key = json.loads(res.data) + fetched_plan_key.should.equal(created_usage_plan_key) + + # Delete usage plan key + res = test_client.delete('/usageplans/{0}/keys/{1}'.format(usage_plan_id, created_api_key["id"])) + res.data.should.equal(b'{}') + + # List usage plans keys (expect to be empty again) + res = test_client.get('/usageplans/{0}/keys'.format(usage_plan_id)) + json.loads(res.data)["item"].should.have.length_of(0) + +def test_create_usage_plans_key_non_existent_api_key(): + backend = server.create_backend_app('apigateway') + test_client = backend.test_client() + usage_plan_id = 'test_plan_id' + + # Create usage plan key with non-existent api key + res = test_client.post('/usageplans/{0}/keys'.format(usage_plan_id), data=json.dumps({'keyId': 'non-existent', 'keyType': 'API_KEY'})) + res.status_code.should.equal(404) +