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     napi.Locales().localize_results([search])
 
 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'}],