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 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']
 
  23 @pytest.mark.parametrize('fmt', FORMATS)
 
  24 def test_format_reverse_minimal(fmt):
 
  25     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
  26                                  ('amenity', 'post_box'),
 
  27                                  napi.Point(0.3, -8.9))
 
  29     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, {})
 
  32         root = ET.fromstring(raw)
 
  33         assert root.tag == 'reversegeocode'
 
  35         result = json.loads(raw)
 
  36         assert isinstance(result, dict)
 
  39 @pytest.mark.parametrize('fmt', FORMATS)
 
  40 def test_format_reverse_no_result(fmt):
 
  41     raw = v1_format.format_result(napi.ReverseResults(), fmt, {})
 
  44         root = ET.fromstring(raw)
 
  45         assert root.find('error').text == 'Unable to geocode'
 
  47         assert json.loads(raw) == {'error': 'Unable to geocode'}
 
  50 @pytest.mark.parametrize('fmt', FORMATS)
 
  51 def test_format_reverse_with_osm_id(fmt):
 
  52     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
  53                                  ('amenity', 'post_box'),
 
  54                                  napi.Point(0.3, -8.9),
 
  58     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, {})
 
  61         root = ET.fromstring(raw).find('result')
 
  62         assert root.attrib['osm_type'] == 'node'
 
  63         assert root.attrib['osm_id'] == '23'
 
  65         result = json.loads(raw)
 
  66         if fmt == 'geocodejson':
 
  67             props = result['features'][0]['properties']['geocoding']
 
  68         elif fmt == 'geojson':
 
  69             props = result['features'][0]['properties']
 
  72         assert props['osm_type'] == 'node'
 
  73         assert props['osm_id'] == 23
 
  76 @pytest.mark.parametrize('fmt', FORMATS)
 
  77 def test_format_reverse_with_address(fmt):
 
  78     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
  82                                  address_rows=napi.AddressLines([
 
  83                                    napi.AddressLine(place_id=None,
 
  85                                                     category=('place', 'county'),
 
  86                                                     names={'name': 'Hello'},
 
  93                                    napi.AddressLine(place_id=None,
 
  95                                                     category=('place', 'county'),
 
  96                                                     names={'name': 'ByeBye'},
 
 104     reverse.localize(napi.Locales())
 
 106     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 107                                  {'addressdetails': True})
 
 111         root = ET.fromstring(raw)
 
 112         assert root.find('addressparts').find('county').text == 'Hello'
 
 114         result = json.loads(raw)
 
 115         assert isinstance(result, dict)
 
 117         if fmt == 'geocodejson':
 
 118             props = result['features'][0]['properties']['geocoding']
 
 119             assert 'admin' in props
 
 120             assert props['county'] == 'Hello'
 
 123                 props = result['features'][0]['properties']
 
 126             assert 'address' in props
 
 129 def test_format_reverse_geocodejson_special_parts():
 
 130     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 132                                  napi.Point(1.0, 2.0),
 
 135                                  address_rows=napi.AddressLines([
 
 136                                    napi.AddressLine(place_id=None,
 
 138                                                     category=('place', 'house_number'),
 
 146                                    napi.AddressLine(place_id=None,
 
 148                                                     category=('place', 'postcode'),
 
 149                                                     names={'ref': '99446'},
 
 156                                    napi.AddressLine(place_id=33,
 
 158                                                     category=('place', 'county'),
 
 159                                                     names={'name': 'Hello'},
 
 168     reverse.localize(napi.Locales())
 
 170     raw = v1_format.format_result(napi.ReverseResults([reverse]), 'geocodejson',
 
 171                                  {'addressdetails': True})
 
 173     props = json.loads(raw)['features'][0]['properties']['geocoding']
 
 174     assert props['housenumber'] == '1'
 
 175     assert props['postcode'] == '99446'
 
 176     assert 'county' not in props
 
 179 @pytest.mark.parametrize('fmt', FORMATS)
 
 180 def test_format_reverse_with_address_none(fmt):
 
 181     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 183                                  napi.Point(1.0, 2.0),
 
 184                                  address_rows=napi.AddressLines())
 
 186     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 187                                  {'addressdetails': True})
 
 191         root = ET.fromstring(raw)
 
 192         assert root.find('addressparts') is None
 
 194         result = json.loads(raw)
 
 195         assert isinstance(result, dict)
 
 197         if fmt == 'geocodejson':
 
 198             props = result['features'][0]['properties']['geocoding']
 
 200             assert 'admin' in props
 
 203                 props = result['features'][0]['properties']
 
 206             assert 'address' in props
 
 209 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
 
 210 def test_format_reverse_with_extratags(fmt):
 
 211     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 213                                  napi.Point(1.0, 2.0),
 
 214                                  extratags={'one': 'A', 'two':'B'})
 
 216     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 220         root = ET.fromstring(raw)
 
 221         assert root.find('extratags').find('tag').attrib['key'] == 'one'
 
 223         result = json.loads(raw)
 
 225             extra = result['features'][0]['properties']['extratags']
 
 227             extra = result['extratags']
 
 229         assert extra == {'one': 'A', 'two':'B'}
 
 232 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
 
 233 def test_format_reverse_with_extratags_none(fmt):
 
 234     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 236                                  napi.Point(1.0, 2.0))
 
 238     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 242         root = ET.fromstring(raw)
 
 243         assert root.find('extratags') is not None
 
 245         result = json.loads(raw)
 
 247             extra = result['features'][0]['properties']['extratags']
 
 249             extra = result['extratags']
 
 254 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
 
 255 def test_format_reverse_with_namedetails_with_name(fmt):
 
 256     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 258                                  napi.Point(1.0, 2.0),
 
 259                                  names={'name': 'A', 'ref':'1'})
 
 261     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 262                                  {'namedetails': True})
 
 265         root = ET.fromstring(raw)
 
 266         assert root.find('namedetails').find('name').text == 'A'
 
 268         result = json.loads(raw)
 
 270             extra = result['features'][0]['properties']['namedetails']
 
 272             extra = result['namedetails']
 
 274         assert extra == {'name': 'A', 'ref':'1'}
 
 277 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
 
 278 def test_format_reverse_with_namedetails_without_name(fmt):
 
 279     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 281                                  napi.Point(1.0, 2.0))
 
 283     raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 284                                  {'namedetails': True})
 
 287         root = ET.fromstring(raw)
 
 288         assert root.find('namedetails') is not None
 
 290         result = json.loads(raw)
 
 292             extra = result['features'][0]['properties']['namedetails']
 
 294             extra = result['namedetails']
 
 299 @pytest.mark.parametrize('fmt', ['json', 'jsonv2'])
 
 300 def test_search_details_with_icon_available(fmt):
 
 301     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 302                                  ('amenity', 'restaurant'),
 
 303                                  napi.Point(1.0, 2.0))
 
 305     result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 306                                     {'icon_base_url': 'foo'})
 
 308     js = json.loads(result)
 
 310     assert js['icon'] == 'foo/food_restaurant.p.20.png'
 
 313 @pytest.mark.parametrize('fmt', ['json', 'jsonv2'])
 
 314 def test_search_details_with_icon_not_available(fmt):
 
 315     reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
 
 317                                  napi.Point(1.0, 2.0))
 
 319     result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
 
 320                                     {'icon_base_url': 'foo'})
 
 322     assert 'icon' not in json.loads(result)