aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--conftest.py80
-rw-r--r--tests/helpers.py51
-rw-r--r--tests/test_server.py115
3 files changed, 205 insertions, 41 deletions
diff --git a/conftest.py b/conftest.py
index 1aef80a..6b6d1cf 100644
--- a/conftest.py
+++ b/conftest.py
@@ -7,5 +7,85 @@
import sys
from pathlib import Path
+import pytest
+import pkgutil
+import functools
+from tempfile import TemporaryDirectory
+from typing import Iterable
+
here = Path(__file__).resolve().parent
sys.path.insert(0, str(here / 'src'))
+
+@pytest.fixture(autouse=True)
+def no_requests(monkeypatch):
+ """Remove requests.sessions.Session.request for all tests."""
+ monkeypatch.delattr('requests.sessions.Session.request')
+
+def _mock_subprocess_run(monkeypatch, where, mocked_run):
+ """Temporarily replace subprocess.run() with the given function."""
+ class MockedSubprocess:
+ """Minimal mocked version of the subprocess module."""
+ run = mocked_run
+
+ monkeypatch.setattr(where, 'subprocess', MockedSubprocess)
+
+@pytest.fixture
+def mock_subprocess_run(monkeypatch, request):
+ """
+ Facilitate temporarily replacing subprocess.run() with a different function.
+
+ If the 'subprocess_run' pytest marker has been used, perform the replacement
+ for the module-function pair supplied through it.
+
+ Return a function that can be called to perform the same replacement in
+ another fixture or from inside a test function.
+ """
+ mocker = functools.partial(_mock_subprocess_run, monkeypatch)
+
+ marker = request.node.get_closest_marker('subprocess_run')
+ if marker:
+ where, mocked_run = marker.args
+ mocker(where, mocked_run)
+
+ return mocker
+
+@pytest.fixture(autouse=True)
+def no_gettext(monkeypatch, request):
+ """
+ Make gettext return all strings untranslated unless we request otherwise.
+ """
+ if request.node.get_closest_marker('enable_gettext'):
+ return
+
+ import hydrilla
+ modules_to_process = [hydrilla]
+
+ def add_child_modules(parent):
+ """
+ Recursuvely collect all modules descending from 'parent' into an array.
+ """
+ try:
+ load_paths = parent.__path__
+ except AttributeError:
+ return
+
+ for module_info in pkgutil.iter_modules(load_paths):
+ if module_info.name != '__main__':
+ __import__(f'{parent.__name__}.{module_info.name}')
+ modules_to_process.append(getattr(parent, module_info.name))
+ add_child_modules(getattr(parent, module_info.name))
+
+ add_child_modules(hydrilla)
+
+ for module in modules_to_process:
+ if hasattr(module, '_'):
+ monkeypatch.setattr(module, '_', lambda message: message)
+
+@pytest.fixture
+def tmpdir() -> Iterable[Path]:
+ """
+ Provide test case with a temporary directory that will be automatically
+ deleted after the test.
+ """
+ with TemporaryDirectory() as tmpdir:
+ yield Path(tmpdir)
diff --git a/tests/helpers.py b/tests/helpers.py
new file mode 100644
index 0000000..df474b0
--- /dev/null
+++ b/tests/helpers.py
@@ -0,0 +1,51 @@
+# SPDX-License-Identifier: CC0-1.0
+
+# Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
+#
+# Available under the terms of Creative Commons Zero v1.0 Universal.
+
+import re
+
+variable_word_re = re.compile(r'^<(.+)>$')
+
+def process_command(command, expected_command):
+ """Validate the command line and extract its variable parts (if any)."""
+ assert len(command) == len(expected_command)
+
+ extracted = {}
+ for word, expected_word in zip(command, expected_command):
+ match = variable_word_re.match(expected_word)
+ if match:
+ extracted[match.group(1)] = word
+ else:
+ assert word == expected_word
+
+ return extracted
+
+def run_missing_executable(command, **kwargs):
+ """
+ Instead of running a command, raise FileNotFoundError as if its executable
+ was missing.
+ """
+ raise FileNotFoundError('dummy')
+
+class MockedCompletedProcess:
+ """
+ Object with some fields similar to those of subprocess.CompletedProcess.
+ """
+ def __init__(self, args, returncode=0,
+ stdout='some output', stderr='some error output',
+ text_output=True):
+ """
+ Initialize MockedCompletedProcess. Convert strings to bytes if needed.
+ """
+ self.args = args
+ self.returncode = returncode
+
+ if type(stdout) is str and not text_output:
+ stdout = stdout.encode()
+ if type(stderr) is str and not text_output:
+ stderr = stderr.encode()
+
+ self.stdout = stdout
+ self.stderr = stderr
diff --git a/tests/test_server.py b/tests/test_server.py
index 02b9742..65ae8ce 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -43,9 +43,10 @@ from markupsafe import escape
from werkzeug import Response
from hydrilla import util as hydrilla_util
-from hydrilla.builder import Build
-from hydrilla.server import config, _version
-from hydrilla.server.serve import HydrillaApp
+from hydrilla.builder import build
+from hydrilla.server import config, _version, serve
+
+from .helpers import *
here = Path(__file__).resolve().parent
config_path = here / 'config.json'
@@ -56,13 +57,41 @@ expected_generated_by = {
'version': _version.version
}
-SetupMod = Optional[Callable['Setup', None]]
+SetupMod = Optional[Callable[['Setup'], None]]
source_files = (
'index.json', 'hello.js', 'bye.js', 'message.js', 'README.txt',
'README.txt.license', '.reuse/dep5', 'LICENSES/CC0-1.0.txt'
)
+def run_reuse(command, **kwargs):
+ """
+ Instead of running a 'reuse' command, check if 'mock_reuse_missing' file
+ exists under root directory. If yes, raise FileNotFoundError as if 'reuse'
+ command was missing. If not, check if 'README.txt.license' file exists
+ in the requested directory and return zero if it does.
+ """
+ expected = ['reuse', '--root', '<root>',
+ 'lint' if 'lint' in command else 'spdx']
+
+ root_path = Path(process_command(command, expected)['root'])
+
+ if (root_path / 'mock_reuse_missing').exists():
+ raise FileNotFoundError('dummy')
+
+ is_reuse_compliant = (root_path / 'README.txt.license').exists()
+
+ return MockedCompletedProcess(command, 1 - is_reuse_compliant,
+ stdout=f'dummy {expected[-1]} output',
+ text_output=kwargs.get('text'))
+
+@pytest.fixture
+def mock_reuse(mock_subprocess_run):
+ """
+ Mock the REUSE command when executed through subprocess.run() from serve.py.
+ """
+ mock_subprocess_run(build, run_reuse)
+
class Setup:
"""
Facilitate preparing test malcontent directory, Hydrilla config file and the
@@ -96,8 +125,8 @@ class Setup:
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)
+ build_obj = build.Build(self.source_dir, self.index_json)
+ build_obj.write_package_files(self.malcontent_dir)
if self._modify_after_build:
self._modify_after_build(self)
@@ -115,7 +144,8 @@ class Setup:
Provide app client that serves the objects from built sample package.
"""
if self._client is None:
- app = HydrillaApp(self.config(), flask_config={'TESTING': True})
+ app = serve.HydrillaApp(self.config(),
+ flask_config={'TESTING': True})
self._client = app.test_client()
return self._client
@@ -155,32 +185,23 @@ def bump_schema_v2(index_json) -> None:
definition['type'] == 'resource':
definition['required_mappings'] = {'identifier': 'helloapple'}
-default_setup = Setup()
-uuidless_setup = Setup(modify_before_build=remove_all_uuids)
-schema_v2_setup = Setup(modify_before_build=bump_schema_v2)
+default_setup = lambda: Setup()
+uuidless_setup = lambda: Setup(modify_before_build=remove_all_uuids)
+schema_v2_setup = lambda: Setup(modify_before_build=bump_schema_v2)
-setups = [default_setup, uuidless_setup, schema_v2_setup]
+setup_makers = [default_setup, uuidless_setup, schema_v2_setup]
-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 = def_get('/')
- assert b'html' in response.data
- project_url = default_setup.config()['hydrilla_project_url']
- assert escape(project_url).encode() in response.data
-
-@pytest.mark.parametrize('setup', setups)
+@pytest.mark.usefixtures('mock_reuse')
+@pytest.mark.parametrize('setup_maker', setup_makers)
@pytest.mark.parametrize('item_type', ['resource', 'mapping'])
-def test_get_newest(setup: Setup, item_type: str) -> None:
+def test_get_newest(setup_maker, 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}'
"""
+ setup = setup_maker()
response = setup.client().get(f'/{item_type}/helloapple.json')
assert response.status_code == 200
definition = json.loads(response.data.decode())
@@ -191,46 +212,58 @@ def test_get_newest(setup: Setup, item_type: str) -> None:
assert response.status_code == 200
assert definition == json.loads(response.data.decode())
- assert ('uuid' in definition) == (setup is not uuidless_setup)
+ assert ('uuid' in definition) == (setup_maker is not uuidless_setup)
hydrilla_util.validator_for(f'api_{item_type}_description-1.0.1.schema.json')\
.validate(definition)
+@pytest.fixture
+def setup(mock_reuse):
+ """Prepare server test environment in the default way."""
+ return default_setup()
+
+def test_project_url(setup) -> None:
+ """Fetch index.html and verify project URL from config is present there."""
+ response = setup.client().get('/')
+ assert b'html' in response.data
+ project_url = setup.config()['hydrilla_project_url']
+ assert escape(project_url).encode() in response.data
+
@pytest.mark.parametrize('item_type', ['resource', 'mapping'])
-def test_get_nonexistent(item_type: str) -> None:
+def test_get_nonexistent(setup, item_type: str) -> None:
"""
Verify that attempts to GET a JSON definition of a nonexistent item or item
version result in 404.
"""
- response = def_get(f'/{item_type}/nonexistentapple.json')
+ response = setup.client().get(f'/{item_type}/nonexistentapple.json')
assert response.status_code == 404
- response = def_get(f'/{item_type}/helloapple/1.2.3.999')
+ response = setup.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(item_type: str) -> None:
+def test_file_refs(setup, item_type: str) -> None:
"""
Verify that files referenced by definitions are accessible under their
proper URLs and that their hashes match.
"""
- response = def_get(f'/{item_type}/helloapple/2021.11.10')
+ response = setup.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 = def_get(f'/file/sha256/{hash_sum}')
+ response = setup.client().get(f'/file/sha256/{hash_sum}')
assert response.status_code == 200
assert sha256(response.data).digest().hex() == hash_sum
-def test_empty_query() -> None:
+def test_empty_query(setup) -> None:
"""
Verify that querying mappings for URL gives an empty list when there're no
mathes.
"""
- response = def_get(f'/query?url=https://nonexiste.nt/example')
+ response = setup.client().get(f'/query?url=https://nonexiste.nt/example')
assert response.status_code == 200
response_object = json.loads(response.data.decode())
@@ -244,12 +277,12 @@ def test_empty_query() -> None:
hydrilla_util.validator_for('api_query_result-1.0.1.schema.json')\
.validate(response_object)
-def test_query() -> None:
+def test_query(setup) -> None:
"""
Verify that querying mappings for URL gives a list with reference(s) the the
matching mapping(s).
"""
- response = def_get(f'/query?url=https://hydrillabugs.koszko.org/')
+ response = setup.client().get(f'/query?url=https://hydrillabugs.koszko.org/')
assert response.status_code == 200
response_object = json.loads(response.data.decode())
@@ -267,9 +300,9 @@ def test_query() -> None:
hydrilla_util.validator_for('api_query_result-1.schema.json')\
.validate(response_object)
-def test_source() -> None:
+def test_source(setup) -> None:
"""Verify source descriptions are properly served."""
- response = def_get(f'/source/hello.json')
+ response = setup.client().get(f'/source/hello.json')
assert response.status_code == 200
description = json.loads(response.data.decode())
@@ -279,18 +312,18 @@ def test_source() -> None:
['hello-message', 'helloapple', 'helloapple']
zipfile_hash = description['source_archives']['zip']['sha256']
- response = def_get(f'/source/hello.zip')
+ response = setup.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() -> None:
+def test_missing_source(setup) -> None:
"""Verify requests for nonexistent sources result in 404."""
- response = def_get(f'/source/nonexistent.json')
+ response = setup.client().get(f'/source/nonexistent.json')
assert response.status_code == 404
- response = def_get(f'/source/nonexistent.zip')
+ response = setup.client().get(f'/source/nonexistent.zip')
assert response.status_code == 404
def test_normalize_version():