diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql index 7fe9b66..1ad0a67 100644 --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -1,1129 +1,1144 @@ """ SoftWare Heritage persistent Identifier """ scalar SWHID """ ISO-8601 encoded date string """ scalar DateTime """ Object with an id """ interface Node { """ Id of the object. This is for caching purpose and should not be used outside the GraphQL API """ id: ID! } """ SWH merkle node object with a SWHID """ interface SWHNode { """ SWHID of the object """ swhid: SWHID! } """ Information about pagination """ type PageInfo { """ Cursor to request the next page in the connection """ endCursor: String """ Are there more pages in the connection? """ hasNextPage: Boolean! } """ Binary string with multiple encodings """ type BinaryString { """ Utf-8 encoded value, any non Unicode character will be replaced """ text: String! """ base64 encoded value """ base64: String! } """ Connection to origins """ type OriginConnection { """ List of origin edges """ edges: [OriginEdge] """ List of origin objects """ nodes: [Origin] """ Information for pagination """ pageInfo: PageInfo! """ Total number of origin objects in the connection """ totalCount: Int } """ Edge in origin connection """ type OriginEdge { - """ - Cursor to request the next page after the item - """ - cursor: String! + # Cursor in this edge is removed for the time being + # see https://forge.softwareheritage.org/D8911 for details + + # """ + # Cursor to request the next page after the item + # """ + # cursor: String! """ Origin object """ node: Origin } """ A software origin object """ type Origin implements Node { """ Unique identifier """ id: ID! """ Origin URL """ url: String! """ Connection to all the visit objects for the origin """ visits( """ Returns the first _n_ elements from the list """ first: Int! """ Returns the page after this cursor """ after: String ): VisitConnection """ Latest visit object for the origin """ latestVisit( """ Return the latest visit with the given visit type """ visitType: String """ Return the latest visit with any of the given statuses """ allowedStatuses: [VisitStatusState] """ If True, the latest visit with a snapshot will be returned """ requireSnapshot: Boolean ): Visit """ Connection to all the snapshots for the origin """ snapshots( """ Returns the first _n_ elements from the list """ first: Int! """ Returns the page after this cursor """ after: String ): SnapshotConnection } """ Connection to origin visits """ type VisitConnection { """ List of visit edges """ edges: [VisitEdge] """ List of visit objects """ nodes: [Visit] """ Information for pagination """ pageInfo: PageInfo! """ Total number of visit objects in the connection """ totalCount: Int } """ Edge in origin visit connection """ type VisitEdge { - """ - Cursor to request the next page after the item - """ - cursor: String! + # Cursor in this edge is removed for the time being + # see https://forge.softwareheritage.org/D8911 for details + + # """ + # Cursor to request the next page after the item + # """ + # cursor: String! """ Visit object """ node: Visit } """ Possible visit status states """ enum VisitStatusState { created ongoing partial full not_found failed } """ An origin visit object """ type Visit implements Node { """ Unique identifier """ id: ID! """ Visit number for the origin """ visitId: Int """ Visit date ISO-8601 encoded """ date: DateTime! """ Type of the origin visited. Eg: git/hg/svn/tar/deb """ type: String """ Connection to all the status objects for the visit """ statuses( """ Returns the first _n_ elements from the list """ first: Int """ Returns the page after this cursor """ after: String ): VisitStatusConnection """ Latest status object for the Visit """ latestStatus( """ Filter by status state """ allowedStatuses: [VisitStatusState] """ Filter by the availability of a snapshot in the status """ requireSnapshot: Boolean ): VisitStatus } """ Connection to visit status """ type VisitStatusConnection { """ List of visit status edges """ edges: [VisitStatusEdge] """ List of visit status objects """ nodes: [VisitStatus] """ Information for pagination """ pageInfo: PageInfo! """ Total number of visit status objects in the connection """ totalCount: Int } """ Edge in visit status connection """ type VisitStatusEdge { - """ - Cursor to request the next page after the item - """ - cursor: String! + # Cursor in this edge is removed for the time being + # see https://forge.softwareheritage.org/D8911 for details + + # """ + # Cursor to request the next page after the item + # """ + # cursor: String! """ Visit status object """ node: VisitStatus } """ A visit status object """ type VisitStatus { """ Status string of the visit (either full, partial or ongoing) """ status: VisitStatusState! """ ISO-8601 encoded date string """ date: DateTime! """ Snapshot object """ snapshot: Snapshot """ Type of the origin visited. Eg: git/hg/svn/tar/deb """ type: String } """ Connection to snapshots """ type SnapshotConnection { """ List of snapshot edges """ edges: [SnapshotEdge] """ List of snapshot objects """ nodes: [Snapshot] """ Information for pagination """ pageInfo: PageInfo! """ Total number of snapshot objects in the connection """ totalCount: Int } """ Edge in snapshot connection """ type SnapshotEdge { """ Cursor to request the next page after the item """ cursor: String! """ Snapshot object """ node: Snapshot } """ A snapshot object """ type Snapshot implements SWHNode & Node { """ Unique identifier """ id: ID! """ SWHID of the snapshot object """ swhid: SWHID! """ Connection to all the snapshot branches """ branches( """ Returns the first _n_ elements from the list """ first: Int! """ Returns the page after this cursor """ after: String """ Filter by branch target types """ types: [BranchTargetType] """ Return branches whose name contains the given substring """ nameInclude: String """ Do not return branches whose name contains the given prefix """ nameExcludePrefix: String ): BranchConnection } """ Connection to snapshot branches """ type BranchConnection { """ List of branch edges """ edges: [BranchConnectionEdge] """ List of branch objects """ nodes: [Branch] """ Information for pagination """ pageInfo: PageInfo! """ Total number of branch objects in the connection """ totalCount: Int } """ Edge in snapshot branch connection """ type BranchConnectionEdge { - """ - Cursor to request the next page after the item - """ - cursor: String! + # Cursor in this edge is removed for the time being + # see https://forge.softwareheritage.org/D8911 for details + + # """ + # Cursor to request the next page after the item + # """ + # cursor: String! """ Branch object """ node: Branch } """ A user object """ type Person { """ User's email address """ email: BinaryString """ User's name """ name: BinaryString """ User's full name """ fullname: BinaryString } """ Possible branch target objects """ union BranchTarget = Revision | Release | Branch | Content | Directory | Snapshot """ Possible Branch target types """ enum BranchTargetType { revision release alias content directory snapshot } """ A snapshot branch object """ type Branch { """ Branch name """ name: BinaryString """ Type of Branch target """ targetType: BranchTargetType """ Branch target object """ target: BranchTarget } """ Connection to revisions """ type RevisionConnection { """ List of revision edges """ edges: [RevisionEdge] """ List of revision objects """ nodes: [Revision] """ Information for pagination """ pageInfo: PageInfo! """ Total number of revision objects in the connection """ totalCount: Int } """ Edge in revision connection """ type RevisionEdge { """ Cursor to request the next page after the item """ cursor: String! """ Revision object """ node: Revision } """ A revision object """ type Revision implements SWHNode & Node { """ Unique identifier """ id: ID! """ SWHID of the revision object """ swhid: SWHID! """ Message associated to the revision """ message: BinaryString """ Revision author """ author: Person """ Revision committer """ committer: Person """ Revision date ISO-8601 encoded """ date: DateTime """ Type of the revision, eg: git/hg """ type: String """ The unique directory object that revision points to """ directory: Directory """ Connection to all the parents of the revision """ parents( """ Returns the first _n_ elements from the list """ first: Int """ Returns the page after this cursor """ after: String ): RevisionConnection """ Connection to all the revisions heading to this one aka the commit log """ revisionLog( """ Returns the first _n_ elements from the list """ first: Int! """ Returns the page after the cursor """ after: String ): RevisionConnection } """ Possible release target objects """ union ReleaseTarget = Release | Revision | Directory | Content """ Possible release target types """ enum ReleaseTargetType { release revision content directory } """ A release object """ type Release implements SWHNode & Node { """ Unique identifier """ id: ID! """ SWHID of the release object """ swhid: SWHID! """ The name of the release """ name: BinaryString """ The message associated to the release """ message: BinaryString """ Release author """ author: Person """ Release date ISO-8601 encoded """ date: DateTime """ Type of release target """ targetType: ReleaseTargetType """ Release target object """ target: ReleaseTarget } """ Connection to directory entries """ type DirectoryEntryConnection { """ List of directory entry edges """ edges: [DirectoryEntryEdge] """ List of directory entry objects """ nodes: [DirectoryEntry] """ Information for pagination """ pageInfo: PageInfo! """ Total number of directory entry objects in the connection """ totalCount: Int } """ Edge in directory entry connection """ type DirectoryEntryEdge { """ Cursor to request the next page after the item """ cursor: String! """ Directory entry object """ node: DirectoryEntry } """ Possible directory entry target objects """ union DirectoryEntryTarget = Directory | Content | Revision """ Possible directory entry types """ enum DirectoryEntryTargetType { directory content revision } """ A directory entry object """ type DirectoryEntry { """ The directory entry name """ name: BinaryString """ Directory entry object type; can be file, dir or rev """ targetType: DirectoryEntryTargetType """ Directory entry target object """ target: DirectoryEntryTarget } """ A directory object """ type Directory implements SWHNode & Node { """ Unique identifier """ id: ID! """ SWHID of the directory object """ swhid: SWHID! """ Connection to the directory entries """ entries( """ Returns the first _n_ elements from the list """ first: Int """ Returns the page after this cursor """ after: String """ Filter by entry name """ nameInclude: String ): DirectoryEntryConnection } """ An object with different content hashes """ type ContentHashes { blake2s256: String sha1: String sha1_git: String sha256: String } """ Object with different content data representations """ type ContentData { """ URL to download the file data """ url: String } type ContentMimeType { """ Detected content encoding """ encoding: String """ Detected MIME type of the content """ mimetype: String } type ContentLanguage { """ Detected programming language if any """ lang: String } type ContentLicense { """ Array of strings containing the detected license names """ licenses: [String] } """ A content object """ type Content implements SWHNode & Node { """ Unique identifier """ id: ID! """ SWHID of the content object """ swhid: SWHID! """ Hashes for the content """ hashes: ContentHashes """ Length of the content in bytes """ length: Int """ Content status, visible or hidden """ status: String """ File content """ data: ContentData """ Information about the content MIME type """ mimeType: ContentMimeType """ Information about the programming language used in the content """ language: ContentLanguage """ Information about the license of the content """ license: ContentLicense } """ Connection to SearchResults """ type SearchResultConnection { """ List of SearchResult edges """ edges: [SearchResultEdge] """ List of SearchResult objects """ nodes: [SearchResult] """ Information for pagination """ pageInfo: PageInfo! """ Total number of result objects in the connection """ totalCount: Int } """ Edge in SearchResult connection """ type SearchResultEdge { - """ - Cursor to request the next page after the item - """ - cursor: String! + # Cursor in this edge is removed for the time being + # see https://forge.softwareheritage.org/D8911 for details + + # """ + # Cursor to request the next page after the item + # """ + # cursor: String! """ SearchResult object """ node: SearchResult } union SearchResultTarget = Origin | Revision | Release | Content | Directory | Snapshot enum SearchResultTargetType { origin revision release content directory snapshot } """ A SearchResult object """ type SearchResult { """ Result target type """ targetType: SearchResultTargetType """ Result target object """ target: SearchResultTarget } """ The query root of the GraphQL interface. """ type Query { """ Get an origin with its url """ origin( """ URL of the Origin """ url: String! ): Origin """ Get a Connection to all the origins """ origins( """ Returns the first _n_ elements from the list """ first: Int! """ Returns the page after the cursor """ after: String """ Filter origins with a URL pattern """ urlPattern: String ): OriginConnection """ Get the visit object with an origin URL and a visit id """ visit( """ URL of the origin """ originUrl: String! """ Visit id to get """ visitId: Int! ): Visit """ Get the snapshot with a SWHID """ snapshot( """ SWHID of the snapshot object """ swhid: SWHID! ): Snapshot """ Get the revision with a SWHID """ revision( """ SWHID of the revision object """ swhid: SWHID! ): Revision """ Get the release with a SWHID """ release( """ SWHID of the release object """ swhid: SWHID! ): Release """ Get the directory with a SWHID """ directory( """ SWHID of the directory object """ swhid: SWHID! ): Directory """ Get a directory entry with directory SWHID and a path """ directoryEntry( """ SWHID of the directory object """ directorySWHID: SWHID! """ Relative path to the requested object """ path: String! ): DirectoryEntry """ Get the content with a SWHID """ content( """ SWHID of the content object """ swhid: SWHID! ): Content """ Get a content that match all the given hashes. This entrypoint can be used to uniquely identify a content in the event of hash conflicts. Use multiple hashes to get an accurate result. At least one of the four hashes must be provided. """ contentByHashes( sha1: String sha256: String sha1_git: String blake2s256: String ): Content """ Resolve the given SWHID to an object """ resolveSWHID( """ SWHID to look for """ swhid: SWHID! ): SearchResultConnection """ Search in SWH """ search( """ String to search for """ query: String! """ Returns the first _n_ elements from the list """ first: Int! """ Returns the page after the cursor """ after: String ): SearchResultConnection } diff --git a/swh/graphql/tests/functional/test_branch_connection.py b/swh/graphql/tests/functional/test_branch_connection.py index 7a0c3a4..9d48ce0 100644 --- a/swh/graphql/tests/functional/test_branch_connection.py +++ b/swh/graphql/tests/functional/test_branch_connection.py @@ -1,152 +1,148 @@ # Copyright (C) 2022 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import pytest from . import utils def get_branches(client, **kwargs) -> tuple: query_str = """ query getSnapshot($swhid: SWHID!, $first: Int!, $after: String, $types: [BranchTargetType], $nameInclude: String, $excludePrefix: String) { snapshot(swhid: $swhid) { branches(first: $first, after: $after, types: $types, nameInclude: $nameInclude, nameExcludePrefix: $excludePrefix ) { pageInfo { endCursor } - edges { - cursor - } nodes { targetType name { text } target { __typename ...on Branch { name { text } } ...on Revision { swhid } ...on Release { swhid } ...on Content { swhid } ...on Directory { swhid } ...on Snapshot { swhid } } } } } } """ return utils.get_query_response(client, query_str, **kwargs) def test_get_data(client): swhid = "swh:1:snp:0e7f84ede9a254f2cd55649ad5240783f557e65f" data, errors = get_branches(client, swhid=swhid, first=10, types=["revision"]) assert len(data["snapshot"]["branches"]["nodes"]) == 1 # filter 'type' will return a single revision object and is used to assert data node = data["snapshot"]["branches"]["nodes"][0] assert node == { "name": {"text": "target/revision"}, "target": { "__typename": "Revision", "swhid": "swh:1:rev:66c7c1cd9673275037140f2abff7b7b11fc9439c", }, "targetType": "revision", } def test_get_branches_with_alias(client): swhid = "swh:1:snp:0e7f84ede9a254f2cd55649ad5240783f557e65f" data, _ = get_branches(client, swhid=swhid, first=10, types=["alias"]) node = data["snapshot"]["branches"]["nodes"][0] assert node == { "name": {"text": "target/alias"}, "target": {"__typename": "Branch", "name": {"text": "target/revision"}}, "targetType": "alias", } @pytest.mark.parametrize( "filter_type, count, target_type, swhid_pattern", [ ("revision", 1, "Revision", "swh:1:rev"), ("release", 1, "Release", "swh:1:rel"), ("directory", 1, "Directory", "swh:1:dir"), ("content", 0, "Content", "swh:1:cnt"), ("snapshot", 1, "Snapshot", "swh:1:snp"), ], ) def test_get_type_filter(client, filter_type, count, target_type, swhid_pattern): swhid = "swh:1:snp:0e7f84ede9a254f2cd55649ad5240783f557e65f" data, _ = get_branches(client, swhid=swhid, first=10, types=[filter_type]) assert len(data["snapshot"]["branches"]["nodes"]) == count for node in data["snapshot"]["branches"]["nodes"]: assert node["target"]["__typename"] == target_type assert node["target"]["swhid"].startswith(swhid_pattern) @pytest.mark.parametrize( "filter_types, count", [ (["revision", "release"], 2), (["revision", "snapshot", "release"], 3), ], ) def test_get_type_filter_multiple(client, filter_types, count): swhid = "swh:1:snp:0e7f84ede9a254f2cd55649ad5240783f557e65f" data, _ = get_branches(client, swhid=swhid, first=10, types=filter_types) assert len(data["snapshot"]["branches"]["nodes"]) == count @pytest.mark.parametrize("name", ["rel", "rev", "non-exist"]) def test_get_name_include_filter(client, name): swhid = "swh:1:snp:0e7f84ede9a254f2cd55649ad5240783f557e65f" data, _ = get_branches(client, swhid=swhid, first=10, nameInclude=name) for node in data["snapshot"]["branches"]["nodes"]: assert name in node["name"]["text"] @pytest.mark.parametrize("name", ["target", "target/dir"]) def test_get_name_exclude_prefix_filter(client, name): swhid = "swh:1:snp:0e7f84ede9a254f2cd55649ad5240783f557e65f" data, _ = get_branches(client, swhid=swhid, first=10, excludePrefix=name) for node in data["snapshot"]["branches"]["nodes"]: assert not node["name"]["text"].startswith(name) @pytest.mark.parametrize("count", [1, 2]) def test_get_first_arg(client, count): swhid = "swh:1:snp:0e7f84ede9a254f2cd55649ad5240783f557e65f" data, _ = get_branches(client, swhid=swhid, first=count) assert len(data["snapshot"]["branches"]["nodes"]) == count def test_get_after_arg(client): swhid = "swh:1:snp:0e7f84ede9a254f2cd55649ad5240783f557e65f" first_data, _ = get_branches(client, swhid=swhid, first=1) end_cursor = first_data["snapshot"]["branches"]["pageInfo"]["endCursor"] node_name = first_data["snapshot"]["branches"]["nodes"][0]["name"]["text"] second_data, _ = get_branches(client, swhid=swhid, first=3, after=end_cursor) branches = second_data["snapshot"]["branches"] assert len(branches["nodes"]) == 3 - assert branches["edges"][0]["cursor"] == end_cursor for node in branches["nodes"]: assert node["name"]["text"] > node_name diff --git a/swh/graphql/tests/functional/test_pagination.py b/swh/graphql/tests/functional/test_pagination.py index aaf4ac3..37fbf04 100644 --- a/swh/graphql/tests/functional/test_pagination.py +++ b/swh/graphql/tests/functional/test_pagination.py @@ -1,103 +1,151 @@ # Copyright (C) 2022 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from . import utils -from ..data import get_origins +from ..data import get_directories, get_origins # Using Origin object to run functional tests for pagination def get_origin_nodes(client, first, after=""): query_str = """ query getOrigins($first: Int!, $after: String) { origins(first: $first, after: $after) { nodes { id } pageInfo { hasNextPage endCursor } } } """ return utils.get_query_response(client, query_str, first=first, after=after) def test_pagination(client): # requesting the max number of nodes available # endCursor must be None data, _ = get_origin_nodes(client, first=len(get_origins())) assert len(data["origins"]["nodes"]) == len(get_origins()) assert data["origins"]["pageInfo"] == {"hasNextPage": False, "endCursor": None} def test_first_arg(client): data, _ = get_origin_nodes(client, first=1) assert len(data["origins"]["nodes"]) == 1 assert data["origins"]["pageInfo"]["hasNextPage"] is True def test_invalid_first_arg(client): data, errors = get_origin_nodes(client, first=-1) assert data["origins"] is None assert (len(errors)) == 2 # one error for origins and anotehr one for pageInfo assert ( errors[0]["message"] == "Pagination error: Value for argument 'first' is invalid; it must be between 0 and 1000" # noqa: B950 ) def test_too_big_first_arg(client): data, errors = get_origin_nodes(client, first=1001) # max page size is 1000 assert data["origins"] is None assert (len(errors)) == 2 assert ( errors[0]["message"] == "Pagination error: Value for argument 'first' is invalid; it must be between 0 and 1000" # noqa: B950 ) def test_after_arg(client): first_data, _ = get_origin_nodes(client, first=1) end_cursor = first_data["origins"]["pageInfo"]["endCursor"] # get again with endcursor as the after argument data, _ = get_origin_nodes(client, first=1, after=end_cursor) assert len(data["origins"]["nodes"]) == 1 assert data["origins"]["pageInfo"] == {"hasNextPage": False, "endCursor": None} def test_invalid_after_arg(client): data, errors = get_origin_nodes(client, first=1, after="invalid") assert data["origins"] is None assert (len(errors)) == 2 assert ( errors[0]["message"] == "Pagination error: Invalid value for argument 'after'" ) def test_edge_cursor(client): - origins = get_origin_nodes(client, first=1)[0]["origins"] - # end cursor here must be the item cursor for the second item - end_cursor = origins["pageInfo"]["endCursor"] - + # Use DirectoryEntry connection to test connection edges + # The same code is used in all the other connections, hence a single test + directory = get_directories()[1] query_str = """ - query getOrigins($first: Int!, $after: String) { - origins(first: $first, after: $after) { - edges { - cursor - node { - id + query getDirectory($swhid: SWHID!, $entries_first: Int!, $entries_after: String) { + directory(swhid: $swhid) { + entries(first: $entries_first, after: $entries_after) { + edges { + cursor + node { + name { + text + } + } + } + pageInfo { + endCursor + hasNextPage + } + nodes { + name { + text + } } - } - nodes { - id } } } """ - data, _ = utils.get_query_response(client, query_str, first=1, after=end_cursor) - origins = data["origins"] - assert [edge["node"] for edge in origins["edges"]] == origins["nodes"] - assert origins["edges"][0]["cursor"] == end_cursor + data, _ = utils.get_query_response( + client, query_str, swhid=str(directory.swhid()), entries_first=1 + ) + entries = data["directory"]["entries"] + # Make sure the first node object in edges and nodes is the same + assert entries["edges"][0]["node"] == entries["nodes"][0] + assert entries["edges"][0]["node"] == { + "name": {"text": directory.entries[1].name.decode()} + } + assert entries["pageInfo"]["hasNextPage"] is True + # FIXME, Following behaviour is not in compliance with the relay spec. + # last-item-cursor and endcursor should be the same as per relay. + # This test will fail once the pagination becomes fully relay complaint. + assert entries["pageInfo"]["endCursor"] != entries["edges"][-1]["cursor"] + # Make another query with the after argument, after argument is the first item + # cursor here, result will be the same as the last one + new_data, _ = utils.get_query_response( + client, + query_str, + swhid=str(directory.swhid()), + entries_first=1, + entries_after=entries["edges"][0]["cursor"], + ) + assert new_data == data + # Make another query with the end cursor from the first query + final_data, _ = utils.get_query_response( + client, + query_str, + swhid=str(directory.swhid()), + entries_first=2, + entries_after=entries["pageInfo"]["endCursor"], + ) + final_result_entries = final_data["directory"]["entries"] + # endcursor from the first query will be the first item cursor here + # FIXME, this behaviour is not in compliance with the relay spec. + # With relay spec, items after the given cursor ($after) will be returned + assert ( + final_result_entries["edges"][0]["cursor"] == entries["pageInfo"]["endCursor"] + ) + assert final_result_entries["nodes"] == [ + {"name": {"text": directory.entries[0].name.decode()}}, + {"name": {"text": directory.entries[2].name.decode()}}, + ] diff --git a/swh/graphql/tests/functional/test_visit_status.py b/swh/graphql/tests/functional/test_visit_status.py index 4171ffb..bbf1164 100644 --- a/swh/graphql/tests/functional/test_visit_status.py +++ b/swh/graphql/tests/functional/test_visit_status.py @@ -1,103 +1,100 @@ # Copyright (C) 2022 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import pytest from . import utils from ..data import get_origins, get_visit_status, get_visits @pytest.mark.parametrize( "visit, visit_status", list(zip(get_visits(), get_visit_status())) ) def test_get_visit_status(client, visit, visit_status): query_str = """ query getVisit($origin: String!, $visitId: Int!) { visit(originUrl: $origin, visitId: $visitId) { statuses(first: 3) { nodes { status date type snapshot { swhid } } } } } """ data, _ = utils.get_query_response( client, query_str, origin=visit.origin, visitId=visit.visit ) assert data["visit"]["statuses"]["nodes"][0] == { "date": visit_status.date.isoformat(), "snapshot": {"swhid": f"swh:1:snp:{visit_status.snapshot.hex()}"} if visit_status.snapshot is not None else None, "status": visit_status.status, "type": visit_status.type, } def test_visit_status_pagination(client): # visit status is using a different cursor, hence separate test query_str = """ query getVisit($origin: String!, $visitId: Int!) { visit(originUrl: $origin, visitId: $visitId) { statuses(first: 1) { pageInfo { hasNextPage endCursor } edges { - cursor node { status } } } } } """ data, _ = utils.get_query_response( client, query_str, origin=get_origins()[0].url, visitId=1 ) # request again with the endcursor end_cursor = data["visit"]["statuses"]["pageInfo"]["endCursor"] query_str = """ query getVisit($origin: String!, $visitId: Int!, $after: String) { visit(originUrl: $origin, visitId: $visitId) { statuses(first: 1, after: $after) { pageInfo { hasNextPage endCursor } edges { - cursor node { status } } } } } """ data, _ = utils.get_query_response( client, query_str, origin=get_origins()[0].url, visitId=1, after=end_cursor, ) assert data["visit"]["statuses"] == { "edges": [ { - "cursor": "MjAxNC0wNS0wN1QwNDoyMDozOS40MzIyMjIrMDA6MDA=", "node": {"status": "ongoing"}, } ], "pageInfo": {"endCursor": None, "hasNextPage": False}, }