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 reverse results for the V1 API.
 
  10 These test only ensure that the Python code is correct.
 
  11 For functional tests see BDD test suite.
 
  14 import xml.etree.ElementTree as ET
 
  18 from nominatim_api.v1.format import dispatch as v1_format
 
  19 import nominatim_api as napi
 
  21 FORMATS = ['json', 'jsonv2', 'geojson', 'geocodejson', 'xml']
 
  24 @pytest.mark.parametrize('fmt', FORMATS)
 
  25 def test_format_reverse_minimal(fmt):
 
  26     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
  27                                  ('amenity', 'post_box'),
 
  28                                  napi.Point(0.3, -8.9))
 
  30     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, {})
 
  33         root = ET.fromstring(raw)
 
  34         assert root.tag == 'reversegeocode'
 
  36         result = json.loads(raw)
 
  37         assert isinstance(result, dict)
 
  40 @pytest.mark.parametrize('fmt', FORMATS)
 
  41 def test_format_reverse_no_result(fmt):
 
  42     raw = v1_format.format_result(napi.ReverseResults(), fmt, {})
 
  45         root = ET.fromstring(raw)
 
  46         assert root.find('error').text == 'Unable to geocode'
 
  48         assert json.loads(raw) == {'error': 'Unable to geocode'}
 
  51 @pytest.mark.parametrize('fmt', FORMATS)
 
  52 def test_format_reverse_with_osm_id(fmt):
 
  53     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
  54                                  ('amenity', 'post_box'),
 
  55                                  napi.Point(0.3, -8.9),
 
  59     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, {})
 
  62         root = ET.fromstring(raw).find('result')
 
  63         assert root.attrib['osm_type'] == 'node'
 
  64         assert root.attrib['osm_id'] == '23'
 
  66         result = json.loads(raw)
 
  67         if fmt == 'geocodejson':
 
  68             props = result['features'][0]['properties']['geocoding']
 
  69         elif fmt == 'geojson':
 
  70             props = result['features'][0]['properties']
 
  73         assert props['osm_type'] == 'node'
 
  74         assert props['osm_id'] == 23
 
  77 @pytest.mark.parametrize('fmt', FORMATS)
 
  78 def test_format_reverse_with_address(fmt):
 
  79     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
  83                                  address_rows=napi.AddressLines([
 
  84                                    napi.AddressLine(place_id=None,
 
  86                                                     category=('place', 'county'),
 
  87                                                     names={'name': 'Hello'},
 
  94                                    napi.AddressLine(place_id=None,
 
  96                                                     category=('place', 'county'),
 
  97                                                     names={'name': 'ByeBye'},
 
 105     napi.Locales().localize_results([reverse])
 
 107     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 108                                   {'addressdetails': True})
 
 110         root = ET.fromstring(raw)
 
 111         assert root.find('addressparts').find('county').text == 'Hello'
 
 113         result = json.loads(raw)
 
 114         assert isinstance(result, dict)
 
 116         if fmt == 'geocodejson':
 
 117             props = result['features'][0]['properties']['geocoding']
 
 118             assert 'admin' in props
 
 119             assert props['county'] == 'Hello'
 
 122                 props = result['features'][0]['properties']
 
 125             assert 'address' in props
 
 128 def test_format_reverse_geocodejson_special_parts():
 
 129     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 131                                  napi.Point(1.0, 2.0),
 
 134                                  address_rows=napi.AddressLines([
 
 135                                    napi.AddressLine(place_id=None,
 
 137                                                     category=('place', 'house_number'),
 
 145                                    napi.AddressLine(place_id=None,
 
 147                                                     category=('place', 'postcode'),
 
 148                                                     names={'ref': '99446'},
 
 155                                    napi.AddressLine(place_id=33,
 
 157                                                     category=('place', 'county'),
 
 158                                                     names={'name': 'Hello'},
 
 167     napi.Locales().localize_results([reverse])
 
 169     raw = v1_format.format_result(napi.ReverseResults([reverse]), 'geocodejson',
 
 170                                   {'addressdetails': True})
 
 172     props = json.loads(raw)['features'][0]['properties']['geocoding']
 
 173     assert props['housenumber'] == '1'
 
 174     assert props['postcode'] == '99446'
 
 175     assert 'county' not in props
 
 178 @pytest.mark.parametrize('fmt', FORMATS)
 
 179 def test_format_reverse_with_address_none(fmt):
 
 180     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 182                                  napi.Point(1.0, 2.0),
 
 183                                  address_rows=napi.AddressLines())
 
 185     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 186                                   {'addressdetails': True})
 
 189         root = ET.fromstring(raw)
 
 190         assert root.find('addressparts') is None
 
 192         result = json.loads(raw)
 
 193         assert isinstance(result, dict)
 
 195         if fmt == 'geocodejson':
 
 196             props = result['features'][0]['properties']['geocoding']
 
 198             assert 'admin' in props
 
 201                 props = result['features'][0]['properties']
 
 204             assert 'address' in props
 
 207 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
 
 208 def test_format_reverse_with_extratags(fmt):
 
 209     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 211                                  napi.Point(1.0, 2.0),
 
 212                                  extratags={'one': 'A', 'two': 'B'})
 
 214     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 218         root = ET.fromstring(raw)
 
 219         assert root.find('extratags').find('tag').attrib['key'] == 'one'
 
 221         result = json.loads(raw)
 
 223             extra = result['features'][0]['properties']['extratags']
 
 225             extra = result['extratags']
 
 227         assert extra == {'one': 'A', 'two': 'B'}
 
 230 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
 
 231 def test_format_reverse_with_extratags_none(fmt):
 
 232     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 234                                  napi.Point(1.0, 2.0))
 
 236     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 240         root = ET.fromstring(raw)
 
 241         assert root.find('extratags') is not None
 
 243         result = json.loads(raw)
 
 245             extra = result['features'][0]['properties']['extratags']
 
 247             extra = result['extratags']
 
 252 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
 
 253 def test_format_reverse_with_namedetails_with_name(fmt):
 
 254     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 256                                  napi.Point(1.0, 2.0),
 
 257                                  names={'name': 'A', 'ref': '1'})
 
 259     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 260                                   {'namedetails': True})
 
 263         root = ET.fromstring(raw)
 
 264         assert root.find('namedetails').find('name').text == 'A'
 
 266         result = json.loads(raw)
 
 268             extra = result['features'][0]['properties']['namedetails']
 
 270             extra = result['namedetails']
 
 272         assert extra == {'name': 'A', 'ref': '1'}
 
 275 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
 
 276 def test_format_reverse_with_namedetails_without_name(fmt):
 
 277     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 279                                  napi.Point(1.0, 2.0))
 
 281     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 282                                   {'namedetails': True})
 
 285         root = ET.fromstring(raw)
 
 286         assert root.find('namedetails') is not None
 
 288         result = json.loads(raw)
 
 290             extra = result['features'][0]['properties']['namedetails']
 
 292             extra = result['namedetails']
 
 297 @pytest.mark.parametrize('fmt', ['json', 'jsonv2'])
 
 298 def test_search_details_with_icon_available(fmt):
 
 299     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 300                                  ('amenity', 'restaurant'),
 
 301                                  napi.Point(1.0, 2.0))
 
 303     result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 304                                      {'icon_base_url': 'foo'})
 
 306     js = json.loads(result)
 
 308     assert js['icon'] == 'foo/food_restaurant.p.20.png'
 
 311 @pytest.mark.parametrize('fmt', ['json', 'jsonv2'])
 
 312 def test_search_details_with_icon_not_available(fmt):
 
 313     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 315                                  napi.Point(1.0, 2.0))
 
 317     result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 318                                      {'icon_base_url': 'foo'})
 
 320     assert 'icon' not in json.loads(result)