diff --git a/poc/bin/kapow b/poc/bin/kapow index 0dd3b0c..93ddd68 100755 --- a/poc/bin/kapow +++ b/poc/bin/kapow @@ -16,11 +16,12 @@ # limitations under the License. # -from urllib.parse import urlparse from itertools import repeat +from urllib.parse import urlparse from uuid import uuid4 import asyncio import io +import json import logging import os import shlex @@ -278,15 +279,32 @@ def append_route(app): async def _append_route(request): """Create a new Kapow! route.""" app.router._frozen = False - content = await request.json() + + try: + content = await request.json() + except Exception as exc: + return web.Response(status=400, reason="Malformed JSON") + name = "ROUTE_" + str(uuid4()).replace('-', '_') - app.router.add_route(content["method"], - content["url_pattern"], - handle_route(content["entrypoint"], - content["command"]), - name=name) - print(f'Route created {content["method"]} {content["url_pattern"]}') - return web.json_response(name) + try: + app.router.add_route(content["method"], + content["url_pattern"], + handle_route(content["entrypoint"], + content["command"]), + name=name) + except KeyError as exc: + missing = list() + for field in ("method", "url_pattern", "entrypoint", "command"): + if field not in content: + missing.append(field) + return web.Response(status=422, + reason="Missing Mandatory Field", + body=json.dumps({"missing_mandatory_fields": missing})) + except ValueError as exc: + return web.Response(status=422, reason="Invalid Route Spec") + else: + print(f'Route created {content["method"]} {content["url_pattern"]}') + return web.json_response(name, status=201) return _append_route diff --git a/poc/dist/kapow-0.0.1-py3.7.egg b/poc/dist/kapow-0.0.1-py3.7.egg new file mode 100644 index 0000000..4bc98c7 Binary files /dev/null and b/poc/dist/kapow-0.0.1-py3.7.egg differ diff --git a/spec/test/Makefile b/spec/test/Makefile index 49b8678..f90954f 100644 --- a/spec/test/Makefile +++ b/spec/test/Makefile @@ -1,5 +1,7 @@ +wip: + pipenv run behave --stop --wip test: gherkin-lint - pipenv run behave --no-capture --stop + pipenv run behave --no-capture catalog: pipenv run behave --steps-catalog diff --git a/spec/test/features/control/append/error_malformed.feature b/spec/test/features/control/append/error_malformed.feature index 1cf657f..4c4f9f0 100644 --- a/spec/test/features/control/append/error_malformed.feature +++ b/spec/test/features/control/append/error_malformed.feature @@ -8,7 +8,7 @@ Feature: Kapow! server reject append requests with malformed JSON bodies. the server will respond with a bad request error. Given I have a running Kapow! server - When I try to append with this JSON document: + When I try to append with this malformed JSON document: """ { "method" "GET", @@ -20,5 +20,5 @@ Feature: Kapow! server reject append requests with malformed JSON bodies. } """ Then I get 400 as response code - And I get "Malformed JSON" as response phrase + And I get "Malformed JSON" as response reason phrase And I get an empty response body diff --git a/spec/test/features/control/insert/error_malformed.feature b/spec/test/features/control/insert/error_malformed.feature index 40e50b2..c5951dd 100644 --- a/spec/test/features/control/insert/error_malformed.feature +++ b/spec/test/features/control/insert/error_malformed.feature @@ -21,5 +21,5 @@ Feature: Kapow! server reject insert requests with malformed JSON bodies. } """ Then I get bad request as response code - And I get "Malformed JSON" as response phrase + And I get "Malformed JSON" as response reason phrase And I get an empty response body diff --git a/spec/test/features/steps/comparedict.py b/spec/test/features/steps/comparedict.py new file mode 100644 index 0000000..7b5f968 --- /dev/null +++ b/spec/test/features/steps/comparedict.py @@ -0,0 +1,38 @@ +from functools import singledispatch + + +def assert_same_type(f): + def wrapper(a, b): + if type(a) != type(b): + raise TypeError("Non-matching types") + return f(a, b) + return wrapper + + +@singledispatch +@assert_same_type +def is_subset(model, obj): + return model == obj + + +@is_subset.register(dict) +@assert_same_type +def _(model, obj): + for key, value in model.items(): + if key not in obj or not is_subset(value, obj[key]): + return False + return True + + +@is_subset.register(list) +@assert_same_type +def _(model, obj): + if type(model) != type(obj): + raise TypeError("Non-matching types") + return is_subset(set(model), set(obj)) + + +@is_subset.register(set) +@assert_same_type +def _(model, obj): + return model <= obj diff --git a/spec/test/features/steps/steps.py b/spec/test/features/steps/steps.py index c95a2d1..f8c4df9 100644 --- a/spec/test/features/steps/steps.py +++ b/spec/test/features/steps/steps.py @@ -1,12 +1,15 @@ -import subprocess +from contextlib import suppress from time import sleep +import json import shlex import socket -from contextlib import suppress +import subprocess import requests -from environconfig import EnvironConfig, StringVar, IntVar +from environconfig import EnvironConfig, StringVar, IntVar, BooleanVar +from comparedict import is_subset +import logging class Env(EnvironConfig): #: How to run Kapow! server @@ -20,9 +23,30 @@ class Env(EnvironConfig): KAPOW_BOOT_TIMEOUT = IntVar(default=10) -@given('I have a just started Kapow! server') -@given('I have a running Kapow! server') -def step_impl(context): + KAPOW_DEBUG_TESTS = BooleanVar(default=True) + + +if Env.KAPOW_DEBUG_TESTS: + # These two lines enable debugging at httplib level + # (requests->urllib3->http.client) You will see the REQUEST, + # including HEADERS and DATA, and RESPONSE with HEADERS but without + # DATA. The only thing missing will be the response.body which is + # not logged. + try: + import http.client as http_client + except ImportError: + # Python 2 + import httplib as http_client + http_client.HTTPConnection.debuglevel = 1 + + # You must initialize logging, otherwise you'll not see debug output. + logging.basicConfig() + logging.getLogger().setLevel(logging.DEBUG) + requests_log = logging.getLogger("requests.packages.urllib3") + requests_log.setLevel(logging.DEBUG) + requests_log.propagate = True + +def run_kapow_server(context): context.server = subprocess.Popen( shlex.split(Env.KAPOW_SERVER_CMD), stdout=subprocess.DEVNULL, @@ -44,6 +68,11 @@ def step_impl(context): assert open_ports, "API is unreachable after KAPOW_BOOT_TIMEOUT" +@given('I have a just started Kapow! server') +@given('I have a running Kapow! server') +def step_impl(context): + run_kapow_server(context) + @when('I request a routes listing') def step_impl(context): @@ -58,13 +87,7 @@ def step_impl(context): @given('I have a Kapow! server whith the following routes') def step_impl(context): - context.server = subprocess.Popen( - Env.KAPOW_SERVER_CMD, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - shell=True) - is_running = context.server.poll() is None - assert is_running, "Server is not running!" + run_kapow_server(context) if not hasattr(context, 'table'): raise RuntimeError("A table must be set for this step.") @@ -97,30 +120,32 @@ def step_impl(context): if not hasattr(context, 'table'): raise RuntimeError("A table must be set for this step.") - for row in context.table: - response = requests.post(f"{Env.KAPOW_CONTROLAPI_URL}/routes", - json={h: row[h] for h in row.headings}) - response.raise_for_status() + row = context.table[0] + context.response = requests.post(f"{Env.KAPOW_CONTROLAPI_URL}/routes", + json={h: row[h] for h in row.headings}) @then('I get {code} as response code') def step_impl(context, code): - raise NotImplementedError('STEP: Then I get unprocessable entity as response code') + assert context.response.status_code == int(code), f"Got {context.response.status_code} instead" @then('I get "{reason}" as response reason phrase') def step_impl(context, reason): - raise NotImplementedError('STEP: Then I get "Missing Mandatory Field" as response phrase') + assert context.response.reason == reason, f"Got {context.response.reason} instead" @then('I get the following entity as response body') def step_impl(context): - raise NotImplementedError('STEP: Then I get the following entity as response body') + for row in context.table: + for name, value in row.items(): + assert name in context.response.json(), f"Field {name} not present in {context.response.json()}" + assert set(json.loads(value)) == set(context.response.json()[name]) @then('I get an empty response body') def step_impl(context): - raise NotImplementedError('STEP: Then I get an empty response body') + assert context.response.content == b'', f"Response body is not empty. Got {context.response.content} instead." @when('I delete the route with id "{id}"') @@ -132,6 +157,16 @@ def step_impl(context, id): def step_impl(context, id): raise NotImplementedError('STEP: Given It has a route with id "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx"') + @when('I insert the route') def step_impl(context): raise NotImplementedError('STEP: When I insert the route') + + +@when('I try to append with this malformed JSON document') +@when('I try to append with this JSON document') +def step_impl(context): + context.response = requests.post( + f"{Env.KAPOW_CONTROLAPI_URL}/routes", + headers={"Content-Type": "application/json"}, + data=context.text)