diff --git a/swh/graphql/resolvers/origin.py b/swh/graphql/resolvers/origin.py --- a/swh/graphql/resolvers/origin.py +++ b/swh/graphql/resolvers/origin.py @@ -1,3 +1,8 @@ +# 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 .base_connection import BaseConnection diff --git a/swh/graphql/server.py b/swh/graphql/server.py --- a/swh/graphql/server.py +++ b/swh/graphql/server.py @@ -1,9 +1,16 @@ +# 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 os from typing import Any, Dict, Optional from swh.core import config from swh.storage import get_storage as get_swhstorage +from .app import schema + graphql_cfg = None storage = None @@ -50,15 +57,14 @@ configuration path to load. """ - from .app import schema - global graphql_cfg if not graphql_cfg: config_path = os.environ.get("SWH_CONFIG_FILENAME") graphql_cfg = load_and_check_config(config_path) - if graphql_cfg.get("server-type") == "asgi": + server_type = graphql_cfg.get("server-type") + if server_type == "asgi": from ariadne.asgi import GraphQL application = GraphQL(schema) diff --git a/swh/graphql/tests/conftest.py b/swh/graphql/tests/conftest.py new file mode 100644 --- /dev/null +++ b/swh/graphql/tests/conftest.py @@ -0,0 +1,47 @@ +# 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 ariadne import graphql_sync +from flask import Flask, jsonify, request +import pytest + +from swh.graphql import server as app_server +from swh.graphql.app import schema +from swh.storage import get_storage as get_swhstorage + +from .data import populate_dummy_data + + +@pytest.fixture +def storage(): + storage = get_swhstorage(cls="memory") + # set the global var to use the in-memory storage + app_server.storage = storage + # populate the in-memory storage + populate_dummy_data(storage) + + +@pytest.fixture +def test_app(storage): + app = Flask(__name__) + + @app.route("/", methods=["POST"]) + def graphql_server(): + # GraphQL queries are always sent as POST + data = request.get_json() + success, result = graphql_sync( + schema, data, context_value=request, debug=app.debug + ) + status_code = 200 if success else 400 + return jsonify(result), status_code + + yield app + + +@pytest.fixture +def client(test_app): + with test_app.test_client() as client: + yield client diff --git a/swh/graphql/tests/data.py b/swh/graphql/tests/data.py new file mode 100644 --- /dev/null +++ b/swh/graphql/tests/data.py @@ -0,0 +1,105 @@ +# 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 + +# This module will be removed once the test data +# generation in SWH-wb moved to a shared location +# or to a new test data project + +from datetime import timedelta + +from swh.model.model import Origin, OriginVisit, OriginVisitStatus, Snapshot +from swh.storage.utils import now + + +def populate_dummy_data(storage): + origins = get_origins() + visits = get_visits(origins) + snapshots = get_snapshots() + status = get_visit_status(visits, snapshots) + + storage.origin_add(origins) + storage.origin_visit_add(visits) + storage.snapshot_add(snapshots) + storage.origin_visit_status_add(status) + + +def get_origins(): + # Return two dummy origins + return [ + Origin(url="http://example.com/forge1"), + Origin(url="http://example.com/forge2"), + ] + + +def get_visits(origins): + # Return two visits each for an origin + origin1, origin2 = origins + return [ + OriginVisit( + origin=origin1.url, + date=now() - timedelta(minutes=200), + type="git", + visit=1, + ), + OriginVisit( + origin=origin1.url, + date=now(), + type="git", + visit=2, + ), + OriginVisit( + origin=origin2.url, + date=now() - timedelta(minutes=500), + type="hg", + visit=1, + ), + OriginVisit( + origin=origin2.url, + date=now(), + type="hg", + visit=2, + ), + ] + + +def get_visit_status(visits, snapshots): + # Return one status per visit, adding only empty statpshots for now + visit1, visit2, visit3, visit4 = visits + (empty_snapshot,) = snapshots + return [ + OriginVisitStatus( + origin=visit1.origin, + visit=visit1.visit, + date=visit1.date, + status="full", + snapshot=empty_snapshot.id, + ), + OriginVisitStatus( + origin=visit2.origin, + visit=visit2.visit, + date=visit1.date, + status="full", + snapshot=empty_snapshot.id, + ), + OriginVisitStatus( + origin=visit3.origin, + visit=visit3.visit, + date=visit3.date, + status="full", + snapshot=empty_snapshot.id, + ), + OriginVisitStatus( + origin=visit4.origin, + visit=visit4.visit, + date=visit4.date, + status="full", + snapshot=empty_snapshot.id, + ), + ] + + +def get_snapshots(): + empty_snapshot = Snapshot(branches={}) + return [empty_snapshot] diff --git a/swh/graphql/tests/functional/test_origin_connection.py b/swh/graphql/tests/functional/test_origin_connection.py new file mode 100644 --- /dev/null +++ b/swh/graphql/tests/functional/test_origin_connection.py @@ -0,0 +1,54 @@ +# 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 .utils import get_query_response + + +def test_get(client): + query_str = """ + { + origins(first: 10) { + nodes { + url + } + } + } + """ + data, _ = get_query_response(client, query_str) + assert len(data["origins"]["nodes"]) == 2 + + +def test_get_filter_by_pattern(client): + query_str = """ + { + origins(first: 10, urlPattern: "forge1") { + nodes { + url + } + } + } + """ + data, _ = get_query_response(client, query_str) + assert len(data["origins"]["nodes"]) == 1 + + +def test_basic_pagination(client): + query_str = """ + { + origins(first: 2) { + nodes { + id + } + pageInfo { + hasNextPage + endCursor + } + } + } + """ + + data, _ = get_query_response(client, query_str) + assert len(data["origins"]["nodes"]) == 2 + assert data["origins"]["pageInfo"] == {"hasNextPage": False, "endCursor": None} diff --git a/swh/graphql/tests/functional/test_origin_node.py b/swh/graphql/tests/functional/test_origin_node.py new file mode 100644 --- /dev/null +++ b/swh/graphql/tests/functional/test_origin_node.py @@ -0,0 +1,50 @@ +# 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 .utils import get_query_response + + +def test_invalid_get(client): + query_str = """ + { + origin(url: "http://example.com/forge1/") { + url + } + } + """ + data, errors = get_query_response(client, query_str) + assert data["origin"] is None + assert len(errors) == 1 + assert errors[0]["message"] == "Requested object is not available" + + +def test_get(client): + query_str = """ + { + origin(url: "http://example.com/forge1") { + url + id + visits(first: 10) { + nodes { + id + } + } + latestVisit { + visitId + } + snapshots(first: 2) { + nodes { + id + } + } + } + } + """ + data, _ = get_query_response(client, query_str) + origin = data["origin"] + assert origin["url"] == "http://example.com/forge1" + assert len(origin["visits"]["nodes"]) == 2 + assert origin["latestVisit"]["visitId"] == 2 + assert len(origin["snapshots"]["nodes"]) == 1 diff --git a/swh/graphql/tests/functional/utils.py b/swh/graphql/tests/functional/utils.py new file mode 100644 --- /dev/null +++ b/swh/graphql/tests/functional/utils.py @@ -0,0 +1,13 @@ +# 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 json + + +def get_query_response(client, query_str): + response = client.post("/", json={"query": query_str}) + assert response.status_code == 200, response.data + result = json.loads(response.data) + return result.get("data"), result.get("errors")