From: Sarah Hoffmann Date: Sat, 17 Dec 2016 16:33:44 +0000 (+0100) Subject: add simple direct API search tests X-Git-Tag: v3.0.0~85^2~10 X-Git-Url: https://git.openstreetmap.org./nominatim.git/commitdiff_plain/b9a58b8f24e914e976cfe6ed84144fcce5eb8762 add simple direct API search tests API tests now no longer require a running Apache installation, instead the website php scripts are called directly using the appropriate enivronment. --- diff --git a/test/bdd/environment.py b/test/bdd/environment.py index 3af3fb58..69d93bd8 100644 --- a/test/bdd/environment.py +++ b/test/bdd/environment.py @@ -16,6 +16,7 @@ userconfig = { 'KEEP_TEST_DB' : False, 'TEMPLATE_DB' : 'test_template_nominatim', 'TEST_DB' : 'test_nominatim', + 'API_TEST_DB' : 'test_api_nominatim', 'TEST_SETTINGS_FILE' : '/tmp/nominatim_settings.php' } @@ -29,6 +30,7 @@ class NominatimEnvironment(object): self.build_dir = os.path.abspath(config['BUILDDIR']) self.template_db = config['TEMPLATE_DB'] self.test_db = config['TEST_DB'] + self.api_test_db = config['API_TEST_DB'] self.local_settings_file = config['TEST_SETTINGS_FILE'] self.reuse_template = not config['REMOVE_TEMPLATE'] self.keep_scenario_db = config['KEEP_TEST_DB'] @@ -98,7 +100,8 @@ class NominatimEnvironment(object): 'create-partition-tables', 'create-partition-functions', 'load-data', 'create-search-indices') - + def setup_api_db(self, context): + self.write_nominatim_config(self.api_test_db) def setup_db(self, context): self.setup_template_db() @@ -213,6 +216,8 @@ def after_all(context): def before_scenario(context, scenario): if 'DB' in context.tags: context.nominatim.setup_db(context) + elif 'APIDB' in context.tags: + context.nominatim.setup_api_db(context) context.scene = None def after_scenario(context, scenario): diff --git a/test/bdd/osm2pgsql/import/tags.feature b/test/bdd/osm2pgsql/import/tags.feature index 0923e47d..d81b6c72 100644 --- a/test/bdd/osm2pgsql/import/tags.feature +++ b/test/bdd/osm2pgsql/import/tags.feature @@ -87,14 +87,14 @@ Feature: Tag evaluation n1 Thighway=yes,name:%20%de=Foo,name=real1 n2 Thighway=yes,name:%a%de=Foo,name=real2 n3 Thighway=yes,name:%9%de=Foo,name:\\=real3 - n4 Thighway=yes,name:%9%de=Foo,name:\=real3 + n4 Thighway=yes,name:%9%de=Foo,name=rea\l3 """ Then place contains | object | name | | N1 | 'name: de' : 'Foo', 'name' : 'real1' | | N2 | 'name: de' : 'Foo', 'name' : 'real2' | | N3 | 'name: de' : 'Foo', 'name:\\\\' : 'real3' | - | N4 | 'name: de' : 'Foo', 'name:\\' : 'real3' | + | N4 | 'name: de' : 'Foo', 'name' : 'rea\\l3' | Scenario Outline: Included places When loading osm data diff --git a/test/bdd/steps/queries.py b/test/bdd/steps/queries.py index f37f7e7b..c62b8a57 100644 --- a/test/bdd/steps/queries.py +++ b/test/bdd/steps/queries.py @@ -6,20 +6,98 @@ import json import os +import io +import re +from tidylib import tidy_document +import xml.etree.ElementTree as ET import subprocess +from urllib.parse import urlencode from collections import OrderedDict from nose.tools import * # for assert functions +BASE_SERVER_ENV = { + 'HTTP_HOST' : 'localhost', + 'HTTP_USER_AGENT' : 'Mozilla/5.0 (X11; Linux x86_64; rv:51.0) Gecko/20100101 Firefox/51.0', + 'HTTP_ACCEPT' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'HTTP_ACCEPT_LANGUAGE' : 'en,de;q=0.5', + 'HTTP_ACCEPT_ENCODING' : 'gzip, deflate', + 'HTTP_CONNECTION' : 'keep-alive', + 'SERVER_SIGNATURE' : '
Nominatim BDD Tests
', + 'SERVER_SOFTWARE' : 'Nominatim test', + 'SERVER_NAME' : 'localhost', + 'SERVER_ADDR' : '127.0.1.1', + 'SERVER_PORT' : '80', + 'REMOTE_ADDR' : '127.0.0.1', + 'DOCUMENT_ROOT' : '/var/www', + 'REQUEST_SCHEME' : 'http', + 'CONTEXT_PREFIX' : '/', + 'SERVER_ADMIN' : 'webmaster@localhost', + 'REMOTE_PORT' : '49319', + 'GATEWAY_INTERFACE' : 'CGI/1.1', + 'SERVER_PROTOCOL' : 'HTTP/1.1', + 'REQUEST_METHOD' : 'GET', + 'REDIRECT_STATUS' : 'CGI' +} + + +def compare(operator, op1, op2): + if operator == 'less than': + return op1 < op2 + elif operator == 'more than': + return op1 > op2 + elif operator == 'exactly': + return op1 == op2 + elif operator == 'at least': + return op1 >= op2 + elif operator == 'at most': + return op1 <= op2 + else: + raise Exception("unknown operator '%s'" % operator) + + class SearchResponse(object): def __init__(self, page, fmt='json', errorcode=200): self.page = page self.format = fmt self.errorcode = errorcode - getattr(self, 'parse_' + fmt)() + self.result = [] + self.header = dict() + + if errorcode == 200: + getattr(self, 'parse_' + fmt)() def parse_json(self): - self.result = json.JSONDecoder(object_pairs_hook=OrderedDict).decode(self.page) + m = re.fullmatch(r'([\w$][^(]*)\((.*)\)', self.page) + if m is None: + code = self.page + else: + code = m.group(2) + self.header['json_func'] = m.group(1) + self.result = json.JSONDecoder(object_pairs_hook=OrderedDict).decode(code) + + def parse_html(self): + content, errors = tidy_document(self.page, + options={'char-encoding' : 'utf8'}) + #eq_(len(errors), 0 , "Errors found in HTML document:\n%s" % errors) + + b = content.find('nominatim_results =') + e = content.find('') + content = content[b:e] + b = content.find('[') + e = content.rfind(']') + + self.result = json.JSONDecoder(object_pairs_hook=OrderedDict).decode(content[b:e+1]) + + def parse_xml(self): + et = ET.fromstring(self.page) + + self.header = dict(et.attrib) + + + for child in et: + assert_equal(child.tag, "place") + self.result.append(dict(child.attrib)) def match_row(self, row): if 'ID' in row.headings: @@ -64,6 +142,112 @@ def query_cmd(context, query, dups): stdout=subprocess.PIPE, stderr=subprocess.PIPE) (outp, err) = proc.communicate() - assert_equals (0, proc.returncode, "query.php failed with message: %s" % err) + assert_equals (0, proc.returncode, "query.php failed with message: %s\noutput: %s" % (err, outp)) context.response = SearchResponse(outp.decode('utf-8'), 'json') + + +@when(u'sending (?P\S+ )?search query "(?P.*)"') +def website_search_request(context, fmt, query): + env = BASE_SERVER_ENV + + params = { 'q' : query } + if fmt is not None: + params['format'] = fmt.strip() + if context.table: + if context.table.headings[0] == 'param': + for line in context.table: + params[line['param']] = line['value'] + else: + for h in context.table.headings: + params[h] = context.table[0][h] + env['QUERY_STRING'] = urlencode(params) + + env['REQUEST_URI'] = '/search.php?' + env['QUERY_STRING'] + env['SCRIPT_NAME'] = '/search.php' + env['CONTEXT_DOCUMENT_ROOT'] = os.path.join(context.nominatim.build_dir, 'website') + env['SCRIPT_FILENAME'] = os.path.join(context.nominatim.build_dir, 'website', 'search.php') + env['NOMINATIM_SETTINGS'] = context.nominatim.local_settings_file + + cmd = [ '/usr/bin/php-cgi', env['SCRIPT_FILENAME']] + for k,v in params.items(): + cmd.append("%s=%s" % (k, v)) + + proc = subprocess.Popen(cmd, cwd=context.nominatim.build_dir, env=env, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + (outp, err) = proc.communicate() + + assert_equals(0, proc.returncode, + "query.php failed with message: %s\noutput: %s" % (err, outp)) + + assert_equals(0, len(err), "Unexpected PHP error: %s" % (err)) + + outp = outp.decode('utf-8') + + if outp.startswith('Status: '): + status = int(outp[8:11]) + else: + status = 200 + + content_start = outp.find('\r\n\r\n') + assert_less(11, content_start) + + if fmt is None: + outfmt = 'html' + elif fmt == 'jsonv2 ': + outfmt = 'json' + else: + outfmt = fmt.strip() + + context.response = SearchResponse(outp[content_start + 4:], outfmt, status) + + +@step(u'(?Pless than|more than|exactly|at least|at most) (?P\d+) results? (?:is|are) returned') +def validate_result_number(context, operator, number): + eq_(context.response.errorcode, 200) + numres = len(context.response.result) + ok_(compare(operator, numres, int(number)), + "Bad number of results: expected %s %s, got %d." % (operator, number, numres)) + +@then(u'a HTTP (?P\d+) is returned') +def check_http_return_status(context, status): + eq_(context.response.errorcode, int(status)) + +@then(u'the result is valid (?P\w+)') +def step_impl(context, fmt): + eq_(context.response.format, fmt) + +@then(u'result header contains') +def check_header_attr(context): + for line in context.table: + assert_is_not_none(re.fullmatch(line['value'], context.response.header[line['attr']]), + "attribute '%s': expected: '%s', got '%s'" + % (line['attr'], line['value'], + context.response.header[line['attr']])) + +@then(u'result header has (?Pnot )?attributes (?P.*)') +def check_header_no_attr(context, neg, attrs): + for attr in attrs.split(','): + if neg: + assert_not_in(attr, context.response.header) + else: + assert_in(attr, context.response.header) + +@then(u'results contain') +def step_impl(context): + context.execute_steps("then at least 1 result is returned") + + for line in context.table: + context.response.match_row(line) + +@then(u'result (?P\d+) has (?Pnot )?attributes (?P.*)') +def validate_attributes(context, lid, neg, attrs): + context.execute_steps("then at least %s result is returned" % lid) + + for attr in attrs.split(','): + if neg: + assert_not_in(attr, context.response.result[int(lid)]) + else: + assert_in(attr, context.response.result[int(lid)]) + diff --git a/test/bdd/steps/results.py b/test/bdd/steps/results.py deleted file mode 100644 index 87fefd4a..00000000 --- a/test/bdd/steps/results.py +++ /dev/null @@ -1,33 +0,0 @@ -""" Steps that check results. -""" - -from nose.tools import * # for assert functions - -def compare(operator, op1, op2): - if operator == 'less than': - return op1 < op2 - elif operator == 'more than': - return op1 > op2 - elif operator == 'exactly': - return op1 == op2 - elif operator == 'at least': - return op1 >= op2 - elif operator == 'at most': - return op1 <= op2 - else: - raise Exception("unknown operator '%s'" % operator) - -@step(u'(?Pless than|more than|exactly|at least|at most) (?P\d+) results? (?:is|are) returned') -def validate_result_number(context, operator, number): - numres = len(context.response.result) - ok_(compare(operator, numres, int(number)), - "Bad number of results: expected %s %s, got %d." % (operator, number, numres)) - - -@then(u'results contain') -def step_impl(context): - context.execute_steps("then at least 1 result is returned") - - for line in context.table: - context.response.match_row(line) - diff --git a/test/bdd/website/search/simple.feature b/test/bdd/website/search/simple.feature new file mode 100644 index 00000000..4d77eac4 --- /dev/null +++ b/test/bdd/website/search/simple.feature @@ -0,0 +1,221 @@ +@APIDB +Feature: Simple Tests + Simple tests for internal server errors and response format. + + Scenario Outline: Testing different parameters + When sending search query "Hamburg" + | param | value | + | | | + Then at least 1 result is returned + When sending html search query "Hamburg" + | param | value | + | | | + Then at least 1 result is returned + When sending xml search query "Hamburg" + | param | value | + | | | + Then at least 1 result is returned + When sending json search query "Hamburg" + | param | value | + | | | + Then at least 1 result is returned + When sending jsonv2 search query "Hamburg" + | param | value | + | | | + Then at least 1 result is returned + + Examples: + | parameter | value | + | addressdetails | 1 | + | addressdetails | 0 | + | polygon | 1 | + | polygon | 0 | + | polygon_text | 1 | + | polygon_text | 0 | + | polygon_kml | 1 | + | polygon_kml | 0 | + | polygon_geojson | 1 | + | polygon_geojson | 0 | + | polygon_svg | 1 | + | polygon_svg | 0 | + | accept-language | de,en | + | countrycodes | de | + | bounded | 1 | + | bounded | 0 | + | exclude_place_ids| 385252,1234515 | + | limit | 1000 | + | dedupe | 1 | + | dedupe | 0 | + | extratags | 1 | + | extratags | 0 | + | namedetails | 1 | + | namedetails | 0 | + + Scenario: Search with invalid output format + When sending search query "Berlin" + | format | + | fd$# | + Then a HTTP 400 is returned + + Scenario Outline: Simple Searches + When sending search query "" + Then the result is valid html + When sending html search query "" + Then the result is valid html + When sending xml search query "" + Then the result is valid xml + When sending json search query "" + Then the result is valid json + When sending jsonv2 search query "" + Then the result is valid json + + Examples: + | query | + | New York, New York | + | France | + | 12, Main Street, Houston | + | München | + | 東京都 | + | hotels in nantes | + | xywxkrf | + | gh; foo() | + | %#$@*&l;der#$! | + | 234 | + | 47.4,8.3 | + + Scenario: Empty XML search + When sending xml search query "xnznxvcx" + Then result header contains + | attr | value | + | querystring | xnznxvcx | + | polygon | false | + | more_url | .*format=xml.*q=xnznxvcx.* | + + Scenario: Empty XML search with special XML characters + When sending xml search query "xfdghn&zxn"xvbyxcssdex" + Then result header contains + | attr | value | + | querystring | xfdghn&zxn"xvbyxcssdex | + | polygon | false | + | more_url | .*format=xml.*q=xfdghn%26zxn%22xvbyx%3Cvxx%3Ecssdex.* | + + Scenario: Empty XML search with viewbox + When sending xml search query "xnznxvcx" + | viewbox | + | 12,45.13,77,33 | + Then result header contains + | attr | value | + | querystring | xnznxvcx | + | polygon | false | + | viewbox | 12,45.13,77,33 | + + Scenario: Empty XML search with viewboxlbrt + When sending xml search query "xnznxvcx" + | viewboxlbrt | + | 12,34.13,77,45 | + Then result header contains + | attr | value | + | querystring | xnznxvcx | + | polygon | false | + | viewbox | 12,45,77,34.13 | + + Scenario: Empty XML search with viewboxlbrt and viewbox + When sending xml search query "pub" + | viewbox | viewboxblrt | + | 12,45.13,77,33 | 1,2,3,4 | + Then result header contains + | attr | value | + | querystring | pub | + | polygon | false | + | viewbox | 12,45.13,77,33 | + + Scenario Outline: Empty XML search with polygon values + When sending xml search query "xnznxvcx" + | param | value | + | polygon | | + Then result header contains + | attr | value | + | polygon | | + + Examples: + | result | polyval | + | false | 0 | + | true | 1 | + | true | True | + | true | true | + | true | false | + | true | FALSE | + | true | yes | + | true | no | + | true | '; delete from foobar; select ' | + + Scenario: Empty XML search with exluded place ids + When sending xml search query "jghrleoxsbwjer" + | exclude_place_ids | + | 123,76,342565 | + Then result header contains + | attr | value | + | exclude_place_ids | 123,76,342565 | + + Scenario: Empty XML search with bad exluded place ids + When sending xml search query "jghrleoxsbwjer" + | exclude_place_ids | + | , | + Then result header has not attributes exclude_place_ids + + Scenario Outline: Wrapping of legal jsonp search requests + When sending json search query "Tokyo" + | param | value | + |json_callback | | + Then result header contains + | attr | value | + | json_func | | + + Examples: + | data | result | + | foo | foo | + | FOO | FOO | + | __world | __world | + | $me | \$me | + | m1[4] | m1\[4\] | + | d_r[$d] | d_r\[\$d\] | + + Scenario Outline: Wrapping of illegal jsonp search requests + When sending json search query "Tokyo" + | param | value | + |json_callback | | + Then a HTTP 400 is returned + + Examples: + | data | + | 1asd | + | bar(foo) | + | XXX['bad'] | + | foo; evil | + + Scenario: Ignore jsonp parameter for anything but json + When sending json search query "Malibu" + | json_callback | + | 234 | + Then a HTTP 400 is returned + When sending xml search query "Malibu" + | json_callback | + | 234 | + Then the result is valid xml + When sending html search query "Malibu" + | json_callback | + | 234 | + Then the result is valid html + + Scenario: Empty JSON search + When sending json search query "YHlERzzx" + Then exactly 0 results are returned + + Scenario: Empty JSONv2 search + When sending jsonv2 search query "Flubb XdfESSaZx" + Then exactly 0 results are returned + + Scenario: Search for non-existing coordinates + When sending json search query "-21.0,-33.0" + Then exactly 0 results are returned +