From af7d66e99f85c7e4506fed10b7b61e13c303db33 Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Tue, 10 Jan 2017 18:17:58 -0800 Subject: [PATCH 01/11] started cloudfeeds CAP service as a separate route --- mimic/core.py | 2 + mimic/resource.py | 9 +++ mimic/rest/cloudfeedscap.py | 129 ++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 mimic/rest/cloudfeedscap.py diff --git a/mimic/core.py b/mimic/core.py index f3a0ad8610..89d6e34ebd 100644 --- a/mimic/core.py +++ b/mimic/core.py @@ -22,6 +22,7 @@ from mimic.model.ironic_objects import IronicNodeStore from mimic.model.glance_objects import GlanceAdminImageStore from mimic.model.valkyrie_objects import ValkyrieStore +from mimic.rest.cloudfeedscap import CustomerAccessEventStore class MimicCoreException(Exception): @@ -112,6 +113,7 @@ def __init__(self, clock, apis, domains=()): self.ironic_node_store = IronicNodeStore() self.glance_admin_image_store = GlanceAdminImageStore() self.valkyrie_store = ValkyrieStore() + self.cloudfeeds_ca_store = CustomerAccessEventStore() self.domains = list(domains) for api in apis: diff --git a/mimic/resource.py b/mimic/resource.py index f61792ca15..b7ef99b975 100644 --- a/mimic/resource.py +++ b/mimic/resource.py @@ -115,6 +115,15 @@ def valkyrie_api(self, request): """ return valkyrie_api.ValkyrieApi(self.core).app.resource() + @app.route("/customer_access_cloudfeeds", branch=True) + def customer_access_cloudfeeds(): + """ + Customer Access policy events as cloudfeeds service. This is seperarate + from `otter.rest.cloudfeeds` as this is not tenant specific produdct + events. Instead is a global list of events about all accounts + """ + return CloudFeedsCAP(self.core, self.clock).app.resource() + @app.route('/mimic/v1.0/presets', methods=['GET']) def get_mimic_presets(self, request): """ diff --git a/mimic/rest/cloudfeedscap.py b/mimic/rest/cloudfeedscap.py new file mode 100644 index 0000000000..25f02c41a9 --- /dev/null +++ b/mimic/rest/cloudfeedscap.py @@ -0,0 +1,129 @@ +from __future__ import absolute_import, division, unicode_literals + +from uuid import uuid4 +from six import text_type +from zope.interface import implementer +from twisted.plugin import IPlugin +from mimic.catalog import Endpoint, Entry +from mimic.imimic import IAPIMock +from mimic.rest.mimicapp import MimicApp +from mimic.core import MimicCore + +import attr + +from toolz.functoolz import compose + + +@attr.s(frozen=True) +class CustomerAccessEvent(object): + """ + A customer access event + """ + tenant_id = attr.ib() + status = attr.ib() + updated = attr.ib() + id = attr.ib(default=attr.Factory(compose(str, uuid4))) + + +@attr.s +class CustomerAccessEventStore(object): + """ + Collection of :obj:`CustomerAccessEvent` + """ + # List of :obj:`CustomerAccessEvent` + events = attr.ib(default=attr.Factory(list)) + # mapping from CustomerAccessEvent.id to index in events list + events_index = attr.ib(default=attr.Factory(dict)) + + +@attr.s +class CloudFeedsCAPRoutes(object): + """ + This class implements routes for cloud feeds customer access events API + """ + core = attr.ib(validator=aiof(MimicCore)) + clock = attr.ib(validator=attr.validators.provides(IReactorTime)) + # Number of entries to return if not provided + BATCH_LIMIT = 25 + + app = MimicApp() + + def __attrs_post_init__(self): + self.store = self.core.cloudfeeds_ca_store + + @app.route("/events", methods=["POST"]) + def add_customer_access_event(self, request): + """ + Add customer access event. Return 201 on success. Sample request:: + + {"events": [ + {"tenant_id": "23535", "status": "SUSPENDED"}, + {"tenant_id": "463423", "status": "TERMINATED"} + ]} + + NOTE: This is control API. + """ + content = json_from_request(request) + events = list(map(CustomerAccessEvent.from_json, content["events"])) + for i, event in enumerate(events): + self.store.events_index[event.id] = i + self.store.events = events + self.store.events + request.setResponseCode(201) + + @app.route("/customer_access_policy/events", methods=["GET"]) + def get_customer_access_events(self, request): + marker = request.args.get("marker", [None])[0] + direction = request.args.get("direction", ["forward"])[0] + limit = request.args.get("limit", [BATCH_LIMIT])[0] + index = self.store.events_index.get(marker, 0) + if direction == "forward": + events = self.store.events[:index][:limit] + elif direction == "backward": + events = self.store.events[index:][:limit] + else: + raise ValueError("Unknown direction " + direction) + request.setHeader(b"Content-Type", [b"application/atom+xml"]) + return generate_cap_feed(events) + + +feed_fmt = """ + + customer_access_policy/events + + + {entries} + +""" + +entry_fmt = """ + + + + + + + CustomerService + + + + + + {updated} + {updated} + +""" + + +def generate_feed(events): + """ + Generate ATOM feed XML for given events + + :param list events: List of :obj:`CustomerAccessEvent` + + :return: XML text as bytes + """ + root = "https://mimic-host-port" + prev = "{}?{}".format(root, urlencode({"marker": events[0].id, "direction": "forward"})) + next = "{}?{}".format(root, urlencode({"marker": events[-1].id, "direction": "backward"})) + entries = ''.join(entry_fmt.format(**event.asdict()) for event in events) + return feed_fmt.format(previous=prev, next=next, entries=entries).encode("utf-8") From 6d9c3310e6f5acd9579db502a00fc30540792fe5 Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Wed, 11 Jan 2017 16:00:06 -0800 Subject: [PATCH 02/11] implemented code. starting tests --- mimic/resource.py | 5 +- mimic/rest/cloudfeedscap.py | 141 +++++++++++++++++++------------ mimic/test/test_cloudfeedscap.py | 20 +++++ 3 files changed, 110 insertions(+), 56 deletions(-) create mode 100644 mimic/test/test_cloudfeedscap.py diff --git a/mimic/resource.py b/mimic/resource.py index b7ef99b975..36cc9dc0a3 100644 --- a/mimic/resource.py +++ b/mimic/resource.py @@ -23,6 +23,7 @@ from mimic.rest.noit_api import NoitApi from mimic.rest import (fastly_api, mailgun_api, customer_api, ironic_api, glance_api, valkyrie_api) +from mimic.rest.cloudfeedscap import CloudFeedsCAPRoutes from mimic.util.helper import json_from_request from mimic.util.helper import seconds_to_timestamp @@ -116,13 +117,13 @@ def valkyrie_api(self, request): return valkyrie_api.ValkyrieApi(self.core).app.resource() @app.route("/customer_access_cloudfeeds", branch=True) - def customer_access_cloudfeeds(): + def customer_access_cloudfeeds(self, request): """ Customer Access policy events as cloudfeeds service. This is seperarate from `otter.rest.cloudfeeds` as this is not tenant specific produdct events. Instead is a global list of events about all accounts """ - return CloudFeedsCAP(self.core, self.clock).app.resource() + return CloudFeedsCAPRoutes(self.core, self.clock).app.resource() @app.route('/mimic/v1.0/presets', methods=['GET']) def get_mimic_presets(self, request): diff --git a/mimic/rest/cloudfeedscap.py b/mimic/rest/cloudfeedscap.py index 25f02c41a9..4ac2857b2d 100644 --- a/mimic/rest/cloudfeedscap.py +++ b/mimic/rest/cloudfeedscap.py @@ -1,17 +1,14 @@ from __future__ import absolute_import, division, unicode_literals from uuid import uuid4 -from six import text_type -from zope.interface import implementer -from twisted.plugin import IPlugin -from mimic.catalog import Endpoint, Entry -from mimic.imimic import IAPIMock +from urllib import urlencode from mimic.rest.mimicapp import MimicApp -from mimic.core import MimicCore +from mimic.util.helper import json_from_request, seconds_to_timestamp import attr -from toolz.functoolz import compose +from twisted.internet.interfaces import IReactorTime +from twisted.web.template import Tag, flattenString @attr.s(frozen=True) @@ -22,7 +19,12 @@ class CustomerAccessEvent(object): tenant_id = attr.ib() status = attr.ib() updated = attr.ib() - id = attr.ib(default=attr.Factory(compose(str, uuid4))) + id = attr.ib(default=attr.Factory(lambda: str(uuid4))) + + @classmethod + def from_dict(cls, d, clock): + return CustomerAccessEvent(d["tenant_id"], d["status"], + seconds_to_timestamp(clock.seconds())) @attr.s @@ -41,7 +43,7 @@ class CloudFeedsCAPRoutes(object): """ This class implements routes for cloud feeds customer access events API """ - core = attr.ib(validator=aiof(MimicCore)) + core = attr.ib() clock = attr.ib(validator=attr.validators.provides(IReactorTime)) # Number of entries to return if not provided BATCH_LIMIT = 25 @@ -64,57 +66,77 @@ def add_customer_access_event(self, request): NOTE: This is control API. """ content = json_from_request(request) - events = list(map(CustomerAccessEvent.from_json, content["events"])) - for i, event in enumerate(events): - self.store.events_index[event.id] = i + events = [CustomerAccessEvent.from_dict(d, self.clock) for d in content["events"]] self.store.events = events + self.store.events + for i, event in enumerate(self.store.events): + self.store.events_index[event.id] = i request.setResponseCode(201) @app.route("/customer_access_policy/events", methods=["GET"]) def get_customer_access_events(self, request): - marker = request.args.get("marker", [None])[0] - direction = request.args.get("direction", ["forward"])[0] - limit = request.args.get("limit", [BATCH_LIMIT])[0] + marker = request.args.get(u"marker", [None])[0] + direction = request.args.get(u"direction", [u"forward"])[0] + limit = request.args.get(u"limit", [self.BATCH_LIMIT])[0] index = self.store.events_index.get(marker, 0) - if direction == "forward": + if direction == u"forward": events = self.store.events[:index][:limit] - elif direction == "backward": + elif direction == u"backward": events = self.store.events[index:][:limit] else: raise ValueError("Unknown direction " + direction) request.setHeader(b"Content-Type", [b"application/atom+xml"]) - return generate_cap_feed(events) - - -feed_fmt = """ - - customer_access_policy/events - - - {entries} - -""" - -entry_fmt = """ - - - - - - - CustomerService - - - - - - {updated} - {updated} - -""" - - -def generate_feed(events): + return generate_feed_xml(events) + + +def feed_tag(): + feed = Tag("feed")(xmlns="http://www.w3.org/2005/Atom") + feed(Tag("title")(type="text")("customer_access_policy/events")) + return feed + +#""" +# +# customer_access_policy/events +# +# +# {entries} +# +#""" + +def entry_tag(): + entry = Tag("entry") + for term in ["rgn:GLOBAL", "dc:GLOBAL", "customerservice.access_policy.info", + "type:customerservice.access_policy.info"]: + entry(Tag("category")(term=term)) + entry(Tag("title")(type="text")("CustomerService")) + entry(Tag("content")(type="application/xml")) + event = Tag("event")(xmlns="http://docs.rackspace.com/core/event", dataCenter="GLOBAL", + environment="PROD", region="GLOBAL", type="INFO", version="2") + product = Tag("product")(xmlns="http://docs.rackspace.com/event/customer/access_policy", + previousEvent="", serviceCode="CustomerService", version="1") + event(product) + entry(event) + return entry, event, product + +#u""" +# +# +# +# +# +# +# CustomerService +# +# +# +# +# +# {updated} +# {updated} +# +#""" + + +def generate_feed_xml(events): """ Generate ATOM feed XML for given events @@ -122,8 +144,19 @@ def generate_feed(events): :return: XML text as bytes """ - root = "https://mimic-host-port" - prev = "{}?{}".format(root, urlencode({"marker": events[0].id, "direction": "forward"})) - next = "{}?{}".format(root, urlencode({"marker": events[-1].id, "direction": "backward"})) - entries = ''.join(entry_fmt.format(**event.asdict()) for event in events) - return feed_fmt.format(previous=prev, next=next, entries=entries).encode("utf-8") + root = u"https://mimic-host-port" + feed = feed_tag() + if events: + prev = "{}?{}".format(root, urlencode({u"marker": events[0].id, u"direction": u"forward"})) + next = "{}?{}".format(root, urlencode({u"marker": events[-1].id, u"direction": u"backward"})) + feed(Tag("link")(href=prev, rel="previous")) + feed(Tag("link")(href=next, rel="next")) + for event in events: + entry, event_tag, product = entry_tag() + entry(Tag("category")(term="tid:{}".format(event.tenant_id))) + event_tag(tenant_id=event.tenant_id) + product(status=event.status) + entry(Tag("updated")(event.updated)) + entry(Tag("published")(event.updated)) + feed(entry) + return flattenString(None, feed).result diff --git a/mimic/test/test_cloudfeedscap.py b/mimic/test/test_cloudfeedscap.py new file mode 100644 index 0000000000..12ef23a793 --- /dev/null +++ b/mimic/test/test_cloudfeedscap.py @@ -0,0 +1,20 @@ +""" +Tests for :obj:`mimic.rest.cloudfeedscap` +""" +from __future__ import print_function + +from mimic.rest.cloudfeedscap import generate_feed_xml + +from twisted.trial.unittest import SynchronousTestCase + + +class GenFeedTests(SynchronousTestCase): + """ + Tests for :func:`generate_feed` + """ + + def test_no_entries(self): + print(generate_feed_xml([])) + + def test_entries(self): + pass From 31171dd6b876a34705f02cd1cb4d739760202cb3 Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Thu, 12 Jan 2017 12:53:46 -0800 Subject: [PATCH 03/11] generate_feed_xml and routes test done --- mimic/rest/cloudfeedscap.py | 11 ++- mimic/test/test_cloudfeedscap.py | 118 +++++++++++++++++++++++++++++-- 2 files changed, 119 insertions(+), 10 deletions(-) diff --git a/mimic/rest/cloudfeedscap.py b/mimic/rest/cloudfeedscap.py index 4ac2857b2d..e2198d6fe4 100644 --- a/mimic/rest/cloudfeedscap.py +++ b/mimic/rest/cloudfeedscap.py @@ -19,12 +19,11 @@ class CustomerAccessEvent(object): tenant_id = attr.ib() status = attr.ib() updated = attr.ib() - id = attr.ib(default=attr.Factory(lambda: str(uuid4))) + id = attr.ib(default=attr.Factory(lambda: str(uuid4()))) @classmethod def from_dict(cls, d, clock): - return CustomerAccessEvent(d["tenant_id"], d["status"], - seconds_to_timestamp(clock.seconds())) + return CustomerAccessEvent(d["tenant_id"], d["status"], clock.seconds()) @attr.s @@ -41,7 +40,7 @@ class CustomerAccessEventStore(object): @attr.s class CloudFeedsCAPRoutes(object): """ - This class implements routes for cloud feeds customer access events API + This class implements routes for cloud feeds customer access policy events API """ core = attr.ib() clock = attr.ib(validator=attr.validators.provides(IReactorTime)) @@ -156,7 +155,7 @@ def generate_feed_xml(events): entry(Tag("category")(term="tid:{}".format(event.tenant_id))) event_tag(tenant_id=event.tenant_id) product(status=event.status) - entry(Tag("updated")(event.updated)) - entry(Tag("published")(event.updated)) + entry(Tag("updated")(seconds_to_timestamp(event.updated))) + entry(Tag("published")(seconds_to_timestamp(event.updated))) feed(entry) return flattenString(None, feed).result diff --git a/mimic/test/test_cloudfeedscap.py b/mimic/test/test_cloudfeedscap.py index 12ef23a793..ee7ca6453a 100644 --- a/mimic/test/test_cloudfeedscap.py +++ b/mimic/test/test_cloudfeedscap.py @@ -1,11 +1,21 @@ """ Tests for :obj:`mimic.rest.cloudfeedscap` """ -from __future__ import print_function +import json +from datetime import datetime -from mimic.rest.cloudfeedscap import generate_feed_xml +import xmltodict + +from mimic.core import MimicCore +from mimic.resource import MimicRoot +from mimic.rest.cloudfeedscap import CustomerAccessEvent, generate_feed_xml +from mimic.test.helpers import request +from mimic.util.helper import seconds_to_timestamp + +from testtools.matchers import MatchesDict, MatchesListwise, Equals, Contains, ContainsDict from twisted.trial.unittest import SynchronousTestCase +from twisted.internet.task import Clock class GenFeedTests(SynchronousTestCase): @@ -14,7 +24,107 @@ class GenFeedTests(SynchronousTestCase): """ def test_no_entries(self): - print(generate_feed_xml([])) + """ + Generates empty feed when given empty list of events. Does not provide next and previous links + """ + self.assertEqual( + generate_feed_xml([]), + ('' + 'customer_access_policy/events')) def test_entries(self): - pass + """ + Generates feed with proper next and previous link and "entry" nodes with event info in them. + Currently only checks if event info i.e. (tenant_id, status, links and updated) are correct. + Ideally, it should ideally check against XML schema also. Will probably add that later. + """ + events = [CustomerAccessEvent("t1", "FULL", 0.0, "1"), + CustomerAccessEvent("t2", "TERMINATED", 100.0, "2")] + updates = [seconds_to_timestamp(e.updated) for e in events] + xml = generate_feed_xml(events) + namespaces = {"http://www.w3.org/2005/Atom": None, + "http://docs.rackspace.com/core/event": "event", + "http://docs.rackspace.com/event/customer/access_policy": "ap"} + d = xmltodict.parse(xml, process_namespaces=True, namespaces=namespaces) + feed_match = MatchesDict( + {"feed": ContainsDict({ + "link": MatchesListwise([ + MatchesDict({"@href": Equals("https://mimic-host-port?marker=1&direction=forward"), + "@rel": Equals("previous")}), + MatchesDict({"@href": Equals("https://mimic-host-port?marker=2&direction=backward"), + "@rel": Equals("next")}) + ]), + "entry": MatchesListwise([ + ContainsDict({ + "event:event": ContainsDict({ + "@tenant_id": Equals("t1"), + "ap:product": ContainsDict({"@status": Equals("FULL")}) + }), + "updated": Equals(updates[0]), + "published": Equals(updates[0]) + }), + ContainsDict({ + "event:event": ContainsDict({ + "@tenant_id": Equals("t2"), + "ap:product": ContainsDict({"@status": Equals("TERMINATED")}) + }), + "updated": Equals(updates[1]), + "published": Equals(updates[1]) + }) + ]) + }) + }) + self.assertIsNone(feed_match.match(d)) + + +class RoutesTests(SynchronousTestCase): + """ + Test for routes in :obj:`CloudFeedsCAPRoutes` + """ + def setUp(self): + self.clock = Clock() + self.core = MimicCore(self.clock, []) + self.root = MimicRoot(self.core, clock=self.clock).app.resource() + from mimic.rest import cloudfeedscap as cf + self.ids = ["id2", "id1"] + self.patch(cf, "uuid4", lambda: self.ids.pop()) + + def test_add_events_empty(self): + """ + Calling `POST ../events` first time will add events in event store and setup + indexes + """ + events = [{"tenant_id": "1234", "status": "SUSPENDED"}, + {"tenant_id": "2345", "status": "FULL"}] + d = request(self, self.root, "POST", "/customer_access_cloudfeeds/events", + body=json.dumps({"events": events}).encode("utf-8")) + self.assertEqual(self.successResultOf(d).code, 201) + self.assertEqual( + self.core.cloudfeeds_ca_store.events, + [CustomerAccessEvent(u"1234", u"SUSPENDED", 0.0, u"id1"), + CustomerAccessEvent(u"2345", u"FULL", 0.0, u"id2")]) + self.assertEqual( + self.core.cloudfeeds_ca_store.events_index, {"id1": 0, "id2": 1}) + + def test_add_events_update(self): + """ + Calling `POST .../events` with existing events will prepend the events + to the store.events list and update the store.events_index too + """ + self.test_add_events_empty() + self.clock.advance(200) + self.ids = ["id5", "id4"] + events = [{"tenant_id": "t1", "status": "TERMINATED"}, + {"tenant_id": "t2", "status": "SUSPENDED"}] + d = request(self, self.root, "POST", "/customer_access_cloudfeeds/events", + body=json.dumps({"events": events}).encode("utf-8")) + self.assertEqual(self.successResultOf(d).code, 201) + self.assertEqual( + self.core.cloudfeeds_ca_store.events, + [CustomerAccessEvent(u"t1", u"TERMINATED", 200.0, u"id4"), + CustomerAccessEvent(u"t2", u"SUSPENDED", 200.0, u"id5"), + CustomerAccessEvent(u"1234", u"SUSPENDED", 0.0, u"id1"), + CustomerAccessEvent(u"2345", u"FULL", 0.0, u"id2")]) + self.assertEqual( + self.core.cloudfeeds_ca_store.events_index, + {"id4": 0, "id5": 1, "id1": 2, "id2": 3}) From 25b6fa9798e64ba4c0dc68f13b829ddc4d7e24ab Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Thu, 12 Jan 2017 18:05:22 -0800 Subject: [PATCH 04/11] cloudfeeds CAP all tests done --- mimic/resource.py | 2 +- mimic/rest/cloudfeedscap.py | 11 +- mimic/test/test_cloudfeedscap.py | 166 ++++++++++++++++++++++--------- 3 files changed, 127 insertions(+), 52 deletions(-) diff --git a/mimic/resource.py b/mimic/resource.py index 36cc9dc0a3..35a7ec7ea9 100644 --- a/mimic/resource.py +++ b/mimic/resource.py @@ -116,7 +116,7 @@ def valkyrie_api(self, request): """ return valkyrie_api.ValkyrieApi(self.core).app.resource() - @app.route("/customer_access_cloudfeeds", branch=True) + @app.route("/cloudfeeds_cap", branch=True) def customer_access_cloudfeeds(self, request): """ Customer Access policy events as cloudfeeds service. This is seperarate diff --git a/mimic/rest/cloudfeedscap.py b/mimic/rest/cloudfeedscap.py index e2198d6fe4..c171c76cf3 100644 --- a/mimic/rest/cloudfeedscap.py +++ b/mimic/rest/cloudfeedscap.py @@ -45,7 +45,7 @@ class CloudFeedsCAPRoutes(object): core = attr.ib() clock = attr.ib(validator=attr.validators.provides(IReactorTime)) # Number of entries to return if not provided - BATCH_LIMIT = 25 + BATCH_LIMIT = 10 app = MimicApp() @@ -73,10 +73,13 @@ def add_customer_access_event(self, request): @app.route("/customer_access_policy/events", methods=["GET"]) def get_customer_access_events(self, request): + """ + Return customer access events atom feed format + """ marker = request.args.get(u"marker", [None])[0] direction = request.args.get(u"direction", [u"forward"])[0] - limit = request.args.get(u"limit", [self.BATCH_LIMIT])[0] - index = self.store.events_index.get(marker, 0) + limit = int(request.args.get(u"limit", [self.BATCH_LIMIT])[0]) + index = self.store.events_index.get(marker, None) if direction == u"forward": events = self.store.events[:index][:limit] elif direction == u"backward": @@ -143,7 +146,7 @@ def generate_feed_xml(events): :return: XML text as bytes """ - root = u"https://mimic-host-port" + root = u"https://mimic-host-port/cloudfeeds_cap/customer_access_events" feed = feed_tag() if events: prev = "{}?{}".format(root, urlencode({u"marker": events[0].id, u"direction": u"forward"})) diff --git a/mimic/test/test_cloudfeedscap.py b/mimic/test/test_cloudfeedscap.py index ee7ca6453a..737e178d49 100644 --- a/mimic/test/test_cloudfeedscap.py +++ b/mimic/test/test_cloudfeedscap.py @@ -3,13 +3,14 @@ """ import json from datetime import datetime +from urllib import urlencode import xmltodict from mimic.core import MimicCore from mimic.resource import MimicRoot from mimic.rest.cloudfeedscap import CustomerAccessEvent, generate_feed_xml -from mimic.test.helpers import request +from mimic.test.helpers import request, request_with_content from mimic.util.helper import seconds_to_timestamp from testtools.matchers import MatchesDict, MatchesListwise, Equals, Contains, ContainsDict @@ -18,6 +19,49 @@ from twisted.internet.task import Clock +empty_feed = ('' + 'customer_access_policy/events') + + +def assert_has_events(testcase, xml, events, prev, next): + """ + Assert that xml has given events with previous and next link + """ + feed_match = MatchesDict( + {"feed": ContainsDict({ + "link": MatchesListwise([ + MatchesDict({"@href": Equals(prev), + "@rel": Equals("previous")}), + MatchesDict({"@href": Equals(next), + "@rel": Equals("next")}) + ]), + "entry": MatchesListwise([ + ContainsDict({ + "event:event": ContainsDict({ + "@tenant_id": Equals(event.tenant_id), + "ap:product": ContainsDict({"@status": Equals(event.status)}) + }), + "updated": Equals(seconds_to_timestamp(event.updated)), + "published": Equals(seconds_to_timestamp(event.updated)) + }) + for event in events + ]) + }) + }) + namespaces = {"http://www.w3.org/2005/Atom": None, + "http://docs.rackspace.com/core/event": "event", + "http://docs.rackspace.com/event/customer/access_policy": "ap"} + d = xmltodict.parse(xml, process_namespaces=True, namespaces=namespaces) + testcase.assertIsNone(feed_match.match(d)) + + +def link(params): + """ + Return full URL with given query params + """ + return u"https://mimic-host-port/cloudfeeds_cap/customer_access_events?{}".format(urlencode(params)) + + class GenFeedTests(SynchronousTestCase): """ Tests for :func:`generate_feed` @@ -27,10 +71,7 @@ def test_no_entries(self): """ Generates empty feed when given empty list of events. Does not provide next and previous links """ - self.assertEqual( - generate_feed_xml([]), - ('' - 'customer_access_policy/events')) + self.assertEqual(generate_feed_xml([]), empty_feed) def test_entries(self): """ @@ -40,41 +81,9 @@ def test_entries(self): """ events = [CustomerAccessEvent("t1", "FULL", 0.0, "1"), CustomerAccessEvent("t2", "TERMINATED", 100.0, "2")] - updates = [seconds_to_timestamp(e.updated) for e in events] xml = generate_feed_xml(events) - namespaces = {"http://www.w3.org/2005/Atom": None, - "http://docs.rackspace.com/core/event": "event", - "http://docs.rackspace.com/event/customer/access_policy": "ap"} - d = xmltodict.parse(xml, process_namespaces=True, namespaces=namespaces) - feed_match = MatchesDict( - {"feed": ContainsDict({ - "link": MatchesListwise([ - MatchesDict({"@href": Equals("https://mimic-host-port?marker=1&direction=forward"), - "@rel": Equals("previous")}), - MatchesDict({"@href": Equals("https://mimic-host-port?marker=2&direction=backward"), - "@rel": Equals("next")}) - ]), - "entry": MatchesListwise([ - ContainsDict({ - "event:event": ContainsDict({ - "@tenant_id": Equals("t1"), - "ap:product": ContainsDict({"@status": Equals("FULL")}) - }), - "updated": Equals(updates[0]), - "published": Equals(updates[0]) - }), - ContainsDict({ - "event:event": ContainsDict({ - "@tenant_id": Equals("t2"), - "ap:product": ContainsDict({"@status": Equals("TERMINATED")}) - }), - "updated": Equals(updates[1]), - "published": Equals(updates[1]) - }) - ]) - }) - }) - self.assertIsNone(feed_match.match(d)) + assert_has_events(self, xml, events, link(dict(marker="1", direction="forward")), + link(dict(marker="2", direction="backward"))) class RoutesTests(SynchronousTestCase): @@ -96,7 +105,7 @@ def test_add_events_empty(self): """ events = [{"tenant_id": "1234", "status": "SUSPENDED"}, {"tenant_id": "2345", "status": "FULL"}] - d = request(self, self.root, "POST", "/customer_access_cloudfeeds/events", + d = request(self, self.root, "POST", "/cloudfeeds_cap/events", body=json.dumps({"events": events}).encode("utf-8")) self.assertEqual(self.successResultOf(d).code, 201) self.assertEqual( @@ -116,15 +125,78 @@ def test_add_events_update(self): self.ids = ["id5", "id4"] events = [{"tenant_id": "t1", "status": "TERMINATED"}, {"tenant_id": "t2", "status": "SUSPENDED"}] - d = request(self, self.root, "POST", "/customer_access_cloudfeeds/events", + d = request(self, self.root, "POST", "/cloudfeeds_cap/events", body=json.dumps({"events": events}).encode("utf-8")) self.assertEqual(self.successResultOf(d).code, 201) - self.assertEqual( - self.core.cloudfeeds_ca_store.events, - [CustomerAccessEvent(u"t1", u"TERMINATED", 200.0, u"id4"), - CustomerAccessEvent(u"t2", u"SUSPENDED", 200.0, u"id5"), - CustomerAccessEvent(u"1234", u"SUSPENDED", 0.0, u"id1"), - CustomerAccessEvent(u"2345", u"FULL", 0.0, u"id2")]) + exp_events = [CustomerAccessEvent(u"t1", u"TERMINATED", 200.0, u"id4"), + CustomerAccessEvent(u"t2", u"SUSPENDED", 200.0, u"id5"), + CustomerAccessEvent(u"1234", u"SUSPENDED", 0.0, u"id1"), + CustomerAccessEvent(u"2345", u"FULL", 0.0, u"id2")] + self.assertEqual(self.core.cloudfeeds_ca_store.events, exp_events) self.assertEqual( self.core.cloudfeeds_ca_store.events_index, {"id4": 0, "id5": 1, "id1": 2, "id2": 3}) + return exp_events + + def test_get_events_empty(self): + """ + `GET ../customer_access_policy/events` returns empty xml feed when there + are no events stored. No previous and next links are provided. + """ + d = request_with_content( + self, self.root, "GET", "/cloudfeeds_cap/customer_access_policy/events") + resp, body = self.successResultOf(d) + self.assertEqual(body, empty_feed) + + def test_get_events(self): + """ + `GET ../customer_access_policy/events` returns events stored in xml feed + format with proper next and previous link + """ + events = self.test_add_events_update() + resp, body = self.successResultOf(request_with_content( + self, self.root, "GET", "/cloudfeeds_cap/customer_access_policy/events")) + assert_has_events( + self, body, events, link(dict(marker="id4", direction="forward")), + link(dict(marker="id2", direction="backward"))) + + def test_get_events_marker_forward(self): + """ + `GET ../customer_access_policy/events?marker=m&direction=forward` returns events occurring + after given marker + """ + events = self.test_add_events_update() + resp, body = self.successResultOf(request_with_content( + self, self.root, "GET", + "/cloudfeeds_cap/customer_access_policy/events?marker=id1&direction=forward")) + assert_has_events( + self, body, events[:2], + link(dict(marker="id4", direction="forward")), + link(dict(marker="id5", direction="backward"))) + + def test_get_events_marker_backward(self): + """ + `GET ../customer_access_policy/events?marker=m&direction=backward` returns events occurring + before given marker + """ + events = self.test_add_events_update() + resp, body = self.successResultOf(request_with_content( + self, self.root, "GET", + "/cloudfeeds_cap/customer_access_policy/events?marker=id1&direction=backward")) + assert_has_events( + self, body, events[2:], + link(dict(marker="id1", direction="forward")), + link(dict(marker="id2", direction="backward"))) + + def test_get_events_limit(self): + """ + `GET ../customer_access_policy/events?limit=3` returns events <= 3 + """ + events = self.test_add_events_update() + resp, body = self.successResultOf(request_with_content( + self, self.root, "GET", + "/cloudfeeds_cap/customer_access_policy/events?limit=3")) + assert_has_events( + self, body, events[:3], + link(dict(marker="id4", direction="forward")), + link(dict(marker="id1", direction="backward"))) From 685a828a9edf6dc0173ad328cbc901b246c8529f Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Thu, 12 Jan 2017 18:14:46 -0800 Subject: [PATCH 05/11] check response header also --- mimic/rest/cloudfeedscap.py | 2 +- mimic/test/test_cloudfeedscap.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mimic/rest/cloudfeedscap.py b/mimic/rest/cloudfeedscap.py index c171c76cf3..8d44b63ea1 100644 --- a/mimic/rest/cloudfeedscap.py +++ b/mimic/rest/cloudfeedscap.py @@ -86,7 +86,7 @@ def get_customer_access_events(self, request): events = self.store.events[index:][:limit] else: raise ValueError("Unknown direction " + direction) - request.setHeader(b"Content-Type", [b"application/atom+xml"]) + request.setHeader(b"Content-Type", b"application/atom+xml") return generate_feed_xml(events) diff --git a/mimic/test/test_cloudfeedscap.py b/mimic/test/test_cloudfeedscap.py index 737e178d49..23906621b1 100644 --- a/mimic/test/test_cloudfeedscap.py +++ b/mimic/test/test_cloudfeedscap.py @@ -146,6 +146,7 @@ def test_get_events_empty(self): d = request_with_content( self, self.root, "GET", "/cloudfeeds_cap/customer_access_policy/events") resp, body = self.successResultOf(d) + self.assertEqual(resp.headers.getRawHeaders("Content-Type"), [b"application/atom+xml"]) self.assertEqual(body, empty_feed) def test_get_events(self): From 6737b027e1ceded51f22e365d85452944e69687b Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Fri, 13 Jan 2017 09:50:10 -0800 Subject: [PATCH 06/11] move comment --- mimic/test/test_cloudfeedscap.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mimic/test/test_cloudfeedscap.py b/mimic/test/test_cloudfeedscap.py index 23906621b1..d75d7938b2 100644 --- a/mimic/test/test_cloudfeedscap.py +++ b/mimic/test/test_cloudfeedscap.py @@ -25,7 +25,9 @@ def assert_has_events(testcase, xml, events, prev, next): """ - Assert that xml has given events with previous and next link + Assert that xml has given events with previous and next link. + Currently only checks if event info i.e. (tenant_id, status, links and updated) are correct. + Ideally, it should ideally check against XML schema also. Will probably add that later. """ feed_match = MatchesDict( {"feed": ContainsDict({ @@ -76,8 +78,6 @@ def test_no_entries(self): def test_entries(self): """ Generates feed with proper next and previous link and "entry" nodes with event info in them. - Currently only checks if event info i.e. (tenant_id, status, links and updated) are correct. - Ideally, it should ideally check against XML schema also. Will probably add that later. """ events = [CustomerAccessEvent("t1", "FULL", 0.0, "1"), CustomerAccessEvent("t2", "TERMINATED", 100.0, "2")] From 40c53d71993af86c537e05b3750773ff26e0a82b Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Fri, 13 Jan 2017 11:07:17 -0800 Subject: [PATCH 07/11] add event id also --- mimic/rest/cloudfeedscap.py | 2 +- mimic/test/test_cloudfeedscap.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mimic/rest/cloudfeedscap.py b/mimic/rest/cloudfeedscap.py index 8d44b63ea1..6740a2279f 100644 --- a/mimic/rest/cloudfeedscap.py +++ b/mimic/rest/cloudfeedscap.py @@ -156,7 +156,7 @@ def generate_feed_xml(events): for event in events: entry, event_tag, product = entry_tag() entry(Tag("category")(term="tid:{}".format(event.tenant_id))) - event_tag(tenant_id=event.tenant_id) + event_tag(id=event.id, tenant_id=event.tenant_id) product(status=event.status) entry(Tag("updated")(seconds_to_timestamp(event.updated))) entry(Tag("published")(seconds_to_timestamp(event.updated))) diff --git a/mimic/test/test_cloudfeedscap.py b/mimic/test/test_cloudfeedscap.py index d75d7938b2..88c135847c 100644 --- a/mimic/test/test_cloudfeedscap.py +++ b/mimic/test/test_cloudfeedscap.py @@ -41,6 +41,7 @@ def assert_has_events(testcase, xml, events, prev, next): ContainsDict({ "event:event": ContainsDict({ "@tenant_id": Equals(event.tenant_id), + "@id": Equals(event.id), "ap:product": ContainsDict({"@status": Equals(event.status)}) }), "updated": Equals(seconds_to_timestamp(event.updated)), From e01b331d97cfdd844d4d5cf5ff089d5c571cb7cf Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Fri, 13 Jan 2017 11:32:59 -0800 Subject: [PATCH 08/11] do not include event at index when asked backward --- mimic/rest/cloudfeedscap.py | 2 +- mimic/test/test_cloudfeedscap.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mimic/rest/cloudfeedscap.py b/mimic/rest/cloudfeedscap.py index 6740a2279f..5a73597453 100644 --- a/mimic/rest/cloudfeedscap.py +++ b/mimic/rest/cloudfeedscap.py @@ -83,7 +83,7 @@ def get_customer_access_events(self, request): if direction == u"forward": events = self.store.events[:index][:limit] elif direction == u"backward": - events = self.store.events[index:][:limit] + events = self.store.events[index + 1:][:limit] else: raise ValueError("Unknown direction " + direction) request.setHeader(b"Content-Type", b"application/atom+xml") diff --git a/mimic/test/test_cloudfeedscap.py b/mimic/test/test_cloudfeedscap.py index 88c135847c..3898b9db45 100644 --- a/mimic/test/test_cloudfeedscap.py +++ b/mimic/test/test_cloudfeedscap.py @@ -184,7 +184,7 @@ def test_get_events_marker_backward(self): events = self.test_add_events_update() resp, body = self.successResultOf(request_with_content( self, self.root, "GET", - "/cloudfeeds_cap/customer_access_policy/events?marker=id1&direction=backward")) + "/cloudfeeds_cap/customer_access_policy/events?marker=id5&direction=backward")) assert_has_events( self, body, events[2:], link(dict(marker="id1", direction="forward")), From d9259d2ad3ee80c8a990cc8f726db20c4db76d26 Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Wed, 18 Jan 2017 22:13:48 -0800 Subject: [PATCH 09/11] cap correct xml "content" tag was not there --- mimic/rest/cloudfeedscap.py | 7 ++++--- mimic/test/test_cloudfeedscap.py | 10 ++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/mimic/rest/cloudfeedscap.py b/mimic/rest/cloudfeedscap.py index 5a73597453..f2ec7fa2de 100644 --- a/mimic/rest/cloudfeedscap.py +++ b/mimic/rest/cloudfeedscap.py @@ -110,13 +110,14 @@ def entry_tag(): "type:customerservice.access_policy.info"]: entry(Tag("category")(term=term)) entry(Tag("title")(type="text")("CustomerService")) - entry(Tag("content")(type="application/xml")) + content = Tag("content")(type="application/xml") + entry(content) event = Tag("event")(xmlns="http://docs.rackspace.com/core/event", dataCenter="GLOBAL", environment="PROD", region="GLOBAL", type="INFO", version="2") product = Tag("product")(xmlns="http://docs.rackspace.com/event/customer/access_policy", previousEvent="", serviceCode="CustomerService", version="1") event(product) - entry(event) + content(event) return entry, event, product #u""" @@ -156,7 +157,7 @@ def generate_feed_xml(events): for event in events: entry, event_tag, product = entry_tag() entry(Tag("category")(term="tid:{}".format(event.tenant_id))) - event_tag(id=event.id, tenant_id=event.tenant_id) + event_tag(id=event.id, tenantId=event.tenant_id) product(status=event.status) entry(Tag("updated")(seconds_to_timestamp(event.updated))) entry(Tag("published")(seconds_to_timestamp(event.updated))) diff --git a/mimic/test/test_cloudfeedscap.py b/mimic/test/test_cloudfeedscap.py index 3898b9db45..b75d9689b2 100644 --- a/mimic/test/test_cloudfeedscap.py +++ b/mimic/test/test_cloudfeedscap.py @@ -39,10 +39,12 @@ def assert_has_events(testcase, xml, events, prev, next): ]), "entry": MatchesListwise([ ContainsDict({ - "event:event": ContainsDict({ - "@tenant_id": Equals(event.tenant_id), - "@id": Equals(event.id), - "ap:product": ContainsDict({"@status": Equals(event.status)}) + "content": ContainsDict({ + "event:event": ContainsDict({ + "@tenantId": Equals(event.tenant_id), + "@id": Equals(event.id), + "ap:product": ContainsDict({"@status": Equals(event.status)}) + }) }), "updated": Equals(seconds_to_timestamp(event.updated)), "published": Equals(seconds_to_timestamp(event.updated)) From 873116d0345febbc20064b307165a4d12481cba3 Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Thu, 19 Jan 2017 15:56:34 -0800 Subject: [PATCH 10/11] lint --- mimic/rest/cloudfeedscap.py | 41 ++++++++++++-------------------- mimic/test/test_cloudfeedscap.py | 9 ++++--- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/mimic/rest/cloudfeedscap.py b/mimic/rest/cloudfeedscap.py index f2ec7fa2de..988fa29486 100644 --- a/mimic/rest/cloudfeedscap.py +++ b/mimic/rest/cloudfeedscap.py @@ -1,3 +1,6 @@ +""" +Cloudfeeds customer access events +""" from __future__ import absolute_import, division, unicode_literals from uuid import uuid4 @@ -23,6 +26,9 @@ class CustomerAccessEvent(object): @classmethod def from_dict(cls, d, clock): + """ + Return new CustomerAccessEvent from given dict and updated from clock + """ return CustomerAccessEvent(d["tenant_id"], d["status"], clock.seconds()) @@ -50,6 +56,9 @@ class CloudFeedsCAPRoutes(object): app = MimicApp() def __attrs_post_init__(self): + """ + Cache store for easy access + """ self.store = self.core.cloudfeeds_ca_store @app.route("/events", methods=["POST"]) @@ -91,20 +100,18 @@ def get_customer_access_events(self, request): def feed_tag(): + """ + Return new tag + """ feed = Tag("feed")(xmlns="http://www.w3.org/2005/Atom") feed(Tag("title")(type="text")("customer_access_policy/events")) return feed -#""" -# -# customer_access_policy/events -# -# -# {entries} -# -#""" def entry_tag(): + """ + Return new , and tag + """ entry = Tag("entry") for term in ["rgn:GLOBAL", "dc:GLOBAL", "customerservice.access_policy.info", "type:customerservice.access_policy.info"]: @@ -120,24 +127,6 @@ def entry_tag(): content(event) return entry, event, product -#u""" -# -# -# -# -# -# -# CustomerService -# -# -# -# -# -# {updated} -# {updated} -# -#""" - def generate_feed_xml(events): """ diff --git a/mimic/test/test_cloudfeedscap.py b/mimic/test/test_cloudfeedscap.py index b75d9689b2..74be80da21 100644 --- a/mimic/test/test_cloudfeedscap.py +++ b/mimic/test/test_cloudfeedscap.py @@ -2,7 +2,6 @@ Tests for :obj:`mimic.rest.cloudfeedscap` """ import json -from datetime import datetime from urllib import urlencode import xmltodict @@ -13,14 +12,14 @@ from mimic.test.helpers import request, request_with_content from mimic.util.helper import seconds_to_timestamp -from testtools.matchers import MatchesDict, MatchesListwise, Equals, Contains, ContainsDict +from testtools.matchers import MatchesDict, MatchesListwise, Equals, ContainsDict from twisted.trial.unittest import SynchronousTestCase from twisted.internet.task import Clock empty_feed = ('' - 'customer_access_policy/events') + 'customer_access_policy/events') def assert_has_events(testcase, xml, events, prev, next): @@ -29,8 +28,8 @@ def assert_has_events(testcase, xml, events, prev, next): Currently only checks if event info i.e. (tenant_id, status, links and updated) are correct. Ideally, it should ideally check against XML schema also. Will probably add that later. """ - feed_match = MatchesDict( - {"feed": ContainsDict({ + feed_match = MatchesDict({ + "feed": ContainsDict({ "link": MatchesListwise([ MatchesDict({"@href": Equals(prev), "@rel": Equals("previous")}), From 61ce221f080caa827c59d47791940a03e10c82f2 Mon Sep 17 00:00:00 2001 From: Glyph Date: Wed, 22 Feb 2017 15:56:36 -0800 Subject: [PATCH 11/11] address python 3 portability issues. --- mimic/rest/cloudfeedscap.py | 29 ++++++++++++++------- mimic/test/test_cloudfeedscap.py | 43 ++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/mimic/rest/cloudfeedscap.py b/mimic/rest/cloudfeedscap.py index 988fa29486..f5ee92ae88 100644 --- a/mimic/rest/cloudfeedscap.py +++ b/mimic/rest/cloudfeedscap.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals from uuid import uuid4 -from urllib import urlencode +from six import text_type from mimic.rest.mimicapp import MimicApp from mimic.util.helper import json_from_request, seconds_to_timestamp @@ -12,6 +12,7 @@ from twisted.internet.interfaces import IReactorTime from twisted.web.template import Tag, flattenString +from twisted.python.url import URL @attr.s(frozen=True) @@ -85,9 +86,12 @@ def get_customer_access_events(self, request): """ Return customer access events atom feed format """ - marker = request.args.get(u"marker", [None])[0] - direction = request.args.get(u"direction", [u"forward"])[0] - limit = int(request.args.get(u"limit", [self.BATCH_LIMIT])[0]) + marker = request.args.get(b"marker", [b""])[0].decode("ascii") or None + direction = (request.args.get(b"direction", [b"forward"])[0] + .decode("ascii")) + limit = int(request.args.get(b"limit", + [text_type(self.BATCH_LIMIT) + .encode("ascii")])[0].decode("ascii")) index = self.store.events_index.get(marker, None) if direction == u"forward": events = self.store.events[:index][:limit] @@ -136,13 +140,20 @@ def generate_feed_xml(events): :return: XML text as bytes """ - root = u"https://mimic-host-port/cloudfeeds_cap/customer_access_events" + root = URL.fromText( + "https://mimic-host-port/cloudfeeds_cap/customer_access_events" + ) feed = feed_tag() if events: - prev = "{}?{}".format(root, urlencode({u"marker": events[0].id, u"direction": u"forward"})) - next = "{}?{}".format(root, urlencode({u"marker": events[-1].id, u"direction": u"backward"})) - feed(Tag("link")(href=prev, rel="previous")) - feed(Tag("link")(href=next, rel="next")) + feed(Tag("link")(href=root + .add("direction", "forward") + .add("marker", text_type(events[0].id)) + .asText(), + rel="previous")) + feed(Tag("link")(href=root + .add("direction", "backward") + .add("marker", text_type(events[-1].id)) + .asText(), rel="next")) for event in events: entry, event_tag, product = entry_tag() entry(Tag("category")(term="tid:{}".format(event.tenant_id))) diff --git a/mimic/test/test_cloudfeedscap.py b/mimic/test/test_cloudfeedscap.py index 74be80da21..f8e7269b40 100644 --- a/mimic/test/test_cloudfeedscap.py +++ b/mimic/test/test_cloudfeedscap.py @@ -2,7 +2,7 @@ Tests for :obj:`mimic.rest.cloudfeedscap` """ import json -from urllib import urlencode +from six.moves.urllib.parse import urlencode import xmltodict @@ -13,20 +13,23 @@ from mimic.util.helper import seconds_to_timestamp from testtools.matchers import MatchesDict, MatchesListwise, Equals, ContainsDict +from testtools.assertions import assert_that from twisted.trial.unittest import SynchronousTestCase from twisted.internet.task import Clock -empty_feed = ('' - 'customer_access_policy/events') +empty_feed = (b'' + b'customer_access_policy/events' + b'') def assert_has_events(testcase, xml, events, prev, next): """ - Assert that xml has given events with previous and next link. - Currently only checks if event info i.e. (tenant_id, status, links and updated) are correct. - Ideally, it should ideally check against XML schema also. Will probably add that later. + Assert that xml has given events with previous and next link. Currently + only checks if event info i.e. (tenant_id, status, links and updated) are + correct. Ideally, it should ideally check against XML schema also. Will + probably add that later. """ feed_match = MatchesDict({ "feed": ContainsDict({ @@ -56,14 +59,15 @@ def assert_has_events(testcase, xml, events, prev, next): "http://docs.rackspace.com/core/event": "event", "http://docs.rackspace.com/event/customer/access_policy": "ap"} d = xmltodict.parse(xml, process_namespaces=True, namespaces=namespaces) - testcase.assertIsNone(feed_match.match(d)) + assert_that(d, feed_match) def link(params): """ Return full URL with given query params """ - return u"https://mimic-host-port/cloudfeeds_cap/customer_access_events?{}".format(urlencode(params)) + return (u"https://mimic-host-port/cloudfeeds_cap/customer_access_events?{}" + .format(urlencode(sorted(params.items())))) class GenFeedTests(SynchronousTestCase): @@ -107,7 +111,7 @@ def test_add_events_empty(self): """ events = [{"tenant_id": "1234", "status": "SUSPENDED"}, {"tenant_id": "2345", "status": "FULL"}] - d = request(self, self.root, "POST", "/cloudfeeds_cap/events", + d = request(self, self.root, b"POST", b"/cloudfeeds_cap/events", body=json.dumps({"events": events}).encode("utf-8")) self.assertEqual(self.successResultOf(d).code, 201) self.assertEqual( @@ -127,7 +131,7 @@ def test_add_events_update(self): self.ids = ["id5", "id4"] events = [{"tenant_id": "t1", "status": "TERMINATED"}, {"tenant_id": "t2", "status": "SUSPENDED"}] - d = request(self, self.root, "POST", "/cloudfeeds_cap/events", + d = request(self, self.root, b"POST", b"/cloudfeeds_cap/events", body=json.dumps({"events": events}).encode("utf-8")) self.assertEqual(self.successResultOf(d).code, 201) exp_events = [CustomerAccessEvent(u"t1", u"TERMINATED", 200.0, u"id4"), @@ -146,9 +150,10 @@ def test_get_events_empty(self): are no events stored. No previous and next links are provided. """ d = request_with_content( - self, self.root, "GET", "/cloudfeeds_cap/customer_access_policy/events") + self, self.root, b"GET", b"/cloudfeeds_cap/customer_access_policy/events") resp, body = self.successResultOf(d) - self.assertEqual(resp.headers.getRawHeaders("Content-Type"), [b"application/atom+xml"]) + self.assertEqual(resp.headers.getRawHeaders("Content-Type"), + ["application/atom+xml"]) self.assertEqual(body, empty_feed) def test_get_events(self): @@ -158,7 +163,7 @@ def test_get_events(self): """ events = self.test_add_events_update() resp, body = self.successResultOf(request_with_content( - self, self.root, "GET", "/cloudfeeds_cap/customer_access_policy/events")) + self, self.root, b"GET", b"/cloudfeeds_cap/customer_access_policy/events")) assert_has_events( self, body, events, link(dict(marker="id4", direction="forward")), link(dict(marker="id2", direction="backward"))) @@ -170,8 +175,8 @@ def test_get_events_marker_forward(self): """ events = self.test_add_events_update() resp, body = self.successResultOf(request_with_content( - self, self.root, "GET", - "/cloudfeeds_cap/customer_access_policy/events?marker=id1&direction=forward")) + self, self.root, b"GET", + b"/cloudfeeds_cap/customer_access_policy/events?marker=id1&direction=forward")) assert_has_events( self, body, events[:2], link(dict(marker="id4", direction="forward")), @@ -184,8 +189,8 @@ def test_get_events_marker_backward(self): """ events = self.test_add_events_update() resp, body = self.successResultOf(request_with_content( - self, self.root, "GET", - "/cloudfeeds_cap/customer_access_policy/events?marker=id5&direction=backward")) + self, self.root, b"GET", + b"/cloudfeeds_cap/customer_access_policy/events?marker=id5&direction=backward")) assert_has_events( self, body, events[2:], link(dict(marker="id1", direction="forward")), @@ -197,8 +202,8 @@ def test_get_events_limit(self): """ events = self.test_add_events_update() resp, body = self.successResultOf(request_with_content( - self, self.root, "GET", - "/cloudfeeds_cap/customer_access_policy/events?limit=3")) + self, self.root, b"GET", + b"/cloudfeeds_cap/customer_access_policy/events?limit=3")) assert_has_events( self, body, events[:3], link(dict(marker="id4", direction="forward")),