1# -*- coding: utf-8 -*- 2"""QGIS Unit tests for QgsServer WFS. 3 4From build dir, run: ctest -R PyQgsServerWFS -V 5 6 7.. note:: This program is free software; you can redistribute it and/or modify 8it under the terms of the GNU General Public License as published by 9the Free Software Foundation; either version 2 of the License, or 10(at your option) any later version. 11 12""" 13__author__ = 'René-Luc Dhont' 14__date__ = '19/09/2017' 15__copyright__ = 'Copyright 2017, The QGIS Project' 16 17import os 18 19# Needed on Qt 5 so that the serialization of XML is consistent among all executions 20os.environ['QT_HASH_SEED'] = '1' 21 22import re 23import urllib.request 24import urllib.parse 25import urllib.error 26 27from qgis.server import QgsServerRequest 28 29from qgis.testing import unittest 30from qgis.PyQt.QtCore import QSize 31from qgis.core import ( 32 QgsVectorLayer, 33 QgsFeatureRequest, 34 QgsExpression, 35 QgsCoordinateReferenceSystem, 36 QgsCoordinateTransform, 37 QgsCoordinateTransformContext, 38 QgsGeometry, 39) 40 41import osgeo.gdal # NOQA 42 43from test_qgsserver import QgsServerTestBase 44 45# Strip path and content length because path may vary 46RE_STRIP_UNCHECKABLE = br'MAP=[^"]+|Content-Length: \d+|timeStamp="[^"]+"' 47RE_ATTRIBUTES = br'[^>\s]+=[^>\s]+' 48 49 50class TestQgsServerWFS(QgsServerTestBase): 51 """QGIS Server WFS Tests""" 52 53 # Set to True in child classes to re-generate reference files for this class 54 regenerate_reference = False 55 56 def wfs_request_compare(self, 57 request, version='', 58 extra_query_string='', 59 reference_base_name=None, 60 project_file="test_project_wfs.qgs", 61 requestMethod=QgsServerRequest.GetMethod, 62 data=None): 63 project = self.testdata_path + project_file 64 assert os.path.exists(project), "Project file not found: " + project 65 66 query_string = '?MAP=%s&SERVICE=WFS&REQUEST=%s' % ( 67 urllib.parse.quote(project), request) 68 if version: 69 query_string += '&VERSION=%s' % version 70 71 if extra_query_string: 72 query_string += '&%s' % extra_query_string 73 74 header, body = self._execute_request( 75 query_string, requestMethod=requestMethod, data=data) 76 self.assert_headers(header, body) 77 response = header + body 78 79 if reference_base_name is not None: 80 reference_name = reference_base_name 81 else: 82 reference_name = 'wfs_' + request.lower() 83 84 if version == '1.0.0': 85 reference_name += '_1_0_0' 86 reference_name += '.txt' 87 88 reference_path = self.testdata_path + reference_name 89 90 self.store_reference(reference_path, response) 91 f = open(reference_path, 'rb') 92 expected = f.read() 93 f.close() 94 response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) 95 expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) 96 97 self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s" % ( 98 query_string, request)) 99 return header, body 100 101 def test_operation_not_supported(self): 102 qs = '?MAP=%s&SERVICE=WFS&VERSION=1.1.0&REQUEST=NotAValidRequest' % urllib.parse.quote(self.projectPath) 103 self._assert_status_code(501, qs) 104 105 def test_project_wfs(self): 106 """Test some WFS request""" 107 108 for request in ('GetCapabilities', 'DescribeFeatureType'): 109 self.wfs_request_compare(request) 110 self.wfs_request_compare(request, '1.0.0') 111 112 def wfs_getfeature_compare(self, requestid, request): 113 114 project = self.testdata_path + "test_project_wfs.qgs" 115 assert os.path.exists(project), "Project file not found: " + project 116 117 query_string = '?MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % ( 118 urllib.parse.quote(project), request) 119 header, body = self._execute_request(query_string) 120 121 if requestid == 'hits': 122 body = re.sub(br'timeStamp="\d+-\d+-\d+T\d+:\d+:\d+"', 123 b'timeStamp="****-**-**T**:**:**"', body) 124 125 self.result_compare( 126 'wfs_getfeature_' + requestid + '.txt', 127 "request %s failed.\n Query: %s" % ( 128 query_string, 129 request, 130 ), 131 header, body 132 ) 133 134 def test_getfeature(self): 135 136 tests = [] 137 tests.append(('nobbox', 'GetFeature&TYPENAME=testlayer')) 138 tests.append( 139 ('startindex2', 'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) 140 tests.append(('limit2', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=2')) 141 tests.append( 142 ('start1_limit1', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=1&STARTINDEX=1')) 143 tests.append( 144 ('srsname', 'GetFeature&TYPENAME=testlayer&SRSNAME=EPSG:3857')) 145 tests.append(('sortby', 'GetFeature&TYPENAME=testlayer&SORTBY=id D')) 146 tests.append(('hits', 'GetFeature&TYPENAME=testlayer&RESULTTYPE=hits')) 147 148 for id, req in tests: 149 self.wfs_getfeature_compare(id, req) 150 151 def test_wfs_getcapabilities_100_url(self): 152 """Check that URL in GetCapabilities response is complete""" 153 154 # empty url in project 155 project = os.path.join( 156 self.testdata_path, "test_project_without_urls.qgs") 157 qs = "?" + "&".join(["%s=%s" % i for i in list({ 158 "MAP": urllib.parse.quote(project), 159 "SERVICE": "WFS", 160 "VERSION": "1.0.0", 161 "REQUEST": "GetCapabilities" 162 }.items())]) 163 164 r, h = self._result(self._execute_request(qs)) 165 166 for item in str(r).split("\\n"): 167 if "onlineResource" in item: 168 self.assertEqual("onlineResource=\"?" in item, True) 169 170 # url well defined in query string 171 project = os.path.join( 172 self.testdata_path, "test_project_without_urls.qgs") 173 qs = "https://www.qgis-server.org?" + "&".join(["%s=%s" % i for i in list({ 174 "MAP": urllib.parse.quote(project), 175 "SERVICE": "WFS", 176 "VERSION": "1.0.0", 177 "REQUEST": "GetCapabilities" 178 }.items())]) 179 180 r, h = self._result(self._execute_request(qs)) 181 182 for item in str(r).split("\\n"): 183 if "onlineResource" in item: 184 self.assertTrue( 185 "onlineResource=\"https://www.qgis-server.org?" in item, True) 186 187 # url well defined in project 188 project = os.path.join( 189 self.testdata_path, "test_project_with_urls.qgs") 190 qs = "?" + "&".join(["%s=%s" % i for i in list({ 191 "MAP": urllib.parse.quote(project), 192 "SERVICE": "WFS", 193 "VERSION": "1.0.0", 194 "REQUEST": "GetCapabilities" 195 }.items())]) 196 197 r, h = self._result(self._execute_request(qs)) 198 199 for item in str(r).split("\\n"): 200 if "onlineResource" in item: 201 self.assertEqual( 202 "onlineResource=\"my_wfs_advertised_url\"" in item, True) 203 204 def result_compare(self, file_name, error_msg_header, header, body): 205 206 self.assert_headers(header, body) 207 response = header + body 208 reference_path = self.testdata_path + file_name 209 self.store_reference(reference_path, response) 210 f = open(reference_path, 'rb') 211 expected = f.read() 212 f.close() 213 response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) 214 expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) 215 self.assertXMLEqual(response, expected, msg="%s\n" % 216 (error_msg_header)) 217 218 def wfs_getfeature_post_compare(self, requestid, request): 219 220 project = self.testdata_path + "test_project_wfs.qgs" 221 assert os.path.exists(project), "Project file not found: " + project 222 223 query_string = '?MAP={}'.format(urllib.parse.quote(project)) 224 header, body = self._execute_request( 225 query_string, requestMethod=QgsServerRequest.PostMethod, data=request.encode('utf-8')) 226 227 self.result_compare( 228 'wfs_getfeature_{}.txt'.format(requestid), 229 "GetFeature in POST for '{}' failed.".format(requestid), 230 header, body, 231 ) 232 233 def test_getfeature_post(self): 234 tests = [] 235 236 template = """<?xml version="1.0" encoding="UTF-8"?> 237<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"> 238 <wfs:Query typeName="testlayer" xmlns:feature="http://www.qgis.org/gml"> 239 <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> 240 <ogc:BBOX> 241 <ogc:PropertyName>geometry</ogc:PropertyName> 242 <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> 243 <gml:lowerCorner>8 44</gml:lowerCorner> 244 <gml:upperCorner>9 45</gml:upperCorner> 245 </gml:Envelope> 246 </ogc:BBOX> 247 </ogc:Filter> 248 </wfs:Query> 249</wfs:GetFeature> 250""" 251 tests.append(('nobbox_post', template.format(""))) 252 tests.append(('startindex2_post', template.format('startIndex="2"'))) 253 tests.append(('limit2_post', template.format('maxFeatures="2"'))) 254 tests.append(('start1_limit1_post', template.format( 255 'startIndex="1" maxFeatures="1"'))) 256 257 srsTemplate = """<?xml version="1.0" encoding="UTF-8"?> 258<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"> 259 <wfs:Query typeName="testlayer" srsName="EPSG:3857" xmlns:feature="http://www.qgis.org/gml"> 260 <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> 261 <ogc:BBOX> 262 <ogc:PropertyName>geometry</ogc:PropertyName> 263 <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> 264 <gml:lowerCorner>890555.92634619 5465442.18332275</gml:lowerCorner> 265 <gml:upperCorner>1001875.41713946 5621521.48619207</gml:upperCorner> 266 </gml:Envelope> 267 </ogc:BBOX> 268 </ogc:Filter> 269 </wfs:Query> 270</wfs:GetFeature> 271""" 272 tests.append(('srsname_post', srsTemplate.format(""))) 273 274 # Issue https://github.com/qgis/QGIS/issues/36398 275 # Check get feature within polygon having srsName=EPSG:4326 (same as the project/layer) 276 within4326FilterTemplate = """<?xml version="1.0" encoding="UTF-8"?> 277<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"> 278 <wfs:Query typeName="testlayer" srsName="EPSG:4326" xmlns:feature="http://www.qgis.org/gml"> 279 <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> 280 <Within> 281 <PropertyName>geometry</PropertyName> 282 <Polygon xmlns="http://www.opengis.net/gml" srsName="EPSG:4326"> 283 <exterior> 284 <LinearRing> 285 <posList srsDimension="2"> 286 8.20344131 44.90137909 287 8.20347748 44.90137909 288 8.20347748 44.90141005 289 8.20344131 44.90141005 290 8.20344131 44.90137909 291 </posList> 292 </LinearRing> 293 </exterior> 294 </Polygon> 295 </Within> 296 </ogc:Filter> 297 </wfs:Query> 298</wfs:GetFeature> 299""" 300 tests.append(('within4326FilterTemplate_post', within4326FilterTemplate.format(""))) 301 302 # Check get feature within polygon having srsName=EPSG:3857 (different from the project/layer) 303 # The coordinates are converted from the one in 4326 304 within3857FilterTemplate = """<?xml version="1.0" encoding="UTF-8"?> 305<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"> 306 <wfs:Query typeName="testlayer" srsName="EPSG:3857" xmlns:feature="http://www.qgis.org/gml"> 307 <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> 308 <Within> 309 <PropertyName>geometry</PropertyName> 310 <Polygon xmlns="http://www.opengis.net/gml" srsName="EPSG:3857"> 311 <exterior> 312 <LinearRing> 313 <posList srsDimension="2"> 314 913202.90938171 5606008.98136456 315 913206.93580769 5606008.98136456 316 913206.93580769 5606013.84701639 317 913202.90938171 5606013.84701639 318 913202.90938171 5606008.98136456 319 </posList> 320 </LinearRing> 321 </exterior> 322 </Polygon> 323 </Within> 324 </ogc:Filter> 325 </wfs:Query> 326</wfs:GetFeature> 327""" 328 tests.append(('within3857FilterTemplate_post', within3857FilterTemplate.format(""))) 329 330 srsTwoLayersTemplate = """<?xml version="1.0" encoding="UTF-8"?> 331<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"> 332 <wfs:Query typeName="testlayer" srsName="EPSG:3857" xmlns:feature="http://www.qgis.org/gml"> 333 <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> 334 <ogc:BBOX> 335 <ogc:PropertyName>geometry</ogc:PropertyName> 336 <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> 337 <gml:lowerCorner>890555.92634619 5465442.18332275</gml:lowerCorner> 338 <gml:upperCorner>1001875.41713946 5621521.48619207</gml:upperCorner> 339 </gml:Envelope> 340 </ogc:BBOX> 341 </ogc:Filter> 342 </wfs:Query> 343 <wfs:Query typeName="testlayer" srsName="EPSG:3857" xmlns:feature="http://www.qgis.org/gml"> 344 <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> 345 <ogc:BBOX> 346 <ogc:PropertyName>geometry</ogc:PropertyName> 347 <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> 348 <gml:lowerCorner>890555.92634619 5465442.18332275</gml:lowerCorner> 349 <gml:upperCorner>1001875.41713946 5621521.48619207</gml:upperCorner> 350 </gml:Envelope> 351 </ogc:BBOX> 352 </ogc:Filter> 353 </wfs:Query> 354</wfs:GetFeature> 355""" 356 tests.append(('srs_two_layers_post', srsTwoLayersTemplate.format(""))) 357 358 sortTemplate = """<?xml version="1.0" encoding="UTF-8"?> 359<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"> 360 <wfs:Query typeName="testlayer" xmlns:feature="http://www.qgis.org/gml"> 361 <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> 362 <ogc:BBOX> 363 <ogc:PropertyName>geometry</ogc:PropertyName> 364 <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> 365 <gml:lowerCorner>8 44</gml:lowerCorner> 366 <gml:upperCorner>9 45</gml:upperCorner> 367 </gml:Envelope> 368 </ogc:BBOX> 369 </ogc:Filter> 370 <ogc:SortBy> 371 <ogc:SortProperty> 372 <ogc:PropertyName>id</ogc:PropertyName> 373 <ogc:SortOrder>DESC</ogc:SortOrder> 374 </ogc:SortProperty> 375 </ogc:SortBy> 376 </wfs:Query> 377</wfs:GetFeature> 378""" 379 tests.append(('sortby_post', sortTemplate.format(""))) 380 381 andTemplate = """<?xml version="1.0" encoding="UTF-8"?> 382<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"> 383 <wfs:Query typeName="testlayer" srsName="EPSG:3857" xmlns:feature="http://www.qgis.org/gml"> 384 <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> 385 <ogc:And> 386 <ogc:PropertyIsGreaterThan> 387 <ogc:PropertyName>id</ogc:PropertyName> 388 <ogc:Literal>1</ogc:Literal> 389 </ogc:PropertyIsGreaterThan> 390 <ogc:PropertyIsLessThan> 391 <ogc:PropertyName>id</ogc:PropertyName> 392 <ogc:Literal>3</ogc:Literal> 393 </ogc:PropertyIsLessThan> 394 </ogc:And> 395 </ogc:Filter> 396 </wfs:Query> 397</wfs:GetFeature> 398""" 399 tests.append(('and_post', andTemplate.format(""))) 400 401 andBboxTemplate = """<?xml version="1.0" encoding="UTF-8"?> 402<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"> 403 <wfs:Query typeName="testlayer" srsName="EPSG:3857" xmlns:feature="http://www.qgis.org/gml"> 404 <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> 405 <ogc:And> 406 <ogc:BBOX> 407 <ogc:PropertyName>geometry</ogc:PropertyName> 408 <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> 409 <gml:lowerCorner>890555.92634619 5465442.18332275</gml:lowerCorner> 410 <gml:upperCorner>1001875.41713946 5621521.48619207</gml:upperCorner> 411 </gml:Envelope> 412 </ogc:BBOX> 413 <ogc:PropertyIsGreaterThan> 414 <ogc:PropertyName>id</ogc:PropertyName> 415 <ogc:Literal>1</ogc:Literal> 416 </ogc:PropertyIsGreaterThan> 417 <ogc:PropertyIsLessThan> 418 <ogc:PropertyName>id</ogc:PropertyName> 419 <ogc:Literal>3</ogc:Literal> 420 </ogc:PropertyIsLessThan> 421 </ogc:And> 422 </ogc:Filter> 423 </wfs:Query> 424</wfs:GetFeature> 425""" 426 tests.append(('bbox_inside_and_post', andBboxTemplate.format(""))) 427 428 # With namespace 429 template = """<?xml version="1.0" encoding="UTF-8"?> 430<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"> 431 <wfs:Query typeName="feature:testlayer" xmlns:feature="http://www.qgis.org/gml"> 432 <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> 433 <ogc:BBOX> 434 <ogc:PropertyName>geometry</ogc:PropertyName> 435 <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> 436 <gml:lowerCorner>8 44</gml:lowerCorner> 437 <gml:upperCorner>9 45</gml:upperCorner> 438 </gml:Envelope> 439 </ogc:BBOX> 440 </ogc:Filter> 441 </wfs:Query> 442</wfs:GetFeature> 443""" 444 tests.append(('nobbox_post', template.format(""))) 445 446 template = """<?xml version="1.0" encoding="UTF-8"?> 447<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"> 448 <wfs:Query typeName="testlayer" xmlns="http://www.qgis.org/gml"> 449 <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> 450 <ogc:BBOX> 451 <ogc:PropertyName>geometry</ogc:PropertyName> 452 <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> 453 <gml:lowerCorner>8 44</gml:lowerCorner> 454 <gml:upperCorner>9 45</gml:upperCorner> 455 </gml:Envelope> 456 </ogc:BBOX> 457 </ogc:Filter> 458 </wfs:Query> 459</wfs:GetFeature> 460""" 461 tests.append(('nobbox_post', template.format(""))) 462 463 for id, req in tests: 464 self.wfs_getfeature_post_compare(id, req) 465 466 def test_getFeatureBBOX(self): 467 """Test with (1.1.0) and without (1.0.0) CRS""" 468 469 # Tests without CRS 470 self.wfs_request_compare( 471 "GetFeature", '1.0.0', "TYPENAME=testlayer&RESULTTYPE=hits&BBOX=8.20347,44.901471,8.2035354,44.901493", 'wfs_getFeature_1_0_0_bbox_1_feature') 472 self.wfs_request_compare( 473 "GetFeature", '1.0.0', "TYPENAME=testlayer&RESULTTYPE=hits&BBOX=8.203127,44.9012765,8.204138,44.901632", 'wfs_getFeature_1_0_0_bbox_3_feature') 474 475 # Tests with CRS 476 self.wfs_request_compare( 477 "GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&RESULTTYPE=hits&BBOX=8.20347,44.901471,8.2035354,44.901493,EPSG:4326", 'wfs_getFeature_1_0_0_epsgbbox_1_feature') 478 self.wfs_request_compare( 479 "GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&RESULTTYPE=hits&BBOX=8.203127,44.9012765,8.204138,44.901632,EPSG:4326", 'wfs_getFeature_1_0_0_epsgbbox_3_feature') 480 self.wfs_request_compare( 481 "GetFeature", '1.1.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&RESULTTYPE=hits&BBOX=8.20347,44.901471,8.2035354,44.901493,EPSG:4326", 'wfs_getFeature_1_1_0_epsgbbox_1_feature') 482 self.wfs_request_compare( 483 "GetFeature", '1.1.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&RESULTTYPE=hits&BBOX=8.203127,44.9012765,8.204138,44.901632,EPSG:4326", 'wfs_getFeature_1_1_0_epsgbbox_3_feature') 484 485 self.wfs_request_compare("GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&RESULTTYPE=hits&BBOX=913144,5605992,913303,5606048,EPSG:3857", 486 'wfs_getFeature_1_0_0_epsgbbox_3_feature_3857') 487 self.wfs_request_compare("GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&RESULTTYPE=hits&BBOX=913206,5606024,913213,5606026,EPSG:3857", 488 'wfs_getFeature_1_0_0_epsgbbox_1_feature_3857') 489 self.wfs_request_compare("GetFeature", '1.1.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&RESULTTYPE=hits&BBOX=913144,5605992,913303,5606048,EPSG:3857", 490 'wfs_getFeature_1_1_0_epsgbbox_3_feature_3857') 491 self.wfs_request_compare("GetFeature", '1.1.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&RESULTTYPE=hits&BBOX=913206,5606024,913213,5606026,EPSG:3857", 492 'wfs_getFeature_1_1_0_epsgbbox_1_feature_3857') 493 494 self.wfs_request_compare("GetFeature", '1.0.0', "SRSNAME=EPSG:3857&TYPENAME=testlayer&RESULTTYPE=hits&BBOX=913144,5605992,913303,5606048,EPSG:3857", 495 'wfs_getFeature_1_0_0_epsgbbox_3_feature_3857') 496 self.wfs_request_compare("GetFeature", '1.0.0', "SRSNAME=EPSG:3857&TYPENAME=testlayer&RESULTTYPE=hits&BBOX=913206,5606024,913213,5606026,EPSG:3857", 497 'wfs_getFeature_1_0_0_epsgbbox_1_feature_3857') 498 self.wfs_request_compare("GetFeature", '1.1.0', "SRSNAME=EPSG:3857&TYPENAME=testlayer&RESULTTYPE=hits&BBOX=913144,5605992,913303,5606048,EPSG:3857", 499 'wfs_getFeature_1_1_0_epsgbbox_3_feature_3857') 500 self.wfs_request_compare("GetFeature", '1.1.0', "SRSNAME=EPSG:3857&TYPENAME=testlayer&RESULTTYPE=hits&BBOX=913206,5606024,913213,5606026,EPSG:3857", 501 'wfs_getFeature_1_1_0_epsgbbox_1_feature_3857') 502 503 def test_getFeatureFeatureId(self): 504 """Test GetFeature with featureid""" 505 506 self.wfs_request_compare( 507 "GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&FEATUREID=testlayer.0", 'wfs_getFeature_1_0_0_featureid_0') 508 509 def test_getFeatureFeature11urn(self): 510 """Test GetFeature with SRSNAME urn:ogc:def:crs:EPSG::4326""" 511 512 self.wfs_request_compare( 513 "GetFeature", '1.1.0', "SRSNAME=urn:ogc:def:crs:EPSG::4326&TYPENAME=testlayer&FEATUREID=testlayer.0", 'wfs_getFeature_1_1_0_featureid_0_1_1_0') 514 515 def test_getFeature_EXP_FILTER_regression_20927(self): 516 """Test expressions with EXP_FILTER""" 517 518 self.wfs_request_compare( 519 "GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&FEATUREID=testlayer.0&EXP_FILTER=\"name\"='one'", 'wfs_getFeature_1_0_0_EXP_FILTER_FID_one') 520 # Note that FEATUREID takes precedence over EXP_FILTER and the filter is completely ignored when FEATUREID is set 521 self.wfs_request_compare( 522 "GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&FEATUREID=testlayer.0&EXP_FILTER=\"name\"='two'", 'wfs_getFeature_1_0_0_EXP_FILTER_FID_one') 523 self.wfs_request_compare( 524 "GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&EXP_FILTER=\"name\"='two'", 'wfs_getFeature_1_0_0_EXP_FILTER_two') 525 self.wfs_request_compare( 526 "GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&EXP_FILTER=\"name\"=concat('tw', 'o')", 'wfs_getFeature_1_0_0_EXP_FILTER_two') 527 # Syntax ok but function does not exist 528 self.wfs_request_compare("GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&EXP_FILTER=\"name\"=invalid_expression('tw', 'o')", 529 'wfs_getFeature_1_0_0_EXP_FILTER_invalid_expression') 530 # Syntax error in exp 531 self.wfs_request_compare("GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&EXP_FILTER=\"name\"=concat('tw, 'o')", 532 'wfs_getFeature_1_0_0_EXP_FILTER_syntax_error') 533 # BBOX gml expressions 534 self.wfs_request_compare("GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&EXP_FILTER=intersects($geometry, geom_from_gml('<gml:Box> <gml:coordinates cs=\",\" ts=\" \">8.20344750430995617,44.9013881888184514 8.20347909100379269,44.90140004005827024</gml:coordinates></gml:Box>'))", 'wfs_getFeature_1_0_0_EXP_FILTER_gml_bbox_three') 535 self.wfs_request_compare("GetFeature", '1.0.0', "SRSNAME=EPSG:4326&TYPENAME=testlayer&EXP_FILTER=intersects($geometry, geom_from_gml('<gml:Box> <gml:coordinates cs=\",\" ts=\" \">8.20348458304175665,44.90147459621791626 8.20351616973559317,44.9014864474577351</gml:coordinates></gml:Box>'))", 'wfs_getFeature_1_0_0_EXP_FILTER_gml_bbox_one') 536 537 def test_describeFeatureType(self): 538 """Test DescribeFeatureType with TYPENAME filters""" 539 540 project_file = "test_project_wms_grouped_layers.qgs" 541 self.wfs_request_compare("DescribeFeatureType", '1.0.0', "TYPENAME=as_areas&", 542 'wfs_describeFeatureType_1_0_0_typename_as_areas', project_file=project_file) 543 self.wfs_request_compare("DescribeFeatureType", '1.1.0', "TYPENAME=as_areas&", 544 'wfs_describeFeatureType_1_1_0_typename_as_areas', project_file=project_file) 545 self.wfs_request_compare("DescribeFeatureType", '1.0.0', "", 546 'wfs_describeFeatureType_1_0_0_typename_empty', project_file=project_file) 547 self.wfs_request_compare("DescribeFeatureType", '1.1.0', "", 548 'wfs_describeFeatureType_1_1_0_typename_empty', project_file=project_file) 549 self.wfs_request_compare("DescribeFeatureType", '1.0.0', "TYPENAME=does_not_exist&", 550 'wfs_describeFeatureType_1_0_0_typename_wrong', project_file=project_file) 551 self.wfs_request_compare("DescribeFeatureType", '1.1.0', "TYPENAME=does_not_exist&", 552 'wfs_describeFeatureType_1_1_0_typename_wrong', project_file=project_file) 553 554 def test_describeFeatureTypeVirtualFields(self): 555 """Test DescribeFeatureType with virtual fields: bug GH-29767""" 556 557 project_file = "bug_gh29767_double_vfield.qgs" 558 self.wfs_request_compare("DescribeFeatureType", '1.1.0', "", 559 'wfs_describeFeatureType_1_1_0_virtual_fields', project_file=project_file) 560 561 def test_getFeatureFeature_0_nulls(self): 562 """Test that 0 and null in integer columns are reported correctly""" 563 564 # Test transactions with 0 and nulls 565 566 post_data = """<?xml version="1.0" ?> 567<wfs:Transaction service="WFS" version="{version}" 568 xmlns:ogc="http://www.opengis.net/ogc" 569 xmlns:wfs="http://www.opengis.net/wfs" 570 xmlns:gml="http://www.opengis.net/gml"> 571 <wfs:Update typeName="cdb_lines"> 572 <wfs:Property> 573 <wfs:Name>{field}</wfs:Name> 574 <wfs:Value>{value}</wfs:Value> 575 </wfs:Property> 576 <fes:Filter> 577 <fes:FeatureId fid="cdb_lines.22"/> 578 </fes:Filter> 579 </wfs:Update> 580</wfs:Transaction> 581 """ 582 583 def _round_trip(value, field, version='1.1.0'): 584 """Set a value on fid 22 and field and check it back""" 585 586 encoded_data = post_data.format(field=field, value=value, version=version).encode('utf8') 587 # Strip the field if NULL 588 if value is None: 589 encoded_data = encoded_data.replace(b'<wfs:Value>None</wfs:Value>', b'') 590 591 header, body = self._execute_request("?MAP=%s&SERVICE=WFS&VERSION=%s" % ( 592 self.testdata_path + 'test_project_wms_grouped_layers.qgs', version), QgsServerRequest.PostMethod, encoded_data) 593 if version == '1.0.0': 594 self.assertTrue(b'<SUCCESS/>' in body, body) 595 else: 596 self.assertTrue(b'<totalUpdated>1</totalUpdated>' in body, body) 597 header, body = self._execute_request("?MAP=%s&SERVICE=WFS&REQUEST=GetFeature&TYPENAME=cdb_lines&FEATUREID=cdb_lines.22" % ( 598 self.testdata_path + 'test_project_wms_grouped_layers.qgs')) 599 if value is not None: 600 xml_value = '<qgs:{0}>{1}</qgs:{0}>'.format(field, value).encode('utf8') 601 self.assertTrue(xml_value in body, "%s not found in body" % xml_value) 602 else: 603 xml_value = '<qgs:{0}>'.format(field).encode('utf8') 604 self.assertFalse(xml_value in body) 605 # Check the backend 606 vl = QgsVectorLayer( 607 self.testdata_path + 'test_project_wms_grouped_layers.gpkg|layername=cdb_lines', 'vl', 'ogr') 608 self.assertTrue(vl.isValid()) 609 self.assertEqual( 610 str(vl.getFeature(22)[field]), value if value is not None else 'NULL') 611 612 for version in ('1.0.0', '1.1.0'): 613 _round_trip('0', 'id_long', version) 614 _round_trip('12345', 'id_long', version) 615 _round_trip('0', 'id', version) 616 _round_trip('12345', 'id', version) 617 _round_trip(None, 'id', version) 618 _round_trip(None, 'id_long', version) 619 620 # "name" is NOT NULL: try to set it to empty string 621 _round_trip('', 'name', version) 622 # Then NULL 623 data = post_data.format(field='name', value='', version=version).encode('utf8') 624 encoded_data = data.replace(b'<wfs:Value></wfs:Value>', b'') 625 header, body = self._execute_request("?MAP=%s&SERVICE=WFS" % ( 626 self.testdata_path + 'test_project_wms_grouped_layers.qgs'), QgsServerRequest.PostMethod, encoded_data) 627 if version == '1.0.0': 628 self.assertTrue(b'<ERROR/>' in body, body) 629 else: 630 self.assertTrue(b'<totalUpdated>0</totalUpdated>' in body) 631 self.assertTrue(b'<Message>NOT NULL constraint error on layer \'cdb_lines\', field \'name\'</Message>' in body, body) 632 633 def test_describeFeatureTypeGeometryless(self): 634 """Test DescribeFeatureType with geometryless tables - bug GH-30381""" 635 636 project_file = "test_project_geometryless_gh30381.qgs" 637 self.wfs_request_compare("DescribeFeatureType", '1.1.0', 638 reference_base_name='wfs_describeFeatureType_1_1_0_geometryless', 639 project_file=project_file) 640 641 def test_getFeatureFeatureIdJson(self): 642 """Test GetFeature with featureid JSON format and various content types""" 643 644 for ct in ('GeoJSON', 'application/vnd.geo+json', 'application/json', 'application/geo+json'): 645 self.wfs_request_compare( 646 "GetFeature", 647 '1.0.0', 648 ("OUTPUTFORMAT=%s" % ct) 649 + "&SRSNAME=EPSG:4326&TYPENAME=testlayer&FEATUREID=testlayer.0", 650 'wfs_getFeature_1_0_0_featureid_0_json') 651 652 def test_insert_srsName(self): 653 """Test srsName is respected when insering""" 654 655 post_data = """ 656 <Transaction xmlns="http://www.opengis.net/wfs" xsi:schemaLocation="http://www.qgis.org/gml http://localhost:8000/?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=as_symbols" service="WFS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{version}" xmlns:gml="http://www.opengis.net/gml"> 657 <Insert xmlns="http://www.opengis.net/wfs"> 658 <as_symbols xmlns="http://www.qgis.org/gml"> 659 <name xmlns="http://www.qgis.org/gml">{name}</name> 660 <geometry xmlns="http://www.qgis.org/gml"> 661 <gml:Point srsName="{srsName}"> 662 <gml:coordinates cs="," ts=" ">{coordinates}</gml:coordinates> 663 </gml:Point> 664 </geometry> 665 </as_symbols> 666 </Insert> 667 </Transaction> 668 """ 669 670 project = self.testdata_path + \ 671 "test_project_wms_grouped_layers.qgs" 672 assert os.path.exists(project), "Project file not found: " + project 673 674 query_string = '?SERVICE=WFS&MAP={}'.format( 675 urllib.parse.quote(project)) 676 request = post_data.format( 677 name='4326-test1', 678 version='1.1.0', 679 srsName='EPSG:4326', 680 coordinates='10.67,52.48' 681 ) 682 header, body = self._execute_request( 683 query_string, requestMethod=QgsServerRequest.PostMethod, data=request.encode('utf-8')) 684 685 # Verify 686 vl = QgsVectorLayer(self.testdata_path + 'test_project_wms_grouped_layers.gpkg|layername=as_symbols', 'as_symbols') 687 self.assertTrue(vl.isValid()) 688 feature = next(vl.getFeatures(QgsFeatureRequest(QgsExpression('"name" = \'4326-test1\'')))) 689 geom = feature.geometry() 690 691 tr = QgsCoordinateTransform(QgsCoordinateReferenceSystem.fromEpsgId(4326), vl.crs(), QgsCoordinateTransformContext()) 692 693 geom_4326 = QgsGeometry.fromWkt('point( 10.67 52.48)') 694 geom_4326.transform(tr) 695 self.assertEqual(geom.asWkt(0), geom_4326.asWkt(0)) 696 697 # Now: insert a feature in layer's CRS 698 request = post_data.format( 699 name='25832-test1', 700 version='1.1.0', 701 srsName='EPSG:25832', 702 coordinates='613412,5815738' 703 ) 704 header, body = self._execute_request( 705 query_string, requestMethod=QgsServerRequest.PostMethod, data=request.encode('utf-8')) 706 707 feature = next(vl.getFeatures(QgsFeatureRequest(QgsExpression('"name" = \'25832-test1\'')))) 708 geom = feature.geometry() 709 self.assertEqual(geom.asWkt(0), geom_4326.asWkt(0)) 710 711 # Tests for inverted axis issue GH #36584 712 # Cleanup 713 self.assertTrue(vl.startEditing()) 714 vl.selectByExpression('"name" LIKE \'4326-test%\'') 715 vl.deleteSelectedFeatures() 716 self.assertTrue(vl.commitChanges()) 717 718 self.i = 0 719 720 def _test(version, srsName, lat_lon=False): 721 self.i += 1 722 name = '4326-test_%s' % self.i 723 request = post_data.format( 724 name=name, 725 version=version, 726 srsName=srsName, 727 coordinates='52.48,10.67' if lat_lon else '10.67,52.48' 728 ) 729 header, body = self._execute_request( 730 query_string, requestMethod=QgsServerRequest.PostMethod, data=request.encode('utf-8')) 731 feature = next(vl.getFeatures(QgsFeatureRequest(QgsExpression('"name" = \'%s\'' % name)))) 732 geom = feature.geometry() 733 self.assertEqual(geom.asWkt(0), geom_4326.asWkt(0), "Transaction Failed: %s , %s, lat_lon=%s" % (version, srsName, lat_lon)) 734 735 _test('1.1.0', 'urn:ogc:def:crs:EPSG::4326', lat_lon=True) 736 _test('1.1.0', 'http://www.opengis.net/def/crs/EPSG/0/4326', lat_lon=True) 737 _test('1.1.0', 'http://www.opengis.net/gml/srs/epsg.xml#4326', lat_lon=False) 738 _test('1.1.0', 'EPSG:4326', lat_lon=False) 739 740 _test('1.0.0', 'urn:ogc:def:crs:EPSG::4326', lat_lon=True) 741 _test('1.0.0', 'http://www.opengis.net/def/crs/EPSG/0/4326', lat_lon=True) 742 _test('1.0.0', 'http://www.opengis.net/gml/srs/epsg.xml#4326', lat_lon=False) 743 _test('1.0.0', 'EPSG:4326', lat_lon=False) 744 745 def _test_getFeature(version, srsName, lat_lon=False): 746 # Now get the feature through WFS using BBOX filter 747 bbox = QgsGeometry.fromWkt('point( 10.7006 52.4317)').boundingBox() 748 bbox.grow(0.0001) 749 bbox_text = "%s,%s,%s,%s" % ((bbox.yMinimum(), bbox.xMinimum(), bbox.yMaximum(), bbox.xMaximum()) if lat_lon else (bbox.xMinimum(), bbox.yMinimum(), bbox.xMaximum(), bbox.yMaximum())) 750 req = query_string + '&REQUEST=GetFeature&VERSION={version}&TYPENAME=as_symbols&SRSNAME={srsName}&BBOX={bbox},{srsName}'.format(version=version, srsName=srsName, bbox=bbox_text) 751 header, body = self._execute_request(req) 752 self.assertTrue(b'gid>7' in body, "GetFeature Failed: %s , %s, lat_lon=%s" % (version, srsName, lat_lon)) 753 754 _test_getFeature('1.1.0', 'urn:ogc:def:crs:EPSG::4326', lat_lon=True) 755 _test_getFeature('1.1.0', 'EPSG:4326', lat_lon=False) 756 757 _test_getFeature('1.0.0', 'urn:ogc:def:crs:EPSG::4326', lat_lon=True) 758 _test_getFeature('1.0.0', 'EPSG:4326', lat_lon=False) 759 760 # Cleanup 761 self.assertTrue(vl.startEditing()) 762 vl.selectByExpression('"name" LIKE \'4326-test%\'') 763 vl.deleteSelectedFeatures() 764 self.assertTrue(vl.commitChanges()) 765 766 767if __name__ == '__main__': 768 unittest.main() 769