From: Sarah Hoffmann Date: Wed, 7 Dec 2022 18:47:46 +0000 (+0100) Subject: add unit tests for new Python API X-Git-Tag: v4.3.0~118^2~8 X-Git-Url: https://git.openstreetmap.org./nominatim.git/commitdiff_plain/9d31a6711605cd4ebea09ca1675e2549b706ba5b add unit tests for new Python API --- diff --git a/nominatim/api.py b/nominatim/api.py index f5e942d1..e129c4f8 100644 --- a/nominatim/api.py +++ b/nominatim/api.py @@ -38,6 +38,14 @@ class NominatimAPIAsync: future=True) + async def close(self) -> None: + """ Close all active connections to the database. The NominatimAPIAsync + object remains usable after closing. If a new API functions is + called, new connections are created. + """ + await self.engine.dispose() + + async def status(self) -> StatusResult: """ Return the status of the database. """ @@ -53,6 +61,14 @@ class NominatimAPI: self.async_api = NominatimAPIAsync(project_dir, environ) + def close(self) -> None: + """ Close all active connections to the database. The NominatimAPIAsync + object remains usable after closing. If a new API functions is + called, new connections are created. + """ + asyncio.get_event_loop().run_until_complete(self.async_api.close()) + + def status(self) -> StatusResult: """ Return the status of the database. """ diff --git a/nominatim/apicmd/status.py b/nominatim/apicmd/status.py index 628b6ce9..b5ee9cb9 100644 --- a/nominatim/apicmd/status.py +++ b/nominatim/apicmd/status.py @@ -60,7 +60,7 @@ async def get_status(engine: AsyncEngine) -> StatusResult: async with engine.begin() as conn: status.data_updated = await _get_database_date(conn) status.database_version = await _get_database_version(conn) - except asyncpg.PostgresError as err: - return StatusResult(700, str(err)) + except asyncpg.PostgresError: + return StatusResult(700, 'No database') return status diff --git a/test/python/api/conftest.py b/test/python/api/conftest.py new file mode 100644 index 00000000..4c2e0cc0 --- /dev/null +++ b/test/python/api/conftest.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# This file is part of Nominatim. (https://nominatim.org) +# +# Copyright (C) 2022 by the Nominatim developer community. +# For a full list of authors see the git log. +""" +Helper fixtures for API call tests. +""" +from pathlib import Path +import pytest +import time + +from nominatim.api import NominatimAPI + +@pytest.fixture +def apiobj(temp_db): + """ Create an asynchronous SQLAlchemy engine for the test DB. + """ + api = NominatimAPI(Path('/invalid'), {}) + yield api + api.close() diff --git a/test/python/api/test_api_status.py b/test/python/api/test_api_status.py new file mode 100644 index 00000000..aae5055e --- /dev/null +++ b/test/python/api/test_api_status.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# This file is part of Nominatim. (https://nominatim.org) +# +# Copyright (C) 2022 by the Nominatim developer community. +# For a full list of authors see the git log. +""" +Tests for the status API call. +""" +from pathlib import Path +import datetime as dt +import pytest + +from nominatim.version import version_str +from nominatim.api import NominatimAPI + +def test_status_no_extra_info(apiobj, table_factory): + table_factory('import_status', + definition="lastimportdate timestamp with time zone NOT NULL") + table_factory('nominatim_properties', + definition='property TEXT, value TEXT') + + result = apiobj.status() + + assert result.status == 0 + assert result.message == 'OK' + assert result.software_version == version_str() + assert result.database_version is None + assert result.data_updated is None + + +def test_status_full(apiobj, table_factory): + table_factory('import_status', + definition="lastimportdate timestamp with time zone NOT NULL", + content=(('2022-12-07 15:14:46+01',),)) + table_factory('nominatim_properties', + definition='property TEXT, value TEXT', + content=(('database_version', '99.5.4-2'), )) + + result = apiobj.status() + + assert result.status == 0 + assert result.message == 'OK' + assert result.software_version == version_str() + assert result.database_version == '99.5.4-2' + assert result.data_updated == dt.datetime(2022, 12, 7, 14, 14, 46, 0, tzinfo=dt.timezone.utc) + + +def test_status_database_not_found(monkeypatch): + monkeypatch.setenv('NOMINATIM_DATABASE_DSN', 'dbname=rgjdfkgjedkrgdfkngdfkg') + + api = NominatimAPI(Path('/invalid'), {}) + + result = api.status() + + assert result.status == 700 + assert result.message == 'No database' + assert result.software_version == version_str() + assert result.database_version is None + assert result.data_updated is None diff --git a/test/python/cli/test_cli.py b/test/python/cli/test_cli.py index 1072f6c9..d0e3307e 100644 --- a/test/python/cli/test_cli.py +++ b/test/python/cli/test_cli.py @@ -11,6 +11,7 @@ These tests just check that the various command line parameters route to the correct functionionality. They use a lot of monkeypatching to avoid executing the actual functions. """ +import importlib import pytest import nominatim.indexer.indexer @@ -59,7 +60,7 @@ def test_cli_add_data_tiger_data(cli_call, cli_tokenizer_mock, mock_func_factory assert mock.called == 1 -def test_cli_serve_command(cli_call, mock_func_factory): +def test_cli_serve_php(cli_call, mock_func_factory): func = mock_func_factory(nominatim.cli, 'run_php_server') cli_call('serve') == 0 @@ -67,6 +68,47 @@ def test_cli_serve_command(cli_call, mock_func_factory): assert func.called == 1 +def test_cli_serve_sanic(cli_call, mock_func_factory): + mod = pytest.importorskip("sanic") + func = mock_func_factory(mod.Sanic, "run") + + cli_call('serve', '--engine', 'sanic') == 0 + + assert func.called == 1 + + +def test_cli_serve_starlette_custom_server(cli_call, mock_func_factory): + pytest.importorskip("starlette") + mod = pytest.importorskip("uvicorn") + func = mock_func_factory(mod, "run") + + cli_call('serve', '--engine', 'starlette', '--server', 'foobar:4545') == 0 + + assert func.called == 1 + assert func.last_kwargs['host'] == 'foobar' + assert func.last_kwargs['port'] == 4545 + + +def test_cli_serve_starlette_custom_server_bad_port(cli_call, mock_func_factory): + pytest.importorskip("starlette") + mod = pytest.importorskip("uvicorn") + func = mock_func_factory(mod, "run") + + cli_call('serve', '--engine', 'starlette', '--server', 'foobar:45:45') == 1 + + +@pytest.mark.parametrize("engine", ['falcon', 'starlette']) +def test_cli_serve_uvicorn_based(cli_call, engine, mock_func_factory): + pytest.importorskip(engine) + mod = pytest.importorskip("uvicorn") + func = mock_func_factory(mod, "run") + + cli_call('serve', '--engine', engine) == 0 + + assert func.called == 1 + assert func.last_kwargs['host'] == '127.0.0.1' + assert func.last_kwargs['port'] == 8088 + def test_cli_export_command(cli_call, mock_run_legacy): assert cli_call('export', '--output-all-postcodes') == 0 diff --git a/test/python/cli/test_cmd_api.py b/test/python/cli/test_cmd_api.py index 96415938..4031441f 100644 --- a/test/python/cli/test_cmd_api.py +++ b/test/python/cli/test_cmd_api.py @@ -7,9 +7,12 @@ """ Tests for API access commands of command-line interface wrapper. """ +import json import pytest import nominatim.clicmd.api +import nominatim.api +from nominatim.apicmd.status import StatusResult @pytest.mark.parametrize("endpoint", (('search', 'reverse', 'lookup', 'details', 'status'))) @@ -27,9 +30,8 @@ def test_no_api_without_phpcgi(endpoint): ('details', '--node', '1'), ('details', '--way', '1'), ('details', '--relation', '1'), - ('details', '--place_id', '10001'), - ('status',)]) -class TestCliApiCall: + ('details', '--place_id', '10001')]) +class TestCliApiCallPhp: @pytest.fixture(autouse=True) def setup_cli_call(self, params, cli_call, mock_func_factory, tmp_path): @@ -55,6 +57,29 @@ class TestCliApiCall: assert self.run_nominatim() == 1 +class TestCliStatusCall: + + @pytest.fixture(autouse=True) + def setup_status_mock(self, monkeypatch): + monkeypatch.setattr(nominatim.api.NominatimAPI, 'status', + lambda self: StatusResult(200, 'OK')) + + + def test_status_simple(self, cli_call, tmp_path): + result = cli_call('status', '--project-dir', str(tmp_path)) + + assert result == 0 + + + def test_status_json_format(self, cli_call, tmp_path, capsys): + result = cli_call('status', '--project-dir', str(tmp_path), + '--format', 'json') + + assert result == 0 + + json.loads(capsys.readouterr().out) + + QUERY_PARAMS = { 'search': ('--query', 'somewhere'), 'reverse': ('--lat', '20', '--lon', '30'), diff --git a/test/python/result_formatter/test_v1.py b/test/python/result_formatter/test_v1.py new file mode 100644 index 00000000..da944c25 --- /dev/null +++ b/test/python/result_formatter/test_v1.py @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# This file is part of Nominatim. (https://nominatim.org) +# +# Copyright (C) 2022 by the Nominatim developer community. +# For a full list of authors see the git log. +""" +Tests for formatting results for the V1 API. +""" +import datetime as dt +import pytest + +import nominatim.result_formatter.v1 as format_module +from nominatim.apicmd.status import StatusResult +from nominatim.version import version_str + +STATUS_FORMATS = {'text', 'json'} + +class TestStatusResultFormat: + + + @pytest.fixture(autouse=True) + def make_formatter(self): + self.formatter = format_module.create(StatusResult) + + + def test_format_list(self): + assert set(self.formatter.list_formats()) == STATUS_FORMATS + + + @pytest.mark.parametrize('fmt', list(STATUS_FORMATS)) + def test_supported(self, fmt): + assert self.formatter.supports_format(fmt) + + + def test_unsupported(self): + assert not self.formatter.supports_format('gagaga') + + + def test_format_text(self): + assert self.formatter.format(StatusResult(0, 'message here'), 'text') == 'message here' + + + def test_format_json_minimal(self): + status = StatusResult(700, 'Bad format.') + + result = self.formatter.format(status, 'json') + + assert result == '{"status": 700, "message": "Bad format.", "software_version": "%s"}' % (version_str()) + + + def test_format_json_full(self): + status = StatusResult(0, 'OK') + status.data_updated = dt.datetime(2010, 2, 7, 20, 20, 3, 0, tzinfo=dt.timezone.utc) + status.database_version = '5.6' + + result = self.formatter.format(status, 'json') + + assert result == '{"status": 0, "message": "OK", "data_updated": "2010-02-07T20:20:03+00:00", "software_version": "%s", "database_version": "5.6"}' % (version_str())