]> git.openstreetmap.org Git - nominatim.git/commitdiff
add unit tests for new Python API
authorSarah Hoffmann <lonvia@denofr.de>
Wed, 7 Dec 2022 18:47:46 +0000 (19:47 +0100)
committerSarah Hoffmann <lonvia@denofr.de>
Tue, 3 Jan 2023 09:03:00 +0000 (10:03 +0100)
nominatim/api.py
nominatim/apicmd/status.py
test/python/api/conftest.py [new file with mode: 0644]
test/python/api/test_api_status.py [new file with mode: 0644]
test/python/cli/test_cli.py
test/python/cli/test_cmd_api.py
test/python/result_formatter/test_v1.py [new file with mode: 0644]

index f5e942d1f3377936de57bc9e8afc5d87a533d579..e129c4f8de4e9e13c5f87a9183cc6deabbf14ec4 100644 (file)
@@ -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.
         """
index 628b6ce9bbb2c5320a290dd79f9ffdd027ba56b2..b5ee9cb94ab4c9dec514a6caf57b401ae89c5fd8 100644 (file)
@@ -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 (file)
index 0000000..4c2e0cc
--- /dev/null
@@ -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 (file)
index 0000000..aae5055
--- /dev/null
@@ -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
index 1072f6c934195b9b1332497619fa24172f100396..d0e3307ebf93be5209e1102e2b7f3e6eac5e0315 100644 (file)
@@ -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
 
index 96415938211241a924e022c52b377dd008ef6453..4031441f4032245d4527984e661cf33a869ed83a 100644 (file)
@@ -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 (file)
index 0000000..da944c2
--- /dev/null
@@ -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())