From fb6dd284fad64a4b69e44aea38852b38819b5eb1 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Fri, 18 Feb 2022 16:02:16 +0100 Subject: make "uuid" an optional property --- pyproject.toml | 2 +- pytest.ini | 4 ++ src/hydrilla/server/serve.py | 7 +- src/test/source-package-example | 2 +- src/test/test_server.py | 150 ++++++++++++++++++++++++++++------------ 5 files changed, 116 insertions(+), 49 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f053c4f..566d0ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ [build-system] build-backend = "setuptools.build_meta" -requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"] +requires = ["setuptools>=45", "wheel", "setuptools_scm>=5.0"] [tool.setuptools_scm] write_to = "src/hydrilla/server/_version.py" diff --git a/pytest.ini b/pytest.ini index 030df26..b4ea538 100644 --- a/pytest.ini +++ b/pytest.ini @@ -10,3 +10,7 @@ filterwarnings = ignore::DeprecationWarning:werkzeug.*: ignore::DeprecationWarning:jinja2.*: + +markers = + mod_before_build: define a callback to use to modify test packages before their build + mod_after_build: define a callback to use to modify test packages after their build diff --git a/src/hydrilla/server/serve.py b/src/hydrilla/server/serve.py index bb53c0a..9bcfb93 100644 --- a/src/hydrilla/server/serve.py +++ b/src/hydrilla/server/serve.py @@ -57,7 +57,7 @@ class ItemInfo(ABC): """Initialize ItemInfo using item definition read from JSON.""" self.version = util.normalize_version(item_obj['version']) self.identifier = item_obj['identifier'] - self.uuid = item_obj['uuid'] + self.uuid = item_obj.get('uuid') self.long_name = item_obj['long_name'] def path(self) -> str: @@ -111,8 +111,11 @@ class VersionedItemInfo: """ if self.identifier is None: self.identifier = item_info.identifier + + if self.uuid is None: self.uuid = item_info.uuid - elif self.uuid != item_info.uuid: + + if self.uuid is not None and self.uuid != item_info.uuid: raise ValueError(f_('uuid_mismatch_{identifier}') .format(identifier=self.identifier)) diff --git a/src/test/source-package-example b/src/test/source-package-example index 48606f2..92a4d31 160000 --- a/src/test/source-package-example +++ b/src/test/source-package-example @@ -1 +1 @@ -Subproject commit 48606f288c89aaadb28a021ab71945e1e87fd14e +Subproject commit 92a4d31c659b2336e5e188877d1ce6bfad2fa310 diff --git a/src/test/test_server.py b/src/test/test_server.py index bbea257..2c2d50e 100644 --- a/src/test/test_server.py +++ b/src/test/test_server.py @@ -32,10 +32,11 @@ import json from pathlib import Path from hashlib import sha256 from tempfile import TemporaryDirectory -from typing import Iterable, Callable +from typing import Callable, Optional from flask.testing import FlaskClient from markupsafe import escape +from werkzeug import Response from hydrilla import util as hydrilla_util from hydrilla.builder import Build @@ -50,98 +51,157 @@ expected_generated_by = { 'version': _version.version } -@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) - } +SetupMod = Optional[Callable['Setup', None]] - setup['config_path'].symlink_to(config_path) +source_files = ( + 'index.json', 'hello.js', 'bye.js', 'message.js', 'README.txt', + 'README.txt.license', '.reuse/dep5', 'LICENSES/CC0-1.0.txt' +) - build = Build(source_path, Path('index.json')) - build.write_package_files(setup['malcontent_dir']) +class Setup: + """ + Facilitate preparing test malcontent directory, Hydrilla config file and the + actual Flask client. In a customizable way. + """ + def __init__(self, modify_before_build: SetupMod=None, + modify_after_build: SetupMod=None) -> None: + """Initialize Setup.""" + self._modify_before_build = modify_before_build + self._modify_after_build = modify_after_build + self._config = None + self._client = None + + def _prepare(self) -> None: + """Perform the build and call the callbacks as appropriate.""" + self.tmpdir = TemporaryDirectory() + + self.containing_dir = Path(self.tmpdir.name) + self.malcontent_dir = self.containing_dir / 'sample_malcontent' + self.index_json = Path('index.json') + + self.source_dir = self.containing_dir / 'sample_source_package' + for source_file in source_files: + dst_path = self.source_dir / source_file + dst_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source_path / source_file, dst_path) + + self.config_path = self.containing_dir / 'config.json' + shutil.copyfile(config_path, self.config_path) + + if self._modify_before_build: + self._modify_before_build(self) + + build = Build(self.source_dir, self.index_json) + build.write_package_files(self.malcontent_dir) - yield setup + if self._modify_after_build: + self._modify_after_build(self) -@pytest.fixture(scope="session") -def test_config(default_setup) -> Iterable[dict]: - """Provide the contents of JSON config file fed to the client.""" - yield config.load([default_setup['config_path']]) + def config(self) -> dict: + """Provide the contents of JSON config file used.""" + if self._config is None: + self._prepare() + self._config = config.load([self.config_path]) -@pytest.fixture(scope="session") -def client(test_config: dict) -> Iterable[FlaskClient]: - """Provide app client that serves the object from built sample package.""" - app = HydrillaApp(test_config, flask_config={'TESTING': True}) + return self._config - with app.test_client() as client: - yield client + def client(self) -> FlaskClient: + """ + Provide app client that serves the objects from built sample package. + """ + if self._client is None: + app = HydrillaApp(self.config(), flask_config={'TESTING': True}) + self._client = app.test_client() -def test_project_url(client: FlaskClient, test_config: dict) -> None: + return self._client + +def remove_all_uuids(setup: Setup) -> None: + """Modify sample packages before build to contain no (optional) UUIDs""" + index_json = (setup.source_dir / 'index.json').read_text() + index_json = json.loads(hydrilla_util.strip_json_comments(index_json)) + + for definition in index_json['definitions']: + del definition['uuid'] + + index_json = ("// SPDX-License-Identifier: CC0-1.0\n" + + "// Copyright (C) 2021, 2022 Wojtek Kosior\n" + + json.dumps(index_json)) + + (setup.source_dir / 'index.json').write_text(index_json) + +default_setup = Setup() +uuidless_setup = Setup(modify_before_build=remove_all_uuids) + +def def_get(url: str) -> Response: + """Convenience wrapper for def_get()""" + return default_setup.client().get(url) + +def test_project_url() -> None: """Fetch index.html and verify project URL from config is present there.""" - response = client.get('/') + response = def_get('/') assert b'html' in response.data - project_url = test_config['hydrilla_project_url'] + project_url = default_setup.config()['hydrilla_project_url'] assert escape(project_url).encode() in response.data +@pytest.mark.parametrize('setup', [default_setup, uuidless_setup]) @pytest.mark.parametrize('item_type', ['resource', 'mapping']) -def test_get_newest(client: FlaskClient, item_type: str) -> None: +def test_get_newest(setup: Setup, 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') + response = setup.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') + response = setup.client().get(f'/{item_type}/helloapple/2021.11.10') assert response.status_code == 200 assert definition == json.loads(response.data.decode()) + assert ('uuid' in definition) == (setup is not uuidless_setup) + 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: +def test_get_nonexistent(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') + response = def_get(f'/{item_type}/nonexistentapple.json') assert response.status_code == 404 - response = client.get(f'/{item_type}/helloapple/1.2.3.999') + response = def_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: +def test_file_refs(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') + response = def_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}') + response = def_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: +def test_empty_query() -> 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') + response = def_get(f'/query?url=https://nonexiste.nt/example') assert response.status_code == 200 response_object = json.loads(response.data.decode()) @@ -155,12 +215,12 @@ def test_empty_query(client: FlaskClient) -> None: hydrilla_util.validator_for('api_query_result-1.schema.json')\ .validate(response_object) -def test_query(client: FlaskClient) -> None: +def test_query() -> 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/') + response = def_get(f'/query?url=https://hydrillabugs.koszko.org/') assert response.status_code == 200 response_object = json.loads(response.data.decode()) @@ -178,9 +238,9 @@ def test_query(client: FlaskClient) -> None: hydrilla_util.validator_for('api_query_result-1.schema.json')\ .validate(response_object) -def test_source(client: FlaskClient) -> None: +def test_source() -> None: """Verify source descriptions are properly served.""" - response = client.get(f'/source/hello.json') + response = def_get(f'/source/hello.json') assert response.status_code == 200 description = json.loads(response.data.decode()) @@ -190,18 +250,18 @@ def test_source(client: FlaskClient) -> None: ['hello-message', 'helloapple', 'helloapple'] zipfile_hash = description['source_archives']['zip']['sha256'] - response = client.get(f'/source/hello.zip') + response = def_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: +def test_missing_source() -> None: """Verify requests for nonexistent sources result in 404.""" - response = client.get(f'/source/nonexistent.json') + response = def_get(f'/source/nonexistent.json') assert response.status_code == 404 - response = client.get(f'/source/nonexistent.zip') + response = def_get(f'/source/nonexistent.zip') assert response.status_code == 404 def test_normalize_version(): -- cgit v1.2.3