diff --git a/swh/graphql/resolvers/directory_entry.py b/swh/graphql/resolvers/directory_entry.py index 6fd30c1..7d46b31 100644 --- a/swh/graphql/resolvers/directory_entry.py +++ b/swh/graphql/resolvers/directory_entry.py @@ -1,42 +1,46 @@ # 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 swh.graphql.backends import archive from swh.graphql.utils import utils from swh.storage.interface import PagedResult from .base_connection import BaseConnection from .base_node import BaseNode class DirectoryEntryNode(BaseNode): """ Node resolver for a directory entry """ @property def target_hash(self): # for DirectoryNode return self._node.target + @property + def targetType(self): # To support the schema naming convention + return self._node.type + class DirectoryEntryConnection(BaseConnection): """ Connection resolver for entries in a directory """ from .directory import BaseDirectoryNode obj: BaseDirectoryNode _node_class = DirectoryEntryNode def _get_paged_result(self) -> PagedResult: # FIXME, using dummy(local) pagination, move pagination to backend # To remove localpagination, just drop the paginated call # STORAGE-TODO entries = ( archive.Archive().get_directory_entries(self.obj.swhid.object_id).results ) return utils.paginated(entries, self._get_first_arg(), self._get_after_arg()) diff --git a/swh/graphql/resolvers/search.py b/swh/graphql/resolvers/search.py index 2eed876..cf706b5 100644 --- a/swh/graphql/resolvers/search.py +++ b/swh/graphql/resolvers/search.py @@ -1,49 +1,53 @@ # 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 swh.graphql.backends import archive, search from swh.storage.interface import PagedResult from .base_connection import BaseConnection from .base_node import BaseNode class SearchResultNode(BaseNode): """ """ + @property + def targetType(self): # To support the schema naming convention + return self._node.type + class ResolveSwhidConnection(BaseConnection): _node_class = SearchResultNode def _get_paged_result(self) -> PagedResult: swhid = self.kwargs.get("swhid") results = [] if archive.Archive().is_object_available(swhid.object_id, swhid.object_type): results = [ { "target_hash": swhid.object_id, "type": swhid.object_type.name.lower(), } ] return PagedResult(results=results) class SearchConnection(BaseConnection): _node_class = SearchResultNode def _get_paged_result(self) -> PagedResult: origins = search.Search().get_origins( query=self.kwargs.get("query"), after=self._get_after_arg(), first=self._get_first_arg(), ) # FIXME hard coding type to origin for now, as it is the only searchable object results = [ {"target_url": ori["url"], "type": "origin"} for ori in origins.results ] return PagedResult(results=results, next_page_token=origins.next_page_token) diff --git a/swh/graphql/resolvers/snapshot_branch.py b/swh/graphql/resolvers/snapshot_branch.py index cf4bcd7..f751d2d 100644 --- a/swh/graphql/resolvers/snapshot_branch.py +++ b/swh/graphql/resolvers/snapshot_branch.py @@ -1,121 +1,125 @@ # 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 collections import namedtuple from swh.graphql.backends import archive from swh.graphql.errors import ObjectNotFoundError from swh.graphql.utils import utils from swh.storage.interface import PagedResult from .base_connection import BaseConnection from .base_node import BaseNode class BaseSnapshotBranchNode(BaseNode): # target field for this node is a UNION type # It is resolved in the top level (resolvers.resolvers.py) def _get_node_from_data(self, node_data: tuple): # node_data is a tuple as returned by _get_paged_result in # SnapshotBranchConnection and _get_node_data in AliasSnapshotBranchNode # overriding to support this special data structure branch_name, branch_obj = node_data node = { "name": branch_name, "type": branch_obj.target_type.value, "target_hash": branch_obj.target, } return namedtuple("NodeObj", node.keys())(*node.values()) def is_type_of(self): return "Branch" + @property + def targetType(self): # To support the schema naming convention + return self._node.type + def snapshot_swhid(self): """ Logic to handle multiple branch alias redirects Alias redirects can be any level deep. Hence the parent snapshot can be reached only by a loop This code expects every BranchNode to have a Snapshot parent in the GraphQL query at some level. """ from .snapshot import BaseSnapshotNode parent = self.obj while parent: if isinstance( parent, BaseSnapshotNode ): # Reached the nearest SnapshotNode object return parent.swhid parent = parent.obj # Reached the root query node. This will never happen with the current entrypoints raise ObjectNotFoundError("There is no snapshot associated with the branch") class AliasSnapshotBranchNode(BaseSnapshotBranchNode): obj: BaseSnapshotBranchNode def _get_node_data(self): snapshot_swhid = self.snapshot_swhid() target_branch = self.obj.target_hash alias_branch = archive.Archive().get_snapshot_branches( snapshot_swhid.object_id, first=1, name_include=target_branch ) if target_branch not in alias_branch["branches"]: raise ObjectNotFoundError( f"Branch name with {target_branch.decode()} is not available" ) # this will be serialized in _get_node_from_data method in the base class return (target_branch, alias_branch["branches"][target_branch]) class SnapshotBranchConnection(BaseConnection): """ Connection resolver for the branches in a snapshot """ from .snapshot import BaseSnapshotNode obj: BaseSnapshotNode _node_class = BaseSnapshotBranchNode def _get_paged_result(self) -> PagedResult: result = archive.Archive().get_snapshot_branches( self.obj.swhid.object_id, after=self._get_after_arg(), first=self._get_first_arg(), target_types=self.kwargs.get("types"), name_include=self._get_name_include_arg(), ) # endCursor is the last branch name, logic for that end_cusrsor = ( result["next_branch"] if result["next_branch"] is not None else None ) # FIXME, this pagination is not consistent with other connections # FIX in swh-storage to return PagedResult # STORAGE-TODO # this will be serialized in _get_node_from_data method in the node class return PagedResult( results=result["branches"].items(), next_page_token=end_cusrsor ) def _get_after_arg(self): # after argument must be an empty string by default after = super()._get_after_arg() return after.encode() if after else b"" def _get_name_include_arg(self): name_include = self.kwargs.get("nameInclude", None) return name_include.encode() if name_include else None def _get_index_cursor(self, index: int, node: BaseSnapshotBranchNode): # Snapshot branch is using a different cursor, hence the override return utils.get_encoded_cursor(node.name) diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql index 9696926..0d61a85 100644 --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -1,1063 +1,1063 @@ """ SoftWare Heritage persistent Identifier """ scalar SWHID """ ISO-8601 encoded date string """ scalar DateTime """ Content identifier in the form hash-type:hash-value """ scalar ContentHash """ 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 MerkleNode { """ 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 strings; different 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! """ 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: 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! """ Visit object """ node: Visit } """ 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 """ status( """ 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: 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! """ Visit status object """ node: VisitStatus } """ A visit status object """ type VisitStatus { """ Status string of the visit (either full, partial or ongoing) """ status: String! """ 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 MerkleNode & 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] """ Filter by branch name """ nameInclude: 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! """ 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 """ - type: BranchTargetType + 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 MerkleNode & Node { """ Unique identifier """ id: ID! """ SWHID of the revision object """ swhid: SWHID! """ Message associated to the revision """ message: BinaryString """ """ author: Person """ """ 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 MerkleNode & 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 """ """ 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 """ Possible directory entry types """ -enum DirectoryEntryType { +enum DirectoryEntryTargetType { dir file rev } """ A directory entry object """ type DirectoryEntry { """ The directory entry name """ name: BinaryString """ Directory entry object type; can be file, dir or rev """ - type: DirectoryEntryType + targetType: DirectoryEntryTargetType """ Directory entry target object """ target: DirectoryEntryTarget } """ A directory object """ type Directory implements MerkleNode & 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 ): DirectoryEntryConnection } """ An object with different content checksums """ type ContentChecksum { 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 ContentFileType { """ 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 MerkleNode & Node { """ Unique identifier """ id: ID! """ SWHID of the content object """ swhid: SWHID! """ Checksums for the content """ checksum: ContentChecksum """ 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 """ fileType: ContentFileType """ 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! """ 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 """ - type: SearchResultTargetType + 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 the content with a SWHID """ content( """ SWHID of the content object """ swhid: SWHID! ): Content """ Get the content by one or more hashes Use multiple hashes for an accurate result """ contentByHash( """ List of hashType:hashValue strings """ checksums: [ContentHash]! ): 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 bc91497..e636daf 100644 --- a/swh/graphql/tests/functional/test_branch_connection.py +++ b/swh/graphql/tests/functional/test_branch_connection.py @@ -1,147 +1,147 @@ # 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, swhid: str, first: int, **args) -> tuple: args["first"] = first params = utils.get_query_params_from_args(**args) query_str = """ { snapshot(swhid: "%s") { branches(%s) { pageInfo { endCursor } edges { cursor } nodes { - type + targetType name { text } target { __typename ...on Branch { name { text } } ...on Revision { swhid } ...on Release { swhid } ...on Content { swhid } ...on Directory { swhid } ...on Snapshot { swhid } } } } } } """ % ( swhid, params, ) return utils.get_query_response(client, query_str) def test_get_data(client): swhid = "swh:1:snp:0e7f84ede9a254f2cd55649ad5240783f557e65f" data, errors = get_branches(client, swhid, 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", }, - "type": "revision", + "targetType": "revision", } def test_get_branches_with_alias(client): swhid = "swh:1:snp:0e7f84ede9a254f2cd55649ad5240783f557e65f" data, _ = get_branches(client, swhid, 10, types="[alias]") node = data["snapshot"]["branches"]["nodes"][0] assert node == { "name": {"text": "target/alias"}, "target": {"__typename": "Branch", "name": {"text": "target/revision"}}, - "type": "alias", + "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, 10, types=f"[{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, 10, types=f"[{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, 10, nameInclude=f'"{name}"') for node in data["snapshot"]["branches"]["nodes"]: assert name in node["name"]["text"] @pytest.mark.parametrize("count", [1, 2]) def test_get_first_arg(client, count): swhid = "swh:1:snp:0e7f84ede9a254f2cd55649ad5240783f557e65f" data, _ = get_branches(client, 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, 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, first=3, after=f'"{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_content.py b/swh/graphql/tests/functional/test_content.py index 30c2b48..3a42d26 100644 --- a/swh/graphql/tests/functional/test_content.py +++ b/swh/graphql/tests/functional/test_content.py @@ -1,163 +1,163 @@ # 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_contents @pytest.mark.parametrize("content", get_contents()) def test_get_content_with_swhid(client, content): query_str = """ { content(swhid: "%s") { swhid checksum { blake2s256 sha1 sha1_git sha256 } length status data { url } fileType { encoding } language { lang } license { licenses } } } """ data, _ = utils.get_query_response(client, query_str % content.swhid()) archive_url = "https://archive.softwareheritage.org/api/1/" response = { "swhid": str(content.swhid()), "checksum": { "blake2s256": content.blake2s256.hex(), "sha1": content.sha1.hex(), "sha1_git": content.sha1_git.hex(), "sha256": content.sha256.hex(), }, "length": content.length, "status": content.status, "data": { "url": f"{archive_url}content/sha1:{content.sha1.hex()}/raw/", }, "fileType": None, "language": None, "license": None, } assert data["content"] == response @pytest.mark.parametrize("content", get_contents()) def test_get_content_with_hash(client, content): query_str = """ { contentByHash(checksums: ["blake2s256:%s", "sha1:%s", "sha1_git:%s", "sha256:%s"]) { swhid } } """ data, _ = utils.get_query_response( client, query_str % ( content.blake2s256.hex(), content.sha1.hex(), content.sha1_git.hex(), content.sha256.hex(), ), ) assert data["contentByHash"] == {"swhid": str(content.swhid())} def test_get_content_with_invalid_swhid(client): query_str = """ { content(swhid: "swh:1:cnt:invalid") { swhid } } """ errors = utils.get_error_response(client, query_str) # API will throw an error in case of an invalid SWHID assert len(errors) == 1 assert "Input error: Invalid SWHID" in errors[0]["message"] def test_get_content_with_invalid_hashes(client): content = get_contents()[0] query_str = """ { contentByHash(checksums: ["blake2s256:%s", "sha1:%s", "sha1_git:%s", "sha256:%s"]) { swhid } } """ errors = utils.get_error_response( client, query_str % ( "invalid", # Only one hash is invalid content.sha1.hex(), content.sha1_git.hex(), content.sha256.hex(), ), ) # API will throw an error in case of an invalid content hash assert len(errors) == 1 assert "Input error: Invalid content checksum" in errors[0]["message"] def test_get_content_with_invalid_hash_algorithm(client): content = get_contents()[0] query_str = """ { contentByHash(checksums: ["test:%s"]) { swhid } } """ errors = utils.get_error_response(client, query_str % content.sha1.hex()) assert len(errors) == 1 assert "Input error: Invalid hash algorithm" in errors[0]["message"] def test_get_content_as_target(client): # SWHID of a test dir with a file entry directory_swhid = "swh:1:dir:87b339104f7dc2a8163dec988445e3987995545f" query_str = """ { directory(swhid: "%s") { swhid entries(first: 2) { nodes { - type + targetType target { ...on Content { swhid length } } } } } } """ data, _ = utils.get_query_response(client, query_str % directory_swhid) content_obj = data["directory"]["entries"]["nodes"][1]["target"] assert content_obj == { "length": 4, "swhid": "swh:1:cnt:86bc6b377e9d25f9d26777a4a28d08e63e7c5779", } diff --git a/swh/graphql/tests/functional/test_directory_entry.py b/swh/graphql/tests/functional/test_directory_entry.py index 50a882e..411435a 100644 --- a/swh/graphql/tests/functional/test_directory_entry.py +++ b/swh/graphql/tests/functional/test_directory_entry.py @@ -1,37 +1,37 @@ # 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_directories @pytest.mark.parametrize("directory", get_directories()) def test_get_directory_entry_connection(client, directory): query_str = """ { directory(swhid: "%s") { swhid entries { nodes { - type + targetType name { text } } } } } """ data, _ = utils.get_query_response(client, query_str % directory.swhid()) directory_entries = data["directory"]["entries"]["nodes"] assert len(directory_entries) == len(directory.entries) output = [ - {"name": {"text": de.name.decode()}, "type": de.type} + {"name": {"text": de.name.decode()}, "targetType": de.type} for de in directory.entries ] for each_entry in output: assert each_entry in directory_entries diff --git a/swh/graphql/tests/functional/test_revision.py b/swh/graphql/tests/functional/test_revision.py index 6ad3eba..ce8b7c4 100644 --- a/swh/graphql/tests/functional/test_revision.py +++ b/swh/graphql/tests/functional/test_revision.py @@ -1,160 +1,160 @@ # 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 swh.model.swhids import CoreSWHID from . import utils from ..data import get_revisions, get_revisions_with_parents @pytest.mark.parametrize("revision", get_revisions()) def test_get_revision(client, revision): query_str = """ { revision(swhid: "%s") { swhid message { text } author { fullname { text } name { text } email { text } } committer { fullname { text } name { text } email { text } } date type directory { swhid } } } """ data, _ = utils.get_query_response(client, query_str % revision.swhid()) assert data["revision"] == { "swhid": str(revision.swhid()), "message": {"text": revision.message.decode()}, "author": { "fullname": {"text": revision.author.fullname.decode()}, "name": {"text": revision.author.name.decode()}, "email": {"text": revision.author.email.decode()}, }, "committer": { "fullname": {"text": revision.committer.fullname.decode()}, "name": {"text": revision.committer.name.decode()}, "email": {"text": revision.committer.email.decode()}, }, "date": revision.date.to_datetime().isoformat(), "type": revision.type.value, "directory": { "swhid": str(CoreSWHID(object_id=revision.directory, object_type="dir")) }, } def test_get_revision_with_invalid_swhid(client): query_str = """ { revision(swhid: "swh:1:cnt:invalid") { swhid } } """ errors = utils.get_error_response(client, query_str) # API will throw an error in case of an invalid SWHID assert len(errors) == 1 assert "Input error: Invalid SWHID" in errors[0]["message"] def test_get_revision_as_target(client): # SWHID of a snapshot with revision as target snapshot_swhid = "swh:1:snp:9e78d7105c5e0f886487511e2a92377b4ee4c32a" query_str = """ { snapshot(swhid: "%s") { branches(first: 1, types: [revision]) { nodes { - type + targetType target { ...on Revision { swhid } } } } } } """ data, _ = utils.get_query_response(client, query_str % snapshot_swhid) revision_obj = data["snapshot"]["branches"]["nodes"][0]["target"] assert revision_obj == { "swhid": "swh:1:rev:66c7c1cd9673275037140f2abff7b7b11fc9439c" } def test_get_revision_log(client): revision_swhid = get_revisions_with_parents()[0].swhid() query_str = """ { revision(swhid: "%s") { swhid revisionLog(first: 3) { nodes { swhid } } } } """ data, _ = utils.get_query_response(client, query_str % revision_swhid) assert data["revision"]["revisionLog"] == { "nodes": [ {"swhid": str(revision_swhid)}, {"swhid": str(get_revisions()[0].swhid())}, {"swhid": str(get_revisions()[1].swhid())}, ] } def test_get_revision_parents(client): revision_swhid = get_revisions_with_parents()[0].swhid() query_str = """ { revision(swhid: "%s") { swhid parents { nodes { swhid } } } } """ data, _ = utils.get_query_response(client, query_str % revision_swhid) assert data["revision"]["parents"] == { "nodes": [ {"swhid": str(get_revisions()[0].swhid())}, {"swhid": str(get_revisions()[1].swhid())}, ] } diff --git a/swh/graphql/tests/functional/test_search.py b/swh/graphql/tests/functional/test_search.py index 97fd560..9d1596f 100644 --- a/swh/graphql/tests/functional/test_search.py +++ b/swh/graphql/tests/functional/test_search.py @@ -1,64 +1,64 @@ # 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 def test_search_origins(client): query_str = """ { search(query: "fox", first: 1) { nodes { - type + targetType target { ...on Origin { url latestVisit { date } } } } pageInfo { hasNextPage endCursor } } } """ data, _ = utils.get_query_response(client, query_str) assert len(data["search"]["nodes"]) == 1 assert data == { "search": { "nodes": [ { "target": { "url": "https://somewhere.org/den/fox", "latestVisit": {"date": "2018-11-27T17:20:39+00:00"}, }, - "type": "origin", + "targetType": "origin", } ], "pageInfo": {"endCursor": "MQ==", "hasNextPage": True}, } } def test_search_missing_url(client): query_str = """ { search(query: "missing-fox", first: 1) { nodes { - type + targetType } pageInfo { hasNextPage endCursor } } } """ data, _ = utils.get_query_response(client, query_str) assert len(data["search"]["nodes"]) == 0 diff --git a/swh/graphql/tests/functional/test_snapshot_node.py b/swh/graphql/tests/functional/test_snapshot_node.py index 7f9c2d8..f91f570 100644 --- a/swh/graphql/tests/functional/test_snapshot_node.py +++ b/swh/graphql/tests/functional/test_snapshot_node.py @@ -1,57 +1,57 @@ # 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 ..data import get_snapshots from .utils import assert_missing_object, get_error_response, get_query_response @pytest.mark.parametrize("snapshot", get_snapshots()) def test_get_snapshot(client, snapshot): query_str = """ { snapshot(swhid: "%s") { id swhid branches(first:5) { nodes { - type + targetType name { text } } } } } """ data, _ = get_query_response(client, query_str % snapshot.swhid()) assert data["snapshot"]["swhid"] == str(snapshot.swhid()) assert data["snapshot"]["id"] == snapshot.id.hex() assert len(data["snapshot"]["branches"]["nodes"]) == len(snapshot.branches) def test_get_snapshot_missing_swhid(client): query_str = """ { snapshot(swhid: "swh:1:snp:0949d7a8c96347dba09be8d79085b8207f345412") { swhid } } """ assert_missing_object(client, query_str, "snapshot") def test_get_snapshot_invalid_swhid(client): query_str = """ { snapshot(swhid: "swh:1:snp:invalid") { swhid } } """ errors = get_error_response(client, query_str) assert len(errors) == 1 assert "Input error: Invalid SWHID" in errors[0]["message"] diff --git a/swh/graphql/tests/functional/test_swhid_resolve.py b/swh/graphql/tests/functional/test_swhid_resolve.py index 72452b4..bd63746 100644 --- a/swh/graphql/tests/functional/test_swhid_resolve.py +++ b/swh/graphql/tests/functional/test_swhid_resolve.py @@ -1,221 +1,221 @@ # 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_contents, get_directories, get_releases, get_revisions, get_snapshots, ) def test_invalid_swhid(client): query_str = """ { resolveSwhid(swhid: "swh:1:dir:dae0d245988b472abd30a4f968b919d0019b6c7") { nodes { - type + targetType } } } """ errors = utils.get_error_response(client, query_str) # API will throw an error in case of an invalid SWHID assert len(errors) == 1 assert "Input error: Invalid SWHID" in errors[0]["message"] @pytest.mark.parametrize( "swhid", [ "swh:1:rel:0949d7a8c96347dba09be8d79085b8207f345412", "swh:1:rev:0949d7a8c96347dba09be8d79085b8207f345412", "swh:1:dir:0949d7a8c96347dba09be8d79085b8207f345412", "swh:1:cnt:0949d7a8c96347dba09be8d79085b8207f345412", "swh:1:snp:0949d7a8c96347dba09be8d79085b8207f345412", ], ) def test_missing_swhid(client, swhid): query_str = """ { resolveSwhid(swhid: "%s") { nodes { - type + targetType } } } """ data, _ = utils.get_query_response(client, query_str % swhid) # API will return an empty list in case of a valid, non existing SWHID assert data == {"resolveSwhid": {"nodes": []}} @pytest.mark.parametrize("snapshot", get_snapshots()) def test_snapshot_swhid_resolve(client, snapshot): query_str = """ { resolveSwhid(swhid: "%s") { nodes { - type + targetType target { __typename ... on Snapshot { swhid } } } } } """ data, _ = utils.get_query_response(client, query_str % snapshot.swhid()) assert data == { "resolveSwhid": { "nodes": [ { "target": { "__typename": "Snapshot", "swhid": str(snapshot.swhid()), }, - "type": "snapshot", + "targetType": "snapshot", } ] } } @pytest.mark.parametrize("revision", get_revisions()) def test_revision_swhid_resolve(client, revision): query_str = """ { resolveSwhid(swhid: "%s") { nodes { - type + targetType target { __typename ... on Revision { swhid } } } } } """ data, _ = utils.get_query_response(client, query_str % revision.swhid()) assert data == { "resolveSwhid": { "nodes": [ { "target": { "__typename": "Revision", "swhid": str(revision.swhid()), }, - "type": "revision", + "targetType": "revision", } ] } } @pytest.mark.parametrize("release", get_releases()) def test_release_swhid_resolve(client, release): query_str = """ { resolveSwhid(swhid: "%s") { nodes { - type + targetType target { __typename ... on Release { swhid } } } } } """ data, _ = utils.get_query_response(client, query_str % release.swhid()) assert data == { "resolveSwhid": { "nodes": [ { "target": { "__typename": "Release", "swhid": str(release.swhid()), }, - "type": "release", + "targetType": "release", } ] } } @pytest.mark.parametrize("directory", get_directories()) def test_directory_swhid_resolve(client, directory): query_str = """ { resolveSwhid(swhid: "%s") { nodes { - type + targetType target { __typename ... on Directory { swhid } } } } } """ data, _ = utils.get_query_response(client, query_str % directory.swhid()) assert data == { "resolveSwhid": { "nodes": [ { "target": { "__typename": "Directory", "swhid": str(directory.swhid()), }, - "type": "directory", + "targetType": "directory", } ] } } @pytest.mark.parametrize("content", get_contents()) def test_content_swhid_resolve(client, content): query_str = """ { resolveSwhid(swhid: "%s") { nodes { - type + targetType target { __typename ... on Content { swhid } } } } } """ data, _ = utils.get_query_response(client, query_str % content.swhid()) assert data == { "resolveSwhid": { "nodes": [ { "target": { "__typename": "Content", "swhid": str(content.swhid()), }, - "type": "content", + "targetType": "content", } ] } }