def setup(self): self.graph = create_object_graph(name="example", testing=True) self.ns = Namespace(subject="file") self.relation_ns = Namespace(subject=Person, object_="file") self.controller = FileController() UPLOAD_MAPPINGS = { Operation.Upload: EndpointDefinition( func=self.controller.upload, request_schema=FileExtraSchema(), ), } UPLOAD_FOR_MAPPINGS = { Operation.UploadFor: EndpointDefinition( func=self.controller.upload_for_person, request_schema=FileExtraSchema(), response_schema=FileResponseSchema(), ), } configure_upload(self.graph, self.ns, UPLOAD_MAPPINGS) configure_upload(self.graph, self.relation_ns, UPLOAD_FOR_MAPPINGS) configure_swagger(self.graph) self.client = self.graph.flask.test_client()
def test_operation_naming_relation(): """ Complext (subject+object) endpoint naming works. """ ns = Namespace(subject="foo", object_="bar") endpoint = ns.endpoint_for(Operation.SearchFor) assert_that(endpoint, is_(equal_to("foo.search_for.bar.v1")))
def test_endpoint_for(): """ Simple (subject-only) endpoint naming works. """ ns = Namespace(subject="foo") endpoint = ns.endpoint_for(Operation.Search) assert_that(endpoint, is_(equal_to("foo.search.v1")))
def test_operation_href_for(): """ Operations can resolve themselves as fully expanded hrefs. """ graph = create_object_graph(name="example", testing=True) ns = Namespace(subject="foo") @graph.route(ns.collection_path, Operation.Search, ns) def search_foo(): pass with graph.app.test_request_context(): url = ns.href_for(Operation.Search) assert_that(url, is_(equal_to("http://localhost/api/foo")))
def test_operation_url_for_internal(): """ Operations can resolve themselves via Flask's `url_for` and get internal URIs. """ graph = create_object_graph(name="example", testing=True) ns = Namespace(subject="foo") @graph.route(ns.collection_path, Operation.Search, ns) def search_foo(): pass with graph.app.test_request_context(): url = ns.url_for(Operation.Search, _external=False) assert_that(url, is_(equal_to("/api/foo")))
class TestAlias(object): def setup(self): self.graph = create_object_graph(name="example", testing=True) self.ns = Namespace(subject=Person) configure_crud(self.graph, self.ns, PERSON_MAPPINGS) configure_alias(self.graph, self.ns, PERSON_MAPPINGS) self.client = self.graph.flask.test_client() def test_url_for(self): with self.graph.app.test_request_context(): url = self.ns.url_for(Operation.Alias, person_name="foo") assert_that(url, is_(equal_to("http://localhost/api/person/foo"))) def test_swagger_path(self): with self.graph.app.test_request_context(): path = build_path(Operation.Alias, self.ns) assert_that(path, is_(equal_to("/api/person/{person_name}"))) def test_alias(self): response = self.client.get("/api/person/foo") assert_that(response.status_code, is_(equal_to(302))) assert_that(response.headers["Location"], is_(equal_to("http://localhost/api/person/1")))
def __init__(self, graph): super(TaskEventController, self).__init__(graph, graph.task_event_store) self.ns = Namespace( subject=TaskEvent, version="v1", )
def __init__(self, graph): super().__init__(graph, graph.encryptable_store) self.ns = Namespace( subject="encryptable", version="v1", )
def __init__(self, graph): super().__init__(graph, graph.customer_event_store) self.ns = Namespace( subject=CustomerEvent, version="v1", )
def __init__(self, graph): super().__init__(graph, graph.order_store) self.ns = Namespace( subject=Order, version="v1", )
def __init__(self, graph): super().__init__(graph, graph.topping_store) self.ns = Namespace( subject=Topping, version="v1", )
def test_build_integer_valued_param(): graph = create_object_graph(name="example", testing=True) ns = Namespace( subject=Person, version="v1", identifier_type="int", ) configure_crud(graph, ns, PERSON_MAPPINGS) with graph.flask.test_request_context(): operations = list(iter_endpoints(graph, match_function)) swagger_schema = build_swagger(graph, ns, operations) assert_that( build_path_for_integer_param(ns, Operation.Update, set(["person_id"])), equal_to("/api/v1/person/{person_id}"), ) assert_that( swagger_schema, has_entries(paths=has_entries( **{ "/person/{person_id}": has_entries(patch=has_entries(parameters=has_items({ "required": True, "type": "integer", "name": "person_id", "in": "path", }), ), ), }, ), ))
def test_discovery(): graph = create_object_graph(name="example", testing=True) graph.use("discovery_convention") ns = Namespace("foo") @graph.route(ns.collection_path, Operation.Search, ns) def search_foo(): pass client = graph.flask.test_client() response = client.get("/api/") assert_that(response.status_code, is_(equal_to(200))) data = loads(response.get_data().decode("utf-8")) assert_that( data, is_( equal_to({ "_links": { "search": [{ "href": "http://localhost/api/foo?offset=0&limit=20", "type": "foo", }], "self": { "href": "http://localhost/api/?offset=0&limit=20", }, } })))
def __init__(self, graph): super().__init__(graph, graph.follower_relationship_store) self.ns = Namespace( subject=FollowerRelationship, version="v1", )
def test_operation_href_for_qs(): """ Operations can resolve themselves as fully expanded hrefs with custom query string parameter. """ graph = create_object_graph(name="example", testing=True) ns = Namespace(subject="foo") @graph.route(ns.collection_path, Operation.Search, ns) def search_foo(): pass with graph.app.test_request_context(): url = ns.href_for(Operation.Search, offset=0, limit=10, qs=dict(foo="bar")) assert_that(url, matches_uri("http://localhost/api/foo?offset=0&limit=10&foo=bar"))
def __init__(self, graph): super().__init__(graph, graph.account_store) self.ns = Namespace( subject=Account, version="v1", )
def __init__(self, graph): super().__init__(graph, graph.example_store) self.ns = Namespace( subject=Example, version="v1", )
def test_custom_paginated_list(): graph = create_object_graph(name="example", testing=True) ns = Namespace(subject="foo", object_="bar") @graph.route(ns.relation_path, Operation.SearchFor, ns) def search_foo(): pass uid = uuid4() paginated_list = PaginatedList( ns, Page.from_query_string( dict( offset=2, limit=2, baz="baz", uid=uid, value=MyEnum.ONE, )), ["1", "2"], 10, operation=Operation.SearchFor, foo_id="FOO_ID", ) rest = "baz=baz&uid={}&value=ONE".format(uid) with graph.flask.test_request_context(): assert_that( paginated_list.to_dict(), is_( equal_to({ "count": 10, "items": [ "1", "2", ], "offset": 2, "limit": 2, "_links": { "self": { "href": "http://localhost/api/foo/FOO_ID/bar?offset=2&limit=2&{}" .format(rest), }, "next": { "href": "http://localhost/api/foo/FOO_ID/bar?offset=4&limit=2&{}" .format(rest), }, "prev": { "href": "http://localhost/api/foo/FOO_ID/bar?offset=0&limit=2&{}" .format(rest), }, }, "baz": "baz", "uid": str(uid), "value": "ONE", })))
def __init__(self, graph): super().__init__(graph, graph.pizza_store) self.ns = Namespace( subject=Pizza, version="v1", )
def get_links(self, obj): links = Links() links["self"] = Link.for_( Operation.Retrieve, Namespace( subject=User, version="v1", ), user_id=obj.id, ) links["tweets"] = Link.for_( Operation.SearchFor, Namespace( subject=User, object_="tweets", version="v1", ), user_id=obj.id, ) links["followers"] = Link.for_( Operation.SearchFor, Namespace( subject=User, object_="followers", version="v1", ), user_id=obj.id, ) links["following"] = Link.for_( Operation.SearchFor, Namespace( subject=User, object_="following", version="v1", ), user_id=obj.id, ) links["feed"] = Link.for_( Operation.SearchFor, Namespace( subject=User, object_="feed", version="v1", ), user_id=obj.id, ) return links.to_dict()
def setup(self): # override configuration to use "query" operations for swagger def loader(metadata): return dict( swagger_convention=dict( # default behavior appends this list to defaults; use a tuple to override operations=["query"], version="v1", ), ) self.graph = create_object_graph(name="example", testing=True, loader=loader) self.graph.use("swagger_convention") self.ns = Namespace(subject="foo") make_query(self.graph, self.ns, QueryStringSchema(), QueryResultSchema()) self.client = self.graph.flask.test_client()
def get_links(self, obj): links = Links() links["self"] = Link.for_( Operation.Retrieve, Namespace(subject=Address), address_id=obj.id, ) return links.to_dict()
def __init__(self, graph): super().__init__(graph, graph.tweet_store) self.follower_relationship_store = graph.follower_relationship_store self.ns = Namespace( subject=Tweet, version="v1", )
def setup(self): self.graph = create_object_graph(name="example", testing=True) self.person_ns = Namespace(subject=Person) self.ns = Namespace(subject=PersonSearch) # ensure that link hrefs work configure_crud( self.graph, self.person_ns, { Operation.Retrieve: (person_retrieve, PersonLookupSchema(), PersonSchema()), }) # enable saved search configure_saved_search( self.graph, self.ns, { Operation.SavedSearch: (person_search, OffsetLimitPageSchema(), PersonSchema()), }) self.client = self.graph.flask.test_client()
def configure_relation(graph, ns, mappings, path_prefix=""): """ Register relation endpoint(s) between two resources. """ ns = Namespace.make(ns, path=path_prefix) convention = RelationConvention(graph) convention.configure(ns, mappings)
def test_parse_endpoint_relation(): """ Complex (subject+object) endpoints can be parsed. """ operation, ns = Namespace.parse_endpoint("foo.search_for.bar.v1") assert_that(operation, is_(equal_to(Operation.SearchFor))) assert_that(ns.subject, is_(equal_to("foo"))) assert_that(ns.object_, is_(equal_to("bar")))
def test_parse_endpoint(): """ Simple (subject-only) endpoints can be parsed. """ operation, ns = Namespace.parse_endpoint("foo.search.v1") assert_that(operation, is_(equal_to(Operation.Search))) assert_that(ns.subject, is_(equal_to("foo"))) assert_that(ns.object_, is_(none()))
def get_links(self, obj): links = Links() links["self"] = Link.for_( Operation.Retrieve, Namespace(subject=OrderEvent, version="v1",), order_event_id=obj.id, ) return links.to_dict()
def setup(self): self.graph = create_object_graph(name="example", testing=True) self.ns = Namespace(subject="foo") configure_crud(self.graph, self.ns, FOO_MAPPINGS) configure_swagger(self.graph) self.client = self.graph.flask.test_client()
def configure_discovery(graph): """ Build a singleton endpoint that provides a link to all search endpoints. """ ns = Namespace(subject=graph.config.discovery_convention.name, ) convention = DiscoveryConvention(graph) convention.configure(ns, discover=tuple()) return ns.subject
def __init__(self, graph): super().__init__(graph, graph.pizza_store) self.ns = Namespace( subject=Pizza, version="v1", ) self.order_event_factory = graph.order_event_factory self.sns_producer = graph.sns_producer
def test_namespace_accepts_controller(): """ Namespaces may optionally contain a controller. """ graph = create_object_graph(name="example", testing=True) controller = Mock() ns = Namespace(subject="foo", controller=controller) @graph.route(ns.collection_path, Operation.Search, ns) def search_foo(): pass with graph.app.test_request_context(): url = ns.href_for(Operation.Search) assert_that(url, is_(equal_to("http://localhost/api/foo"))) url = ns.url_for(Operation.Search) assert_that(url, is_(equal_to("http://localhost/api/foo"))) assert_that(ns.controller, is_(equal_to(controller)))
def test_qualified_operation_href_for(): """ Qualified operations add to the URI. """ graph = create_object_graph(name="example", testing=True) ns = Namespace(subject="foo", qualifier="bar", version="v1") @graph.route(ns.collection_path, Operation.Search, ns) def search_foo(): pass @graph.route(ns.instance_path, Operation.Retrieve, ns) def get_foo(foo_id): pass with graph.app.test_request_context(): url = ns.href_for(Operation.Retrieve, foo_id="baz") assert_that(url, is_(equal_to("http://localhost/api/v1/bar/foo/baz")))
def configure_build_info(graph): """ Configure the build info endpoint. """ ns = Namespace(subject=BuildInfo, ) convention = BuildInfoConvention(graph) convention.configure(ns, retrieve=tuple()) return convention.build_info
def test_parse_endpoint_swagger(): """ Versioned discovery endpoint can be parsed. """ operation, ns = Namespace.parse_endpoint("swagger.discover.v2") assert_that(operation, is_(equal_to(Operation.Discover))) assert_that(ns.subject, is_(equal_to("swagger"))) assert_that(ns.version, is_(equal_to("v2"))) assert_that(ns.object_, is_(none()))
def setup(self): self.graph = create_object_graph(name="example", testing=True) self.ns = Namespace( subject=Person, ) configure_crud(self.graph, self.ns, PERSON_MAPPINGS) configure_alias(self.graph, self.ns, PERSON_MAPPINGS) self.graph.config.swagger_convention.operations.append("alias") configure_swagger(self.graph) self.client = self.graph.flask.test_client()
class TestAlias: def setup(self): self.graph = create_object_graph(name="example", testing=True) self.ns = Namespace( subject=Person, ) configure_crud(self.graph, self.ns, PERSON_MAPPINGS) configure_alias(self.graph, self.ns, PERSON_MAPPINGS) self.graph.config.swagger_convention.operations.append("alias") configure_swagger(self.graph) self.client = self.graph.flask.test_client() def test_url_for(self): with self.graph.app.test_request_context(): url = self.ns.url_for(Operation.Alias, person_name="foo") assert_that(url, is_(equal_to("http://localhost/api/person/foo"))) def test_swagger_path(self): with self.graph.app.test_request_context(): path = build_path(Operation.Alias, self.ns) assert_that(path, is_(equal_to("/api/person/{person_name}"))) def test_alias(self): response = self.client.get("/api/person/foo") assert_that(response.status_code, is_(equal_to(302))) assert_that(response.headers["Location"], is_(equal_to("http://localhost/api/person/1"))) def test_swagger(self): response = self.client.get("/api/swagger") assert_that(response.status_code, is_(equal_to(200))) data = loads(response.data) alias = data["paths"]["/person/{person_name}"]["get"] assert_that( alias["responses"], has_key("302"), ) retrieve = data["paths"]["/person/{person_id}"]["get"] assert_that( retrieve["responses"], has_key("200"), )
def iter_endpoints(graph, match_func): """ Iterate through matching endpoints. The `match_func` is expected to have a signature of: def matches(operation, ns, rule): return True :returns: a generator over (`Operation`, `Namespace`, rule, func) tuples. """ for rule in graph.flask.url_map.iter_rules(): try: operation, ns = Namespace.parse_endpoint(rule.endpoint, get_converter(rule)) except (IndexError, ValueError, InternalServerError): # operation follows a different convention (e.g. "static") continue else: # match_func gets access to rule to support path version filtering if match_func(operation, ns, rule): func = graph.flask.view_functions[rule.endpoint] yield operation, ns, rule, func
class TestCreateCollection: def setup(self): self.graph = create_object_graph(name="example", testing=True) self.ns = Namespace(subject="foo") configure_crud(self.graph, self.ns, FOO_MAPPINGS) configure_swagger(self.graph) self.client = self.graph.flask.test_client() def test_create_collection_url(self): with self.graph.app.test_request_context(): url = self.ns.url_for(Operation.CreateCollection) assert_that(url, is_(equal_to("http://localhost/api/foo"))) def test_swagger_path(self): with self.graph.app.test_request_context(): path = build_path(Operation.CreateCollection, self.ns) assert_that(path, is_(equal_to("/api/foo"))) def test_swagger(self): response = self.client.get("/api/swagger") assert_that(response.status_code, is_(equal_to(200))) data = loads(response.data)["paths"]["/foo"]["post"] assert_that( data["parameters"], has_items( has_entry( "in", "body", ), has_entry( "schema", has_entry( "$ref", "#/definitions/FooRequest", ), ), ), ) assert_that( data["responses"], all_of( has_key("200"), is_not(has_key("204")), has_entry( "200", has_entry( "schema", has_entry( "$ref", "#/definitions/FooList", ), ), ), ), ) def test_create_collection(self): text = "Some text..." response = self.client.post( "/api/foo", data=dumps({"text": text}), ) assert_that(response.status_code, is_(equal_to(200))) assert_that(loads(response.data), is_(equal_to({ "count": 1, "offset": 0, "limit": 20, "items": [{"text": text}], "_links": { "self": { "href": "http://localhost/api/foo?offset=0&limit=20", }, }, })))
class TestCommand: def setup(self): # override configuration to use "query" operations for swagger def loader(metadata): return dict( swagger_convention=dict( # default behavior appends this list to defaults; use a tuple to override operations=["command"], version="v1", ), ) self.graph = create_object_graph(name="example", testing=True, loader=loader) self.graph.use("swagger_convention") self.ns = Namespace(subject="foo") make_command(self.graph, self.ns, CommandArgumentSchema(), CommandResultSchema()) self.client = self.graph.flask.test_client() def test_url_for(self): """ The operation knowns how to resolve a URI for this command. """ with self.graph.flask.test_request_context(): assert_that(self.ns.url_for(Operation.Command), is_(equal_to("http://localhost/api/v1/foo/do"))) def test_command(self): """ The command can take advantage of boilerplate encoding/decoding. """ uri = "/api/v1/foo/do" request_data = { "value": "bar", } response = self.client.post(uri, data=dumps(request_data)) assert_that(response.status_code, is_(equal_to(200))) assert_that(loads(response.get_data().decode("utf-8")), is_(equal_to({ "result": True, "value": "bar", }))) def test_swagger(self): """ Swagger definitions including this operation. """ response = self.client.get("/api/v1/swagger") assert_that(response.status_code, is_(equal_to(200))) swagger = loads(response.get_data().decode("utf-8")) assert_that(swagger["paths"], is_(equal_to({ "/foo/do": { "post": { "tags": ["foo"], "responses": { "default": { "description": "An error occurred", "schema": { "$ref": "#/definitions/Error", } }, "200": { "description": "My doc string", "schema": { "$ref": "#/definitions/CommandResult", } } }, "parameters": [ { "in": "header", "name": "X-Response-Skip-Null", "required": False, "type": "string", }, { "schema": { "$ref": "#/definitions/CommandArgument", }, "name": "body", "in": "body", }, ], "operationId": "command", } } })))
class TestUpload: def setup(self): self.graph = create_object_graph(name="example", testing=True) self.ns = Namespace(subject="file") self.relation_ns = Namespace(subject=Person, object_="file") self.controller = FileController() UPLOAD_MAPPINGS = { Operation.Upload: EndpointDefinition( func=self.controller.upload, request_schema=FileExtraSchema(), ), } UPLOAD_FOR_MAPPINGS = { Operation.UploadFor: EndpointDefinition( func=self.controller.upload_for_person, request_schema=FileExtraSchema(), response_schema=FileResponseSchema(), ), } configure_upload(self.graph, self.ns, UPLOAD_MAPPINGS) configure_upload(self.graph, self.relation_ns, UPLOAD_FOR_MAPPINGS) configure_swagger(self.graph) self.client = self.graph.flask.test_client() def test_upload_url_for(self): with self.graph.app.test_request_context(): url = self.ns.url_for(Operation.Upload) assert_that(url, is_(equal_to("http://localhost/api/file"))) def test_upload_for_url_for(self): with self.graph.app.test_request_context(): url = self.relation_ns.url_for(Operation.UploadFor, person_id=1) assert_that(url, is_(equal_to("http://localhost/api/person/1/file"))) def test_upload_swagger_path(self): with self.graph.app.test_request_context(): path = build_path(Operation.Upload, self.ns) assert_that(path, is_(equal_to("/api/file"))) def test_upload_for_swagger_path(self): with self.graph.app.test_request_context(): path = build_path(Operation.UploadFor, self.relation_ns) assert_that(path, is_(equal_to("/api/person/{person_id}/file"))) def test_swagger(self): response = self.client.get("/api/swagger") assert_that(response.status_code, is_(equal_to(200))) data = loads(response.data) upload = data["paths"]["/file"]["post"] upload_for = data["paths"]["/person/{person_id}/file"]["post"] # both endpoints return form data assert_that( upload["consumes"], contains("multipart/form-data"), ) assert_that( upload_for["consumes"], contains("multipart/form-data"), ) # one endpoint gets an extra query string parameter (and the other doesn't) assert_that( upload["parameters"], has_item( has_entries(name="extra"), ), ) assert_that( upload_for["parameters"], has_item( is_not(has_entries(name="extra")), ), ) # one endpoint gets a custom response type (and the other doesn't) assert_that( upload["responses"], all_of( has_key("204"), is_not(has_key("200")), has_entry("204", is_not(has_key("schema"))), ), ) assert_that( upload_for["responses"], all_of( has_key("200"), is_not(has_key("204")), has_entry("200", has_entry("schema", has_entry("$ref", "#/definitions/FileResponse"))), ), ) def test_upload(self): response = self.client.post( "/api/file", data=dict( file=(BytesIO(b"Hello World\n"), "hello.txt"), ), ) assert_that(response.status_code, is_(equal_to(204))) assert_that(self.controller.calls, contains( has_entries( files=contains(contains("file", anything(), "hello.txt")), extra="something", ), )) def test_upload_for(self): person_id = uuid4() response = self.client.post( "/api/person/{}/file".format(person_id), data=dict( file=(BytesIO(b"Hello World\n"), "hello.txt"), ), ) assert_that(response.status_code, is_(equal_to(200))) response_data = loads(response.get_data().decode("utf-8")) assert_that(response_data, is_(equal_to(dict( id=str(person_id), )))) assert_that(self.controller.calls, contains( has_entries( files=contains(contains("file", anything(), "hello.txt")), extra="something", person_id=person_id, ), )) def test_upload_multipart(self): response = self.client.post( "/api/file", data=dict( file=(BytesIO(b"Hello World\n"), "hello.txt"), extra="special", ), ) assert_that(response.status_code, is_(equal_to(204))) assert_that(self.controller.calls, contains( has_entries( files=contains(contains("file", anything(), "hello.txt")), extra="special", ), ))