]> git.openstreetmap.org Git - nominatim.git/blob - test/python/api/test_server_glue_v1.py
Stop creating the place_classtype tables from special phrases
[nominatim.git] / test / python / api / test_server_glue_v1.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2025 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Tests for the Python web frameworks adaptor, v1 API.
9 """
10 import json
11 import xml.etree.ElementTree as ET
12
13 import pytest
14
15 from fake_adaptor import FakeAdaptor, FakeError, FakeResponse
16
17 import nominatim_api.v1.server_glue as glue
18 import nominatim_api as napi
19 import nominatim_api.logging as loglib
20 from nominatim_api.config import Configuration
21
22
23 def debug_config(enabled):
24     """ Return a configuration where the HTML debug output is explicitly
25         enabled or disabled.
26     """
27     return Configuration(None,
28                          environ={'NOMINATIM_SERVE_DEBUG_OUTPUT':
29                                   'yes' if enabled else 'no'})
30
31
32 # ASGIAdaptor.get_int/bool()
33
34 @pytest.mark.parametrize('func', ['get_int', 'get_bool'])
35 def test_adaptor_get_int_missing_but_required(func):
36     with pytest.raises(FakeError, match='^400 -- .*missing'):
37         getattr(FakeAdaptor(), func)('something')
38
39
40 @pytest.mark.parametrize('func, val', [('get_int', 23), ('get_bool', True)])
41 def test_adaptor_get_int_missing_with_default(func, val):
42     assert getattr(FakeAdaptor(), func)('something', val) == val
43
44
45 @pytest.mark.parametrize('inp', ['0', '234', '-4566953498567934876'])
46 def test_adaptor_get_int_success(inp):
47     assert FakeAdaptor(params={'foo': inp}).get_int('foo') == int(inp)
48     assert FakeAdaptor(params={'foo': inp}).get_int('foo', 4) == int(inp)
49
50
51 @pytest.mark.parametrize('inp', ['rs', '4.5', '6f'])
52 def test_adaptor_get_int_bad_number(inp):
53     with pytest.raises(FakeError, match='^400 -- .*must be a number'):
54         FakeAdaptor(params={'foo': inp}).get_int('foo')
55
56
57 @pytest.mark.parametrize('inp', ['1', 'true', 'whatever', 'false'])
58 def test_adaptor_get_bool_trueish(inp):
59     assert FakeAdaptor(params={'foo': inp}).get_bool('foo')
60
61
62 def test_adaptor_get_bool_falsish():
63     assert not FakeAdaptor(params={'foo': '0'}).get_bool('foo')
64
65
66 # ASGIAdaptor.parse_format()
67
68 def test_adaptor_parse_format_use_default():
69     adaptor = FakeAdaptor()
70
71     assert glue.parse_format(adaptor, napi.StatusResult, 'text') == 'text'
72     assert adaptor.content_type == 'text/plain; charset=utf-8'
73
74
75 def test_adaptor_parse_format_use_configured():
76     adaptor = FakeAdaptor(params={'format': 'json'})
77
78     assert glue.parse_format(adaptor, napi.StatusResult, 'text') == 'json'
79     assert adaptor.content_type == 'application/json; charset=utf-8'
80
81
82 def test_adaptor_parse_format_invalid_value():
83     adaptor = FakeAdaptor(params={'format': '@!#'})
84
85     with pytest.raises(FakeError, match='^400 -- .*must be one of'):
86         glue.parse_format(adaptor, napi.StatusResult, 'text')
87
88
89 # ASGIAdaptor.get_accepted_languages()
90
91 def test_accepted_languages_from_param():
92     a = FakeAdaptor(params={'accept-language': 'de'})
93     assert glue.get_accepted_languages(a) == 'de'
94
95
96 def test_accepted_languages_from_header():
97     a = FakeAdaptor(headers={'accept-language': 'de'})
98     assert glue.get_accepted_languages(a) == 'de'
99
100
101 def test_accepted_languages_from_default(monkeypatch):
102     monkeypatch.setenv('NOMINATIM_DEFAULT_LANGUAGE', 'de')
103     a = FakeAdaptor()
104     assert glue.get_accepted_languages(a) == 'de'
105
106
107 def test_accepted_languages_param_over_header():
108     a = FakeAdaptor(params={'accept-language': 'de'},
109                     headers={'accept-language': 'en'})
110     assert glue.get_accepted_languages(a) == 'de'
111
112
113 def test_accepted_languages_header_over_default(monkeypatch):
114     monkeypatch.setenv('NOMINATIM_DEFAULT_LANGUAGE', 'en')
115     a = FakeAdaptor(headers={'accept-language': 'de'})
116     assert glue.get_accepted_languages(a) == 'de'
117
118
119 # NOMINATIM_SERVE_DEBUG_OUTPUT enables debug=1
120
121 @pytest.mark.parametrize('environ', [{}, {'NOMINATIM_SERVE_DEBUG_OUTPUT': 'no'}])
122 def test_setup_debugging_rejected_when_disabled(environ):
123     a = FakeAdaptor(params={'debug': '1'},
124                     config=Configuration(None, environ=environ))
125
126     with pytest.raises(FakeError, match='^400 -- .*not enabled'):
127         glue.setup_debugging(a)
128
129
130 def test_setup_debugging_enabled():
131     a = FakeAdaptor(params={'debug': '1'}, config=debug_config(True))
132
133     assert glue.setup_debugging(a)
134     assert a.content_type == 'text/html; charset=utf-8'
135
136
137 @pytest.mark.parametrize('params', [{}, {'debug': '0'}])
138 def test_setup_debugging_not_requested(params):
139     a = FakeAdaptor(params=params, config=debug_config(True))
140
141     assert not glue.setup_debugging(a)
142     assert a.content_type == 'text/plain; charset=utf-8'
143
144
145 @pytest.mark.parametrize('content_type', ['application/json; charset=utf-8',
146                                           'text/xml; charset=utf-8'])
147 def test_setup_debugging_rejection_keeps_output_format(content_type):
148     a = FakeAdaptor(params={'debug': '1'}, config=debug_config(False))
149     a.content_type = content_type
150
151     with pytest.raises(FakeError, match='(?s)^400 -- .*not enabled'):
152         glue.setup_debugging(a)
153
154
155 # ASGIAdaptor.raise_error()
156
157 class TestAdaptorRaiseError:
158
159     @pytest.fixture(autouse=True)
160     def init_adaptor(self):
161         self.adaptor = FakeAdaptor()
162         glue.setup_debugging(self.adaptor)
163
164     def run_raise_error(self, msg, status):
165         with pytest.raises(FakeError) as excinfo:
166             self.adaptor.raise_error(msg, status=status)
167
168         return excinfo.value
169
170     def test_without_content_set(self):
171         err = self.run_raise_error('TEST', 404)
172
173         assert self.adaptor.content_type == 'text/plain; charset=utf-8'
174         assert err.msg == 'ERROR 404: TEST'
175         assert err.status == 404
176
177     def test_json(self):
178         self.adaptor.content_type = 'application/json; charset=utf-8'
179
180         err = self.run_raise_error('TEST', 501)
181
182         content = json.loads(err.msg)['error']
183         assert content['code'] == 501
184         assert content['message'] == 'TEST'
185
186     def test_xml(self):
187         self.adaptor.content_type = 'text/xml; charset=utf-8'
188
189         err = self.run_raise_error('this!', 503)
190
191         content = ET.fromstring(err.msg)
192
193         assert content.tag == 'error'
194         assert content.find('code').text == '503'
195         assert content.find('message').text == 'this!'
196
197
198 def test_raise_error_during_debug():
199     a = FakeAdaptor(params={'debug': '1'}, config=debug_config(True))
200     glue.setup_debugging(a)
201     loglib.log().section('Ongoing')
202
203     with pytest.raises(FakeError) as excinfo:
204         a.raise_error('badstate')
205
206     content = ET.fromstring(excinfo.value.msg)
207
208     assert content.tag == 'html'
209
210     assert '>Ongoing<' in excinfo.value.msg
211     assert 'badstate' in excinfo.value.msg
212
213
214 # ASGIAdaptor.build_response
215
216 def test_build_response_without_content_type():
217     resp = glue.build_response(FakeAdaptor(), 'attention')
218
219     assert isinstance(resp, FakeResponse)
220     assert resp.status == 200
221     assert resp.output == 'attention'
222     assert resp.content_type == 'text/plain; charset=utf-8'
223
224
225 def test_build_response_with_status():
226     a = FakeAdaptor(params={'format': 'json'})
227     glue.parse_format(a, napi.StatusResult, 'text')
228
229     resp = glue.build_response(a, 'stuff\nmore stuff', status=404)
230
231     assert isinstance(resp, FakeResponse)
232     assert resp.status == 404
233     assert resp.output == 'stuff\nmore stuff'
234     assert resp.content_type == 'application/json; charset=utf-8'
235
236
237 def test_build_response_jsonp_with_json():
238     a = FakeAdaptor(params={'format': 'json', 'json_callback': 'test.func'})
239     glue.parse_format(a, napi.StatusResult, 'text')
240
241     resp = glue.build_response(a, '{}')
242
243     assert isinstance(resp, FakeResponse)
244     assert resp.status == 200
245     assert resp.output == 'test.func({})'
246     assert resp.content_type == 'application/javascript; charset=utf-8'
247
248
249 def test_build_response_jsonp_without_json():
250     a = FakeAdaptor(params={'format': 'text', 'json_callback': 'test.func'})
251     glue.parse_format(a, napi.StatusResult, 'text')
252
253     resp = glue.build_response(a, '{}')
254
255     assert isinstance(resp, FakeResponse)
256     assert resp.status == 200
257     assert resp.output == '{}'
258     assert resp.content_type == 'text/plain; charset=utf-8'
259
260
261 @pytest.mark.parametrize('param', ['alert(); func', '\\n', '', 'a b'])
262 def test_build_response_jsonp_bad_format(param):
263     a = FakeAdaptor(params={'format': 'json', 'json_callback': param})
264     glue.parse_format(a, napi.StatusResult, 'text')
265
266     with pytest.raises(FakeError, match='^400 -- .*Invalid'):
267         glue.build_response(a, '{}')
268
269
270 # get_layers()
271
272 def test_get_layers_no_param():
273     assert glue.get_layers(FakeAdaptor()) is None
274
275
276 @pytest.mark.parametrize('param,expected', [
277     ('address', napi.DataLayer.ADDRESS),
278     ('POI', napi.DataLayer.POI),
279     ('address,poi', napi.DataLayer.ADDRESS | napi.DataLayer.POI),
280     ('  address , poi ', napi.DataLayer.ADDRESS | napi.DataLayer.POI),
281     ('address,address', napi.DataLayer.ADDRESS),
282     ('address,', napi.DataLayer.ADDRESS)])
283 def test_get_layers_success(param, expected):
284     assert glue.get_layers(FakeAdaptor(params={'layer': param})) == expected
285
286
287 @pytest.mark.parametrize('param', ['', ' ', ',', ' , '])
288 def test_get_layers_empty_disables_filter(param):
289     assert glue.get_layers(FakeAdaptor(params={'layer': param})) is None
290
291
292 @pytest.mark.parametrize('param', ['bogus', 'address,bogus', 'name', '_name_',
293                                    'address poi', '<script>'])
294 def test_get_layers_invalid_value(param):
295     with pytest.raises(FakeError, match='^400 -- .*must be a comma-separated list'):
296         glue.get_layers(FakeAdaptor(params={'layer': param}))
297
298
299 # status_endpoint()
300
301 class TestStatusEndpoint:
302
303     @pytest.fixture(autouse=True)
304     def patch_status_func(self, monkeypatch):
305         async def _status(*args, **kwargs):
306             return self.status
307
308         monkeypatch.setattr(napi.NominatimAPIAsync, 'status', _status)
309
310     @pytest.mark.asyncio
311     async def test_status_without_params(self):
312         a = FakeAdaptor()
313         self.status = napi.StatusResult(0, 'foo')
314
315         resp = await glue.status_endpoint(napi.NominatimAPIAsync(), a)
316
317         assert isinstance(resp, FakeResponse)
318         assert resp.status == 200
319         assert resp.content_type == 'text/plain; charset=utf-8'
320
321     @pytest.mark.asyncio
322     async def test_status_with_error(self):
323         a = FakeAdaptor()
324         self.status = napi.StatusResult(405, 'foo')
325
326         resp = await glue.status_endpoint(napi.NominatimAPIAsync(), a)
327
328         assert isinstance(resp, FakeResponse)
329         assert resp.status == 500
330         assert resp.content_type == 'text/plain; charset=utf-8'
331
332     @pytest.mark.asyncio
333     async def test_status_json_with_error(self):
334         a = FakeAdaptor(params={'format': 'json'})
335         self.status = napi.StatusResult(405, 'foo')
336
337         resp = await glue.status_endpoint(napi.NominatimAPIAsync(), a)
338
339         assert isinstance(resp, FakeResponse)
340         assert resp.status == 200
341         assert resp.content_type == 'application/json; charset=utf-8'
342
343     @pytest.mark.asyncio
344     async def test_status_bad_format(self):
345         a = FakeAdaptor(params={'format': 'foo'})
346         self.status = napi.StatusResult(0, 'foo')
347
348         with pytest.raises(FakeError):
349             await glue.status_endpoint(napi.NominatimAPIAsync(), a)
350
351
352 # details_endpoint()
353
354 class TestDetailsEndpoint:
355
356     @pytest.fixture(autouse=True)
357     def patch_lookup_func(self, monkeypatch):
358         self.result = napi.DetailedResult(napi.SourceTable.PLACEX,
359                                           ('place', 'thing'),
360                                           napi.Point(1.0, 2.0))
361         self.lookup_args = []
362
363         async def _lookup(*args, **kwargs):
364             self.lookup_args.extend(args[1:])
365             return self.result
366
367         monkeypatch.setattr(napi.NominatimAPIAsync, 'details', _lookup)
368
369     @pytest.mark.asyncio
370     async def test_details_no_params(self):
371         a = FakeAdaptor()
372
373         with pytest.raises(FakeError, match='^400 -- .*Missing'):
374             await glue.details_endpoint(napi.NominatimAPIAsync(), a)
375
376     @pytest.mark.asyncio
377     async def test_details_by_place_id(self):
378         a = FakeAdaptor(params={'place_id': '4573'})
379
380         await glue.details_endpoint(napi.NominatimAPIAsync(), a)
381
382         assert self.lookup_args[0].place_id == 4573
383
384     @pytest.mark.asyncio
385     async def test_details_by_osm_id(self):
386         a = FakeAdaptor(params={'osmtype': 'N', 'osmid': '45'})
387
388         await glue.details_endpoint(napi.NominatimAPIAsync(), a)
389
390         assert self.lookup_args[0].osm_type == 'N'
391         assert self.lookup_args[0].osm_id == 45
392         assert self.lookup_args[0].osm_class is None
393
394     @pytest.mark.asyncio
395     async def test_details_by_postcode(self):
396         a = FakeAdaptor(params={'postcode': 'us:94110'})
397
398         await glue.details_endpoint(napi.NominatimAPIAsync(), a)
399
400         assert self.lookup_args[0].country_code == 'us'
401         assert self.lookup_args[0].postcode == '94110'
402
403     @pytest.mark.asyncio
404     async def test_details_by_postcode_id(self):
405         a = FakeAdaptor(params={'postcode': 'Pus:94110'})
406
407         await glue.details_endpoint(napi.NominatimAPIAsync(), a)
408
409         assert self.lookup_args[0].country_code == 'us'
410         assert self.lookup_args[0].postcode == '94110'
411
412     @pytest.mark.asyncio
413     async def test_details_with_debugging(self):
414         a = FakeAdaptor(params={'osmtype': 'N', 'osmid': '45', 'debug': '1'},
415                         config=debug_config(True))
416
417         resp = await glue.details_endpoint(napi.NominatimAPIAsync(), a)
418         content = ET.fromstring(resp.output)
419
420         assert resp.content_type == 'text/html; charset=utf-8'
421         assert content.tag == 'html'
422
423     @pytest.mark.asyncio
424     async def test_details_no_result(self):
425         a = FakeAdaptor(params={'place_id': '4573'})
426         self.result = None
427
428         with pytest.raises(FakeError, match='^404 -- .*found'):
429             await glue.details_endpoint(napi.NominatimAPIAsync(), a)
430
431
432 # reverse_endpoint()
433 class TestReverseEndPoint:
434
435     @pytest.fixture(autouse=True)
436     def patch_reverse_func(self, monkeypatch):
437         self.result = napi.ReverseResult(napi.SourceTable.PLACEX,
438                                          ('place', 'thing'),
439                                          napi.Point(1.0, 2.0))
440
441         async def _reverse(*args, **kwargs):
442             return self.result
443
444         monkeypatch.setattr(napi.NominatimAPIAsync, 'reverse', _reverse)
445
446     @pytest.mark.asyncio
447     @pytest.mark.parametrize('params', [{}, {'lat': '3.4'}, {'lon': '6.7'}])
448     async def test_reverse_no_params(self, params):
449         a = FakeAdaptor()
450         a.params = params
451         a.params['format'] = 'xml'
452
453         with pytest.raises(FakeError, match='^400 -- (?s:.*)missing'):
454             await glue.reverse_endpoint(napi.NominatimAPIAsync(), a)
455
456     @pytest.mark.asyncio
457     async def test_reverse_success(self):
458         a = FakeAdaptor()
459         a.params['lat'] = '56.3'
460         a.params['lon'] = '6.8'
461
462         assert await glue.reverse_endpoint(napi.NominatimAPIAsync(), a)
463
464     @pytest.mark.asyncio
465     async def test_reverse_from_search(self):
466         a = FakeAdaptor()
467         a.params['q'] = '34.6 2.56'
468         a.params['format'] = 'json'
469
470         res = await glue.search_endpoint(napi.NominatimAPIAsync(), a)
471
472         assert len(json.loads(res.output)) == 1
473
474
475 # lookup_endpoint()
476
477 class TestLookupEndpoint:
478
479     @pytest.fixture(autouse=True)
480     def patch_lookup_func(self, monkeypatch):
481         self.results = [napi.SearchResult(napi.SourceTable.PLACEX,
482                                           ('place', 'thing'),
483                                           napi.Point(1.0, 2.0))]
484
485         async def _lookup(*args, **kwargs):
486             return napi.SearchResults(self.results)
487
488         monkeypatch.setattr(napi.NominatimAPIAsync, 'lookup', _lookup)
489
490     @pytest.mark.asyncio
491     async def test_lookup_no_params(self):
492         a = FakeAdaptor()
493         a.params['format'] = 'json'
494
495         res = await glue.lookup_endpoint(napi.NominatimAPIAsync(), a)
496
497         assert res.output == '[]'
498
499     @pytest.mark.asyncio
500     @pytest.mark.parametrize('param', ['w', 'bad', ''])
501     async def test_lookup_bad_params(self, param):
502         a = FakeAdaptor()
503         a.params['format'] = 'json'
504         a.params['osm_ids'] = f'W34,{param},N33333'
505
506         res = await glue.lookup_endpoint(napi.NominatimAPIAsync(), a)
507
508         assert len(json.loads(res.output)) == 1
509
510     @pytest.mark.asyncio
511     @pytest.mark.parametrize('param', ['p234234', '4563'])
512     async def test_lookup_bad_osm_type(self, param):
513         a = FakeAdaptor()
514         a.params['format'] = 'json'
515         a.params['osm_ids'] = f'W34,{param},N33333'
516
517         res = await glue.lookup_endpoint(napi.NominatimAPIAsync(), a)
518
519         assert len(json.loads(res.output)) == 1
520
521     @pytest.mark.asyncio
522     async def test_lookup_working(self):
523         a = FakeAdaptor()
524         a.params['format'] = 'json'
525         a.params['osm_ids'] = 'N23,W34'
526
527         res = await glue.lookup_endpoint(napi.NominatimAPIAsync(), a)
528
529         assert len(json.loads(res.output)) == 1
530
531
532 # search_endpoint()
533
534 class TestSearchEndPointSearch:
535
536     @pytest.fixture(autouse=True)
537     def patch_lookup_func(self, monkeypatch):
538         self.results = [napi.SearchResult(napi.SourceTable.PLACEX,
539                                           ('place', 'thing'),
540                                           napi.Point(1.0, 2.0))]
541
542         async def _search(*args, **kwargs):
543             return napi.SearchResults(self.results)
544
545         monkeypatch.setattr(napi.NominatimAPIAsync, 'search', _search)
546
547     @pytest.mark.asyncio
548     async def test_search_free_text(self):
549         a = FakeAdaptor()
550         a.params['q'] = 'something'
551
552         res = await glue.search_endpoint(napi.NominatimAPIAsync(), a)
553
554         assert len(json.loads(res.output)) == 1
555
556     @pytest.mark.asyncio
557     async def test_search_free_text_xml(self):
558         a = FakeAdaptor()
559         a.params['q'] = 'something'
560         a.params['format'] = 'xml'
561
562         res = await glue.search_endpoint(napi.NominatimAPIAsync(), a)
563
564         assert res.status == 200
565         assert res.output.index('something') > 0
566
567     @pytest.mark.asyncio
568     async def test_search_free_text_xml_uses_stable_postcode_exclude_ids(self):
569         self.results = [napi.SearchResult(napi.SourceTable.POSTCODE,
570                                           ('place', 'postcode'),
571                                           napi.Point(1.0, 2.0),
572                                           place_id=123,
573                                           names={'ref': 'EH4 7EA'},
574                                           country_code='gb')]
575         a = FakeAdaptor(params={'q': 'something', 'format': 'xml'})
576
577         res = await glue.search_endpoint(napi.NominatimAPIAsync(), a)
578
579         assert 'exclude_place_ids="Pgb:EH4_7EA"' in res.output
580
581     @pytest.mark.asyncio
582     async def test_search_free_text_jsonv2_emits_postcode_id(self):
583         self.results = [napi.SearchResult(napi.SourceTable.POSTCODE,
584                                           ('place', 'postcode'),
585                                           napi.Point(1.0, 2.0),
586                                           place_id=123,
587                                           names={'ref': 'EH4 7EA'},
588                                           country_code='gb')]
589         a = FakeAdaptor(params={'q': 'something'})
590
591         res = await glue.search_endpoint(napi.NominatimAPIAsync(), a)
592
593         assert '"postcode_id":"Pgb:EH4_7EA"' in res.output
594
595     @pytest.mark.asyncio
596     async def test_search_free_and_structured(self):
597         a = FakeAdaptor()
598         a.params['q'] = 'something'
599         a.params['city'] = 'ignored'
600
601         with pytest.raises(FakeError, match='^400 -- .*cannot be used together'):
602             await glue.search_endpoint(napi.NominatimAPIAsync(), a)
603
604     @pytest.mark.asyncio
605     @pytest.mark.parametrize('dedupe,numres', [(True, 1), (False, 2)])
606     async def test_search_dedupe(self, dedupe, numres):
607         self.results = self.results * 2
608         a = FakeAdaptor()
609         a.params['q'] = 'something'
610         if not dedupe:
611             a.params['dedupe'] = '0'
612
613         res = await glue.search_endpoint(napi.NominatimAPIAsync(), a)
614
615         assert len(json.loads(res.output)) == numres
616
617
618 class TestSearchEndPointSearchAddress:
619
620     @pytest.fixture(autouse=True)
621     def patch_lookup_func(self, monkeypatch):
622         self.results = [napi.SearchResult(napi.SourceTable.PLACEX,
623                                           ('place', 'thing'),
624                                           napi.Point(1.0, 2.0))]
625
626         async def _search(*args, **kwargs):
627             return napi.SearchResults(self.results)
628
629         monkeypatch.setattr(napi.NominatimAPIAsync, 'search_address', _search)
630
631     @pytest.mark.asyncio
632     async def test_search_structured(self):
633         a = FakeAdaptor()
634         a.params['street'] = 'something'
635
636         res = await glue.search_endpoint(napi.NominatimAPIAsync(), a)
637
638         assert len(json.loads(res.output)) == 1
639
640
641 class TestSearchEndPointSearchCategory:
642
643     @pytest.fixture(autouse=True)
644     def patch_lookup_func(self, monkeypatch):
645         self.results = [napi.SearchResult(napi.SourceTable.PLACEX,
646                                           ('place', 'thing'),
647                                           napi.Point(1.0, 2.0))]
648
649         async def _search(*args, **kwargs):
650             return napi.SearchResults(self.results)
651
652         monkeypatch.setattr(napi.NominatimAPIAsync, 'search_category', _search)
653
654     @pytest.mark.asyncio
655     async def test_search_category(self):
656         a = FakeAdaptor()
657         a.params['q'] = '[shop=fog]'
658
659         res = await glue.search_endpoint(napi.NominatimAPIAsync(), a)
660
661         assert len(json.loads(res.output)) == 1