1 # SPDX-License-Identifier: GPL-3.0-or-later
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2025 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Tests for formatting results for the V1 API.
10 These test only ensure that the Python code is correct.
11 For functional tests see BDD test suite.
18 from nominatim_api.v1.format import dispatch as v1_format
19 import nominatim_api as napi
21 STATUS_FORMATS = {'text', 'json'}
26 def test_status_format_list():
27 assert set(v1_format.list_formats(napi.StatusResult)) == STATUS_FORMATS
30 @pytest.mark.parametrize('fmt', list(STATUS_FORMATS))
31 def test_status_supported(fmt):
32 assert v1_format.supports_format(napi.StatusResult, fmt)
35 def test_status_unsupported():
36 assert not v1_format.supports_format(napi.StatusResult, 'gagaga')
39 def test_status_format_text():
40 assert v1_format.format_result(napi.StatusResult(0, 'message here'), 'text', {}) \
44 def test_status_format_error_text():
45 assert v1_format.format_result(napi.StatusResult(500, 'message here'), 'text', {}) \
46 == 'ERROR: message here'
49 def test_status_format_json_minimal():
50 status = napi.StatusResult(700, 'Bad format.')
52 result = v1_format.format_result(status, 'json', {})
54 assert json.loads(result) == {'status': 700,
55 'message': 'Bad format.',
56 'software_version': napi.__version__}
59 def test_status_format_json_full():
60 status = napi.StatusResult(0, 'OK')
61 status.data_updated = dt.datetime(2010, 2, 7, 20, 20, 3, 0, tzinfo=dt.timezone.utc)
62 status.database_version = '5.6'
64 result = v1_format.format_result(status, 'json', {})
66 assert json.loads(result) == {'status': 0,
68 'data_updated': '2010-02-07T20:20:03+00:00',
69 'software_version': napi.__version__,
70 'database_version': '5.6'}
75 def test_search_details_minimal():
76 search = napi.DetailedResult(napi.SourceTable.PLACEX,
80 result = v1_format.format_result(search, 'json', {})
82 assert json.loads(result) == \
88 'calculated_importance': pytest.approx(0.00001),
94 'centroid': {'type': 'Point', 'coordinates': [1.0, 2.0]},
95 'geometry': {'type': 'Point', 'coordinates': [1.0, 2.0]},
99 def test_search_details_full():
100 import_date = dt.datetime(2010, 2, 7, 20, 20, 3, 0, tzinfo=dt.timezone.utc)
101 search = napi.DetailedResult(
102 source_table=napi.SourceTable.PLACEX,
103 category=('amenity', 'bank'),
104 centroid=napi.Point(56.947, -87.44),
107 linked_place_id=55693,
108 osm_object=('W', 442100),
110 names={'name': 'Bank', 'name:fr': 'Banque'},
111 address={'city': 'Niento', 'housenumber': ' 3'},
112 extratags={'atm': 'yes'},
120 indexed_date=import_date
122 search.localize(napi.Locales())
124 result = v1_format.format_result(search, 'json', {})
126 assert json.loads(result) == \
128 'parent_place_id': 114,
131 'category': 'amenity',
135 'names': {'name': 'Bank', 'name:fr': 'Banque'},
136 'addresstags': {'city': 'Niento', 'housenumber': ' 3'},
138 'calculated_postcode': '556 X23',
139 'country_code': 'll',
140 'indexed_date': '2010-02-07T20:20:03+00:00',
141 'importance': pytest.approx(0.0443),
142 'calculated_importance': pytest.approx(0.0443),
143 'extratags': {'atm': 'yes'},
144 'calculated_wikipedia': 'en:Bank',
148 'centroid': {'type': 'Point', 'coordinates': [56.947, -87.44]},
149 'geometry': {'type': 'Point', 'coordinates': [56.947, -87.44]},
153 @pytest.mark.parametrize('gtype,isarea', [('ST_Point', False),
154 ('ST_LineString', False),
155 ('ST_Polygon', True),
156 ('ST_MultiPolygon', True)])
157 def test_search_details_no_geometry(gtype, isarea):
158 search = napi.DetailedResult(napi.SourceTable.PLACEX,
160 napi.Point(1.0, 2.0),
161 geometry={'type': gtype})
163 result = v1_format.format_result(search, 'json', {})
164 js = json.loads(result)
166 assert js['geometry'] == {'type': 'Point', 'coordinates': [1.0, 2.0]}
167 assert js['isarea'] == isarea
170 def test_search_details_with_geometry():
171 search = napi.DetailedResult(
172 napi.SourceTable.PLACEX,
174 napi.Point(1.0, 2.0),
175 geometry={'geojson': '{"type":"Point","coordinates":[56.947,-87.44]}'})
177 result = v1_format.format_result(search, 'json', {})
178 js = json.loads(result)
180 assert js['geometry'] == {'type': 'Point', 'coordinates': [56.947, -87.44]}
181 assert js['isarea'] is False
184 def test_search_details_with_icon_available():
185 search = napi.DetailedResult(napi.SourceTable.PLACEX,
186 ('amenity', 'restaurant'),
187 napi.Point(1.0, 2.0))
189 result = v1_format.format_result(search, 'json', {'icon_base_url': 'foo'})
190 js = json.loads(result)
192 assert js['icon'] == 'foo/food_restaurant.p.20.png'
195 def test_search_details_with_icon_not_available():
196 search = napi.DetailedResult(napi.SourceTable.PLACEX,
198 napi.Point(1.0, 2.0))
200 result = v1_format.format_result(search, 'json', {'icon_base_url': 'foo'})
201 js = json.loads(result)
203 assert 'icon' not in js
206 def test_search_details_with_address_minimal():
207 search = napi.DetailedResult(napi.SourceTable.PLACEX,
209 napi.Point(1.0, 2.0),
211 napi.AddressLine(place_id=None,
213 category=('bnd', 'note'),
223 result = v1_format.format_result(search, 'json', {})
224 js = json.loads(result)
226 assert js['address'] == [{'localname': '',
234 @pytest.mark.parametrize('field,outfield', [('address_rows', 'address'),
235 ('linked_rows', 'linked_places'),
236 ('parented_rows', 'hierarchy')
238 def test_search_details_with_further_infos(field, outfield):
239 search = napi.DetailedResult(napi.SourceTable.PLACEX,
241 napi.Point(1.0, 2.0))
243 setattr(search, field, [napi.AddressLine(place_id=3498,
244 osm_object=('R', 442),
245 category=('bnd', 'note'),
246 names={'name': 'Trespass'},
247 extratags={'access': 'no',
248 'place_type': 'spec'},
256 result = v1_format.format_result(search, 'json', {})
257 js = json.loads(result)
259 assert js[outfield] == [{'localname': 'Trespass',
263 'place_type': 'spec',
272 def test_search_details_grouped_hierarchy():
273 search = napi.DetailedResult(napi.SourceTable.PLACEX,
275 napi.Point(1.0, 2.0),
276 parented_rows=[napi.AddressLine(
278 osm_object=('R', 442),
279 category=('bnd', 'note'),
280 names={'name': 'Trespass'},
281 extratags={'access': 'no',
282 'place_type': 'spec'},
289 result = v1_format.format_result(search, 'json', {'group_hierarchy': True})
290 js = json.loads(result)
292 assert js['hierarchy'] == {'note': [{'localname': 'Trespass',
296 'place_type': 'spec',
305 def test_search_details_keywords_name():
306 search = napi.DetailedResult(napi.SourceTable.PLACEX,
308 napi.Point(1.0, 2.0),
310 napi.WordInfo(23, 'foo', 'mefoo'),
311 napi.WordInfo(24, 'foo', 'bafoo')])
313 result = v1_format.format_result(search, 'json', {'keywords': True})
314 js = json.loads(result)
316 assert js['keywords'] == {'name': [{'id': 23, 'token': 'foo'},
317 {'id': 24, 'token': 'foo'}],
321 def test_search_details_keywords_address():
322 search = napi.DetailedResult(napi.SourceTable.PLACEX,
324 napi.Point(1.0, 2.0),
326 napi.WordInfo(23, 'foo', 'mefoo'),
327 napi.WordInfo(24, 'foo', 'bafoo')])
329 result = v1_format.format_result(search, 'json', {'keywords': True})
330 js = json.loads(result)
332 assert js['keywords'] == {'address': [{'id': 23, 'token': 'foo'},
333 {'id': 24, 'token': 'foo'}],