1 # SPDX-License-Identifier: GPL-3.0-or-later
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2024 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'}
 
  25 def test_status_format_list():
 
  26     assert set(v1_format.list_formats(napi.StatusResult)) == STATUS_FORMATS
 
  29 @pytest.mark.parametrize('fmt', list(STATUS_FORMATS))
 
  30 def test_status_supported(fmt):
 
  31     assert v1_format.supports_format(napi.StatusResult, fmt)
 
  34 def test_status_unsupported():
 
  35     assert not v1_format.supports_format(napi.StatusResult, 'gagaga')
 
  38 def test_status_format_text():
 
  39     assert v1_format.format_result(napi.StatusResult(0, 'message here'), 'text', {}) == 'OK'
 
  42 def test_status_format_text():
 
  43     assert v1_format.format_result(napi.StatusResult(500, 'message here'), 'text', {}) == 'ERROR: message here'
 
  46 def test_status_format_json_minimal():
 
  47     status = napi.StatusResult(700, 'Bad format.')
 
  49     result = v1_format.format_result(status, 'json', {})
 
  52            f'{{"status":700,"message":"Bad format.","software_version":"{napi.__version__}"}}'
 
  55 def test_status_format_json_full():
 
  56     status = napi.StatusResult(0, 'OK')
 
  57     status.data_updated = dt.datetime(2010, 2, 7, 20, 20, 3, 0, tzinfo=dt.timezone.utc)
 
  58     status.database_version = '5.6'
 
  60     result = v1_format.format_result(status, 'json', {})
 
  63            f'{{"status":0,"message":"OK","data_updated":"2010-02-07T20:20:03+00:00","software_version":"{napi.__version__}","database_version":"5.6"}}'
 
  68 def test_search_details_minimal():
 
  69     search = napi.DetailedResult(napi.SourceTable.PLACEX,
 
  73     result = v1_format.format_result(search, 'json', {})
 
  75     assert json.loads(result) == \
 
  81             'calculated_importance': pytest.approx(0.00001),
 
  87             'centroid': {'type': 'Point', 'coordinates': [1.0, 2.0]},
 
  88             'geometry': {'type': 'Point', 'coordinates': [1.0, 2.0]},
 
  92 def test_search_details_full():
 
  93     import_date = dt.datetime(2010, 2, 7, 20, 20, 3, 0, tzinfo=dt.timezone.utc)
 
  94     search = napi.DetailedResult(
 
  95                   source_table=napi.SourceTable.PLACEX,
 
  96                   category=('amenity', 'bank'),
 
  97                   centroid=napi.Point(56.947, -87.44),
 
 100                   linked_place_id=55693,
 
 101                   osm_object=('W', 442100),
 
 103                   names={'name': 'Bank', 'name:fr': 'Banque'},
 
 104                   address={'city': 'Niento', 'housenumber': '  3'},
 
 105                   extratags={'atm': 'yes'},
 
 113                   indexed_date = import_date
 
 115     search.localize(napi.Locales())
 
 117     result = v1_format.format_result(search, 'json', {})
 
 119     assert json.loads(result) == \
 
 121             'parent_place_id': 114,
 
 124             'category': 'amenity',
 
 128             'names': {'name': 'Bank', 'name:fr': 'Banque'},
 
 129             'addresstags': {'city': 'Niento', 'housenumber': '  3'},
 
 131             'calculated_postcode': '556 X23',
 
 132             'country_code': 'll',
 
 133             'indexed_date': '2010-02-07T20:20:03+00:00',
 
 134             'importance': pytest.approx(0.0443),
 
 135             'calculated_importance': pytest.approx(0.0443),
 
 136             'extratags': {'atm': 'yes'},
 
 137             'calculated_wikipedia': 'en:Bank',
 
 141             'centroid': {'type': 'Point', 'coordinates': [56.947, -87.44]},
 
 142             'geometry': {'type': 'Point', 'coordinates': [56.947, -87.44]},
 
 146 @pytest.mark.parametrize('gtype,isarea', [('ST_Point', False),
 
 147                                           ('ST_LineString', False),
 
 148                                           ('ST_Polygon', True),
 
 149                                           ('ST_MultiPolygon', True)])
 
 150 def test_search_details_no_geometry(gtype, isarea):
 
 151     search = napi.DetailedResult(napi.SourceTable.PLACEX,
 
 153                                napi.Point(1.0, 2.0),
 
 154                                geometry={'type': gtype})
 
 156     result = v1_format.format_result(search, 'json', {})
 
 157     js = json.loads(result)
 
 159     assert js['geometry'] == {'type': 'Point', 'coordinates': [1.0, 2.0]}
 
 160     assert js['isarea'] == isarea
 
 163 def test_search_details_with_geometry():
 
 164     search = napi.DetailedResult(napi.SourceTable.PLACEX,
 
 166                                  napi.Point(1.0, 2.0),
 
 167                                  geometry={'geojson': '{"type":"Point","coordinates":[56.947,-87.44]}'})
 
 169     result = v1_format.format_result(search, 'json', {})
 
 170     js = json.loads(result)
 
 172     assert js['geometry'] == {'type': 'Point', 'coordinates': [56.947, -87.44]}
 
 173     assert js['isarea'] == False
 
 176 def test_search_details_with_icon_available():
 
 177     search = napi.DetailedResult(napi.SourceTable.PLACEX,
 
 178                                  ('amenity', 'restaurant'),
 
 179                                  napi.Point(1.0, 2.0))
 
 181     result = v1_format.format_result(search, 'json', {'icon_base_url': 'foo'})
 
 182     js = json.loads(result)
 
 184     assert js['icon'] == 'foo/food_restaurant.p.20.png'
 
 187 def test_search_details_with_icon_not_available():
 
 188     search = napi.DetailedResult(napi.SourceTable.PLACEX,
 
 190                                  napi.Point(1.0, 2.0))
 
 192     result = v1_format.format_result(search, 'json', {'icon_base_url': 'foo'})
 
 193     js = json.loads(result)
 
 195     assert 'icon' not in js
 
 198 def test_search_details_with_address_minimal():
 
 199     search = napi.DetailedResult(napi.SourceTable.PLACEX,
 
 201                                  napi.Point(1.0, 2.0),
 
 203                                    napi.AddressLine(place_id=None,
 
 205                                                     category=('bnd', 'note'),
 
 215     result = v1_format.format_result(search, 'json', {})
 
 216     js = json.loads(result)
 
 218     assert js['address'] == [{'localname': '',
 
 226 @pytest.mark.parametrize('field,outfield', [('address_rows', 'address'),
 
 227                                             ('linked_rows', 'linked_places'),
 
 228                                             ('parented_rows', 'hierarchy')
 
 230 def test_search_details_with_further_infos(field, outfield):
 
 231     search = napi.DetailedResult(napi.SourceTable.PLACEX,
 
 233                                  napi.Point(1.0, 2.0))
 
 235     setattr(search, field, [napi.AddressLine(place_id=3498,
 
 236                                              osm_object=('R', 442),
 
 237                                              category=('bnd', 'note'),
 
 238                                              names={'name': 'Trespass'},
 
 239                                              extratags={'access': 'no',
 
 240                                                         'place_type': 'spec'},
 
 248     result = v1_format.format_result(search, 'json', {})
 
 249     js = json.loads(result)
 
 251     assert js[outfield] == [{'localname': 'Trespass',
 
 255                               'place_type': 'spec',
 
 264 def test_search_details_grouped_hierarchy():
 
 265     search = napi.DetailedResult(napi.SourceTable.PLACEX,
 
 267                                  napi.Point(1.0, 2.0),
 
 269                                      [napi.AddressLine(place_id=3498,
 
 270                                              osm_object=('R', 442),
 
 271                                              category=('bnd', 'note'),
 
 272                                              names={'name': 'Trespass'},
 
 273                                              extratags={'access': 'no',
 
 274                                                         'place_type': 'spec'},
 
 282     result = v1_format.format_result(search, 'json', {'group_hierarchy': True})
 
 283     js = json.loads(result)
 
 285     assert js['hierarchy'] == {'note': [{'localname': 'Trespass',
 
 289                               'place_type': 'spec',
 
 298 def test_search_details_keywords_name():
 
 299     search = napi.DetailedResult(napi.SourceTable.PLACEX,
 
 301                                  napi.Point(1.0, 2.0),
 
 303                                      napi.WordInfo(23, 'foo', 'mefoo'),
 
 304                                      napi.WordInfo(24, 'foo', 'bafoo')])
 
 306     result = v1_format.format_result(search, 'json', {'keywords': True})
 
 307     js = json.loads(result)
 
 309     assert js['keywords'] == {'name': [{'id': 23, 'token': 'foo'},
 
 310                                       {'id': 24, 'token': 'foo'}],
 
 314 def test_search_details_keywords_address():
 
 315     search = napi.DetailedResult(napi.SourceTable.PLACEX,
 
 317                                  napi.Point(1.0, 2.0),
 
 319                                      napi.WordInfo(23, 'foo', 'mefoo'),
 
 320                                      napi.WordInfo(24, 'foo', 'bafoo')])
 
 322     result = v1_format.format_result(search, 'json', {'keywords': True})
 
 323     js = json.loads(result)
 
 325     assert js['keywords'] == {'address': [{'id': 23, 'token': 'foo'},
 
 326                                       {'id': 24, 'token': 'foo'}],