From 43e430560c82699ef1f34b87714348114db8a8c5 Mon Sep 17 00:00:00 2001 From: Aidan Fewster Date: Tue, 10 Jul 2018 14:58:02 +0100 Subject: [PATCH 1/2] APIGateway: Added API for usage plans --- moto/apigateway/models.py | 29 +++++++++++++++ moto/apigateway/responses.py | 22 ++++++++++++ moto/apigateway/urls.py | 2 ++ tests/test_apigateway/test_apigateway.py | 37 +++++++++++++++++++ tests/test_apigateway/test_server.py | 45 +++++++++++++++++++++++- 5 files changed, 134 insertions(+), 1 deletion(-) diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index 5fdaed1e8..d29a7669f 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -309,6 +309,19 @@ class ApiKey(BaseModel, dict): self['stageKeys'] = stageKeys +class UsagePlan(BaseModel, dict): + + def __init__(self, name=None, description=None, apiStages=[], + throttle=None, quota=None): + super(UsagePlan, self).__init__() + self['id'] = create_id() + self['name'] = name + self['description'] = description + self['apiStages'] = apiStages + self['throttle'] = throttle + self['quota'] = quota + + class RestAPI(BaseModel): def __init__(self, id, region_name, name, description): @@ -408,6 +421,7 @@ class APIGatewayBackend(BaseBackend): super(APIGatewayBackend, self).__init__() self.apis = {} self.keys = {} + self.usage_plans = {} self.region_name = region_name def reset(self): @@ -576,6 +590,21 @@ class APIGatewayBackend(BaseBackend): self.keys.pop(api_key_id) return {} + def create_usage_plan(self, payload): + plan = UsagePlan(**payload) + self.usage_plans[plan['id']] = plan + return plan + + def get_usage_plans(self): + return list(self.usage_plans.values()) + + def get_usage_plan(self, usage_plan_id): + return self.usage_plans[usage_plan_id] + + def delete_usage_plan(self, usage_plan_id): + self.usage_plans.pop(usage_plan_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 ff6ef1f33..1d119baf7 100644 --- a/moto/apigateway/responses.py +++ b/moto/apigateway/responses.py @@ -248,3 +248,25 @@ class APIGatewayResponse(BaseResponse): elif self.method == 'DELETE': apikey_response = self.backend.delete_apikey(apikey) return 200, {}, json.dumps(apikey_response) + + def usage_plans(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + + if self.method == 'POST': + usage_plan_response = self.backend.create_usage_plan(json.loads(self.body)) + elif self.method == 'GET': + usage_plans_response = self.backend.get_usage_plans() + return 200, {}, json.dumps({"item": usage_plans_response}) + return 200, {}, json.dumps(usage_plan_response) + + def usage_plan_individual(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + + url_path_parts = self.path.split("/") + usage_plan = url_path_parts[2] + + if self.method == 'GET': + usage_plan_response = self.backend.get_usage_plan(usage_plan) + elif self.method == 'DELETE': + usage_plan_response = self.backend.delete_usage_plan(usage_plan) + return 200, {}, json.dumps(usage_plan_response) diff --git a/moto/apigateway/urls.py b/moto/apigateway/urls.py index ca1f445a7..a2f4ace9a 100644 --- a/moto/apigateway/urls.py +++ b/moto/apigateway/urls.py @@ -20,4 +20,6 @@ url_paths = { '{0}/restapis/(?P[^/]+)/resources/(?P[^/]+)/methods/(?P[^/]+)/integration/responses/(?P\d+)/?$': APIGatewayResponse().integration_responses, '{0}/apikeys$': APIGatewayResponse().apikeys, '{0}/apikeys/(?P[^/]+)': APIGatewayResponse().apikey_individual, + '{0}/usageplans$': APIGatewayResponse().usage_plans, + '{0}/usageplans/(?P[^/]+)': APIGatewayResponse().usage_plan_individual, } diff --git a/tests/test_apigateway/test_apigateway.py b/tests/test_apigateway/test_apigateway.py index 99fef0481..ea57c43f4 100644 --- a/tests/test_apigateway/test_apigateway.py +++ b/tests/test_apigateway/test_apigateway.py @@ -995,3 +995,40 @@ def test_api_keys(): response = client.get_api_keys() len(response['items']).should.equal(1) + +@mock_apigateway +def test_usage_plans(): + region_name = 'us-west-2' + client = boto3.client('apigateway', region_name=region_name) + response = client.get_usage_plans() + len(response['items']).should.equal(0) + + usage_plan_name = 'TEST-PLAN' + payload = {'name': usage_plan_name} + response = client.create_usage_plan(**payload) + usage_plan = client.get_usage_plan(usagePlanId=response['id']) + usage_plan['name'].should.equal(usage_plan_name) + usage_plan['apiStages'].should.equal([]) + + usage_plan_name = 'TEST-PLAN-2' + usage_plan_description = 'Description' + usage_plan_quota = {'limit': 10, 'period': 'DAY', 'offset': 0} + usage_plan_throttle = {'rateLimit': 2, 'burstLimit': 1} + usage_plan_api_stages = [{'apiId': 'foo', 'stage': 'bar'}] + payload = {'name': usage_plan_name, 'description': usage_plan_description, 'quota': usage_plan_quota, 'throttle': usage_plan_throttle, 'apiStages': usage_plan_api_stages} + response = client.create_usage_plan(**payload) + usage_plan_id = response['id'] + usage_plan = client.get_usage_plan(usagePlanId=usage_plan_id) + usage_plan['name'].should.equal(usage_plan_name) + usage_plan['description'].should.equal(usage_plan_description) + usage_plan['apiStages'].should.equal(usage_plan_api_stages) + usage_plan['throttle'].should.equal(usage_plan_throttle) + usage_plan['quota'].should.equal(usage_plan_quota) + + response = client.get_usage_plans() + len(response['items']).should.equal(2) + + client.delete_usage_plan(usagePlanId=usage_plan_id) + + response = client.get_usage_plans() + len(response['items']).should.equal(1) diff --git a/tests/test_apigateway/test_server.py b/tests/test_apigateway/test_server.py index f2a29e253..b76a39e53 100644 --- a/tests/test_apigateway/test_server.py +++ b/tests/test_apigateway/test_server.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import sure # noqa +import json import moto.server as server @@ -9,8 +10,50 @@ Test the different server responses def test_list_apis(): - backend = server.create_backend_app("apigateway") + backend = server.create_backend_app('apigateway') test_client = backend.test_client() res = test_client.get('/restapis') res.data.should.equal(b'{"item": []}') + +def test_usage_plans_apis(): + backend = server.create_backend_app('apigateway') + test_client = backend.test_client() + + ''' + List usage plans (expect empty) + ''' + res = test_client.get('/usageplans') + json.loads(res.data)["item"].should.have.length_of(0) + + ''' + 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) + ''' + res = test_client.get('/usageplans') + json.loads(res.data)["item"].should.have.length_of(1) + + ''' + 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 + ''' + res = test_client.delete('/usageplans/{0}'.format(created_plan["id"])) + res.data.should.equal(b'{}') + + ''' + List usage plans (expect empty again) + ''' + res = test_client.get('/usageplans') + json.loads(res.data)["item"].should.have.length_of(0) From 9bd6f0a725b5fea685e8b470155495193e643fc8 Mon Sep 17 00:00:00 2001 From: Aidan Fewster Date: Wed, 11 Jul 2018 17:17:58 +0100 Subject: [PATCH 2/2] 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) +