def test_backlinks_via_components_ref(): """ We should be able to specify a backlink by using a $ref to refer to a shared backlink component. """ doc_uri = fixture_uri("backlinks-components-ref.yaml") apigraph = APIGraph(doc_uri) assert apigraph.docs.keys() == {doc_uri} expected_nodes = [ NodeKey(doc_uri, "/users", HttpMethod.POST), NodeKey(doc_uri, "/1.0/users/{username}", HttpMethod.GET), NodeKey(doc_uri, "/2.0/users/{username}", HttpMethod.GET), ] expected_edges = [ ( expected_nodes[0], expected_nodes[1], (None, "201"), { "response_id": "201", "chain_id": None, "detail": LinkDetail( link_type=LinkType.BACKLINK, name="CreateUser", description="Create a new user that matches the username of current request url segment", parameters={}, requestBody=None, requestBodyParameters={"/username": "******"}, ), }, ), ( expected_nodes[0], expected_nodes[2], (None, "201"), { "response_id": "201", "chain_id": None, "detail": LinkDetail( link_type=LinkType.BACKLINK, name="CreateUser", description="Create a new user that matches the username of current request url segment", parameters={}, requestBody=None, requestBodyParameters={"/username": "******"}, ), }, ), ] assert [node for node in apigraph.graph.nodes] == expected_nodes assert [ edge for edge in apigraph.graph.edges(data=True, keys=True) ] == expected_edges
def test_links_multiple_chains(httpx_mock): doc_uri = fixture_uri("links-with-multiple-chain-id.yaml") apigraph = APIGraph(doc_uri) assert apigraph.docs.keys() == {doc_uri} # sorted expected_nodes = [ NodeKey(doc_uri, "/1.0/users/{username}", HttpMethod.GET), NodeKey(doc_uri, "/2.0/repositories/{username}", HttpMethod.GET), NodeKey(doc_uri, "/2.0/users/{username}", HttpMethod.GET), ] expected_edges = [ ( expected_nodes[0], expected_nodes[1], ("v1", "200"), { "response_id": "200", "chain_id": "v1", "detail": LinkDetail( link_type=LinkType.LINK, name="userRepositories", description="Get list of repositories", parameters={"username": "******"}, requestBody=None, requestBodyParameters={}, ), }, ), ( expected_nodes[2], expected_nodes[1], ("default", "200"), { "response_id": "200", "chain_id": "default", "detail": LinkDetail( link_type=LinkType.LINK, name="userRepositories", description="Get list of repositories", parameters={"username": "******"}, requestBody=None, requestBodyParameters={}, ), }, ), ] assert sorted([node for node in apigraph.graph.nodes]) == expected_nodes assert [ edge for edge in apigraph.graph.edges(data=True, keys=True) ] == expected_edges
def test_backlinks_request_body_params(): doc_uri = fixture_uri("backlinks-request-body-params.yaml") apigraph = APIGraph(doc_uri) assert apigraph.docs.keys() == {doc_uri} expected_nodes = [ NodeKey(doc_uri, "/users", HttpMethod.POST), NodeKey(doc_uri, "/pets", HttpMethod.POST), ] expected_edges = [ ( expected_nodes[0], expected_nodes[1], (None, "201"), { "response_id": "201", "chain_id": None, "detail": LinkDetail( link_type=LinkType.BACKLINK, name="New User", description="", parameters={}, requestBody=None, requestBodyParameters={"/owner": "$response.body#/username"}, ), }, ), ] assert [node for node in apigraph.graph.nodes] == expected_nodes assert [ edge for edge in apigraph.graph.edges(data=True, keys=True) ] == expected_edges
def test_links_request_body(httpx_mock): doc_uri = fixture_uri("links-request-body.yaml") apigraph = APIGraph(doc_uri) assert apigraph.docs.keys() == {doc_uri} expected_nodes = [ NodeKey(doc_uri, "/users", HttpMethod.POST), NodeKey(doc_uri, "/pets/{id}/add-owner", HttpMethod.POST), ] expected_edges = [ ( expected_nodes[0], expected_nodes[1], (None, "201"), { "response_id": "201", "chain_id": None, "detail": LinkDetail( link_type=LinkType.LINK, name="Add Pet", description="", parameters={}, requestBody="$response.body", requestBodyParameters={}, ), }, ), ] assert [node for node in apigraph.graph.nodes] == expected_nodes assert [ edge for edge in apigraph.graph.edges(data=True, keys=True) ] == expected_edges
def test_cross_doc_links_circular_ref(httpx_mock): """ Test that we don't get stuck in infinite loop when resolving doc refs. NOTE: cross-doc-links.yaml and cross-doc-links-circular-ref.yaml together form a circular dependency in that both docs contain links to each other. This is via a mutual backlink and a link, since both edges are directed identically this should form a redundant edge. So we have a circular ref between docs, but not a circular dependency in the request graph. NOTE: since we are re-using cross-doc-links.yaml but with a different `fixture_uri` pre-parse substitution, this test relies on `clear_cache` auto fixture having `function` scope... """ doc_uri = "https://fakeurl/cross-doc-links.yaml" other_doc_uri = fixture_uri("cross-doc-circular-ref.yaml") raw_doc = str_doc_with_substitutions( "tests/fixtures/cross-doc-links.yaml", {"fixture_uri": other_doc_uri}, ) httpx_mock.add_response(url=doc_uri, data=raw_doc) apigraph = APIGraph(doc_uri) assert apigraph.docs.keys() == {doc_uri, other_doc_uri} expected_nodes = [ NodeKey(doc_uri, "/2.0/users", HttpMethod.POST), NodeKey(other_doc_uri, "/2.0/users/{username}", HttpMethod.GET), ] # the backlink+link edges in this case are redundant # we expect apigraph to take the backlink over the link expected_edges = [ ( expected_nodes[0], expected_nodes[1], (None, "201"), { "response_id": "201", "chain_id": None, "detail": LinkDetail( link_type=LinkType.BACKLINK, name="createUser", description="", parameters={}, requestBody=None, requestBodyParameters={"/username": "******"}, ), }, ), ] assert [node for node in apigraph.graph.nodes] == expected_nodes assert [ edge for edge in apigraph.graph.edges(data=True, keys=True) ] == expected_edges
def test_links(fixture, chain_id): """ NOTE: these fixtures use within-doc $refs, so that is tested too links.yaml and links-with-chain-id.yaml use `operationId` while links-local-operationref.yaml uses a relative (local) `operationRef` NOTE: these links all use `parameters` and not `requestBody` or `x-apigraph-requestBodyParameters` """ doc_uri = fixture_uri(fixture) apigraph = APIGraph(doc_uri) assert apigraph.docs.keys() == {doc_uri} expected_nodes = [ NodeKey(doc_uri, "/2.0/users/{username}", HttpMethod.GET), NodeKey(doc_uri, "/2.0/repositories/{username}", HttpMethod.GET), ] expected_edges = [ ( expected_nodes[0], expected_nodes[1], (chain_id, "200"), { "response_id": "200", "chain_id": chain_id, "detail": LinkDetail( link_type=LinkType.LINK, name="userRepositories", description="Get list of repositories", parameters={"username": "******"}, requestBody=None, requestBodyParameters={}, ), }, ), ] assert [node for node in apigraph.graph.nodes] == expected_nodes assert [ edge for edge in apigraph.graph.edges(data=True, keys=True) ] == expected_edges
def test_link_backlink_same_chain_consolidation(): """ in case of redundant edge key defined as both link and backlink we should store a single edge with detail from the backlink (we expect backlinks to be more explicit as they are an apigraph extension to OpenAPI) """ doc_uri = fixture_uri("backlinks-with-links-same-chain-id.yaml") apigraph = APIGraph(doc_uri) assert apigraph.docs.keys() == {doc_uri} expected_nodes = [ NodeKey(doc_uri, "/2.0/users/{username}", HttpMethod.GET), NodeKey(doc_uri, "/2.0/repositories/{username}", HttpMethod.GET), ] expected_edges = [ ( expected_nodes[0], expected_nodes[1], ("default", "200"), { "response_id": "200", "chain_id": "default", "detail": LinkDetail( link_type=LinkType.BACKLINK, name="Get User by Username", description="", parameters={"username": "******"}, requestBody=None, requestBodyParameters={}, ), }, ), ] assert [node for node in apigraph.graph.nodes] == expected_nodes assert [ edge for edge in apigraph.graph.edges(data=True, keys=True) ] == expected_edges
def test_backlinks(fixture, chain_id): """ NOTE: these links all use `parameters` and not `requestBody` or `x-apigraph-requestBodyParameters` """ doc_uri = fixture_uri(fixture) apigraph = APIGraph(doc_uri) assert apigraph.docs.keys() == {doc_uri} expected_nodes = [ NodeKey(doc_uri, "/2.0/users/{username}", HttpMethod.GET), NodeKey(doc_uri, "/2.0/repositories/{username}", HttpMethod.GET), ] expected_edges = [ ( expected_nodes[0], expected_nodes[1], (chain_id, "200"), { "response_id": "200", "chain_id": chain_id, "detail": LinkDetail( link_type=LinkType.BACKLINK, name="Get User by Username", description="", parameters={"username": "******"}, requestBody=None, requestBodyParameters={}, ), }, ), ] assert [node for node in apigraph.graph.nodes] == expected_nodes assert [ edge for edge in apigraph.graph.edges(data=True, keys=True) ] == expected_edges
def test_chain_for_node(traverse_anonymous): """ The test fixture contains three dependency chains, two of which end at `createUser` and `createUserv1` respectively and both beginning at `getRepository`. The third chain is the 'anonymous' link (no chainId specified) which extends the "default" chain to begin at /invite. We request dependencies of `getRepositoriesByOwner`, which is not at the end of either chain (is followed by `getRepository`) and check that we only select up to the requested node and no further. """ doc_uri = fixture_uri("dependencies.yaml") apigraph = APIGraph(doc_uri) assert apigraph.docs.keys() == {doc_uri} default_deps = apigraph.chain_for_node( node_key=NodeKey(doc_uri, "/2.0/repositories/{username}", "get"), chain_id="default", traverse_anonymous=traverse_anonymous, ) v1_deps = apigraph.chain_for_node( node_key=NodeKey(doc_uri, "/2.0/repositories/{username}", "get"), chain_id="v1", traverse_anonymous=traverse_anonymous, ) # dependencies from the "default" chain # NOTE: subsequent op `/2.0/repositories/{username}/{slug}` is not included # (nodes here manually sorted in url order for test case) default_expected_nodes = [ NodeKey(doc_uri, "/2.0/repositories/{username}", "get"), NodeKey(doc_uri, "/2.0/users", "post"), NodeKey(doc_uri, "/2.0/users/{username}", "get"), ] # the "invite" predecessor has no chainId and is only included when # `traverse_anonymous=True` if traverse_anonymous: default_expected_nodes.append(NodeKey( doc_uri, "/invite", "post", )) assert sorted([node for node in default_deps.nodes]) == default_expected_nodes # (edges here manually sorted in from-node??? order for test case) default_expected_edges = [ ( default_expected_nodes[1], default_expected_nodes[2], ("default", "201"), { "response_id": "201", "chain_id": "default", "detail": LinkDetail( link_type=LinkType.LINK, name="userByUsername", description="", parameters={"username": "******"}, requestBody=None, requestBodyParameters={}, ), }, ), ( default_expected_nodes[2], default_expected_nodes[0], ("default", "200"), { "response_id": "200", "chain_id": "default", "detail": LinkDetail( link_type=LinkType.LINK, name="userRepositories", description="Get list of repositories", parameters={"username": "******"}, requestBody=None, requestBodyParameters={}, ), }, ), ] # the "invite" predecessor has no chainId and is only included when # `traverse_anonymous=True` if traverse_anonymous: default_expected_edges.append(( default_expected_nodes[3], default_expected_nodes[1], (None, "201"), { "response_id": "201", "chain_id": None, "detail": LinkDetail( link_type=LinkType.BACKLINK, name="Redeem Invite", description="Create a user by redeeming an invite id+token", parameters={"invite-id": "$response.body#/id"}, requestBody=None, requestBodyParameters={ "/invite-token": "$response.body#/token" }, ), }, ), ) assert (sorted([edge for edge in default_deps.edges(data=True, keys=True) ]) == default_expected_edges) # dependencies from the "v1" chain # (this chain is not extended by any anonymous links in the document) v1_expected_nodes = [ NodeKey(doc_uri, "/1.0/users", "post"), NodeKey(doc_uri, "/1.0/users/{username}", "get"), NodeKey(doc_uri, "/2.0/repositories/{username}", "get"), ] v1_expected_edges = [ ( v1_expected_nodes[0], v1_expected_nodes[1], ("v1", "201"), { "response_id": "201", "chain_id": "v1", "detail": LinkDetail( link_type=LinkType.LINK, name="userByUsername", description="", parameters={"username": "******"}, requestBody=None, requestBodyParameters={}, ), }, ), ( v1_expected_nodes[1], v1_expected_nodes[2], ("v1", "200"), { "response_id": "200", "chain_id": "v1", "detail": LinkDetail( link_type=LinkType.LINK, name="userRepositories", description="Get list of repositories", parameters={"username": "******"}, requestBody=None, requestBodyParameters={}, ), }, ), ] assert sorted([node for node in v1_deps.nodes]) == v1_expected_nodes assert (sorted([edge for edge in v1_deps.edges(data=True, keys=True) ]) == v1_expected_edges)
def test_cross_doc_links(httpx_mock): """ NOTE: cross-doc-links.yaml links via `operationRef` URI to links.yaml So between this and `test_links` both `operationRef` and `operationId` links are tested """ doc_uri = "https://fakeurl/cross-doc-links.yaml" other_doc_uri = fixture_uri("links.yaml") raw_doc = str_doc_with_substitutions( "tests/fixtures/cross-doc-links.yaml", {"fixture_uri": other_doc_uri}, ) httpx_mock.add_response(url=doc_uri, data=raw_doc) apigraph = APIGraph(doc_uri) assert apigraph.docs.keys() == {doc_uri, other_doc_uri} expected_nodes = [ NodeKey(doc_uri, "/2.0/users", HttpMethod.POST), NodeKey(other_doc_uri, "/2.0/users/{username}", HttpMethod.GET), NodeKey(other_doc_uri, "/2.0/repositories/{username}", HttpMethod.GET), ] expected_edges = [ ( expected_nodes[0], expected_nodes[1], (None, "201"), { "response_id": "201", "chain_id": None, "detail": LinkDetail( link_type=LinkType.LINK, name="userByUsername", description="", parameters={"username": "******"}, requestBody=None, requestBodyParameters={}, ), }, ), ( expected_nodes[1], expected_nodes[2], (None, "200"), { "response_id": "200", "chain_id": None, "detail": LinkDetail( link_type=LinkType.LINK, name="userRepositories", description="Get list of repositories", parameters={"username": "******"}, requestBody=None, requestBodyParameters={}, ), }, ), ] assert [node for node in apigraph.graph.nodes] == expected_nodes assert [ edge for edge in apigraph.graph.edges(data=True, keys=True) ] == expected_edges