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.*] @@ -11,5 +10,11 @@ [mypy-pytest.*] ignore_missing_imports = True +[mypy-uvicorn.*] +ignore_missing_imports = True + +[mypy-requests.*] +ignore_missing_imports = True + # [mypy-add_your_lib_here.*] # ignore_missing_imports = True 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,7 +60,7 @@ 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": 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,49 @@ +# 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 multiprocessing import Process +import time + +from _pytest.monkeypatch import MonkeyPatch +import pytest +import uvicorn + +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) + + +def run_server(): + app = app_server.make_app_from_configfile("config/dev.yml") + # Using a different port + uvicorn.run(app, host="localhost", port=9834, log_level="info") + + +@pytest.fixture(scope="session") +def server(storage): + proc = Process(target=run_server, args=(), daemon=True) + proc.start() + # Wait for the server to start; in session scope + time.sleep(5) + yield + proc.kill() 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,84 @@ +# 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 .test_utils import get_query_response + + +class TestOriginConnection: + def test_get(self, server): + query_str = """ + { + origins(first: 10) { + nodes { + url + } + } + } + """ + data, _ = get_query_response(query_str) + assert len(data["origins"]["nodes"]) == 2 + + def test_basic_pagination(self, server): + query_str = """ + { + origins(first: 2) { + nodes { + id + } + pageInfo { + hasNextPage + endCursor + } + } + } + """ + + data, _ = get_query_response(query_str) + assert len(data["origins"]["nodes"]) == 2 + assert data["origins"]["pageInfo"] == {"hasNextPage": False, "endCursor": None} + + +class TestOriginNode: + def test_invalid_get(self, server): + query_str = """ + { + origin(url: "http://example.com/forge1/") { + url + } + } + """ + data, errors = get_query_response(query_str) + assert data["origin"] is None + assert len(errors) == 1 + assert errors[0]["message"] == "Requested object is not available" + + def test_get(self, server): + 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(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/test_utils.py b/swh/graphql/tests/functional/test_utils.py new file mode 100644 --- /dev/null +++ b/swh/graphql/tests/functional/test_utils.py @@ -0,0 +1,15 @@ +# 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 requests + + +def get_query_response(query_str): + url = "http://127.0.0.1:9834/" # test server URL + response = requests.post(url, json={"query": query_str}) + assert response.status_code == 200 + result = response.json() + data, errors = result.get("data"), result.get("errors") + return data, errors