diff --git a/config/test.yml b/config/test.yml new file mode 100644 --- /dev/null +++ b/config/test.yml @@ -0,0 +1,6 @@ +storage: + cls: "memory" + +debug: yes + +server-type: test diff --git a/mypy.ini b/mypy.ini --- a/mypy.ini +++ b/mypy.ini @@ -2,7 +2,6 @@ namespace_packages = True warn_unused_ignores = True - # 3rd party libraries without stubs (yet) [mypy-pkg_resources.*] 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,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 + import os from typing import Any, Dict, Optional @@ -42,7 +47,7 @@ return cfg -def make_app_from_configfile(): +def make_app_from_configfile(config_path=""): """Loading the configuration from a configuration file. @@ -55,15 +60,38 @@ global graphql_cfg if not graphql_cfg: - config_path = os.environ.get("SWH_CONFIG_FILENAME") + config_path = os.environ.get("SWH_CONFIG_FILENAME") or config_path graphql_cfg = load_and_check_config(config_path) if graphql_cfg.get("server-type") == "asgi": from ariadne.asgi import GraphQL application = GraphQL(schema) + if graphql_cfg.get("server-type") == "test": + return get_test_falsk_app(schema) else: from ariadne.wsgi import GraphQL application = GraphQL(schema) return application + + +def get_test_falsk_app(schema): + from ariadne import graphql_sync + from flask import Flask, jsonify, request + + 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 + + return app 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,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 + + +from _pytest.monkeypatch import MonkeyPatch +import pytest + +from swh.graphql import server as app_server +from swh.storage import get_storage as get_swhstorage + +from .data import populate_dummy_data + + +@pytest.fixture(scope="session") +def sessionmonkeypatch(*args, **kw): + sessionpatch = MonkeyPatch() + yield sessionpatch + sessionpatch.undo() + + +@pytest.fixture(scope="session") +def storage(sessionmonkeypatch): + def mock_storage(*args, **kw): + storage = get_swhstorage(cls="memory") + populate_dummy_data(storage) + return storage + + sessionmonkeypatch.setattr(app_server, "get_storage", mock_storage) + + +@pytest.fixture(scope="session") +def client(storage): + app = app_server.make_app_from_configfile("config/test.yml") + with 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.py b/swh/graphql/tests/functional/test_origin.py new file mode 100644 --- /dev/null +++ b/swh/graphql/tests/functional/test_origin.py @@ -0,0 +1,97 @@ +# 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 + + +class TestOriginConnection: + def test_get(self, 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(self, 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(self, 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} + + +class TestOriginNode: + def test_invalid_get(self, 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(self, 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 + result = json.loads(response.data) + return result.get("data"), result.get("errors")