From 6676b4ed90e19e2fd6ee5f4242cf85f64db145d8 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Tue, 8 Feb 2022 15:29:49 +0100 Subject: rework Hydrilla to use a separate tool for building its source packages * Hydrilla now depends on "Hydrilla builder" developed at: https://git.koszko.org/hydrilla-builder/ * Hydrilla repository is now REUSE-compliant * The debian packaging is temporarily not tested and likely to be broken * JSON schemas are now in use (through 'jsonschema' Python library) * This is not yet a release and some minor changes to the API on-fisk format are going to occur before that --- src/test/test_server.py | 199 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 src/test/test_server.py (limited to 'src/test/test_server.py') diff --git a/src/test/test_server.py b/src/test/test_server.py new file mode 100644 index 0000000..def48dc --- /dev/null +++ b/src/test/test_server.py @@ -0,0 +1,199 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Repository tests +# +# This file is part of Hydrilla +# +# Copyright (C) 2021, 2022 Wojtek Kosior +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# +# I, Wojtek Kosior, thereby promise not to sue for violation of this +# file's license. Although I request that you do not make use this code +# in a proprietary program, I am not going to enforce this in court. + +import pytest +import sys +import shutil +import json + +from pathlib import Path +from hashlib import sha256 +from tempfile import TemporaryDirectory +from typing import Iterable, Callable + +from flask.testing import FlaskClient +from markupsafe import escape + +from hydrilla import util as hydrilla_util +from hydrilla.builder import Build +from hydrilla.server import create_app + +here = Path(__file__).resolve().parent +config_path = here / 'development_config.json' +source_path = here / 'source-package-example' + +@pytest.fixture(scope="session") +def default_setup() -> Iterable[dict[str, Path]]: + with TemporaryDirectory() as tmpdir: + setup = { + 'malcontent_dir': Path(tmpdir) / 'sample_malcontent', + 'config_path': Path(tmpdir) / 'config.json', + 'containing_dir': Path(tmpdir) + } + + setup['config_path'].symlink_to(config_path) + + build = Build(source_path, Path('index.json')) + build.write_package_files(setup['malcontent_dir']) + + yield setup + +@pytest.fixture(scope="session") +def client(default_setup: dict[str, Path]) -> Iterable[FlaskClient]: + """Provide app client that serves the object from built sample package.""" + app = create_app(default_setup['config_path'], + flask_config={'TESTING': True}) + + with app.test_client() as client: + yield client + +@pytest.fixture(scope="session") +def development_config(default_setup) -> Iterable[dict]: + """Provide the contents of JSON config file fed to the client.""" + contents = default_setup['config_path'].read_text() + yield json.loads(hydrilla_util.strip_json_comments(contents)) + +def test_project_url(client: FlaskClient, development_config: dict) -> None: + """Fetch index.html and verify project URL fro config is present there.""" + response = client.get('/') + assert b'html' in response.data + project_url = development_config['hydrilla_project_url'] + assert escape(project_url).encode() in response.data + +@pytest.mark.parametrize('item_type', ['resource', 'mapping']) +def test_get_newest(client: FlaskClient, item_type: str) -> None: + """ + Verify that + GET '/{item_type}/{item_identifier}.json' + returns proper definition that is also served at: + GET '/{item_type}/{item_identifier}/{item_version}' + """ + response = client.get(f'/{item_type}/helloapple.json') + assert response.status_code == 200 + definition = json.loads(response.data.decode()) + assert definition['type'] == item_type + assert definition['identifier'] == 'helloapple' + + response = client.get(f'/{item_type}/helloapple/2021.11.10') + assert response.status_code == 200 + assert definition == json.loads(response.data.decode()) + + hydrilla_util.validator_for(f'api_{item_type}_description-1.schema.json')\ + .validate(definition) + +@pytest.mark.parametrize('item_type', ['resource', 'mapping']) +def test_get_nonexistent(client: FlaskClient, item_type: str) -> None: + """ + Verify that attempts to GET a JSON definition of a nonexistent item or item + version result in 404. + """ + response = client.get(f'/{item_type}/nonexistentapple.json') + assert response.status_code == 404 + response = client.get(f'/{item_type}/helloapple/1.2.3.999') + assert response.status_code == 404 + +@pytest.mark.parametrize('item_type', ['resource', 'mapping']) +def test_file_refs(client: FlaskClient, item_type: str) -> None: + """ + Verify that files referenced by definitions are accessible under their + proper URLs and that their hashes match. + """ + response = client.get(f'/{item_type}/helloapple/2021.11.10') + assert response.status_code == 200 + definition = json.loads(response.data.decode()) + + for file_ref in [*definition.get('scripts', []), + *definition['source_copyright']]: + hash_sum = file_ref["sha256"] + response = client.get(f'/file/sha256-{hash_sum}') + + assert response.status_code == 200 + assert sha256(response.data).digest().hex() == hash_sum + +def test_empty_query(client: FlaskClient) -> None: + """ + Verify that querying mappings for URL gives an empty list when there're no + mathes. + """ + response = client.get(f'/query?url=https://nonexiste.nt/example') + assert response.status_code == 200 + + response_object = json.loads(response.data.decode()) + + assert response_object['mappings'] == [] + + hydrilla_util.validator_for('api_query_result-1.schema.json')\ + .validate(response_object) + +def test_query(client: FlaskClient) -> None: + """ + Verify that querying mappings for URL gives a list with reference(s) the the + matching mapping(s). + """ + response = client.get(f'/query?url=https://hydrillabugs.koszko.org/') + assert response.status_code == 200 + + response_object = json.loads(response.data.decode()) + + assert response_object['mappings'] == [{ + 'identifier': 'helloapple', + 'long_name': 'Hello Apple', + 'version': [2021, 11, 10] + }] + + hydrilla_util.validator_for('api_query_result-1.schema.json')\ + .validate(response_object) + +def test_source(client: FlaskClient) -> None: + """Verify source descriptions are properly served.""" + response = client.get(f'/source/hello.json') + assert response.status_code == 200 + + description = json.loads(response.data.decode()) + assert description['source_name'] == 'hello' + + assert sorted([d['identifier'] for d in description['definitions']]) == \ + ['hello-message', 'helloapple', 'helloapple'] + + zipfile_hash = description['source_archives']['zip']['sha256'] + response = client.get(f'/source/hello.zip') + assert sha256(response.data).digest().hex() == zipfile_hash + + hydrilla_util.validator_for('api_source_description-1.schema.json')\ + .validate(description) + +def test_missing_source(client: FlaskClient) -> None: + """Verify requests for nonexistent sources result in 404.""" + response = client.get(f'/source/nonexistent.json') + assert response.status_code == 404 + + response = client.get(f'/source/nonexistent.zip') + assert response.status_code == 404 + +def test_normalize_version(): + assert hydrilla_util.normalize_version([4, 5, 3, 0, 0]) == [4, 5, 3] + assert hydrilla_util.normalize_version([1, 0, 5, 0]) == [1, 0, 5] + assert hydrilla_util.normalize_version([3, 3]) == [3, 3] -- cgit v1.2.3