class TestModules(unittest.TestCase): """ """ @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_modules(self): """ Tests that modules are loaded """ # Check that our 'SampleService is registered iface = self.server.serverInterface() service = iface.serviceRegistry().getService('SampleService') self.assertIsNotNone(service)
class _Client: def __init__(self) -> None: self.cachedir = request.config.rootdir.join('__cadastre___') self.datapath = request.config.rootdir.join('data') os.environ['QGIS_CADASTRE_CACHE_DIR'] = self.cachedir.strpath self.server = QgsServer() # Load plugins load_plugins(self.server.serverInterface()) def getplugin(self, name) -> Any: """ retourne l'instance du plugin """ return server_plugins.get(name) def getprojectpath(self, name: str) -> str: return self.datapath.join(name) def get(self, query: str, project: str=None) -> _Response: """ Return server response from query """ request = QgsBufferServerRequest(query, QgsServerRequest.GetMethod, {}, None) response = QgsBufferServerResponse() if project is not None and not os.path.isabs(project): projectpath = self.datapath.join(project) qgsproject = QgsProject() if not qgsproject.read(projectpath.strpath): raise ValueError("Error reading project '%s':" % projectpath.strpath) else: qgsproject = None self.server.handleRequest(request, response, project=qgsproject) return _Response(response)
class TestQgsServer(unittest.TestCase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def assert_headers(self, header, body): headers = Message(StringIO(header)) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual( content_length, body_length, msg= "Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [str(_v) for _v in self.server.handleRequest()] response = header + body expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = '<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(body, expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('Hello from SimpleServer!') serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!') class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!') filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [ str(_v) for _v in self.server.handleRequest('service=simple') ] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [ str(_v) for _v in self.server.handleRequest('service=simple') ] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request, extra=None, reference_file=None): project = self.testdata_path + "test+project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % ( urllib.quote(project), request) if extra is not None: query_string += extra header, body = [ str(_v) for _v in self.server.handleRequest(query_string) ] response = header + body f = open(self.testdata_path + (request.lower() if not reference_file else reference_file) + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() #""" response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace( 'typeName="Integer64" precision="0" length="10" editType="TextEdit" type="qlonglong"', 'typeName="Integer" precision="0" length="10" editType="TextEdit" type="int"' ) self.assertEqual( response, expected, msg= "request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # Test getfeatureinfo response self.wms_request_compare( 'GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'info_format=text%2Fhtml&transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-html') # Test getfeatureinfo default info_format self.wms_request_compare( 'GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-plain') # Regression for #8656 # Mind the gap! (the space in the FILTER expression) self.wms_request_compare( 'GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&' + 'INFO_FORMAT=text%2Fxml&' + 'width=600&height=400&srs=EPSG%3A3857&' + 'query_layers=testlayer%20%C3%A8%C3%A9&' + 'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.quote(':"NAME" = \'two\''), 'wms_getfeatureinfo_filter') def wms_inspire_request_compare(self, request): """WMS INSPIRE tests""" project = self.testdata_path + "test+project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % ( urllib.quote(project), request) header, body = [ str(_v) for _v in self.server.handleRequest(query_string) ] response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual( response, expected, msg= "request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities', ): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % ( urllib.quote(project), request) header, body = [ str(_v) for _v in self.server.handleRequest(query_string) ] self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace('<element type="long" name="id"/>', '<element type="integer" name="id"/>') self.assertEqual( response, expected, msg= "request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wfs(self): """Test some WFS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % ( urllib.quote(project), request) header, body = [ str(_v) for _v in self.server.handleRequest(query_string) ] self.result_compare( 'wfs_getfeature_' + requestid + '.txt', u"request %s failed.\n Query: %s" % ( query_string, request, ), header, body) def result_compare(self, file_name, error_msg_header, header, body): self.assert_headers(header, body) response = header + body f = open(self.testdata_path + file_name) expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual( response, expected, msg=u"%s\n Expected:\n%s\n\n Response:\n%s" % (error_msg_header, unicode(expected, errors='replace'), unicode(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', u'GetFeature&TYPENAME=testlayer')) tests.append( ('startindex2', u'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) tests.append( ('limit2', u'GetFeature&TYPENAME=testlayer&MAXFEATURES=2')) tests.append( ('start1_limit1', u'GetFeature&TYPENAME=testlayer&MAXFEATURES=1&STARTINDEX=1')) for id, req in tests: self.wfs_getfeature_compare(id, req) def wfs_getfeature_post_compare(self, requestid, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP={}'.format(urllib.quote(project)) self.server.putenv("REQUEST_METHOD", "POST") self.server.putenv("REQUEST_BODY", request) header, body = self.server.handleRequest(query_string) self.server.putenv("REQUEST_METHOD", '') self.server.putenv("REQUEST_BODY", '') self.result_compare( 'wfs_getfeature_{}.txt'.format(requestid), "GetFeature in POST for '{}' failed.".format(requestid), header, body, ) def test_getfeature_post(self): template = """<?xml version="1.0" encoding="UTF-8"?> <wfs:GetFeature service="WFS" version="1.0.0" {} xmlns:wfs="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"> <wfs:Query typeName="testlayer" xmlns:feature="http://www.qgis.org/gml"> <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> <ogc:BBOX> <ogc:PropertyName>geometry</ogc:PropertyName> <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> <gml:lowerCorner>8 44</gml:lowerCorner> <gml:upperCorner>9 45</gml:upperCorner> </gml:Envelope> </ogc:BBOX> </ogc:Filter> </wfs:Query> </wfs:GetFeature> """ tests = [] tests.append(('nobbox', template.format(""))) tests.append(('startindex2', template.format('startIndex="2"'))) tests.append(('limit2', template.format('maxFeatures="2"'))) tests.append(('start1_limit1', template.format('startIndex="1" maxFeatures="1"'))) for id, req in tests: self.wfs_getfeature_post_compare(id, req) def test_getLegendGraphics(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test%2Bproject.qgs", 'SERVICE': 'WMS', 'VERSION': '1.0.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer+èé', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.iteritems()]) h, r = self.server.handleRequest(qs) self.assertEqual(-1, h.find('Content-Type: text/xml; charset=utf-8'), "Header: %s\nResponse:\n%s" % (h, r)) self.assertNotEquals(-1, h.find('Content-Type: image/png'), "Header: %s\nResponse:\n%s" % (h, r))
class TestQgsServer(unittest.TestCase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def assert_headers(self, header, body): headers = Message(StringIO(header)) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual(content_length, body_length, msg="Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [str(_v) for _v in self.server.handleRequest()] response = header + body expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = '<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(body, expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('Hello from SimpleServer!') serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!') class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!') filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [str(_v) for _v in self.server.handleRequest('service=simple')] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [str(_v) for _v in self.server.handleRequest('service=simple')] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request, extra=None, reference_file=None): project = self.testdata_path + "test+project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (urllib.quote(project), request) if extra is not None: query_string += extra header, body = [str(_v) for _v in self.server.handleRequest(query_string)] response = header + body f = open(self.testdata_path + (request.lower() if not reference_file else reference_file) + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() #""" response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace('typeName="Integer64" precision="0" length="10" editType="TextEdit" type="qlonglong"', 'typeName="Integer" precision="0" length="10" editType="TextEdit" type="int"') self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # Test getfeatureinfo response self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'info_format=text%2Fhtml&transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-html') # Test getfeatureinfo default info_format self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-plain') # Regression for #8656 # Mind the gap! (the space in the FILTER expression) self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&' + 'INFO_FORMAT=text%2Fxml&' + 'width=600&height=400&srs=EPSG%3A3857&' + 'query_layers=testlayer%20%C3%A8%C3%A9&' + 'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.quote(':"NAME" = \'two\''), 'wms_getfeatureinfo_filter') def wms_inspire_request_compare(self, request): """WMS INSPIRE tests""" project = self.testdata_path + "test+project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities',): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace('<element type="long" name="id"/>', '<element type="integer" name="id"/>') self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wfs(self): """Test some WFS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] self.result_compare( 'wfs_getfeature_' + requestid + '.txt', u"request %s failed.\n Query: %s" % ( query_string, request, ), header, body ) def result_compare(self, file_name, error_msg_header, header, body): self.assert_headers(header, body) response = header + body f = open(self.testdata_path + file_name) expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg=u"%s\n Expected:\n%s\n\n Response:\n%s" % (error_msg_header, unicode(expected, errors='replace'), unicode(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', u'GetFeature&TYPENAME=testlayer')) tests.append(('startindex2', u'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) tests.append(('limit2', u'GetFeature&TYPENAME=testlayer&MAXFEATURES=2')) tests.append(('start1_limit1', u'GetFeature&TYPENAME=testlayer&MAXFEATURES=1&STARTINDEX=1')) for id, req in tests: self.wfs_getfeature_compare(id, req) def wfs_getfeature_post_compare(self, requestid, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP={}'.format(urllib.quote(project)) self.server.putenv("REQUEST_METHOD", "POST") self.server.putenv("REQUEST_BODY", request) header, body = self.server.handleRequest(query_string) self.server.putenv("REQUEST_METHOD", '') self.server.putenv("REQUEST_BODY", '') self.result_compare( 'wfs_getfeature_{}.txt'.format(requestid), "GetFeature in POST for '{}' failed.".format(requestid), header, body, ) def test_getfeature_post(self): template = """<?xml version="1.0" encoding="UTF-8"?> <wfs:GetFeature service="WFS" version="1.0.0" {} xmlns:wfs="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"> <wfs:Query typeName="testlayer" xmlns:feature="http://www.qgis.org/gml"> <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> <ogc:BBOX> <ogc:PropertyName>geometry</ogc:PropertyName> <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> <gml:lowerCorner>8 44</gml:lowerCorner> <gml:upperCorner>9 45</gml:upperCorner> </gml:Envelope> </ogc:BBOX> </ogc:Filter> </wfs:Query> </wfs:GetFeature> """ tests = [] tests.append(('nobbox', template.format(""))) tests.append(('startindex2', template.format('startIndex="2"'))) tests.append(('limit2', template.format('maxFeatures="2"'))) tests.append(('start1_limit1', template.format('startIndex="1" maxFeatures="1"'))) for id, req in tests: self.wfs_getfeature_post_compare(id, req) def test_getLegendGraphics(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test%2Bproject.qgs", 'SERVICE': 'WMS', 'VERSION': '1.0.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer+èé', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.iteritems()]) h, r = self.server.handleRequest(qs) self.assertEqual(-1, h.find('Content-Type: text/xml; charset=utf-8'), "Header: %s\nResponse:\n%s" % (h, r)) self.assertNotEquals(-1, h.find('Content-Type: image/png'), "Header: %s\nResponse:\n%s" % (h, r))
try: QGIS_SERVER_DEFAULT_PORT = int(os.environ['QGIS_SERVER_DEFAULT_PORT']) except KeyError: QGIS_SERVER_DEFAULT_PORT = 8081 try: QGIS_SERVER_DEFAULT_SERVERNAME = os.environ['QGIS_SERVER_DEFAULT_SERVERNAME'] except KeyError: QGIS_SERVER_DEFAULT_SERVERNAME = 'localhost' qgs_server = QgsServer() # OAuth 2 plugin loading start serverIface = qgs_server.serverInterface() from oauth_settings import * import importlib module = importlib.import_module('filters.%s' % OAUTH2_AUTHORIZATION_SERVICE_PROVIDER) klass_name = 'OAuth2Filter%s' % OAUTH2_AUTHORIZATION_SERVICE_PROVIDER.title() klass = getattr(module, klass_name) serverIface.registerFilter(klass(serverIface), 100) # OAuth 2 plugin loading End class Handler(BaseHTTPRequestHandler): def __init__(self, request, client_address, server): BaseHTTPRequestHandler.__init__(self, request, client_address, server) def do_GET(self):
def responseComplete(self): handler = self.serverInterface().requestHandler() auth = handler.requestHeader('HTTP_AUTHORIZATION') if auth: username, password = base64.b64decode(auth[6:]).split(b':') if (username.decode('utf-8') == os.environ.get('QGIS_SERVER_USERNAME', 'username') and password.decode('utf-8') == os.environ.get('QGIS_SERVER_PASSWORD', 'password')): return # No auth ... handler.clear() handler.setResponseHeader('Status', '401 Authorization required') handler.setResponseHeader( 'WWW-Authenticate', 'Basic realm="QGIS Server"') handler.appendBody(b'<h1>Authorization required</h1>') filter = HTTPBasicFilter(qgs_server.serverInterface()) qgs_server.serverInterface().registerFilter(filter) def num2deg(xtile, ytile, zoom): """This returns the NW-corner of the square. Use the function with xtile+1 and/or ytile+1 to get the other corners. With xtile+0.5 & ytile+0.5 it will return the center of the tile.""" n = 2.0 ** zoom lon_deg = xtile / n * 360.0 - 180.0 lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) lat_deg = math.degrees(lat_rad) return (lat_deg, lon_deg) class XYZFilter(QgsServerFilter): """XYZ server, example: ?MAP=/path/to/projects.qgs&SERVICE=XYZ&X=1&Y=0&Z=1&LAYERS=world"""
def allowToEdit(self, layer, feature): """ Are we authorise to modify the following geometry """ if not self._active: return super(RestrictedAccessControl, self).allowToEdit(layer, feature) return feature.attribute("color") in ["red", "yellow"] def cacheKey(self): return "r" if self._active else "f" server = QgsServer() server.handleRequest() server_iface = server.serverInterface() accesscontrol = RestrictedAccessControl(server_iface) server_iface.registerAccessControl(accesscontrol, 100) class TestQgsServerAccessControl(TestCase): def setUp(self): self.testdata_path = unitTestDataPath("qgis_server_accesscontrol") copyfile(path.join(self.testdata_path, "helloworld.db"), path.join(self.testdata_path, "_helloworld.db")) for k in ["QUERY_STRING", "QGIS_PROJECT_FILE"]: if k in environ: del environ[k]
def allowToEdit(self, layer, feature): """ Are we authorise to modify the following geometry """ if not self._active: return super(RestrictedAccessControl, self).allowToEdit(layer, feature) return feature.attribute("color") in ["red", "yellow"] def cacheKey(self): return "r" if self._active else "f" server = QgsServer() server.handleRequest() server_iface = server.serverInterface() accesscontrol = RestrictedAccessControl(server_iface) server_iface.registerAccessControl(accesscontrol, 100) class TestQgsServerAccessControl(TestCase): def setUp(self): self.testdata_path = unitTestDataPath("qgis_server_accesscontrol") dataFile = os.path.join(self.testdata_path, "helloworld.db") self.assertTrue(os.path.isfile(dataFile), 'Could not find data file "{}"'.format(dataFile)) copyfile(dataFile, os.path.join(self.testdata_path, "_helloworld.db")) for k in ["QUERY_STRING", "QGIS_PROJECT_FILE"]: if k in os.environ:
if self.serverInterface().getEnv('HTTP_AUTHORIZATION'): username, password = base64.b64decode( self.serverInterface().getEnv( 'HTTP_AUTHORIZATION')[6:]).split(':') if (username == os.environ.get('QGIS_SERVER_USERNAME', 'username') and password == os.environ.get('QGIS_SERVER_PASSWORD', 'password')): return # No auth ... request.clearHeaders() request.setHeader('Status', '401 Authorization required') request.setHeader('WWW-Authenticate', 'Basic realm="QGIS Server"') request.clearBody() request.appendBody('<h1>Authorization required</h1>') filter = HTTPBasicFilter(qgs_server.serverInterface()) qgs_server.serverInterface().registerFilter(filter) from filters.wpsFilter import wpsFilter try: filter = wpsFilter(qgs_server.serverInterface()) qgs_server.serverInterface().registerFilter(filter, 100) QgsMessageLog.logMessage("wps4server - Loaded successfully") except Exception, e: QgsMessageLog.logMessage("wps4server - Error loading filter wps : %s" % e) class Handler(BaseHTTPRequestHandler): def do_GET(self): # For PKI: check the username from client certificate if https:
class TestQgsServer(unittest.TestCase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [str(_v) for _v in self.server.handleRequest()] response = header + body expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = '<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(body, expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print "QGIS Server plugins are not compiled. Skipping test" return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('Hello from SimpleServer!') serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!') class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!') filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [str(_v) for _v in self.server.handleRequest('service=simple')] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [str(_v) for _v in self.server.handleRequest('service=simple')] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request): project = self.testdata_path + "test+project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] response = header + body f = open(self.testdata_path + request.lower() + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # WMS INSPIRE tests def wms_inspire_request_compare(self, request): project = self.testdata_path + "test+project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities',): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wfs(self): """Test some WMS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_getfeature_' + requestid + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg=u"request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, unicode(expected, errors='replace'), unicode(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', u'GetFeature&TYPENAME=testlayer')) tests.append(('startindex2', u'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) for id, req in tests: self.wfs_getfeature_compare(id, req) def assert_headers(self, header, body): headers = Message(StringIO(header)) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual(content_length, body_length, msg="Header reported content-length: %d Actual body length was: %d" % (content_length, body_length))
self.send_response(200) for k, v in headers_dict.items(): self.send_header(k, v) self.end_headers() self.wfile.write(body) return def do_POST(self): content_len = int(self.headers.get('content-length', 0)) post_body = self.rfile.read(content_len).decode() QgsMessageLog.logMessage("OAUTH2 POST BODY: %s" % post_body) #request = post_body[1:post_body.find(' ')] #self.path = self.path + '&REQUEST=' + request return self.do_GET(post_body) filter = OAuth2ResOwnerFilter(qgs_server.serverInterface()) qgs_server.serverInterface().registerFilter(filter, 100) if __name__ == '__main__': server = HTTPServer((QGIS_SERVER_HOST, QGIS_SERVER_PORT), Handler) message = 'Starting server on %s://%s:%s, use <Ctrl-C> to stop' % \ ('http', QGIS_SERVER_HOST, server.server_port) try: print(message, flush=True) except: print(message) sys.stdout.flush() server.serve_forever()
except: self.send_response(200) for k, v in headers_dict.items(): self.send_header(k, v) self.end_headers() self.wfile.write(body) return def do_POST(self): content_len = int(self.headers.get('content-length', 0)) post_body = self.rfile.read(content_len).decode() QgsMessageLog.logMessage("OAUTH2 POST BODY: %s" % post_body) #request = post_body[1:post_body.find(' ')] #self.path = self.path + '&REQUEST=' + request return self.do_GET(post_body) filter = OAuth2ResOwnerFilter(qgs_server.serverInterface()) qgs_server.serverInterface().registerFilter(filter, 100) if __name__ == '__main__': server = HTTPServer((QGIS_SERVER_HOST, QGIS_SERVER_PORT), Handler) message = 'Starting server on %s://%s:%s, use <Ctrl-C> to stop' % \ ('http', QGIS_SERVER_HOST, server.server_port) try: print(message, flush=True) except: print(message) sys.stdout.flush() server.serve_forever()
class TestQgsServerPlugins(QgsServerTestBase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def onSendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.onSendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clear() request.setResponseHeader('Content-type', 'text/plain') request.appendBody( 'Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # global to be modified inside plugin filters globals()['status_code'] = 0 # body to be checked inside plugin filters globals()['body2'] = None # headers to be checked inside plugin filters globals()['headers2'] = None # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) class Filter3(QgsServerFilter): """Test get and set status code""" def responseComplete(self): global status_code request = self.serverInterface().requestHandler() request.setStatusCode(999) status_code = request.statusCode() class Filter4(QgsServerFilter): """Body getter""" def responseComplete(self): global body2 request = self.serverInterface().requestHandler() body2 = request.body() class Filter5(QgsServerFilter): """Body setter, clear body, keep headers""" def responseComplete(self): global headers2 request = self.serverInterface().requestHandler() request.clearBody() headers2 = request.responseHeaders() request.appendBody('new body, new life!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) filter3 = Filter3(serverIface) filter4 = Filter4(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) serverIface.registerFilter(filter3, 300) serverIface.registerFilter(filter4, 400) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Check status code self.assertEqual(status_code, 999) # Check body getter from filter self.assertEqual( body2, b'Hello from SimpleServer!Hello from Filter1!Hello from Filter2!') # Check that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = self._execute_request('?service=simple') response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Now, re-run with body setter filter5 = Filter5(serverIface) serverIface.registerFilter(filter5, 500) header, body = self._execute_request('?service=simple') response = header + body expected = b'Content-Length: 19\nContent-type: text/plain\n\nnew body, new life!' self.assertEqual(response, expected) self.assertEqual(headers2, {'Content-type': 'text/plain'}) def test_configpath(self): """ Test plugin can read confif path """ try: from qgis.server import QgsServerFilter from qgis.core import QgsProject except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") self.server = QgsServer() # global to be modified inside plugin filters globals()['configFilePath2'] = None class Filter0(QgsServerFilter): """Body setter, clear body, keep headers""" def requestReady(self): global configFilePath2 configFilePath2 = self.serverInterface().configFilePath() serverIface = self.server.serverInterface() serverIface.registerFilter(Filter0(serverIface), 100) # Test using MAP self._execute_request('?service=simple&MAP=%s' % self.projectPath) # Check config file path self.assertEqual(configFilePath2, self.projectPath) # Reset result globals()['configFilePath2'] = None # Test with prqject as argument project = QgsProject() project.read(self.projectPath) self._execute_request_project('?service=simple', project=project) # Check config file path self.assertEqual(configFilePath2, project.fileName()) def test_exceptions(self): """Test that plugin filter Python exceptions can be caught""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class FilterBroken(QgsServerFilter): def responseComplete(self): raise Exception("There was something very wrong!") serverIface = self.server.serverInterface() filter1 = FilterBroken(serverIface) filters = {100: [filter1]} serverIface.setFilters(filters) header, body = self._execute_request('') self.assertEqual(body, b'Internal Server Error') serverIface.setFilters({}) def test_get_path(self): """Test get url and path from plugins""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class Filter1(QgsServerFilter): def responseComplete(self): handler = self.serverInterface().requestHandler() self.url = handler.url() self.path = handler.path() serverIface = self.server.serverInterface() filter1 = Filter1(serverIface) filters = {100: [filter1]} serverIface.setFilters(filters) header, body = self._execute_request( 'http://myserver/mypath/?myparam=1') self.assertEqual(filter1.url, 'http://myserver/mypath/?myparam=1') self.assertEqual(filter1.path, '/mypath/') serverIface.setFilters({}) def test_streaming_pipeline(self): """ Test streaming pipeline propagation """ try: from qgis.server import QgsServerFilter from qgis.core import QgsProject except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return # create a service for streaming data class StreamedService(QgsService): def __init__(self): super().__init__() self._response = b"Should never appear" self._name = "TestStreamedService" self._version = "1.0" def name(self): return self._name def version(self): return self._version def executeRequest(self, request, response, project): response.setStatusCode(206) response.write(self._response) response.flush() class Filter1(QgsServerFilter): def onRequestReady(self): request = self.serverInterface().requestHandler() return self.propagate def onSendResponse(self): request = self.serverInterface().requestHandler() request.clearBody() request.appendBody(b'A') request.sendResponse() request.appendBody(b'B') request.sendResponse() # Stop propagating return self.propagate def onResponseComplete(self): request = self.serverInterface().requestHandler() request.appendBody(b'C') return self.propagate # Methods should be called only if filter1 propagate class Filter2(QgsServerFilter): def __init__(self, iface): super().__init__(iface) self.request_ready = False def onRequestReady(self): request = self.serverInterface().requestHandler() self.request_ready = True return True def onSendResponse(self): request = self.serverInterface().requestHandler() request.appendBody(b'D') return True def onResponseComplete(self): request = self.serverInterface().requestHandler() request.appendBody(b'E') return True serverIface = self.server.serverInterface() serverIface.setFilters({}) service0 = StreamedService() reg = serverIface.serviceRegistry() reg.registerService(service0) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 200) serverIface.registerFilter(filter2, 300) project = QgsProject() # Test no propagation filter1.propagate = False _, body = self._execute_request_project('?service=%s' % service0.name(), project=project) self.assertFalse(filter2.request_ready) self.assertEqual(body, b'ABC') # Test with propagation filter1.propagate = True _, body = self._execute_request_project('?service=%s' % service0.name(), project=project) self.assertTrue(filter2.request_ready) self.assertEqual(body, b'ABDCE') serverIface.setFilters({}) reg.unregisterService(service0.name(), service0.version())
class TestQgsServerPlugins(QgsServerTestBase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clear() request.setResponseHeader('Content-type', 'text/plain') request.appendBody( 'Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # global to be modified inside plugin filters globals()['status_code'] = 0 # body to be checked inside plugin filters globals()['body2'] = None # headers to be checked inside plugin filters globals()['headers2'] = None # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) class Filter3(QgsServerFilter): """Test get and set status code""" def responseComplete(self): global status_code request = self.serverInterface().requestHandler() request.setStatusCode(999) status_code = request.statusCode() class Filter4(QgsServerFilter): """Body getter""" def responseComplete(self): global body2 request = self.serverInterface().requestHandler() body2 = request.body() class Filter5(QgsServerFilter): """Body setter, clear body, keep headers""" def responseComplete(self): global headers2 request = self.serverInterface().requestHandler() request.clearBody() headers2 = request.responseHeaders() request.appendBody('new body, new life!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) filter3 = Filter3(serverIface) filter4 = Filter4(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) serverIface.registerFilter(filter3, 300) serverIface.registerFilter(filter4, 400) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Check status code self.assertEqual(status_code, 999) # Check body getter from filter self.assertEqual( body2, b'Hello from SimpleServer!Hello from Filter1!Hello from Filter2!') # Check that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Now, re-run with body setter filter5 = Filter5(serverIface) serverIface.registerFilter(filter5, 500) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 19\nContent-type: text/plain\n\nnew body, new life!' self.assertEqual(response, expected) self.assertEqual(headers2, {'Content-type': 'text/plain'})
class TestQgsServer(unittest.TestCase): def assertXMLEqual(self, response, expected, msg=''): """Compare XML line by line and sorted attributes""" response_lines = response.splitlines() expected_lines = expected.splitlines() line_no = 1 for expected_line in expected_lines: expected_line = expected_line.strip() response_line = response_lines[line_no - 1].strip() # Compare tag try: self.assertEqual(re.findall(b'<([^>\s]+)[ >]', expected_line)[0], re.findall(b'<([^>\s]+)[ >]', response_line)[0], msg=msg + "\nTag mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) except IndexError: self.assertEqual(expected_line, response_line, msg=msg + "\nTag line mismatch %s: %s != %s" % (line_no, expected_line, response_line)) #print("---->%s\t%s == %s" % (line_no, expected_line, response_line)) # Compare attributes if re.match(RE_ATTRIBUTES, expected_line): # has attrs expected_attrs = sorted(re.findall(RE_ATTRIBUTES, expected_line)) response_attrs = sorted(re.findall(RE_ATTRIBUTES, response_line)) self.assertEqual(expected_attrs, response_attrs, msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format(line_no, expected_attrs, response_attrs)) line_no += 1 @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def strip_version_xmlns(self, text): """Order of attributes is random, strip version and xmlns""" return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'') def assert_headers(self, header, body): stream = StringIO() header_string = header.decode('utf-8') stream.write(header_string) headers = email.message_from_string(header_string) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual(content_length, body_length, msg="Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest("") def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [_v for _v in self.server.handleRequest("")] response = self.strip_version_xmlns(header + body) expected = self.strip_version_xmlns(b'Content-Length: 54\nContent-Type: text/xml; charset=utf-8\n\n<ServerException>Project file error</ServerException>\n') self.assertEqual(response, expected) expected = b'Content-Length: 54\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test response when project is specified but without service project = self.testdata_path + "test_project_wfs.qgs" qs = 'MAP=%s' % (urllib.parse.quote(project)) header, body = [_v for _v in self.server.handleRequest(qs)] response = self.strip_version_xmlns(header + body) expected = self.strip_version_xmlns(b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n') self.assertEqual(response, expected) expected = b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = self.strip_version_xmlns(b'<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n') self.assertEqual(self.strip_version_xmlns(body), expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clear() request.setHeader('Content-type', 'text/plain') request.appendBody('Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self.server.handleRequest('service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self.server.handleRequest('service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request, extra=None, reference_file=None): project = self.testdata_path + "test_project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (urllib.parse.quote(project), request) if extra is not None: query_string += extra header, body = self.server.handleRequest(query_string) response = header + body reference_path = self.testdata_path + (request.lower() if not reference_file else reference_file) + '.txt' f = open(reference_path, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(reference_path, 'wb+') f.write(response) f.close() f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() #""" response = re.sub(RE_STRIP_UNCHECKABLE, b'*****', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'*****', expected) self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # Test getfeatureinfo response self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'info_format=text%2Fhtml&transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-html') # Test getfeatureinfo default info_format self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-plain') # Regression for #8656 # Mind the gap! (the space in the FILTER expression) self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&' + 'INFO_FORMAT=text%2Fxml&' + 'width=600&height=400&srs=EPSG%3A3857&' + 'query_layers=testlayer%20%C3%A8%C3%A9&' + 'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\''), 'wms_getfeatureinfo_filter') def wms_inspire_request_compare(self, request): """WMS INSPIRE tests""" project = self.testdata_path + "test_project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities',): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wfs(self): """Test some WFS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.result_compare( 'wfs_getfeature_' + requestid + '.txt', "request %s failed.\n Query: %s" % ( query_string, request, ), header, body ) def test_wfs_getcapabilities_url(self): # empty url in project project = os.path.join(self.testdata_path, "test_project_without_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WFS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) for item in str(r).split("\\n"): if "onlineResource" in item: self.assertEqual("onlineResource=\"?" in item, True) # url well defined in project project = os.path.join(self.testdata_path, "test_project_with_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WFS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) for item in str(r).split("\\n"): if "onlineResource" in item: print("onlineResource: ", item) self.assertEqual("onlineResource=\"my_wfs_advertised_url\"" in item, True) def result_compare(self, file_name, error_msg_header, header, body): self.assert_headers(header, body) response = header + body f = open(self.testdata_path + file_name, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="%s\n Expected:\n%s\n\n Response:\n%s" % (error_msg_header, str(expected, errors='replace'), str(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', 'GetFeature&TYPENAME=testlayer')) tests.append(('startindex2', 'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) tests.append(('limit2', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=2')) tests.append(('start1_limit1', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=1&STARTINDEX=1')) for id, req in tests: self.wfs_getfeature_compare(id, req) def wfs_getfeature_post_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP={}'.format(urllib.parse.quote(project)) self.server.putenv("REQUEST_METHOD", "POST") self.server.putenv("REQUEST_BODY", request) header, body = self.server.handleRequest(query_string) self.server.putenv("REQUEST_METHOD", '') self.server.putenv("REQUEST_BODY", '') self.result_compare( 'wfs_getfeature_{}.txt'.format(requestid), "GetFeature in POST for '{}' failed.".format(requestid), header, body, ) def test_getfeature_post(self): template = """<?xml version="1.0" encoding="UTF-8"?> <wfs:GetFeature service="WFS" version="1.0.0" {} xmlns:wfs="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"> <wfs:Query typeName="testlayer" xmlns:feature="http://www.qgis.org/gml"> <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> <ogc:BBOX> <ogc:PropertyName>geometry</ogc:PropertyName> <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> <gml:lowerCorner>8 44</gml:lowerCorner> <gml:upperCorner>9 45</gml:upperCorner> </gml:Envelope> </ogc:BBOX> </ogc:Filter> </wfs:Query> </wfs:GetFeature> """ tests = [] tests.append(('nobbox', template.format(""))) tests.append(('startindex2', template.format('startIndex="2"'))) tests.append(('limit2', template.format('maxFeatures="2"'))) tests.append(('start1_limit1', template.format('startIndex="1" maxFeatures="1"'))) for id, req in tests: self.wfs_getfeature_post_compare(id, req) def test_wms_getmap_basic(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Basic") def test_wms_getmap_transparent(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "TRANSPARENT": "TRUE" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Transparent") def test_wms_getmap_background(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "BGCOLOR": "green" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Background") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "BGCOLOR": "0x008000" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Background_Hex") def test_wms_getcapabilities_url(self): # empty url in project project = os.path.join(self.testdata_path, "test_project_without_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WMS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) item_found = False for item in str(r).split("\\n"): if "OnlineResource" in item: self.assertEqual("xlink:href=\"?" in item, True) item_found = True self.assertTrue(item_found) # url well defined in project project = os.path.join(self.testdata_path, "test_project_with_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WMS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) item_found = False for item in str(r).split("\\n"): if "OnlineResource" in item: self.assertEqual("xlink:href=\"my_wms_advertised_url?" in item, True) item_found = True self.assertTrue(item_found) def test_wms_getmap_invalid_size(self): project = os.path.join(self.testdata_path, "test_project_with_size.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WMS", "VERSION": "1.3.0", "REQUEST": "GetMap", "LAYERS": "Hello", "STYLES": "", "FORMAT": "image/png", "HEIGHT": "5001", "WIDTH": "5000" }.items())]) expected = self.strip_version_xmlns(b'<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Size error">The requested map size is too large</ServiceException>\n</ServiceExceptionReport>\n') r, h = self._result(self.server.handleRequest(qs)) self.assertEqual(self.strip_version_xmlns(r), expected) def test_wms_getmap_order(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Hello,Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_LayerOrder") def test_wms_getmap_srs(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-151.7,-38.9,51.0,78.0", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:4326" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_SRS") def test_wms_getmap_style(self): # default style qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country_Labels", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_StyleDefault") # custom style qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country_Labels", "STYLES": "custom", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_StyleCustom") def test_wms_getmap_filter(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "FILTER": "Country:\"name\" = 'eurasia'" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Filter") def test_wms_getmap_selection(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "SRS": "EPSG:3857", "SELECTION": "Country: 4" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Selection") def test_wms_getmap_opacities(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "OPACITIES": "125, 50" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Opacities") def test_wms_getprint_basic(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Basic") def test_wms_getprint_srs(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-309.015,-133.011,312.179,133.949", "map0:LAYERS": "Country,Hello", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:4326" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_SRS") def test_wms_getprint_scale(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "map0:SCALE": "36293562", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Scale") def test_wms_getprint_grid(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "map0:GRID_INTERVAL_X": "1000000", "map0:GRID_INTERVAL_Y": "2000000", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Grid") def test_wms_getprint_rotation(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "map0:ROTATION": "45", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Rotation") def test_wms_getprint_selection(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "SELECTION": "Country: 4" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Selection") def test_getLegendGraphics(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': 'testlayer%20èé', } qs = '&'.join(["%s=%s" % (k, v) for k, v in parms.items()]) h, r = self.server.handleRequest(qs) self.assertEqual(-1, h.find(b'Content-Type: text/xml; charset=utf-8'), "Header: %s\nResponse:\n%s" % (h, r)) self.assertNotEqual(-1, h.find(b'Content-Type: image/png'), "Header: %s\nResponse:\n%s" % (h, r)) def test_getLegendGraphics_layertitle(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'TRUE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test", 250, QSize(10, 10)) parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'FALSE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test_layertitle_false", 250, QSize(10, 10)) def test_wms_GetLegendGraphic_Basic(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Basic") def test_wms_GetLegendGraphic_Transparent(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "TRANSPARENT": "TRUE" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Transparent") def test_wms_GetLegendGraphic_Background(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "BGCOLOR": "green" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Background") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "BGCOLOR": "0x008000" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Background_Hex") def test_wms_GetLegendGraphic_BoxSpace(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "BOXSPACE": "100", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_BoxSpace") def test_wms_GetLegendGraphic_SymbolSpace(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "SYMBOLSPACE": "100", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_SymbolSpace") def test_wms_GetLegendGraphic_IconLabelSpace(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "ICONLABELSPACE": "100", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_IconLabelSpace") def test_wms_GetLegendGraphic_SymbolSize(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "SYMBOLWIDTH": "50", "SYMBOLHEIGHT": "30", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_SymbolSize") def test_wms_GetLegendGraphic_BBox(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello,db_point", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "BBOX": "-151.7,-38.9,51.0,78.0", "CRS": "EPSG:4326" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_BBox") def test_wms_GetLegendGraphic_BBox2(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello,db_point", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "BBOX": "-76.08,-6.4,-19.38,38.04", "SRS": "EPSG:4326" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_BBox2") def test_wcs_getcapabilities_url(self): # empty url in project project = os.path.join(self.testdata_path, "test_project_without_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WCS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) item_found = False for item in str(r).split("\\n"): if "OnlineResource" in item: self.assertEqual("=\"?" in item, True) item_found = True self.assertTrue(item_found) # url well defined in project project = os.path.join(self.testdata_path, "test_project_with_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WCS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) item_found = False for item in str(r).split("\\n"): if "OnlineResource" in item: print("OnlineResource: ", item) self.assertEqual("\"my_wcs_advertised_url" in item, True) item_found = True self.assertTrue(item_found) def _result(self, data): headers = {} for line in data[0].decode('UTF-8').split("\n"): if line != "": header = line.split(":") self.assertEqual(len(header), 2, line) headers[str(header[0])] = str(header[1]).strip() return data[1], headers def _img_diff(self, image, control_image, max_diff, max_size_diff=QSize()): temp_image = os.path.join(tempfile.gettempdir(), "%s_result.png" % control_image) with open(temp_image, "wb") as f: f.write(image) control = QgsRenderChecker() control.setControlPathPrefix("qgis_server") control.setControlName(control_image) control.setRenderedImage(temp_image) if max_size_diff.isValid(): control.setSizeTolerance(max_size_diff.width(), max_size_diff.height()) return control.compareImages(control_image), control.report() def _img_diff_error(self, response, headers, image, max_diff=10, max_size_diff=QSize()): self.assertEqual( headers.get("Content-Type"), "image/png", "Content type is wrong: %s" % headers.get("Content-Type")) test, report = self._img_diff(response, image, max_diff, max_size_diff) with open(os.path.join(tempfile.gettempdir(), image + "_result.png"), "rb") as rendered_file: encoded_rendered_file = base64.b64encode(rendered_file.read()) message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.png" % ( report, encoded_rendered_file.strip(), tempfile.gettempdir(), image ) # If the failure is in image sizes the diff file will not exists. if os.path.exists(os.path.join(tempfile.gettempdir(), image + "_result_diff.png")): with open(os.path.join(tempfile.gettempdir(), image + "_result_diff.png"), "rb") as diff_file: encoded_diff_file = base64.b64encode(diff_file.read()) message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.png" % ( encoded_diff_file.strip(), tempfile.gettempdir(), image ) self.assertTrue(test, message)
'HTTP_AUTHORIZATION') if auth: username, password = base64.b64decode(auth[6:]).split(b':') if (username.decode('utf-8') == os.environ.get( 'QGIS_SERVER_USERNAME', 'username') and password.decode('utf-8') == os.environ.get( 'QGIS_SERVER_PASSWORD', 'password')): return # No auth ... handler.clear() handler.setResponseHeader('Status', '401 Authorization required') handler.setResponseHeader('WWW-Authenticate', 'Basic realm="QGIS Server"') handler.appendBody(b'<h1>Authorization required</h1>') filter = HTTPBasicFilter(qgs_server.serverInterface()) qgs_server.serverInterface().registerFilter(filter) def num2deg(xtile, ytile, zoom): """This returns the NW-corner of the square. Use the function with xtile+1 and/or ytile+1 to get the other corners. With xtile+0.5 & ytile+0.5 it will return the center of the tile.""" n = 2.0**zoom lon_deg = xtile / n * 360.0 - 180.0 lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) lat_deg = math.degrees(lat_rad) return (lat_deg, lon_deg) class XYZFilter(QgsServerFilter): """XYZ server, example: ?MAP=/path/to/projects.qgs&SERVICE=XYZ&X=1&Y=0&Z=1&LAYERS=world"""
class TestQgsServerPlugins(unittest.TestCase): def assertXMLEqual(self, response, expected, msg=''): """Compare XML line by line and sorted attributes""" response_lines = response.splitlines() expected_lines = expected.splitlines() line_no = 1 for expected_line in expected_lines: expected_line = expected_line.strip() response_line = response_lines[line_no - 1].strip() # Compare tag try: self.assertEqual( re.findall(b'<([^>\s]+)[ >]', expected_line)[0], re.findall(b'<([^>\s]+)[ >]', response_line)[0], msg=msg + "\nTag mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) except IndexError: self.assertEqual(expected_line, response_line, msg=msg + "\nTag line mismatch %s: %s != %s" % (line_no, expected_line, response_line)) # print("---->%s\t%s == %s" % (line_no, expected_line, response_line)) # Compare attributes if re.match(RE_ATTRIBUTES, expected_line): # has attrs expected_attrs = sorted( re.findall(RE_ATTRIBUTES, expected_line)) response_attrs = sorted( re.findall(RE_ATTRIBUTES, response_line)) self.assertEqual( expected_attrs, response_attrs, msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format( line_no, expected_attrs, response_attrs)) line_no += 1 @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def strip_version_xmlns(self, text): """Order of attributes is random, strip version and xmlns""" return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'') def assert_headers(self, header, body): stream = StringIO() header_string = header.decode('utf-8') stream.write(header_string) headers = email.message_from_string(header_string) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual( content_length, body_length, msg= "Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clear() request.setHeader('Content-type', 'text/plain') request.appendBody( 'Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # global to be modified inside plugin filters globals()['status_code'] = 0 # body to be checked inside plugin filters globals()['body2'] = None # headers to be checked inside plugin filters globals()['headers2'] = None # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) class Filter3(QgsServerFilter): """Test get and set status code""" def responseComplete(self): global status_code request = self.serverInterface().requestHandler() request.setStatusCode(999) status_code = request.statusCode() class Filter4(QgsServerFilter): """Body getter""" def responseComplete(self): global body2 request = self.serverInterface().requestHandler() body2 = request.body() class Filter5(QgsServerFilter): """Body setter, clear body, keep headers""" def responseComplete(self): global headers2 request = self.serverInterface().requestHandler() request.clearBody() headers2 = {k: request.header(k) for k in request.headerKeys()} request.appendBody('new body, new life!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) filter3 = Filter3(serverIface) filter4 = Filter4(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) serverIface.registerFilter(filter3, 300) serverIface.registerFilter(filter4, 400) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [ _v for _v in self.server.handleRequest('?service=simple') ] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Check status code self.assertEqual(status_code, 999) # Check body getter from filter self.assertEqual( body2, b'Hello from SimpleServer!Hello from Filter1!Hello from Filter2!') # Check that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [ _v for _v in self.server.handleRequest('?service=simple') ] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Now, re-run with body setter filter5 = Filter5(serverIface) serverIface.registerFilter(filter5, 500) header, body = [ _v for _v in self.server.handleRequest('?service=simple') ] response = header + body expected = b'Content-Length: 19\nContent-type: text/plain\n\nnew body, new life!' self.assertEqual(response, expected) self.assertEqual(headers2, {'Content-type': 'text/plain'})
class TestQgsServerPlugins(QgsServerTestBase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clear() request.setResponseHeader('Content-type', 'text/plain') request.appendBody('Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # global to be modified inside plugin filters globals()['status_code'] = 0 # body to be checked inside plugin filters globals()['body2'] = None # headers to be checked inside plugin filters globals()['headers2'] = None # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) class Filter3(QgsServerFilter): """Test get and set status code""" def responseComplete(self): global status_code request = self.serverInterface().requestHandler() request.setStatusCode(999) status_code = request.statusCode() class Filter4(QgsServerFilter): """Body getter""" def responseComplete(self): global body2 request = self.serverInterface().requestHandler() body2 = request.body() class Filter5(QgsServerFilter): """Body setter, clear body, keep headers""" def responseComplete(self): global headers2 request = self.serverInterface().requestHandler() request.clearBody() headers2 = request.responseHeaders() request.appendBody('new body, new life!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) filter3 = Filter3(serverIface) filter4 = Filter4(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) serverIface.registerFilter(filter3, 300) serverIface.registerFilter(filter4, 400) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Check status code self.assertEqual(status_code, 999) # Check body getter from filter self.assertEqual(body2, b'Hello from SimpleServer!Hello from Filter1!Hello from Filter2!') # Check that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Now, re-run with body setter filter5 = Filter5(serverIface) serverIface.registerFilter(filter5, 500) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 19\nContent-type: text/plain\n\nnew body, new life!' self.assertEqual(response, expected) self.assertEqual(headers2, {'Content-type': 'text/plain'})
class TestQgsServer(unittest.TestCase): def assertXMLEqual(self, response, expected, msg=''): """Compare XML line by line and sorted attributes""" response_lines = response.splitlines() expected_lines = expected.splitlines() line_no = 1 for expected_line in expected_lines: expected_line = expected_line.strip() response_line = response_lines[line_no - 1].strip() # Compare tag try: self.assertEqual( re.findall(b'<([^>\s]+)[ >]', expected_line)[0], re.findall(b'<([^>\s]+)[ >]', response_line)[0], msg=msg + "\nTag mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) except IndexError: self.assertEqual(expected_line, response_line, msg=msg + "\nTag line mismatch %s: %s != %s" % (line_no, expected_line, response_line)) #print("---->%s\t%s == %s" % (line_no, expected_line, response_line)) # Compare attributes if re.match(RE_ATTRIBUTES, expected_line): # has attrs expected_attrs = re.findall(RE_ATTRIBUTES, expected_line) expected_attrs.sort() response_attrs = re.findall(RE_ATTRIBUTES, response_line) response_attrs.sort() self.assertEqual( expected_attrs, response_attrs, msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format( line_no, expected_attrs, response_attrs)) line_no += 1 @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def strip_version_xmlns(self, text): """Order of attributes is random, strip version and xmlns""" return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'') def assert_headers(self, header, body): stream = StringIO() header_string = header.decode('utf-8') stream.write(header_string) headers = email.message_from_string(header_string) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual( content_length, body_length, msg= "Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [_v for _v in self.server.handleRequest()] response = self.strip_version_xmlns(header + body) expected = self.strip_version_xmlns( b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' ) self.assertEqual(response, expected) expected = b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = self.strip_version_xmlns( b'<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' ) self.assertEqual(self.strip_version_xmlns(body), expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody( 'Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [ _v for _v in self.server.handleRequest('service=simple') ] response = header + body expected = b'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [ _v for _v in self.server.handleRequest('service=simple') ] response = header + body expected = b'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request, extra=None, reference_file=None): project = self.testdata_path + "test_project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % ( urllib.parse.quote(project), request) if extra is not None: query_string += extra header, body = self.server.handleRequest(query_string) response = header + body reference_path = self.testdata_path + ( request.lower() if not reference_file else reference_file) + '.txt' f = open(reference_path, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(reference_path, 'wb+') f.write(response) f.close() f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() #""" response = re.sub(RE_STRIP_UNCHECKABLE, b'*****', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'*****', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace( b'typeName="Integer64" precision="0" length="10" editType="TextEdit" type="qlonglong"', b'typeName="Integer" precision="0" length="10" editType="TextEdit" type="int"' ) self.assertXMLEqual( response, expected, msg= "request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # Test getfeatureinfo response self.wms_request_compare( 'GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'info_format=text%2Fhtml&transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-html') # Test getfeatureinfo default info_format self.wms_request_compare( 'GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-plain') # Regression for #8656 # Mind the gap! (the space in the FILTER expression) self.wms_request_compare( 'GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&' + 'INFO_FORMAT=text%2Fxml&' + 'width=600&height=400&srs=EPSG%3A3857&' + 'query_layers=testlayer%20%C3%A8%C3%A9&' + 'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\''), 'wms_getfeatureinfo_filter') def wms_inspire_request_compare(self, request): """WMS INSPIRE tests""" project = self.testdata_path + "test_project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % ( urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual( response, expected, msg= "request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities', ): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % ( urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace( b'<element type="long" name="id"/>', b'<element type="integer" name="id"/>') self.assertXMLEqual( response, expected, msg= "request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wfs(self): """Test some WFS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % ( urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.result_compare( 'wfs_getfeature_' + requestid + '.txt', "request %s failed.\n Query: %s" % ( query_string, request, ), header, body) def result_compare(self, file_name, error_msg_header, header, body): self.assert_headers(header, body) response = header + body f = open(self.testdata_path + file_name, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="%s\n Expected:\n%s\n\n Response:\n%s" % (error_msg_header, str(expected, errors='replace'), str(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', 'GetFeature&TYPENAME=testlayer')) tests.append( ('startindex2', 'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) tests.append(('limit2', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=2')) tests.append( ('start1_limit1', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=1&STARTINDEX=1')) for id, req in tests: self.wfs_getfeature_compare(id, req) def wfs_getfeature_post_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP={}'.format(urllib.parse.quote(project)) self.server.putenv("REQUEST_METHOD", "POST") self.server.putenv("REQUEST_BODY", request) header, body = self.server.handleRequest(query_string) self.server.putenv("REQUEST_METHOD", '') self.server.putenv("REQUEST_BODY", '') self.result_compare( 'wfs_getfeature_{}.txt'.format(requestid), "GetFeature in POST for '{}' failed.".format(requestid), header, body, ) def test_getfeature_post(self): template = """<?xml version="1.0" encoding="UTF-8"?> <wfs:GetFeature service="WFS" version="1.0.0" {} xmlns:wfs="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"> <wfs:Query typeName="testlayer" xmlns:feature="http://www.qgis.org/gml"> <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> <ogc:BBOX> <ogc:PropertyName>geometry</ogc:PropertyName> <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> <gml:lowerCorner>8 44</gml:lowerCorner> <gml:upperCorner>9 45</gml:upperCorner> </gml:Envelope> </ogc:BBOX> </ogc:Filter> </wfs:Query> </wfs:GetFeature> """ tests = [] tests.append(('nobbox', template.format(""))) tests.append(('startindex2', template.format('startIndex="2"'))) tests.append(('limit2', template.format('maxFeatures="2"'))) tests.append(('start1_limit1', template.format('startIndex="1" maxFeatures="1"'))) for id, req in tests: self.wfs_getfeature_post_compare(id, req) def test_getLegendGraphics(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': 'testlayer%20èé', } qs = '&'.join(["%s=%s" % (k, v) for k, v in parms.items()]) print(qs) h, r = self.server.handleRequest(qs) self.assertEqual(-1, h.find(b'Content-Type: text/xml; charset=utf-8'), "Header: %s\nResponse:\n%s" % (h, r)) self.assertNotEqual(-1, h.find(b'Content-Type: image/png'), "Header: %s\nResponse:\n%s" % (h, r)) def test_getLegendGraphics_layertitle(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'TRUE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test", 250, QSize(10, 10)) parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'FALSE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test_layertitle_false", 250, QSize(10, 10)) def _result(self, data): headers = {} for line in data[0].decode('UTF-8').split("\n"): if line != "": header = line.split(":") self.assertEqual(len(header), 2, line) headers[str(header[0])] = str(header[1]).strip() return data[1], headers def _img_diff(self, image, control_image, max_diff, max_size_diff=QSize()): temp_image = os.path.join(tempfile.gettempdir(), "%s_result.png" % control_image) with open(temp_image, "wb") as f: f.write(image) control = QgsRenderChecker() control.setControlPathPrefix("qgis_server") control.setControlName(control_image) control.setRenderedImage(temp_image) if max_size_diff.isValid(): control.setSizeTolerance(max_size_diff.width(), max_size_diff.height()) return control.compareImages(control_image), control.report() def _img_diff_error(self, response, headers, image, max_diff=10, max_size_diff=QSize()): self.assertEqual( headers.get("Content-Type"), "image/png", "Content type is wrong: %s" % headers.get("Content-Type")) test, report = self._img_diff(response, image, max_diff, max_size_diff) with open(os.path.join(tempfile.gettempdir(), image + "_result.png"), "rb") as rendered_file: encoded_rendered_file = base64.b64encode(rendered_file.read()) message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.png" % ( report, encoded_rendered_file.strip(), tempfile.gettempdir(), image) # If the failure is in image sizes the diff file will not exists. if os.path.exists( os.path.join(tempfile.gettempdir(), image + "_result_diff.png")): with open( os.path.join(tempfile.gettempdir(), image + "_result_diff.png"), "rb") as diff_file: encoded_diff_file = base64.b64encode(diff_file.read()) message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.png" % ( encoded_diff_file.strip(), tempfile.gettempdir(), image) self.assertTrue(test, message)
class TestQgsServer(unittest.TestCase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole response = str(self.server.handleRequest()) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) # Test header response = str(self.server.handleRequestGetHeaders()) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(response, expected) # Test body response = str(self.server.handleRequestGetBody()) expected = '<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print "QGIS Server plugins are not compiled. Skipping test" return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('Hello from SimpleServer!') serverIface = self.server.serverInterface() serverIface.registerFilter(SimpleHelloFilter(serverIface), 100 ) response = str(self.server.handleRequest('service=simple')) expected = 'Content-type: text/plain\n\nHello from SimpleServer!' self.assertEqual(response, expected) ## WMS tests def wms_request_compare(self, request): map = self.testdata_path + "testproject.qgs" query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (map, request) response = str(self.server.handleRequest(query_string)) f = open(self.testdata_path + request.lower() + '.txt') expected = f.read() f.close() # Store for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg="request %s failed. Expected:\n%s\n\nResponse:\n%s" % (request, expected, response)) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request)
class TestQgsServer(unittest.TestCase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole response = str(self.server.handleRequest()) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) # Test header response = str(self.server.handleRequestGetHeaders()) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(response, expected) # Test body response = str(self.server.handleRequestGetBody()) expected = '<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print "QGIS Server plugins are not compiled. Skipping test" return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('Hello from SimpleServer!') serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!') class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!') filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) response = str(self.server.handleRequest('service=simple')) expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) response = str(self.server.handleRequest('service=simple')) expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) ## WMS tests def wms_request_compare(self, request): project = self.testdata_path + "test+project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (urllib.quote(project), request) response = str(self.server.handleRequest(query_string)) f = open(self.testdata_path + request.lower() + '.txt') expected = f.read() f.close() # Store for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request)
class TestQgsServer(unittest.TestCase): def assertXMLEqual(self, response, expected, msg=''): """Compare XML line by line and sorted attributes""" response_lines = response.splitlines() expected_lines = expected.splitlines() line_no = 1 for expected_line in expected_lines: expected_line = expected_line.strip() response_line = response_lines[line_no - 1].strip() # Compare tag try: self.assertEqual(re.findall(b'<([^>\s]+)[ >]', expected_line)[0], re.findall(b'<([^>\s]+)[ >]', response_line)[0], msg=msg + "\nTag mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) except IndexError: self.assertEqual(expected_line, response_line, msg=msg + "\nTag line mismatch %s: %s != %s" % (line_no, expected_line, response_line)) #print("---->%s\t%s == %s" % (line_no, expected_line, response_line)) # Compare attributes if re.match(RE_ATTRIBUTES, expected_line): # has attrs expected_attrs = re.findall(RE_ATTRIBUTES, expected_line) expected_attrs.sort() response_attrs = re.findall(RE_ATTRIBUTES, response_line) response_attrs.sort() self.assertEqual(expected_attrs, response_attrs, msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format(line_no, expected_attrs, response_attrs)) line_no += 1 @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def strip_version_xmlns(self, text): """Order of attributes is random, strip version and xmlns""" return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'') def assert_headers(self, header, body): stream = StringIO() header_string = header.decode('utf-8') stream.write(header_string) headers = email.message_from_string(header_string) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual(content_length, body_length, msg="Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [_v for _v in self.server.handleRequest()] response = self.strip_version_xmlns(header + body) expected = self.strip_version_xmlns(b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n') self.assertEqual(response, expected) expected = b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = self.strip_version_xmlns(b'<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n') self.assertEqual(self.strip_version_xmlns(body), expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self.server.handleRequest('service=simple')] response = header + body expected = b'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self.server.handleRequest('service=simple')] response = header + body expected = b'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request, extra=None, reference_file=None): project = self.testdata_path + "test_project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (urllib.parse.quote(project), request) if extra is not None: query_string += extra header, body = self.server.handleRequest(query_string) response = header + body reference_path = self.testdata_path + (request.lower() if not reference_file else reference_file) + '.txt' f = open(reference_path, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(reference_path, 'wb+') f.write(response) f.close() f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() #""" response = re.sub(RE_STRIP_UNCHECKABLE, b'*****', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'*****', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace(b'typeName="Integer64" precision="0" length="10" editType="TextEdit" type="qlonglong"', b'typeName="Integer" precision="0" length="10" editType="TextEdit" type="int"') self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # Test getfeatureinfo response self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'info_format=text%2Fhtml&transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-html') # Test getfeatureinfo default info_format self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-plain') # Regression for #8656 # Mind the gap! (the space in the FILTER expression) self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&' + 'INFO_FORMAT=text%2Fxml&' + 'width=600&height=400&srs=EPSG%3A3857&' + 'query_layers=testlayer%20%C3%A8%C3%A9&' + 'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\''), 'wms_getfeatureinfo_filter') def wms_inspire_request_compare(self, request): """WMS INSPIRE tests""" project = self.testdata_path + "test_project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities',): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace(b'<element type="long" name="id"/>', b'<element type="integer" name="id"/>') self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wfs(self): """Test some WFS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.result_compare( 'wfs_getfeature_' + requestid + '.txt', "request %s failed.\n Query: %s" % ( query_string, request, ), header, body ) def result_compare(self, file_name, error_msg_header, header, body): self.assert_headers(header, body) response = header + body f = open(self.testdata_path + file_name, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="%s\n Expected:\n%s\n\n Response:\n%s" % (error_msg_header, str(expected, errors='replace'), str(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', 'GetFeature&TYPENAME=testlayer')) tests.append(('startindex2', 'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) tests.append(('limit2', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=2')) tests.append(('start1_limit1', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=1&STARTINDEX=1')) for id, req in tests: self.wfs_getfeature_compare(id, req) def wfs_getfeature_post_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP={}'.format(urllib.parse.quote(project)) self.server.putenv("REQUEST_METHOD", "POST") self.server.putenv("REQUEST_BODY", request) header, body = self.server.handleRequest(query_string) self.server.putenv("REQUEST_METHOD", '') self.server.putenv("REQUEST_BODY", '') self.result_compare( 'wfs_getfeature_{}.txt'.format(requestid), "GetFeature in POST for '{}' failed.".format(requestid), header, body, ) def test_getfeature_post(self): template = """<?xml version="1.0" encoding="UTF-8"?> <wfs:GetFeature service="WFS" version="1.0.0" {} xmlns:wfs="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"> <wfs:Query typeName="testlayer" xmlns:feature="http://www.qgis.org/gml"> <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> <ogc:BBOX> <ogc:PropertyName>geometry</ogc:PropertyName> <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> <gml:lowerCorner>8 44</gml:lowerCorner> <gml:upperCorner>9 45</gml:upperCorner> </gml:Envelope> </ogc:BBOX> </ogc:Filter> </wfs:Query> </wfs:GetFeature> """ tests = [] tests.append(('nobbox', template.format(""))) tests.append(('startindex2', template.format('startIndex="2"'))) tests.append(('limit2', template.format('maxFeatures="2"'))) tests.append(('start1_limit1', template.format('startIndex="1" maxFeatures="1"'))) for id, req in tests: self.wfs_getfeature_post_compare(id, req) def test_getLegendGraphics(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': 'testlayer%20èé', } qs = '&'.join(["%s=%s" % (k, v) for k, v in parms.items()]) print(qs) h, r = self.server.handleRequest(qs) self.assertEqual(-1, h.find(b'Content-Type: text/xml; charset=utf-8'), "Header: %s\nResponse:\n%s" % (h, r)) self.assertNotEqual(-1, h.find(b'Content-Type: image/png'), "Header: %s\nResponse:\n%s" % (h, r)) def test_getLegendGraphics_layertitle(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'TRUE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test", 250, QSize(10, 10)) parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'FALSE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test_layertitle_false", 250, QSize(10, 10)) def _result(self, data): headers = {} for line in data[0].decode('UTF-8').split("\n"): if line != "": header = line.split(":") self.assertEqual(len(header), 2, line) headers[str(header[0])] = str(header[1]).strip() return data[1], headers def _img_diff(self, image, control_image, max_diff, max_size_diff=QSize()): temp_image = os.path.join(tempfile.gettempdir(), "%s_result.png" % control_image) with open(temp_image, "wb") as f: f.write(image) control = QgsRenderChecker() control.setControlPathPrefix("qgis_server") control.setControlName(control_image) control.setRenderedImage(temp_image) if max_size_diff.isValid(): control.setSizeTolerance(max_size_diff.width(), max_size_diff.height()) return control.compareImages(control_image), control.report() def _img_diff_error(self, response, headers, image, max_diff=10, max_size_diff=QSize()): self.assertEqual( headers.get("Content-Type"), "image/png", "Content type is wrong: %s" % headers.get("Content-Type")) test, report = self._img_diff(response, image, max_diff, max_size_diff) with open(os.path.join(tempfile.gettempdir(), image + "_result.png"), "rb") as rendered_file: encoded_rendered_file = base64.b64encode(rendered_file.read()) message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.png" % ( report, encoded_rendered_file.strip(), tempfile.gettempdir(), image ) with open(os.path.join(tempfile.gettempdir(), image + "_result_diff.png"), "rb") as diff_file: encoded_diff_file = base64.b64encode(diff_file.read()) message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.png" % ( encoded_diff_file.strip(), tempfile.gettempdir(), image ) self.assertTrue(test, message)
class TestQgsServer(unittest.TestCase): def assertXMLEqual(self, response, expected, msg=''): """Compare XML line by line and sorted attributes""" response_lines = response.splitlines() expected_lines = expected.splitlines() line_no = 1 for expected_line in expected_lines: expected_line = expected_line.strip() response_line = response_lines[line_no - 1].strip() # Compare tag try: self.assertEqual(re.findall(b'<([^>\s]+)[ >]', expected_line)[0], re.findall(b'<([^>\s]+)[ >]', response_line)[0], msg=msg + "\nTag mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) except IndexError: self.assertEqual(expected_line, response_line, msg=msg + "\nTag line mismatch %s: %s != %s" % (line_no, expected_line, response_line)) # print("---->%s\t%s == %s" % (line_no, expected_line, response_line)) # Compare attributes if re.match(RE_ATTRIBUTES, expected_line): # has attrs expected_attrs = sorted(re.findall(RE_ATTRIBUTES, expected_line)) response_attrs = sorted(re.findall(RE_ATTRIBUTES, response_line)) self.assertEqual(expected_attrs, response_attrs, msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format(line_no, expected_attrs, response_attrs)) line_no += 1 @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def strip_version_xmlns(self, text): """Order of attributes is random, strip version and xmlns""" return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'') def assert_headers(self, header, body): stream = StringIO() header_string = header.decode('utf-8') stream.write(header_string) headers = email.message_from_string(header_string) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual(content_length, body_length, msg="Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest("") def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [_v for _v in self.server.handleRequest("")] response = self.strip_version_xmlns(header + body) expected = self.strip_version_xmlns(b'Content-Length: 54\nContent-Type: text/xml; charset=utf-8\n\n<ServerException>Project file error</ServerException>\n') self.assertEqual(response, expected) expected = b'Content-Length: 54\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test response when project is specified but without service project = self.testdata_path + "test_project_wfs.qgs" qs = 'MAP=%s' % (urllib.parse.quote(project)) header, body = [_v for _v in self.server.handleRequest(qs)] response = self.strip_version_xmlns(header + body) expected = self.strip_version_xmlns(b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n') self.assertEqual(response, expected) expected = b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = self.strip_version_xmlns(b'<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n') self.assertEqual(self.strip_version_xmlns(body), expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clear() request.setHeader('Content-type', 'text/plain') request.appendBody('Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self.server.handleRequest('service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self.server.handleRequest('service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request, extra=None, reference_file=None): project = self.testdata_path + "test_project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (urllib.parse.quote(project), request) if extra is not None: query_string += extra header, body = self.server.handleRequest(query_string) response = header + body reference_path = self.testdata_path + (request.lower() if not reference_file else reference_file) + '.txt' f = open(reference_path, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(reference_path, 'wb+') f.write(response) f.close() f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() #""" response = re.sub(RE_STRIP_UNCHECKABLE, b'*****', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'*****', expected) self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # Test getfeatureinfo response self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'info_format=text%2Fhtml&transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-html') # Test getfeatureinfo default info_format self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-plain') # Regression for #8656 # Mind the gap! (the space in the FILTER expression) self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&' + 'INFO_FORMAT=text%2Fxml&' + 'width=600&height=400&srs=EPSG%3A3857&' + 'query_layers=testlayer%20%C3%A8%C3%A9&' + 'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\''), 'wms_getfeatureinfo_filter') def wms_inspire_request_compare(self, request): """WMS INSPIRE tests""" project = self.testdata_path + "test_project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities',): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wfs(self): """Test some WFS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.result_compare( 'wfs_getfeature_' + requestid + '.txt', "request %s failed.\n Query: %s" % ( query_string, request, ), header, body ) def test_wfs_getcapabilities_url(self): # empty url in project project = os.path.join(self.testdata_path, "test_project_without_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WFS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) for item in str(r).split("\\n"): if "onlineResource" in item: self.assertEqual("onlineResource=\"?" in item, True) # url well defined in project project = os.path.join(self.testdata_path, "test_project_with_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WFS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) for item in str(r).split("\\n"): if "onlineResource" in item: print("onlineResource: ", item) self.assertEqual("onlineResource=\"my_wfs_advertised_url\"" in item, True) def result_compare(self, file_name, error_msg_header, header, body): self.assert_headers(header, body) response = header + body f = open(self.testdata_path + file_name, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="%s\n Expected:\n%s\n\n Response:\n%s" % (error_msg_header, str(expected, errors='replace'), str(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', 'GetFeature&TYPENAME=testlayer')) tests.append(('startindex2', 'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) tests.append(('limit2', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=2')) tests.append(('start1_limit1', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=1&STARTINDEX=1')) for id, req in tests: self.wfs_getfeature_compare(id, req) def wfs_getfeature_post_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP={}'.format(urllib.parse.quote(project)) self.server.putenv("REQUEST_METHOD", "POST") self.server.putenv("REQUEST_BODY", request) header, body = self.server.handleRequest(query_string) self.server.putenv("REQUEST_METHOD", '') self.server.putenv("REQUEST_BODY", '') self.result_compare( 'wfs_getfeature_{}.txt'.format(requestid), "GetFeature in POST for '{}' failed.".format(requestid), header, body, ) def test_getfeature_post(self): template = """<?xml version="1.0" encoding="UTF-8"?> <wfs:GetFeature service="WFS" version="1.0.0" {} xmlns:wfs="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"> <wfs:Query typeName="testlayer" xmlns:feature="http://www.qgis.org/gml"> <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> <ogc:BBOX> <ogc:PropertyName>geometry</ogc:PropertyName> <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> <gml:lowerCorner>8 44</gml:lowerCorner> <gml:upperCorner>9 45</gml:upperCorner> </gml:Envelope> </ogc:BBOX> </ogc:Filter> </wfs:Query> </wfs:GetFeature> """ tests = [] tests.append(('nobbox', template.format(""))) tests.append(('startindex2', template.format('startIndex="2"'))) tests.append(('limit2', template.format('maxFeatures="2"'))) tests.append(('start1_limit1', template.format('startIndex="1" maxFeatures="1"'))) for id, req in tests: self.wfs_getfeature_post_compare(id, req) def test_wms_getmap_basic(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Basic") def test_wms_getmap_transparent(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "TRANSPARENT": "TRUE" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Transparent") def test_wms_getmap_background(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "BGCOLOR": "green" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Background") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "BGCOLOR": "0x008000" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Background_Hex") def test_wms_getcapabilities_url(self): # empty url in project project = os.path.join(self.testdata_path, "test_project_without_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WMS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) item_found = False for item in str(r).split("\\n"): if "OnlineResource" in item: self.assertEqual("xlink:href=\"?" in item, True) item_found = True self.assertTrue(item_found) # url well defined in project project = os.path.join(self.testdata_path, "test_project_with_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WMS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) item_found = False for item in str(r).split("\\n"): if "OnlineResource" in item: self.assertEqual("xlink:href=\"my_wms_advertised_url?" in item, True) item_found = True self.assertTrue(item_found) def test_wms_getmap_invalid_size(self): project = os.path.join(self.testdata_path, "test_project_with_size.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WMS", "VERSION": "1.3.0", "REQUEST": "GetMap", "LAYERS": "Hello", "STYLES": "", "FORMAT": "image/png", "HEIGHT": "5001", "WIDTH": "5000" }.items())]) expected = self.strip_version_xmlns(b'<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Size error">The requested map size is too large</ServiceException>\n</ServiceExceptionReport>\n') r, h = self._result(self.server.handleRequest(qs)) self.assertEqual(self.strip_version_xmlns(r), expected) def test_wms_getmap_order(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Hello,Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_LayerOrder") def test_wms_getmap_srs(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-151.7,-38.9,51.0,78.0", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:4326" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_SRS") def test_wms_getmap_style(self): # default style qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country_Labels", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_StyleDefault") # custom style qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country_Labels", "STYLES": "custom", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_StyleCustom") def test_wms_getmap_filter(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "FILTER": "Country:\"name\" = 'eurasia'" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Filter") def test_wms_getmap_selection(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "SRS": "EPSG:3857", "SELECTION": "Country: 4" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Selection") def test_wms_getmap_opacities(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "OPACITIES": "125, 50" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Opacities") def test_wms_getprint_basic(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Basic") @unittest.skip('Randomly failing to draw the map layer') def test_wms_getprint_srs(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-309.015,-133.011,312.179,133.949", "map0:LAYERS": "Country,Hello", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:4326" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_SRS") def test_wms_getprint_scale(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "map0:SCALE": "36293562", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Scale") def test_wms_getprint_grid(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "map0:GRID_INTERVAL_X": "1000000", "map0:GRID_INTERVAL_Y": "2000000", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Grid") def test_wms_getprint_rotation(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "map0:ROTATION": "45", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Rotation") def test_wms_getprint_selection(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "SELECTION": "Country: 4" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Selection") def test_getLegendGraphics(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', # 'WIDTH': '20', # optional # 'HEIGHT': '20', # optional 'LAYER': 'testlayer%20èé', } qs = '&'.join(["%s=%s" % (k, v) for k, v in parms.items()]) h, r = self.server.handleRequest(qs) self.assertEqual(-1, h.find(b'Content-Type: text/xml; charset=utf-8'), "Header: %s\nResponse:\n%s" % (h, r)) self.assertNotEqual(-1, h.find(b'Content-Type: image/png'), "Header: %s\nResponse:\n%s" % (h, r)) def test_getLegendGraphics_layertitle(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', # 'WIDTH': '20', # optional # 'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'TRUE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test", 250, QSize(15, 15)) parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', # 'WIDTH': '20', # optional # 'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'FALSE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test_layertitle_false", 250, QSize(15, 15)) def test_wms_GetLegendGraphic_Basic(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Basic") def test_wms_GetLegendGraphic_Transparent(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "TRANSPARENT": "TRUE" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Transparent") def test_wms_GetLegendGraphic_Background(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "BGCOLOR": "green" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Background") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "BGCOLOR": "0x008000" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Background_Hex") def test_wms_GetLegendGraphic_BoxSpace(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "BOXSPACE": "100", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_BoxSpace") def test_wms_GetLegendGraphic_SymbolSpace(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "SYMBOLSPACE": "100", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_SymbolSpace") def test_wms_GetLegendGraphic_IconLabelSpace(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "ICONLABELSPACE": "100", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_IconLabelSpace") def test_wms_GetLegendGraphic_SymbolSize(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "SYMBOLWIDTH": "50", "SYMBOLHEIGHT": "30", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_SymbolSize") def test_wms_GetLegendGraphic_BBox(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello,db_point", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "BBOX": "-151.7,-38.9,51.0,78.0", "CRS": "EPSG:4326" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_BBox") def test_wms_GetLegendGraphic_BBox2(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello,db_point", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "BBOX": "-76.08,-6.4,-19.38,38.04", "SRS": "EPSG:4326" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_BBox2") # WCS tests def wcs_request_compare(self, request): project = self.projectPath assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WCS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wcs_' + request.lower() + '.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.join( tempfile.gettempdir(), 'wcs_' + request.lower() + '_expected.txt' ), 'w+') f.write(expected) f.close() f = open(os.path.join( tempfile.gettempdir(), 'wcs_' + request.lower() + '_response.txt'), 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wcs(self): """Test some WCS request""" for request in ('GetCapabilities', 'DescribeCoverage'): self.wcs_request_compare(request) def test_wcs_getcapabilities_url(self): # empty url in project project = os.path.join(self.testdata_path, "test_project_without_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WCS", "VERSION": "1.0.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) item_found = False for item in str(r).split("\\n"): if "OnlineResource" in item: self.assertEqual("=\"?" in item, True) item_found = True self.assertTrue(item_found) # url well defined in project project = os.path.join(self.testdata_path, "test_project_with_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WCS", "VERSION": "1.0.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) item_found = False for item in str(r).split("\\n"): if "OnlineResource" in item: print("OnlineResource: ", item) self.assertEqual("\"my_wcs_advertised_url" in item, True) item_found = True self.assertTrue(item_found) def _result(self, data): headers = {} for line in data[0].decode('UTF-8').split("\n"): if line != "": header = line.split(":") self.assertEqual(len(header), 2, line) headers[str(header[0])] = str(header[1]).strip() return data[1], headers def _img_diff(self, image, control_image, max_diff, max_size_diff=QSize()): temp_image = os.path.join(tempfile.gettempdir(), "%s_result.png" % control_image) with open(temp_image, "wb") as f: f.write(image) control = QgsRenderChecker() control.setControlPathPrefix("qgis_server") control.setControlName(control_image) control.setRenderedImage(temp_image) if max_size_diff.isValid(): control.setSizeTolerance(max_size_diff.width(), max_size_diff.height()) return control.compareImages(control_image), control.report() def _img_diff_error(self, response, headers, image, max_diff=10, max_size_diff=QSize()): self.assertEqual( headers.get("Content-Type"), "image/png", "Content type is wrong: %s" % headers.get("Content-Type")) test, report = self._img_diff(response, image, max_diff, max_size_diff) with open(os.path.join(tempfile.gettempdir(), image + "_result.png"), "rb") as rendered_file: encoded_rendered_file = base64.b64encode(rendered_file.read()) message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.png" % ( report, encoded_rendered_file.strip(), tempfile.gettempdir(), image ) # If the failure is in image sizes the diff file will not exists. if os.path.exists(os.path.join(tempfile.gettempdir(), image + "_result_diff.png")): with open(os.path.join(tempfile.gettempdir(), image + "_result_diff.png"), "rb") as diff_file: encoded_diff_file = base64.b64encode(diff_file.read()) message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.png" % ( encoded_diff_file.strip(), tempfile.gettempdir(), image ) self.assertTrue(test, message)
class HTTPBasicFilter(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() if self.serverInterface().getEnv('HTTP_AUTHORIZATION'): username, password = base64.b64decode(self.serverInterface().getEnv('HTTP_AUTHORIZATION')[6:]).split(b':') if (username.decode('utf-8') == os.environ.get('QGIS_SERVER_USERNAME', 'username') and password.decode('utf-8') == os.environ.get('QGIS_SERVER_PASSWORD', 'password')): return # No auth ... request.clear() request.setHeader('Status', '401 Authorization required') request.setHeader('WWW-Authenticate', 'Basic realm="QGIS Server"') request.appendBody(b'<h1>Authorization required</h1>') filter = HTTPBasicFilter(qgs_server.serverInterface()) qgs_server.serverInterface().registerFilter(filter) class Handler(BaseHTTPRequestHandler): def do_GET(self): # CGI vars: for k, v in self.headers.items(): qgs_server.putenv('HTTP_%s' % k.replace(' ', '-').replace('-', '_').replace(' ', '-').upper(), v) qgs_server.putenv('SERVER_PORT', str(self.server.server_port)) if https: qgs_server.putenv('HTTPS', 'ON') qgs_server.putenv('SERVER_NAME', self.server.server_name) qgs_server.putenv('REQUEST_URI', self.path) parsed_path = urllib.parse.urlparse(self.path)
and os.path.isfile(QGIS_SERVER_PKI_CERTIFICATE) and QGIS_SERVER_PKI_KEY is not None and os.path.isfile(QGIS_SERVER_PKI_KEY) and QGIS_SERVER_PKI_AUTHORITY is not None and os.path.isfile(QGIS_SERVER_PKI_AUTHORITY) and QGIS_SERVER_PKI_USERNAME) qgs_app = QgsApplication([], False) qgs_server = QgsServer() # Load plugins if os.environ.get('QGIS_PLUGINPATH', False): plugindir = os.environ.get('QGIS_PLUGINPATH') sys.path.append(plugindir) # Magic mistery: without this, virtual QgsServerFilter instances are not wrapped iface = qgs_server.serverInterface() try: for modulename in [(a, b, c) for a, b, c in os.walk(plugindir)][0][1]: try: module = importlib.import_module(modulename) getattr(module, 'serverClassFactory')(iface) QgsMessageLog.logMessage('Python plugin %s loaded!' % modulename) except: QgsMessageLog.logMessage('Could not load Python plugin %s!' % modulename) except: QgsMessageLog.logMessage('No plugins found in %s!' % plugindir) class Handler(BaseHTTPRequestHandler):