Adding web app tests (#13)

* Adding web app tests

Signed-off-by: Denis Makogon <denys.makogon@oracle.com>

* addressing review comments

Signed-off-by: Denis Makogon <denys.makogon@oracle.com>
This commit is contained in:
Denis Makogon 2019-01-17 02:26:55 +02:00 committed by GitHub
parent 5cd67e4122
commit 7070e5124a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 277 additions and 159 deletions

View File

@ -21,8 +21,13 @@ class Converter(object):
TYPE = None
def read(self, event, headers: dict, body: typing.IO,
data_unmarshaller: typing.Callable) -> base.BaseEvent:
def read(
self,
event,
headers: dict,
body: typing.IO,
data_unmarshaller: typing.Callable
) -> base.BaseEvent:
raise Exception("not implemented")
def event_supported(self, event: object) -> bool:
@ -31,6 +36,9 @@ class Converter(object):
def can_read(self, content_type: str) -> bool:
raise Exception("not implemented")
def write(self, event: base.BaseEvent,
data_marshaller: typing.Callable) -> (dict, typing.IO):
def write(
self,
event: base.BaseEvent,
data_marshaller: typing.Callable
) -> (dict, object):
raise Exception("not implemented")

View File

@ -23,7 +23,7 @@ from cloudevents.sdk.event import v02
class BinaryHTTPCloudEventConverter(base.Converter):
TYPE = "binary"
SUPPORTED_VERSIONS = [v02.Event, ]
SUPPORTED_VERSIONS = [v02.Event]
def can_read(self, content_type: str) -> bool:
return True
@ -31,17 +31,21 @@ class BinaryHTTPCloudEventConverter(base.Converter):
def event_supported(self, event: object) -> bool:
return type(event) in self.SUPPORTED_VERSIONS
def read(self,
event: event_base.BaseEvent,
headers: dict, body: typing.IO,
data_unmarshaller: typing.Callable) -> event_base.BaseEvent:
def read(
self,
event: event_base.BaseEvent,
headers: dict,
body: typing.IO,
data_unmarshaller: typing.Callable,
) -> event_base.BaseEvent:
if type(event) not in self.SUPPORTED_VERSIONS:
raise exceptions.UnsupportedEvent(type(event))
event.UnmarshalBinary(headers, body, data_unmarshaller)
return event
def write(self, event: event_base.BaseEvent,
data_marshaller: typing.Callable) -> (dict, typing.IO):
def write(
self, event: event_base.BaseEvent, data_marshaller: typing.Callable
) -> (dict, typing.IO):
return event.MarshalBinary(data_marshaller)

View File

@ -30,17 +30,20 @@ class JSONHTTPCloudEventConverter(base.Converter):
# structured format supported by both spec 0.1 and 0.2
return True
def read(self, event: event_base.BaseEvent,
headers: dict,
body: typing.IO,
data_unmarshaller: typing.Callable) -> event_base.BaseEvent:
def read(
self,
event: event_base.BaseEvent,
headers: dict,
body: typing.IO,
data_unmarshaller: typing.Callable,
) -> event_base.BaseEvent:
event.UnmarshalJSON(body, data_unmarshaller)
return event
def write(self,
event: event_base.BaseEvent,
data_marshaller: typing.Callable) -> (dict, typing.IO):
http_headers = {'content-type': self.MIME_TYPE}
def write(
self, event: event_base.BaseEvent, data_marshaller: typing.Callable
) -> (dict, typing.IO):
http_headers = {"content-type": self.MIME_TYPE}
return http_headers, event.MarshalJSON(data_marshaller)

View File

@ -18,7 +18,6 @@ import typing
class EventGetterSetter(object):
def CloudEventVersion(self) -> str:
raise Exception("not implemented")
@ -76,18 +75,13 @@ class EventGetterSetter(object):
class BaseEvent(EventGetterSetter):
def Properties(self, with_nullable=False) -> dict:
props = dict()
for name, value in self.__dict__.items():
if str(name).startswith("ce__"):
v = value.get()
if v is not None or with_nullable:
props.update(
{
str(name).replace("ce__", ""): value.get()
}
)
props.update({str(name).replace("ce__", ""): value.get()})
return props
@ -119,33 +113,38 @@ class BaseEvent(EventGetterSetter):
props["data"] = data_marshaller(props.get("data"))
return io.BytesIO(json.dumps(props).encode("utf-8"))
def UnmarshalJSON(self, b: typing.IO,
data_unmarshaller: typing.Callable):
def UnmarshalJSON(self, b: typing.IO, data_unmarshaller: typing.Callable):
raw_ce = json.load(b)
for name, value in raw_ce.items():
if name == "data":
value = data_unmarshaller(value)
self.Set(name, value)
def UnmarshalBinary(self, headers: dict, body: typing.IO,
data_unmarshaller: typing.Callable):
BINARY_MAPPING = {
'content-type': 'contenttype',
def UnmarshalBinary(
self,
headers: dict,
body: typing.IO,
data_unmarshaller: typing.Callable
):
binary_mapping = {
"content-type": "contenttype",
# TODO(someone): add Distributed Tracing. It's not clear
# if this is one extension or two.
# https://github.com/cloudevents/spec/blob/master/extensions/distributed-tracing.md
}
for header, value in headers.items():
header = header.lower()
if header in BINARY_MAPPING:
self.Set(BINARY_MAPPING[header], value)
if header in binary_mapping:
self.Set(binary_mapping[header], value)
elif header.startswith("ce-"):
self.Set(header[3:], value)
self.Set("data", data_unmarshaller(body))
def MarshalBinary(
self, data_marshaller: typing.Callable) -> (dict, object):
self,
data_marshaller: typing.Callable
) -> (dict, object):
headers = {}
if self.ContentType():
headers["content-type"] = self.ContentType()
@ -159,5 +158,4 @@ class BaseEvent(EventGetterSetter):
headers["ce-{0}".format(key)] = value
data, _ = self.Get("data")
return headers, io.BytesIO(
str(data_marshaller(data)).encode("utf-8"))
return headers, data_marshaller(data)

View File

@ -14,7 +14,6 @@
class Option(object):
def __init__(self, name, value, is_required):
self.name = name
self.value = value
@ -25,7 +24,9 @@ class Option(object):
if self.is_required and is_none:
raise ValueError(
"Attribute value error: '{0}', "
"invalid new value.".format(self.name))
"" "invalid new value."
.format(self.name)
)
self.value = new_value

View File

@ -12,25 +12,62 @@
# License for the specific language governing permissions and limitations
# under the License.
from cloudevents.sdk.event import opt
from cloudevents.sdk.event import base
from cloudevents.sdk.event import opt
class Event(base.BaseEvent):
def __init__(self):
self.ce__cloudEventsVersion = opt.Option(
"cloudEventsVersion", "0.1", True)
self.ce__eventType = opt.Option("eventType", None, True)
"cloudEventsVersion",
"0.1",
True
)
self.ce__eventType = opt.Option(
"eventType",
None,
True
)
self.ce__eventTypeVersion = opt.Option(
"eventTypeVersion", None, False)
self.ce__source = opt.Option("source", None, True)
self.ce__eventID = opt.Option("eventID", None, True)
self.ce__eventTime = opt.Option("eventTime", None, True)
self.ce__schemaURL = opt.Option("schemaURL", None, False)
self.ce__contentType = opt.Option("contentType", None, False)
self.ce__data = opt.Option("data", None, False)
self.ce__extensions = opt.Option("extensions", dict(), False)
"eventTypeVersion",
None,
False
)
self.ce__source = opt.Option(
"source",
None,
True
)
self.ce__eventID = opt.Option(
"eventID",
None,
True
)
self.ce__eventTime = opt.Option(
"eventTime",
None,
True
)
self.ce__schemaURL = opt.Option(
"schemaURL",
None,
False
)
self.ce__contentType = opt.Option(
"contentType",
None,
False
)
self.ce__data = opt.Option(
"data",
None,
False
)
self.ce__extensions = opt.Option(
"extensions",
dict(),
False
)
def CloudEventVersion(self) -> str:
return self.ce__cloudEventsVersion.get()

View File

@ -12,12 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
from cloudevents.sdk.event import opt
from cloudevents.sdk.event import base
from cloudevents.sdk.event import opt
class Event(base.BaseEvent):
def __init__(self):
self.ce__specversion = opt.Option("specversion", "0.2", True)
self.ce__type = opt.Option("type", None, True)

View File

@ -14,34 +14,34 @@
class UnsupportedEvent(Exception):
def __init__(self, event_class):
super().__init__("Invalid CloudEvent class: "
"'{0}'".format(event_class))
super().__init__(
"Invalid CloudEvent class: '{0}'".format(event_class)
)
class InvalidDataUnmarshaller(Exception):
def __init__(self):
super().__init__(
"Invalid data unmarshaller, is not a callable")
super().__init__("Invalid data unmarshaller, is not a callable")
class InvalidDataMarshaller(Exception):
def __init__(self):
super().__init__(
"Invalid data marshaller, is not a callable")
"Invalid data marshaller, is not a callable"
)
class NoSuchConverter(Exception):
def __init__(self, converter_type):
super().__init__(
"No such converter {0}".format(converter_type))
"No such converter {0}".format(converter_type)
)
class UnsupportedEventConverter(Exception):
def __init__(self, content_type):
super().__init__(
"Unable to identify valid event converter "
"for content-type: '{0}'".format(content_type))
"for content-type: '{0}'".format(content_type)
)

View File

@ -35,14 +35,16 @@ class HTTPMarshaller(object):
:param converters: a list of HTTP-to-CloudEvent-to-HTTP constructors
:type converters: typing.List[base.Converter]
"""
self.__converters = (c for c in converters)
self.__converters = [c for c in converters]
self.__converters_by_type = {c.TYPE: c for c in converters}
def FromRequest(self, event: event_base.BaseEvent,
headers: dict,
body: typing.IO,
data_unmarshaller:
typing.Callable) -> event_base.BaseEvent:
def FromRequest(
self,
event: event_base.BaseEvent,
headers: dict,
body: typing.IO,
data_unmarshaller: typing.Callable,
) -> event_base.BaseEvent:
"""
Reads a CloudEvent from an HTTP headers and request body
:param event: CloudEvent placeholder
@ -59,8 +61,7 @@ class HTTPMarshaller(object):
if not isinstance(data_unmarshaller, typing.Callable):
raise exceptions.InvalidDataUnmarshaller()
content_type = headers.get(
"content-type", headers.get("Content-Type"))
content_type = headers.get("content-type", headers.get("Content-Type"))
for cnvrtr in self.__converters:
if cnvrtr.can_read(content_type) and cnvrtr.event_supported(event):
@ -68,11 +69,16 @@ class HTTPMarshaller(object):
raise exceptions.UnsupportedEventConverter(
"No registered marshaller for {0} in {1}".format(
content_type, self.__converters))
content_type, self.__converters
)
)
def ToRequest(self, event: event_base.BaseEvent,
converter_type: str,
data_marshaller: typing.Callable) -> (dict, typing.IO):
def ToRequest(
self,
event: event_base.BaseEvent,
converter_type: str,
data_marshaller: typing.Callable,
) -> (dict, typing.IO):
"""
Writes a CloudEvent into a HTTP-ready form of headers and request body
:param event: CloudEvent
@ -101,14 +107,17 @@ def NewDefaultHTTPMarshaller() -> HTTPMarshaller:
:return: an instance of HTTP marshaller
:rtype: cloudevents.sdk.marshaller.HTTPMarshaller
"""
return HTTPMarshaller([
structured.NewJSONHTTPCloudEventConverter(),
binary.NewBinaryHTTPCloudEventConverter(),
])
return HTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter(),
binary.NewBinaryHTTPCloudEventConverter(),
]
)
def NewHTTPMarshaller(
converters: typing.List[base.Converter]) -> HTTPMarshaller:
converters: typing.List[base.Converter]
) -> HTTPMarshaller:
"""
Creates the default HTTP marshaller with both
structured and binary converters

View File

@ -25,7 +25,7 @@ headers = {
"ce-id": ce_id,
"ce-time": eventTime,
"ce-source": source,
"Content-Type": contentType
"Content-Type": contentType,
}
ce = {
"specversion": specversion,
@ -33,5 +33,5 @@ ce = {
"id": ce_id,
"time": eventTime,
"source": source,
"contenttype": contentType
"contenttype": contentType,
}

View File

@ -30,10 +30,7 @@ from cloudevents.tests import data
def test_binary_converter_upstream():
m = marshaller.NewHTTPMarshaller(
[
binary.NewBinaryHTTPCloudEventConverter()
]
)
[binary.NewBinaryHTTPCloudEventConverter()])
event = m.FromRequest(v02.Event(), data.headers, None, lambda x: x)
assert event is not None
assert event.Get("type") == (data.ce_type, True)
@ -42,15 +39,12 @@ def test_binary_converter_upstream():
def test_structured_converter_upstream():
m = marshaller.NewHTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter()
]
)
[structured.NewJSONHTTPCloudEventConverter()])
event = m.FromRequest(
v02.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(json.dumps(data.ce)),
lambda x: x.read()
lambda x: x.read(),
)
assert event is not None
@ -60,41 +54,40 @@ def test_structured_converter_upstream():
def test_binary_converter_v01():
m = marshaller.NewHTTPMarshaller(
[
binary.NewBinaryHTTPCloudEventConverter()
]
)
[binary.NewBinaryHTTPCloudEventConverter()])
pytest.raises(
exceptions.UnsupportedEventConverter,
m.FromRequest,
v01.Event, {}, None, lambda x: x)
v01.Event,
{},
None,
lambda x: x,
)
def test_unsupported_converter_v01():
m = marshaller.NewHTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter()
]
)
[structured.NewJSONHTTPCloudEventConverter()])
pytest.raises(
exceptions.UnsupportedEventConverter,
m.FromRequest,
v01.Event, {}, None, lambda x: x)
v01.Event,
{},
None,
lambda x: x,
)
def test_structured_converter_v01():
m = marshaller.NewHTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter()
]
)
[structured.NewJSONHTTPCloudEventConverter()])
event = m.FromRequest(
v01.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(json.dumps(data.ce)),
lambda x: x.read()
lambda x: x.read(),
)
assert event is not None
@ -109,7 +102,7 @@ def test_default_http_marshaller_with_structured():
v02.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(json.dumps(data.ce)),
lambda x: x.read()
lambda x: x.read(),
)
assert event is not None
assert event.Get("type") == (data.ce_type, True)
@ -120,8 +113,7 @@ def test_default_http_marshaller_with_binary():
m = marshaller.NewDefaultHTTPMarshaller()
event = m.FromRequest(
v02.Event(),
data.headers,
v02.Event(), data.headers,
io.StringIO(json.dumps(data.body)),
json.load
)
@ -133,17 +125,14 @@ def test_default_http_marshaller_with_binary():
def test_unsupported_event_configuration():
m = marshaller.NewHTTPMarshaller(
[
binary.NewBinaryHTTPCloudEventConverter()
]
)
[binary.NewBinaryHTTPCloudEventConverter()])
pytest.raises(
exceptions.UnsupportedEventConverter,
m.FromRequest,
v01.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(json.dumps(data.ce)),
lambda x: x.read()
lambda x: x.read(),
)
@ -152,12 +141,12 @@ def test_invalid_data_unmarshaller():
pytest.raises(
exceptions.InvalidDataUnmarshaller,
m.FromRequest,
v01.Event(), {}, None, None)
v01.Event(), {}, None, None
)
def test_invalid_data_marshaller():
m = marshaller.NewDefaultHTTPMarshaller()
pytest.raises(
exceptions.InvalidDataMarshaller,
m.ToRequest,
v01.Event(), "blah", None)
exceptions.InvalidDataMarshaller, m.ToRequest, v01.Event(), "blah", None
)

View File

@ -27,13 +27,13 @@ from cloudevents.tests import data
def test_event_pipeline_upstream():
event = (
v02.Event().
SetContentType(data.contentType).
SetData(data.body).
SetEventID(data.ce_id).
SetSource(data.source).
SetEventTime(data.eventTime).
SetEventType(data.ce_type)
v02.Event()
.SetContentType(data.contentType)
.SetData(data.body)
.SetEventID(data.ce_id)
.SetSource(data.source)
.SetEventTime(data.eventTime)
.SetEventType(data.ce_type)
)
m = marshaller.NewDefaultHTTPMarshaller()
new_headers, body = m.ToRequest(event, converters.TypeBinary, lambda x: x)
@ -44,29 +44,25 @@ def test_event_pipeline_upstream():
assert "ce-id" in new_headers
assert "ce-time" in new_headers
assert "content-type" in new_headers
assert isinstance(body, io.BytesIO)
assert data.body == body.read().decode("utf-8")
assert isinstance(body, str)
assert data.body == body
def test_event_pipeline_v01():
event = (
v01.Event().
SetContentType(data.contentType).
SetData(data.body).
SetEventID(data.ce_id).
SetSource(data.source).
SetEventTime(data.eventTime).
SetEventType(data.ce_type)
)
m = marshaller.NewHTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter()
]
v01.Event()
.SetContentType(data.contentType)
.SetData(data.body)
.SetEventID(data.ce_id)
.SetSource(data.source)
.SetEventTime(data.eventTime)
.SetEventType(data.ce_type)
)
m = marshaller.NewHTTPMarshaller([structured.NewJSONHTTPCloudEventConverter()])
_, body = m.ToRequest(event, converters.TypeStructured, lambda x: x)
assert isinstance(body, io.BytesIO)
new_headers = json.load(io.TextIOWrapper(body, encoding='utf-8'))
new_headers = json.load(io.TextIOWrapper(body, encoding="utf-8"))
assert new_headers is not None
assert "cloudEventsVersion" in new_headers
assert "eventType" in new_headers

View File

@ -33,7 +33,7 @@ def test_binary_event_to_request_upstream():
v02.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(json.dumps(data.ce)),
lambda x: x.read()
lambda x: x.read(),
)
assert event is not None
@ -50,10 +50,7 @@ def test_structured_event_to_request_upstream():
m = marshaller.NewDefaultHTTPMarshaller()
http_headers = {"content-type": "application/cloudevents+json"}
event = m.FromRequest(
v02.Event(),
http_headers,
io.StringIO(json.dumps(data.ce)),
lambda x: x.read()
v02.Event(), http_headers, io.StringIO(json.dumps(data.ce)), lambda x: x.read()
)
assert event is not None
assert event.Get("type") == (data.ce_type, True)
@ -69,17 +66,10 @@ def test_structured_event_to_request_upstream():
def test_structured_event_to_request_v01():
copy_of_ce = copy.deepcopy(data.ce)
m = marshaller.NewHTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter()
]
)
m = marshaller.NewHTTPMarshaller([structured.NewJSONHTTPCloudEventConverter()])
http_headers = {"content-type": "application/cloudevents+json"}
event = m.FromRequest(
v01.Event(),
http_headers,
io.StringIO(json.dumps(data.ce)),
lambda x: x.read()
v01.Event(), http_headers, io.StringIO(json.dumps(data.ce)), lambda x: x.read()
)
assert event is not None
assert event.Get("type") == (data.ce_type, True)

View File

@ -0,0 +1,77 @@
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cloudevents.sdk import marshaller
from cloudevents.sdk import converters
from cloudevents.sdk.event import v02
from sanic import Sanic
from sanic import response
from cloudevents.tests import data as test_data
m = marshaller.NewDefaultHTTPMarshaller()
app = Sanic(__name__)
@app.route("/is-ok", ["POST"])
async def is_ok(request):
m.FromRequest(
v02.Event(),
dict(request.headers),
request.body,
lambda x: x
)
return response.text("OK")
@app.route("/echo", ["POST"])
async def echo(request):
event = m.FromRequest(
v02.Event(),
dict(request.headers),
request.body,
lambda x: x
)
hs, body = m.ToRequest(event, converters.TypeBinary, lambda x: x)
return response.text(body, headers=hs)
def test_reusable_marshaller():
for i in range(10):
_, r = app.test_client.post(
"/is-ok", headers=test_data.headers, data=test_data.body
)
assert r.status == 200
def test_web_app_integration():
_, r = app.test_client.post(
"/is-ok", headers=test_data.headers, data=test_data.body
)
assert r.status == 200
def test_web_app_echo():
_, r = app.test_client.post("/echo", headers=test_data.headers, data=test_data.body)
assert r.status == 200
event = m.FromRequest(v02.Event(), dict(r.headers), r.body, lambda x: x)
assert event is not None
props = event.Properties()
for key in test_data.headers.keys():
if key == "Content-Type":
assert "contenttype" in props
else:
assert key.lstrip("ce-") in props

View File

@ -1,4 +1,10 @@
flake8<2.7.0,>=2.6.0
hacking==1.1.0
flake8
pep8-naming==0.5.0
flake8-import-order
flake8-print
flake8-strict
pytest==4.0.0
pytest-cov==2.4.0
# web app tests
sanic
aiohttp

View File

@ -18,7 +18,8 @@ whitelist_externals = find
go
docker
[testenv:pep8]
commands = flake8
commands =
flake8
[testenv:venv]
commands = {posargs}
@ -30,6 +31,6 @@ commands = pytest -v -s --tb=long --cov=cloudevents {toxinidir}/cloudevents/test
commands = pytest -v -s --tb=long --cov=cloudevents {toxinidir}/cloudevents/tests
[flake8]
ignore = H405,H404,H403,H401,H306
ignore = H405,H404,H403,H401,H306,S101,N802,N803,N806,I202,I201
show-source = True
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,docs,venv,.venv,docs,etc
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,docs,venv,.venv,docs,etc,samples,tests