1# -*- coding: utf-8 -*- 2"""QGIS Unit tests for QgsServer API. 3 4.. note:: This program is free software; you can redistribute it and/or modify 5it under the terms of the GNU General Public License as published by 6the Free Software Foundation; either version 2 of the License, or 7(at your option) any later version. 8 9""" 10__author__ = 'Alessandro Pasotti' 11__date__ = '17/04/2019' 12__copyright__ = 'Copyright 2019, The QGIS Project' 13# This will get replaced with a git SHA1 when you do a git archive 14__revision__ = 'f5778a89df89639ed85834a6cd4f78b24e66ac5d' 15 16import os 17import json 18import re 19import shutil 20 21# Deterministic XML 22os.environ['QT_HASH_SEED'] = '1' 23 24from qgis.server import ( 25 QgsBufferServerRequest, 26 QgsBufferServerResponse, 27 QgsServer, 28 QgsServerApi, 29 QgsServerApiBadRequestException, 30 QgsServerQueryStringParameter, 31 QgsServerApiContext, 32 QgsServerOgcApi, 33 QgsServerOgcApiHandler, 34 QgsServerApiUtils, 35 QgsServiceRegistry 36) 37from qgis.core import QgsProject, QgsRectangle, QgsVectorLayerServerProperties, QgsFeatureRequest 38from qgis.PyQt import QtCore 39 40from qgis.testing import unittest 41from utilities import unitTestDataPath 42from urllib import parse 43 44import tempfile 45 46from test_qgsserver import QgsServerTestBase 47 48 49class QgsServerAPIUtilsTest(QgsServerTestBase): 50 """ QGIS API server utils tests""" 51 52 def test_parse_bbox(self): 53 bbox = QgsServerApiUtils.parseBbox( 54 '8.203495,44.901482,8.203497,44.901484') 55 self.assertEquals(bbox.xMinimum(), 8.203495) 56 self.assertEquals(bbox.yMinimum(), 44.901482) 57 self.assertEquals(bbox.xMaximum(), 8.203497) 58 self.assertEquals(bbox.yMaximum(), 44.901484) 59 60 bbox = QgsServerApiUtils.parseBbox( 61 '8.203495,44.901482,100,8.203497,44.901484,120') 62 self.assertEquals(bbox.xMinimum(), 8.203495) 63 self.assertEquals(bbox.yMinimum(), 44.901482) 64 self.assertEquals(bbox.xMaximum(), 8.203497) 65 self.assertEquals(bbox.yMaximum(), 44.901484) 66 67 bbox = QgsServerApiUtils.parseBbox('something_wrong_here') 68 self.assertTrue(bbox.isEmpty()) 69 bbox = QgsServerApiUtils.parseBbox( 70 '8.203495,44.901482,8.203497,something_wrong_here') 71 self.assertTrue(bbox.isEmpty()) 72 73 def test_published_crs(self): 74 """Test published WMS CRSs""" 75 76 project = QgsProject() 77 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 78 crss = QgsServerApiUtils.publishedCrsList(project) 79 self.assertTrue('http://www.opengis.net/def/crs/OGC/1.3/CRS84' in crss) 80 self.assertTrue( 81 'http://www.opengis.net/def/crs/EPSG/9.6.2/3857' in crss) 82 self.assertTrue( 83 'http://www.opengis.net/def/crs/EPSG/9.6.2/4326' in crss) 84 85 def test_parse_crs(self): 86 crs = QgsServerApiUtils.parseCrs( 87 'http://www.opengis.net/def/crs/OGC/1.3/CRS84') 88 self.assertTrue(crs.isValid()) 89 90 crs = QgsServerApiUtils.parseCrs( 91 'http://www.opengis.net/def/crs/EPSG/9.6.2/4326') 92 self.assertEquals(crs.postgisSrid(), 4326) 93 94 crs = QgsServerApiUtils.parseCrs( 95 'http://www.opengis.net/def/crs/EPSG/9.6.2/3857') 96 self.assertTrue(crs.isValid()) 97 self.assertEquals(crs.postgisSrid(), 3857) 98 99 crs = QgsServerApiUtils.parseCrs( 100 'http://www.opengis.net/something_wrong_here') 101 self.assertFalse(crs.isValid()) 102 103 def test_append_path(self): 104 path = QgsServerApiUtils.appendMapParameter( 105 '/wfs3', QtCore.QUrl('https://www.qgis.org/wfs3?MAP=/some/path')) 106 self.assertEqual(path, '/wfs3?MAP=/some/path') 107 108 def test_temporal_extent(self): 109 project = QgsProject() 110 111 tempDir = QtCore.QTemporaryDir() 112 source_project_path = unitTestDataPath( 113 'qgis_server') + '/test_project_api_timefilters.qgs' 114 source_data_path = unitTestDataPath( 115 'qgis_server') + '/test_project_api_timefilters.gpkg' 116 dest_project_path = os.path.join( 117 tempDir.path(), 'test_project_api_timefilters.qgs') 118 dest_data_path = os.path.join( 119 tempDir.path(), 'test_project_api_timefilters.gpkg') 120 shutil.copy(source_data_path, dest_data_path) 121 shutil.copy(source_project_path, dest_project_path) 122 project.read(dest_project_path) 123 124 layer = list(project.mapLayers().values())[0] 125 126 layer.serverProperties().removeWmsDimension('date') 127 layer.serverProperties().removeWmsDimension('time') 128 self.assertTrue(layer.serverProperties().addWmsDimension( 129 QgsVectorLayerServerProperties.WmsDimensionInfo('time', 'updated_string'))) 130 self.assertEqual(QgsServerApiUtils.temporalExtent(layer), [ 131 ['2010-01-01T01:01:01', '2020-01-01T01:01:01']]) 132 133 layer.serverProperties().removeWmsDimension('date') 134 layer.serverProperties().removeWmsDimension('time') 135 self.assertTrue(layer.serverProperties().addWmsDimension( 136 QgsVectorLayerServerProperties.WmsDimensionInfo('date', 'created'))) 137 self.assertEqual(QgsServerApiUtils.temporalExtent(layer), [ 138 ['2010-01-01T00:00:00', '2019-01-01T00:00:00']]) 139 140 layer.serverProperties().removeWmsDimension('date') 141 layer.serverProperties().removeWmsDimension('time') 142 self.assertTrue(layer.serverProperties().addWmsDimension( 143 QgsVectorLayerServerProperties.WmsDimensionInfo('date', 'created_string'))) 144 self.assertEqual(QgsServerApiUtils.temporalExtent(layer), [ 145 ['2010-01-01T00:00:00', '2019-01-01T00:00:00']]) 146 147 layer.serverProperties().removeWmsDimension('date') 148 layer.serverProperties().removeWmsDimension('time') 149 self.assertTrue(layer.serverProperties().addWmsDimension( 150 QgsVectorLayerServerProperties.WmsDimensionInfo('time', 'updated'))) 151 self.assertEqual(QgsServerApiUtils.temporalExtent(layer), [ 152 ['2010-01-01T01:01:01', '2022-01-01T01:01:01']]) 153 154 layer.serverProperties().removeWmsDimension('date') 155 layer.serverProperties().removeWmsDimension('time') 156 self.assertTrue(layer.serverProperties().addWmsDimension( 157 QgsVectorLayerServerProperties.WmsDimensionInfo('date', 'begin', 'end'))) 158 self.assertEqual(QgsServerApiUtils.temporalExtent(layer), [ 159 ['2010-01-01T00:00:00', '2022-01-01T00:00:00']]) 160 161 162class API(QgsServerApi): 163 164 def __init__(self, iface, version='1.0'): 165 super().__init__(iface) 166 self._version = version 167 168 def name(self): 169 return "TEST" 170 171 def version(self): 172 return self._version 173 174 def rootPath(self): 175 return "/testapi" 176 177 def executeRequest(self, request_context): 178 request_context.response().write(b"\"Test API\"") 179 180 181class QgsServerAPITestBase(QgsServerTestBase): 182 """ QGIS API server tests""" 183 184 # Set to True in child classes to re-generate reference files for this class 185 regeregenerate_api_reference = False 186 187 def assertEqualBrackets(self, actual, expected): 188 """Also counts parenthesis""" 189 190 self.assertEqual(actual.count('('), actual.count(')')) 191 self.assertEqual(actual, expected) 192 193 def dump(self, response): 194 """Returns the response body as str""" 195 196 result = [] 197 for n, v in response.headers().items(): 198 if n == 'Content-Length': 199 continue 200 result.append("%s: %s" % (n, v)) 201 result.append('') 202 result.append(bytes(response.body()).decode('utf8')) 203 return '\n'.join(result) 204 205 def assertLinesEqual(self, actual, expected, reference_file): 206 """Break on first different line""" 207 208 actual_lines = actual.split('\n') 209 expected_lines = expected.split('\n') 210 for i in range(len(actual_lines)): 211 self.assertEqual(actual_lines[i], expected_lines[i], "File: %s\nLine: %s\nActual : %s\nExpected: %s" % ( 212 reference_file, i, actual_lines[i], expected_lines[i])) 213 214 def normalize_json(self, content): 215 """Normalize a json string""" 216 217 reference_content = content.split('\n') 218 j = ''.join(reference_content[reference_content.index('') + 1:]) 219 # Do not test timeStamp 220 j = json.loads(j) 221 try: 222 j['timeStamp'] = '2019-07-05T12:27:07Z' 223 except: 224 pass 225 # Fix coordinate precision differences in Travis 226 try: 227 bbox = j['extent']['spatial']['bbox'][0] 228 bbox = [round(c, 4) for c in bbox] 229 j['extent']['spatial']['bbox'][0] = bbox 230 except: 231 pass 232 json_content = json.dumps(j, indent=4) 233 # Rounding errors 234 json_content = re.sub(r'(\d{5})\d+\.\d+', r'\1', json_content) 235 json_content = re.sub(r'(\d+\.\d{4})\d+', r'\1', json_content) 236 # Poject hash 237 json_content = re.sub( 238 r'[a-f0-9]{32}', r'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', json_content) 239 headers_content = '\n'.join( 240 reference_content[:reference_content.index('') + 1]) 241 return headers_content + '\n' + json_content 242 243 def compareApi(self, request, project, reference_file, subdir='api'): 244 response = QgsBufferServerResponse() 245 # Add json to accept it reference_file is JSON 246 if reference_file.endswith('.json'): 247 request.setHeader('Accept', 'application/json') 248 self.server.handleRequest(request, response, project) 249 result = bytes(response.body()).decode( 250 'utf8') if reference_file.endswith('html') else self.dump(response) 251 path = os.path.join(unitTestDataPath( 252 'qgis_server'), subdir, reference_file) 253 if self.regeregenerate_api_reference: 254 # Try to change timestamp 255 try: 256 content = result.split('\n') 257 j = ''.join(content[content.index('') + 1:]) 258 j = json.loads(j) 259 j['timeStamp'] = '2019-07-05T12:27:07Z' 260 result = '\n'.join(content[:2]) + '\n' + \ 261 json.dumps(j, ensure_ascii=False, indent=2) 262 except: 263 pass 264 f = open(path.encode('utf8'), 'w+', encoding='utf8') 265 f.write(result) 266 f.close() 267 print("Reference file %s regenerated!" % path.encode('utf8')) 268 269 with open(path.encode('utf8'), 'r', encoding='utf8') as f: 270 if reference_file.endswith('json'): 271 self.assertLinesEqual(self.normalize_json( 272 result), self.normalize_json(f.read()), path.encode('utf8')) 273 else: 274 self.assertEqual(f.read(), result) 275 276 return response 277 278 def compareContentType(self, url, headers, content_type, project=QgsProject()): 279 request = QgsBufferServerRequest(url, headers=headers) 280 response = QgsBufferServerResponse() 281 self.server.handleRequest(request, response, project) 282 self.assertEqual(response.headers()['Content-Type'], content_type) 283 284 @classmethod 285 def setUpClass(cls): 286 super(QgsServerAPITestBase, cls).setUpClass() 287 cls.maxDiff = None 288 289 290class QgsServerAPITest(QgsServerAPITestBase): 291 """ QGIS API server tests""" 292 293 def test_api(self): 294 """Test API registering""" 295 296 api = API(self.server.serverInterface()) 297 self.server.serverInterface().serviceRegistry().registerApi(api) 298 request = QgsBufferServerRequest('http://server.qgis.org/testapi') 299 self.compareApi(request, None, 'test_api.json') 300 self.server.serverInterface().serviceRegistry().unregisterApi(api.name()) 301 302 def test_0_version_registration(self): 303 304 reg = QgsServiceRegistry() 305 api = API(self.server.serverInterface()) 306 api1 = API(self.server.serverInterface(), '1.1') 307 308 # 1.1 comes first 309 reg.registerApi(api1) 310 reg.registerApi(api) 311 312 rapi = reg.getApi("TEST") 313 self.assertIsNotNone(rapi) 314 self.assertEqual(rapi.version(), "1.1") 315 316 rapi = reg.getApi("TEST", "2.0") 317 self.assertIsNotNone(rapi) 318 self.assertEqual(rapi.version(), "1.1") 319 320 rapi = reg.getApi("TEST", "1.0") 321 self.assertIsNotNone(rapi) 322 self.assertEqual(rapi.version(), "1.0") 323 324 def test_1_unregister_services(self): 325 326 reg = QgsServiceRegistry() 327 api = API(self.server.serverInterface(), '1.0a') 328 api1 = API(self.server.serverInterface(), '1.0b') 329 api2 = API(self.server.serverInterface(), '1.0c') 330 331 reg.registerApi(api) 332 reg.registerApi(api1) 333 reg.registerApi(api2) 334 335 # Check we get the default version 336 rapi = reg.getApi("TEST") 337 self.assertEqual(rapi.version(), "1.0a") 338 339 # Remove one service 340 removed = reg.unregisterApi("TEST", "1.0a") 341 self.assertEqual(removed, 1) 342 343 # Check that we get the highest version 344 rapi = reg.getApi("TEST") 345 self.assertEqual(rapi.version(), "1.0c") 346 347 # Remove all services 348 removed = reg.unregisterApi("TEST") 349 self.assertEqual(removed, 2) 350 351 # Check that there is no more services available 352 api = reg.getApi("TEST") 353 self.assertIsNone(api) 354 355 def test_wfs3_landing_page(self): 356 """Test WFS3 API landing page in HTML format""" 357 358 request = QgsBufferServerRequest('http://server.qgis.org/wfs3.html') 359 self.compareApi(request, None, 'test_wfs3_landing_page.html') 360 361 def test_content_type_negotiation(self): 362 """Test content-type negotiation and conflicts""" 363 364 # Default: json 365 self.compareContentType( 366 'http://server.qgis.org/wfs3', {}, 'application/json') 367 # Explicit request 368 self.compareContentType('http://server.qgis.org/wfs3', 369 {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'}, 370 'text/html') 371 self.compareContentType('http://server.qgis.org/wfs3', 372 {'Accept': 'application/json'}, 'application/json') 373 # File suffix 374 self.compareContentType( 375 'http://server.qgis.org/wfs3.json', {}, 'application/json') 376 self.compareContentType( 377 'http://server.qgis.org/wfs3.html', {}, 'text/html') 378 # File extension must take precedence over Accept header 379 self.compareContentType( 380 'http://server.qgis.org/wfs3.html', {'Accept': 'application/json'}, 'text/html') 381 self.compareContentType( 382 'http://server.qgis.org/wfs3.json', {'Accept': 'text/html'}, 'application/json') 383 # Alias request (we ask for json but we get geojson) 384 project = QgsProject() 385 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 386 self.compareContentType( 387 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?bbox=8.203495,44.901482,8.203497,44.901484', 388 {'Accept': 'application/json'}, 'application/geo+json', 389 project=project 390 ) 391 self.compareContentType( 392 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?bbox=8.203495,44.901482,8.203497,44.901484', 393 {'Accept': 'application/vnd.geo+json'}, 'application/geo+json', 394 project=project 395 ) 396 self.compareContentType( 397 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?bbox=8.203495,44.901482,8.203497,44.901484', 398 {'Accept': 'application/geojson'}, 'application/geo+json', 399 project=project 400 ) 401 402 def test_wfs3_landing_page_json(self): 403 """Test WFS3 API landing page in JSON format""" 404 request = QgsBufferServerRequest('http://server.qgis.org/wfs3.json') 405 self.compareApi(request, None, 'test_wfs3_landing_page.json') 406 request = QgsBufferServerRequest('http://server.qgis.org/wfs3') 407 request.setHeader('Accept', 'application/json') 408 self.compareApi(request, None, 'test_wfs3_landing_page.json') 409 410 def test_wfs3_api(self): 411 """Test WFS3 API""" 412 413 # No project: error 414 request = QgsBufferServerRequest( 415 'http://server.qgis.org/wfs3/api.openapi3') 416 self.compareApi(request, None, 'test_wfs3_api.json') 417 418 request = QgsBufferServerRequest( 419 'http://server.qgis.org/wfs3/api.openapi3') 420 project = QgsProject() 421 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 422 self.compareApi(request, project, 'test_wfs3_api_project.json') 423 424 def test_wfs3_conformance(self): 425 """Test WFS3 API""" 426 request = QgsBufferServerRequest( 427 'http://server.qgis.org/wfs3/conformance') 428 self.compareApi(request, None, 'test_wfs3_conformance.json') 429 430 def test_wfs3_collections_empty(self): 431 """Test WFS3 collections API""" 432 433 request = QgsBufferServerRequest( 434 'http://server.qgis.org/wfs3/collections') 435 self.compareApi(request, None, 'test_wfs3_collections_empty.json') 436 request = QgsBufferServerRequest( 437 'http://server.qgis.org/wfs3/collections.json') 438 self.compareApi(request, None, 'test_wfs3_collections_empty.json') 439 request = QgsBufferServerRequest( 440 'http://server.qgis.org/wfs3/collections.html') 441 self.compareApi(request, None, 'test_wfs3_collections_empty.html') 442 443 def test_wfs3_collections_json(self): 444 """Test WFS3 API collections in json format""" 445 request = QgsBufferServerRequest( 446 'http://server.qgis.org/wfs3/collections.json') 447 project = QgsProject() 448 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 449 self.compareApi(request, project, 'test_wfs3_collections_project.json') 450 451 def test_wfs3_collections_html(self): 452 """Test WFS3 API collections in html format""" 453 request = QgsBufferServerRequest( 454 'http://server.qgis.org/wfs3/collections.html') 455 project = QgsProject() 456 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 457 self.compareApi(request, project, 'test_wfs3_collections_project.html') 458 459 def test_wfs3_collections_content_type(self): 460 """Test WFS3 API collections in html format with Accept header""" 461 462 request = QgsBufferServerRequest( 463 'http://server.qgis.org/wfs3/collections') 464 request.setHeader('Accept', 'text/html') 465 project = QgsProject() 466 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 467 response = QgsBufferServerResponse() 468 self.server.handleRequest(request, response, project) 469 self.assertEqual(response.headers()['Content-Type'], 'text/html') 470 request = QgsBufferServerRequest( 471 'http://server.qgis.org/wfs3/collections') 472 request.setHeader('Accept', 'text/html') 473 response = QgsBufferServerResponse() 474 self.server.handleRequest(request, response, project) 475 self.assertEqual(response.headers()['Content-Type'], 'text/html') 476 477 def test_wfs3_collection_json(self): 478 """Test WFS3 API collection""" 479 project = QgsProject() 480 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 481 request = QgsBufferServerRequest( 482 'http://server.qgis.org/wfs3/collections/testlayer%20èé') 483 self.compareApi(request, project, 484 'test_wfs3_collection_testlayer_èé.json') 485 486 def test_wfs3_collection_temporal_extent_json(self): 487 """Test collection with timefilter""" 488 project = QgsProject() 489 tempDir = QtCore.QTemporaryDir() 490 source_project_path = unitTestDataPath( 491 'qgis_server') + '/test_project_api_timefilters.qgs' 492 source_data_path = unitTestDataPath( 493 'qgis_server') + '/test_project_api_timefilters.gpkg' 494 dest_project_path = os.path.join( 495 tempDir.path(), 'test_project_api_timefilters.qgs') 496 dest_data_path = os.path.join( 497 tempDir.path(), 'test_project_api_timefilters.gpkg') 498 shutil.copy(source_data_path, dest_data_path) 499 shutil.copy(source_project_path, dest_project_path) 500 project.read(dest_project_path) 501 request = QgsBufferServerRequest( 502 'http://server.qgis.org/wfs3/collections/points') 503 self.compareApi(request, project, 504 'test_wfs3_collection_points_timefilters.json') 505 506 def test_wfs3_collection_html(self): 507 """Test WFS3 API collection""" 508 project = QgsProject() 509 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 510 request = QgsBufferServerRequest( 511 'http://server.qgis.org/wfs3/collections/testlayer%20èé.html') 512 self.compareApi(request, project, 513 'test_wfs3_collection_testlayer_èé.html') 514 request = QgsBufferServerRequest( 515 'http://server.qgis.org/wfs3/collections/testlayer%20èé/') 516 request.setHeader('Accept', 'text/html') 517 self.compareApi(request, project, 518 'test_wfs3_collection_testlayer_èé.html') 519 520 def test_wfs3_collection_items(self): 521 """Test WFS3 API items""" 522 project = QgsProject() 523 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 524 request = QgsBufferServerRequest( 525 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items') 526 self.compareApi(request, project, 527 'test_wfs3_collections_items_testlayer_èé.json') 528 529 def test_wfs3_collection_items_html(self): 530 """Test WFS3 API items""" 531 project = QgsProject() 532 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 533 request = QgsBufferServerRequest( 534 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items.html') 535 self.compareApi(request, project, 536 'test_wfs3_collections_items_testlayer_èé.html') 537 538 def test_wfs3_collection_items_crs(self): 539 """Test WFS3 API items with CRS""" 540 project = QgsProject() 541 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 542 encoded_crs = parse.quote( 543 'http://www.opengis.net/def/crs/EPSG/9.6.2/3857', safe='') 544 request = QgsBufferServerRequest( 545 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?crs={}'.format(encoded_crs)) 546 self.compareApi( 547 request, project, 'test_wfs3_collections_items_testlayer_èé_crs_3857.json') 548 549 def test_wfs3_collection_items_as_areas_crs_4326(self): 550 """Test WFS3 API items with CRS""" 551 project = QgsProject() 552 project.read(unitTestDataPath('qgis_server') + 553 '/test_project_wms_grouped_nested_layers.qgs') 554 encoded_crs = parse.quote( 555 'http://www.opengis.net/def/crs/EPSG/9.6.2/4326', safe='') 556 request = QgsBufferServerRequest( 557 'http://server.qgis.org/wfs3/collections/as-areas-short-name/items?crs={}'.format(encoded_crs)) 558 self.compareApi( 559 request, project, 'test_wfs3_collections_items_as-areas-short-name_4326.json') 560 561 def test_wfs3_collection_items_as_areas_crs_3857(self): 562 """Test WFS3 API items with CRS""" 563 project = QgsProject() 564 project.read(unitTestDataPath('qgis_server') + 565 '/test_project_wms_grouped_nested_layers.qgs') 566 encoded_crs = parse.quote( 567 'http://www.opengis.net/def/crs/EPSG/9.6.2/3857', safe='') 568 request = QgsBufferServerRequest( 569 'http://server.qgis.org/wfs3/collections/as-areas-short-name/items?crs={}'.format(encoded_crs)) 570 self.compareApi( 571 request, project, 'test_wfs3_collections_items_as-areas-short-name_3857.json') 572 573 def test_invalid_args(self): 574 """Test wrong args""" 575 project = QgsProject() 576 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 577 request = QgsBufferServerRequest( 578 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=-1') 579 response = QgsBufferServerResponse() 580 self.server.handleRequest(request, response, project) 581 self.assertEqual(response.statusCode(), 400) # Bad request 582 self.assertEqual(response.body(), 583 b'[{"code":"Bad request error","description":"Argument \'limit\' is not valid. Number of features to retrieve [0-10000]"}]') # Bad request 584 585 request = QgsBufferServerRequest( 586 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=10001') 587 response = QgsBufferServerResponse() 588 self.server.handleRequest(request, response, project) 589 self.assertEqual(response.statusCode(), 400) # Bad request 590 self.assertEqual(response.body(), 591 b'[{"code":"Bad request error","description":"Argument \'limit\' is not valid. Number of features to retrieve [0-10000]"}]') # Bad request 592 593 def test_wfs3_collection_items_limit(self): 594 """Test WFS3 API item limits""" 595 project = QgsProject() 596 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 597 request = QgsBufferServerRequest( 598 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=1') 599 self.compareApi( 600 request, project, 'test_wfs3_collections_items_testlayer_èé_limit_1.json') 601 602 def test_wfs3_collection_items_limit_offset(self): 603 """Test WFS3 API offset""" 604 project = QgsProject() 605 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 606 request = QgsBufferServerRequest( 607 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=1&offset=1') 608 self.compareApi( 609 request, project, 'test_wfs3_collections_items_testlayer_èé_limit_1_offset_1.json') 610 request = QgsBufferServerRequest( 611 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=1&offset=-1') 612 response = QgsBufferServerResponse() 613 self.server.handleRequest(request, response, project) 614 self.assertEqual(response.statusCode(), 400) # Bad request 615 self.assertEqual(response.body(), 616 b'[{"code":"Bad request error","description":"Argument \'offset\' is not valid. Offset for features to retrieve [0-3]"}]') # Bad request 617 request = QgsBufferServerRequest( 618 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=-1&offset=1') 619 response = QgsBufferServerResponse() 620 self.server.handleRequest(request, response, project) 621 self.assertEqual(response.statusCode(), 400) # Bad request 622 self.assertEqual(response.body(), 623 b'[{"code":"Bad request error","description":"Argument \'limit\' is not valid. Number of features to retrieve [0-10000]"}]') # Bad request 624 625 def test_wfs3_collection_items_bbox(self): 626 """Test WFS3 API bbox""" 627 project = QgsProject() 628 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 629 request = QgsBufferServerRequest( 630 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?bbox=8.203495,44.901482,8.203497,44.901484') 631 self.compareApi(request, project, 632 'test_wfs3_collections_items_testlayer_èé_bbox.json') 633 634 # Test with a different CRS 635 encoded_crs = parse.quote( 636 'http://www.opengis.net/def/crs/EPSG/9.6.2/3857', safe='') 637 request = QgsBufferServerRequest( 638 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?bbox=913191,5606014,913234,5606029&bbox-crs={}'.format( 639 encoded_crs)) 640 self.compareApi( 641 request, project, 'test_wfs3_collections_items_testlayer_èé_bbox_3857.json') 642 643 def test_wfs3_static_handler(self): 644 """Test static handler""" 645 request = QgsBufferServerRequest( 646 'http://server.qgis.org/wfs3/static/style.css') 647 response = QgsBufferServerResponse() 648 self.server.handleRequest(request, response, None) 649 body = bytes(response.body()).decode('utf8') 650 self.assertTrue('Content-Length' in response.headers()) 651 self.assertEqual(response.headers()['Content-Type'], 'text/css') 652 self.assertTrue(len(body) > 0) 653 654 request = QgsBufferServerRequest( 655 'http://server.qgis.org/wfs3/static/does_not_exists.css') 656 response = QgsBufferServerResponse() 657 self.server.handleRequest(request, response, None) 658 body = bytes(response.body()).decode('utf8') 659 self.assertEqual(body, 660 '[{"code":"API not found error","description":"Static file does_not_exists.css was not found"}]') 661 662 def test_wfs3_collection_items_post(self): 663 """Test WFS3 API items POST""" 664 665 tmpDir = QtCore.QTemporaryDir() 666 shutil.copy(unitTestDataPath('qgis_server') + '/test_project_api_editing.qgs', 667 tmpDir.path() + '/test_project_api_editing.qgs') 668 shutil.copy(unitTestDataPath('qgis_server') + '/test_project_api_editing.gpkg', 669 tmpDir.path() + '/test_project_api_editing.gpkg') 670 671 project = QgsProject() 672 project.read(tmpDir.path() + '/test_project_api_editing.qgs') 673 674 # Project layers with different permissions 675 insert_layer = r'test%20layer%20èé%203857%20published%20insert' 676 update_layer = r'test%20layer%20èé%203857%20published%20update' 677 delete_layer = r'test%20layer%20èé%203857%20published%20delete' 678 unpublished_layer = r'test%20layer%203857%20èé%20unpublished' 679 hidden_text_2_layer = r'test%20layer%20èé%203857%20published%20hidden%20text_2' 680 681 # Invalid request 682 data = b'not json!' 683 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items' % insert_layer, 684 QgsBufferServerRequest.PostMethod, 685 {'Content-Type': 'application/geo+json'}, 686 data 687 ) 688 response = QgsBufferServerResponse() 689 self.server.handleRequest(request, response, project) 690 self.assertEqual(response.statusCode(), 400) 691 self.assertTrue( 692 '[{"code":"Bad request error","description":"JSON parse error' in bytes(response.body()).decode('utf8')) 693 694 # Valid request 695 data = """{ 696 "geometry": { 697 "coordinates": [[ 698 7.247, 699 44.814 700 ]], 701 "type": "MultiPoint" 702 }, 703 "properties": { 704 "text_1": "Text 1", 705 "text_2": "Text 2", 706 "int_1": 123, 707 "float_1": 12345.678, 708 "datetime_1": "2019-11-07T12:34:56", 709 "date_1": "2019-11-07", 710 "blob_1": "dGVzdA==", 711 "bool_1": true 712 }, 713 "type": "Feature" 714 }""".encode('utf8') 715 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items' % insert_layer, 716 QgsBufferServerRequest.PostMethod, 717 {'Content-Type': 'application/geo+json'}, 718 data 719 ) 720 response = QgsBufferServerResponse() 721 self.server.handleRequest(request, response, project) 722 self.assertEqual(response.statusCode(), 201) 723 self.assertEqual(response.body(), '"string"') 724 # Get last feature 725 req = QgsFeatureRequest() 726 order_by_clause = QgsFeatureRequest.OrderByClause('$id', False) 727 req.setOrderBy(QgsFeatureRequest.OrderBy([order_by_clause])) 728 feature = next(project.mapLayersByName( 729 'test layer èé 3857 published insert')[0].getFeatures(req)) 730 self.assertEqual(response.headers()['Location'], 731 'http://server.qgis.org/wfs3/collections/%s/items/%s' % (insert_layer, feature.id())) 732 self.assertEqual(feature.attribute('text_1'), 'Text 1') 733 self.assertEqual(feature.attribute('text_2'), 'Text 2') 734 self.assertEqual(feature.attribute('int_1'), 123) 735 self.assertEqual(feature.attribute('float_1'), 12345.678) 736 self.assertEqual(feature.attribute('bool_1'), True) 737 self.assertEqual(bytes(feature.attribute('blob_1')), b"test") 738 self.assertEqual(re.sub( 739 r'\.\d+', '', feature.geometry().asWkt().upper()), 'MULTIPOINT ((806732 5592286))') 740 741 def test_wfs3_collection_items_put(self): 742 """Test WFS3 API items PUT""" 743 744 tmpDir = QtCore.QTemporaryDir() 745 shutil.copy(unitTestDataPath('qgis_server') + '/test_project_api_editing.qgs', 746 tmpDir.path() + '/test_project_api_editing.qgs') 747 shutil.copy(unitTestDataPath('qgis_server') + '/test_project_api_editing.gpkg', 748 tmpDir.path() + '/test_project_api_editing.gpkg') 749 750 project = QgsProject() 751 project.read(tmpDir.path() + '/test_project_api_editing.qgs') 752 753 # Project layers with different permissions 754 insert_layer = r'test%20layer%20èé%203857%20published%20insert' 755 update_layer = r'test%20layer%20èé%203857%20published%20update' 756 delete_layer = r'test%20layer%20èé%203857%20published%20delete' 757 unpublished_layer = r'test%20layer%203857%20èé%20unpublished' 758 hidden_text_2_layer = r'test%20layer%20èé%203857%20published%20hidden%20text_2' 759 760 # Invalid request 761 data = b'not json!' 762 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items/1' % update_layer, 763 QgsBufferServerRequest.PutMethod, 764 {'Content-Type': 'application/geo+json'}, 765 data 766 ) 767 response = QgsBufferServerResponse() 768 self.server.handleRequest(request, response, project) 769 self.assertEqual(response.statusCode(), 400) 770 self.assertTrue( 771 '[{"code":"Bad request error","description":"JSON parse error' in bytes(response.body()).decode('utf8')) 772 773 # Valid request: change feature with ID 1 774 data = """{ 775 "geometry": { 776 "coordinates": [[ 777 7.247, 778 44.814 779 ]], 780 "type": "MultiPoint" 781 }, 782 "properties": { 783 "text_1": "Text 1", 784 "text_2": "Text 2", 785 "int_1": 123, 786 "float_1": 12345.678, 787 "datetime_1": "2019-11-07T12:34:56", 788 "date_1": "2019-11-07", 789 "blob_1": "dGVzdA==", 790 "bool_1": true 791 }, 792 "type": "Feature" 793 }""".encode('utf8') 794 795 # Unauthorized layer 796 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items/1' % insert_layer, 797 QgsBufferServerRequest.PutMethod, 798 {'Content-Type': 'application/geo+json'}, 799 data 800 ) 801 response = QgsBufferServerResponse() 802 self.server.handleRequest(request, response, project) 803 self.assertEqual(response.statusCode(), 403) 804 805 # Authorized layer 806 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items/1' % update_layer, 807 QgsBufferServerRequest.PutMethod, 808 {'Content-Type': 'application/geo+json'}, 809 data 810 ) 811 response = QgsBufferServerResponse() 812 self.server.handleRequest(request, response, project) 813 self.assertEqual(response.statusCode(), 200) 814 j = json.loads(bytes(response.body()).decode('utf8')) 815 self.assertEqual(j['properties']['text_1'], 'Text 1') 816 self.assertEqual(j['properties']['text_2'], 'Text 2') 817 self.assertEqual(j['properties']['int_1'], 123) 818 self.assertEqual(j['properties']['float_1'], 12345.678) 819 self.assertEqual(j['properties']['bool_1'], True) 820 self.assertEqual(j['properties']['blob_1'], "dGVzdA==") 821 self.assertEqual(j['geometry']['coordinates'], [[7.247, 44.814]]) 822 823 feature = project.mapLayersByName('test layer èé 3857 published update')[ 824 0].getFeature(1) 825 self.assertEqual(feature.attribute('text_1'), 'Text 1') 826 self.assertEqual(feature.attribute('text_2'), 'Text 2') 827 self.assertEqual(feature.attribute('int_1'), 123) 828 self.assertEqual(feature.attribute('float_1'), 12345.678) 829 self.assertEqual(feature.attribute('bool_1'), True) 830 self.assertEqual(bytes(feature.attribute('blob_1')), b"test") 831 self.assertEqual(re.sub( 832 r'\.\d+', '', feature.geometry().asWkt().upper()), 'MULTIPOINT ((806732 5592286))') 833 834 # Test with partial and unordered properties 835 data = """{ 836 "geometry": { 837 "coordinates": [[ 838 8.247, 839 45.814 840 ]], 841 "type": "MultiPoint" 842 }, 843 "properties": { 844 "bool_1": false, 845 "int_1": 1234, 846 "text_2": "Text 2-bis", 847 "text_1": "Text 1-bis" 848 }, 849 "type": "Feature" 850 }""".encode('utf8') 851 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items/1' % update_layer, 852 QgsBufferServerRequest.PutMethod, 853 {'Content-Type': 'application/geo+json'}, 854 data 855 ) 856 response = QgsBufferServerResponse() 857 self.server.handleRequest(request, response, project) 858 self.assertEqual(response.statusCode(), 200) 859 j = json.loads(bytes(response.body()).decode('utf8')) 860 self.assertEqual(j['properties']['text_1'], 'Text 1-bis') 861 self.assertEqual(j['properties']['text_2'], 'Text 2-bis') 862 self.assertEqual(j['properties']['int_1'], 1234) 863 self.assertEqual(j['properties']['float_1'], 12345.678) 864 self.assertEqual(j['properties']['bool_1'], False) 865 self.assertEqual(j['properties']['blob_1'], "dGVzdA==") 866 self.assertEqual(j['geometry']['coordinates'], [[8.247, 45.814]]) 867 868 feature = project.mapLayersByName('test layer èé 3857 published update')[ 869 0].getFeature(1) 870 self.assertEqual(feature.attribute('text_1'), 'Text 1-bis') 871 self.assertEqual(feature.attribute('text_2'), 'Text 2-bis') 872 self.assertEqual(feature.attribute('int_1'), 1234) 873 self.assertEqual(feature.attribute('float_1'), 12345.678) 874 self.assertEqual(feature.attribute('bool_1'), False) 875 self.assertEqual(bytes(feature.attribute('blob_1')), b"test") 876 self.assertEqual(re.sub( 877 r'\.\d+', '', feature.geometry().asWkt().upper()), 'MULTIPOINT ((918051 5750592))') 878 879 # Try to update a forbidden (unpublished) field 880 data = """{ 881 "geometry": { 882 "coordinates": [[ 883 8.247, 884 45.814 885 ]], 886 "type": "MultiPoint" 887 }, 888 "properties": { 889 "text_2": "Text 2-tris" 890 }, 891 "type": "Feature" 892 }""".encode('utf8') 893 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items/1' % hidden_text_2_layer, 894 QgsBufferServerRequest.PutMethod, 895 {'Content-Type': 'application/geo+json'}, 896 data 897 ) 898 response = QgsBufferServerResponse() 899 self.server.handleRequest(request, response, project) 900 self.assertEqual(response.statusCode(), 403) 901 902 def test_wfs3_collection_items_delete(self): 903 """Test WFS3 API items DELETE""" 904 905 tmpDir = QtCore.QTemporaryDir() 906 shutil.copy(unitTestDataPath('qgis_server') + '/test_project_api_editing.qgs', 907 tmpDir.path() + '/test_project_api_editing.qgs') 908 shutil.copy(unitTestDataPath('qgis_server') + '/test_project_api_editing.gpkg', 909 tmpDir.path() + '/test_project_api_editing.gpkg') 910 911 project = QgsProject() 912 project.read(tmpDir.path() + '/test_project_api_editing.qgs') 913 914 # Project layers with different permissions 915 insert_layer = r'test%20layer%20èé%203857%20published%20insert' 916 update_layer = r'test%20layer%20èé%203857%20published%20update' 917 delete_layer = r'test%20layer%20èé%203857%20published%20delete' 918 unpublished_layer = r'test%20layer%203857%20èé%20unpublished' 919 hidden_text_2_layer = r'test%20layer%20èé%203857%20published%20hidden%20text_2' 920 921 # Valid request on unauthorized layer 922 data = b'' 923 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items/1' % update_layer, 924 QgsBufferServerRequest.DeleteMethod) 925 response = QgsBufferServerResponse() 926 self.server.handleRequest(request, response, project) 927 self.assertEqual(response.statusCode(), 403) 928 929 # Valid request on authorized layer 930 data = b'' 931 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items/1' % delete_layer, 932 QgsBufferServerRequest.DeleteMethod) 933 response = QgsBufferServerResponse() 934 self.server.handleRequest(request, response, project) 935 self.assertEqual(response.statusCode(), 200) 936 937 # Check that it was really deleted 938 layer = project.mapLayersByName( 939 'test layer èé 3857 published delete')[0] 940 self.assertFalse(1 in layer.allFeatureIds()) 941 942 def test_wfs3_collection_items_patch(self): 943 """Test WFS3 API items PATCH""" 944 945 tmpDir = QtCore.QTemporaryDir() 946 shutil.copy(unitTestDataPath('qgis_server') + '/test_project_api_editing.qgs', 947 tmpDir.path() + '/test_project_api_editing.qgs') 948 shutil.copy(unitTestDataPath('qgis_server') + '/test_project_api_editing.gpkg', 949 tmpDir.path() + '/test_project_api_editing.gpkg') 950 951 project = QgsProject() 952 project.read(tmpDir.path() + '/test_project_api_editing.qgs') 953 954 # Project layers with different permissions 955 insert_layer = r'test%20layer%20èé%203857%20published%20insert' 956 update_layer = r'test%20layer%20èé%203857%20published%20update' 957 delete_layer = r'test%20layer%20èé%203857%20published%20delete' 958 unpublished_layer = r'test%20layer%203857%20èé%20unpublished' 959 hidden_text_2_layer = r'test%20layer%20èé%203857%20published%20hidden%20text_2' 960 961 # Invalid request 962 data = b'not json!' 963 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items/1' % update_layer, 964 QgsBufferServerRequest.PutMethod, 965 {'Content-Type': 'application/json'}, 966 data 967 ) 968 response = QgsBufferServerResponse() 969 self.server.handleRequest(request, response, project) 970 self.assertEqual(response.statusCode(), 400) 971 self.assertTrue( 972 '[{"code":"Bad request error","description":"JSON parse error' in bytes(response.body()).decode('utf8')) 973 974 # Invalid request: contains "add" 975 data = b""" 976 { 977 "add": { 978 "a_new_field": 1.234 979 }, 980 "modify": { 981 "text_2": "A new text 2" 982 } 983 } 984 """ 985 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items/1' % update_layer, 986 QgsBufferServerRequest.PatchMethod, 987 {'Content-Type': 'application/json'}, 988 data 989 ) 990 response = QgsBufferServerResponse() 991 self.server.handleRequest(request, response, project) 992 self.assertEqual(response.statusCode(), 400) 993 self.assertEqual(bytes(response.body()).decode('utf8'), 994 r'[{"code":"Not implemented error","description":"\"add\" instruction in PATCH method is not implemented"}]') 995 996 # Valid request: change feature with ID 1 997 data = """{ 998 "modify": { 999 "text_2": "A new text 2", 1000 "blob_1": "dGVzdA==" 1001 } 1002 }""".encode('utf8') 1003 1004 # Unauthorized layer 1005 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items/1' % insert_layer, 1006 QgsBufferServerRequest.PatchMethod, 1007 {'Content-Type': 'application/json'}, 1008 data 1009 ) 1010 response = QgsBufferServerResponse() 1011 self.server.handleRequest(request, response, project) 1012 self.assertEqual(response.statusCode(), 403) 1013 1014 # Authorized layer 1015 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items/1' % update_layer, 1016 QgsBufferServerRequest.PatchMethod, 1017 {'Content-Type': 'application/json'}, 1018 data 1019 ) 1020 response = QgsBufferServerResponse() 1021 self.server.handleRequest(request, response, project) 1022 self.assertEqual(response.statusCode(), 200, msg=response.body()) 1023 j = json.loads(bytes(response.body()).decode('utf8')) 1024 self.assertEqual(j['properties']['text_1'], 'Torre Pellice 1') 1025 self.assertEqual(j['properties']['text_2'], 'A new text 2') 1026 self.assertEqual(j['properties']['int_1'], 7) 1027 self.assertEqual(j['properties']['float_1'], 1234.567) 1028 self.assertEqual(j['properties']['bool_1'], True) 1029 self.assertEqual(j['properties']['blob_1'], "dGVzdA==") 1030 self.assertEqual(j['geometry']['coordinates'], [[7.227328, 44.820762]]) 1031 1032 feature = project.mapLayersByName('test layer èé 3857 published update')[ 1033 0].getFeature(1) 1034 self.assertEqual(feature.attribute('text_1'), 'Torre Pellice 1') 1035 self.assertEqual(feature.attribute('text_2'), 'A new text 2') 1036 self.assertEqual(feature.attribute('int_1'), 7) 1037 self.assertEqual(feature.attribute('float_1'), 1234.567) 1038 self.assertEqual(feature.attribute('bool_1'), True) 1039 self.assertEqual(bytes(feature.attribute('blob_1')), b"test") 1040 self.assertEqual(re.sub( 1041 r'\.\d+', '', feature.geometry().asWkt().upper()), 'MULTIPOINT ((804542 5593348))') 1042 1043 # Try to update a forbidden (unpublished) field 1044 request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/%s/items/1' % hidden_text_2_layer, 1045 QgsBufferServerRequest.PatchMethod, 1046 {'Content-Type': 'application/json'}, 1047 data 1048 ) 1049 response = QgsBufferServerResponse() 1050 self.server.handleRequest(request, response, project) 1051 self.assertEqual(response.statusCode(), 403) 1052 1053 def test_wfs3_field_filters(self): 1054 """Test field filters""" 1055 project = QgsProject() 1056 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 1057 # Check not published 1058 response = QgsBufferServerResponse() 1059 request = QgsBufferServerRequest( 1060 'http://server.qgis.org/wfs3/collections/testlayer3/items?name=two') 1061 self.server.handleRequest(request, response, project) 1062 self.assertEqual(response.statusCode(), 404) # Not found 1063 request = QgsBufferServerRequest( 1064 'http://server.qgis.org/wfs3/collections/layer1_with_short_name/items?name=two') 1065 self.server.handleRequest(request, response, project) 1066 self.assertEqual(response.statusCode(), 200) 1067 self.compareApi( 1068 request, project, 'test_wfs3_collections_items_layer1_with_short_name_eq_two.json') 1069 1070 def test_wfs3_sorting(self): 1071 """Test sorting""" 1072 project = QgsProject() 1073 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 1074 # Check not published 1075 response = QgsBufferServerResponse() 1076 request = QgsBufferServerRequest( 1077 'http://server.qgis.org/wfs3/collections/layer1_with_short_name/items?sortby=does_not_exist') 1078 self.server.handleRequest(request, response, project) 1079 self.assertEqual(response.statusCode(), 400) # Bad request 1080 request = QgsBufferServerRequest( 1081 'http://server.qgis.org/wfs3/collections/layer1_with_short_name/items?sortby=name') 1082 self.server.handleRequest(request, response, project) 1083 self.assertEqual(response.statusCode(), 200) 1084 self.compareApi( 1085 request, project, 'test_wfs3_collections_items_layer1_with_short_name_sort_by_name.json') 1086 request = QgsBufferServerRequest( 1087 'http://server.qgis.org/wfs3/collections/layer1_with_short_name/items?sortby=name&sortdesc=1') 1088 self.server.handleRequest(request, response, project) 1089 self.assertEqual(response.statusCode(), 200) 1090 self.compareApi( 1091 request, project, 'test_wfs3_collections_items_layer1_with_short_name_sort_by_name_desc.json') 1092 request = QgsBufferServerRequest( 1093 'http://server.qgis.org/wfs3/collections/layer1_with_short_name/items?sortby=name&sortdesc=0') 1094 self.server.handleRequest(request, response, project) 1095 self.assertEqual(response.statusCode(), 200) 1096 self.compareApi( 1097 request, project, 'test_wfs3_collections_items_layer1_with_short_name_sort_by_name_asc.json') 1098 1099 def test_wfs3_collection_items_properties(self): 1100 """Test WFS3 API items""" 1101 project = QgsProject() 1102 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 1103 1104 # Invalid request 1105 request = QgsBufferServerRequest( 1106 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?properties') 1107 response = QgsBufferServerResponse() 1108 self.server.handleRequest(request, response, project) 1109 self.assertEqual(bytes(response.body()).decode('utf8'), 1110 '[{"code":"Bad request error","description":"Argument \'properties\' is not valid. Comma separated list of feature property names to be added to the result. Valid values: \'id\', \'name\', \'utf8nameè\'"}]') 1111 1112 # Valid request 1113 response = QgsBufferServerResponse() 1114 request = QgsBufferServerRequest( 1115 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?properties=name') 1116 self.server.handleRequest(request, response, project) 1117 j = json.loads(bytes(response.body()).decode('utf8')) 1118 self.assertTrue('name' in j['features'][0]['properties']) 1119 self.assertFalse('id' in j['features'][0]['properties']) 1120 1121 response = QgsBufferServerResponse() 1122 request = QgsBufferServerRequest( 1123 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?properties=name,id') 1124 self.server.handleRequest(request, response, project) 1125 j = json.loads(bytes(response.body()).decode('utf8')) 1126 self.assertTrue('name' in j['features'][0]['properties']) 1127 self.assertTrue('id' in j['features'][0]['properties']) 1128 1129 response = QgsBufferServerResponse() 1130 request = QgsBufferServerRequest( 1131 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?properties=id') 1132 self.server.handleRequest(request, response, project) 1133 j = json.loads(bytes(response.body()).decode('utf8')) 1134 self.assertFalse('name' in j['features'][0]['properties']) 1135 self.assertTrue('id' in j['features'][0]['properties']) 1136 1137 def test_wfs3_field_filters_star(self): 1138 """Test field filters""" 1139 project = QgsProject() 1140 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 1141 request = QgsBufferServerRequest( 1142 'http://server.qgis.org/wfs3/collections/layer1_with_short_name/items?name=tw*') 1143 response = self.compareApi(request, project, 1144 'test_wfs3_collections_items_layer1_with_short_name_eq_tw_star.json') 1145 self.assertEqual(response.statusCode(), 200) 1146 1147 def test_wfs3_excluded_attributes(self): 1148 """Test excluded attributes""" 1149 project = QgsProject() 1150 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 1151 request = QgsBufferServerRequest( 1152 'http://server.qgis.org/wfs3/collections/exclude_attribute/items/0.geojson') 1153 response = self.compareApi( 1154 request, project, 'test_wfs3_collections_items_exclude_attribute_0.json') 1155 self.assertEqual(response.statusCode(), 200) 1156 1157 def test_wfs3_invalid_fids(self): 1158 """Test exceptions for invalid fids""" 1159 1160 project = QgsProject() 1161 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 1162 request = QgsBufferServerRequest( 1163 'http://server.qgis.org/wfs3/collections/exclude_attribute/items/123456.geojson') 1164 response = QgsBufferServerResponse() 1165 self.server.handleRequest(request, response, project) 1166 self.assertEqual(bytes(response.body()).decode('utf-8'), '[{"code":"Internal server error","description":"Invalid feature [123456]"}]') 1167 1168 request = QgsBufferServerRequest( 1169 'http://server.qgis.org/wfs3/collections/exclude_attribute/items/xYz@#.geojson') 1170 response = QgsBufferServerResponse() 1171 self.server.handleRequest(request, response, project) 1172 self.assertEqual(bytes(response.body()).decode('utf-8'), '[{"code":"Internal server error","description":"Invalid feature ID [xYz@]"}]') 1173 1174 def test_wfs3_time_filters_ranges(self): 1175 """Test datetime filters""" 1176 1177 project = QgsProject() 1178 1179 tempDir = QtCore.QTemporaryDir() 1180 source_project_path = unitTestDataPath( 1181 'qgis_server') + '/test_project_api_timefilters.qgs' 1182 source_data_path = unitTestDataPath( 1183 'qgis_server') + '/test_project_api_timefilters.gpkg' 1184 dest_project_path = os.path.join( 1185 tempDir.path(), 'test_project_api_timefilters.qgs') 1186 dest_data_path = os.path.join( 1187 tempDir.path(), 'test_project_api_timefilters.gpkg') 1188 shutil.copy(source_data_path, dest_data_path) 1189 shutil.copy(source_project_path, dest_project_path) 1190 project.read(dest_project_path) 1191 1192 # Prepare projects with all options 1193 1194 layer = list(project.mapLayers().values())[0] 1195 layer.serverProperties().removeWmsDimension('date') 1196 layer.serverProperties().removeWmsDimension('time') 1197 self.assertEqual(len(layer.serverProperties().wmsDimensions()), 0) 1198 self.assertEqual(len(project.mapLayersByName('points')[ 1199 0].serverProperties().wmsDimensions()), 0) 1200 none_path = os.path.join( 1201 tempDir.path(), 'test_project_api_timefilters_none.qgs') 1202 project.write(none_path) 1203 1204 layer = list(project.mapLayers().values())[0] 1205 layer.serverProperties().removeWmsDimension('date') 1206 layer.serverProperties().removeWmsDimension('time') 1207 self.assertTrue(layer.serverProperties().addWmsDimension( 1208 QgsVectorLayerServerProperties.WmsDimensionInfo('date', 'created'))) 1209 created_path = os.path.join( 1210 tempDir.path(), 'test_project_api_timefilters_created.qgs') 1211 project.write(created_path) 1212 project.read(created_path) 1213 self.assertEqual(len(project.mapLayersByName('points')[ 1214 0].serverProperties().wmsDimensions()), 1) 1215 1216 layer = list(project.mapLayers().values())[0] 1217 layer.serverProperties().removeWmsDimension('date') 1218 layer.serverProperties().removeWmsDimension('time') 1219 self.assertTrue(layer.serverProperties().addWmsDimension( 1220 QgsVectorLayerServerProperties.WmsDimensionInfo('date', 'created_string'))) 1221 created_string_path = os.path.join( 1222 tempDir.path(), 'test_project_api_timefilters_created_string.qgs') 1223 project.write(created_string_path) 1224 project.read(created_string_path) 1225 self.assertEqual(len(project.mapLayersByName('points')[ 1226 0].serverProperties().wmsDimensions()), 1) 1227 1228 layer = list(project.mapLayers().values())[0] 1229 layer.serverProperties().removeWmsDimension('date') 1230 layer.serverProperties().removeWmsDimension('time') 1231 self.assertTrue(layer.serverProperties().addWmsDimension( 1232 QgsVectorLayerServerProperties.WmsDimensionInfo('time', 'updated_string'))) 1233 updated_string_path = os.path.join( 1234 tempDir.path(), 'test_project_api_timefilters_updated_string.qgs') 1235 project.write(updated_string_path) 1236 project.read(updated_string_path) 1237 self.assertEqual(len(project.mapLayersByName('points')[ 1238 0].serverProperties().wmsDimensions()), 1) 1239 1240 layer = list(project.mapLayers().values())[0] 1241 layer.serverProperties().removeWmsDimension('date') 1242 layer.serverProperties().removeWmsDimension('time') 1243 self.assertEqual(len(project.mapLayersByName('points')[ 1244 0].serverProperties().wmsDimensions()), 0) 1245 self.assertTrue(layer.serverProperties().addWmsDimension( 1246 QgsVectorLayerServerProperties.WmsDimensionInfo('time', 'updated'))) 1247 updated_path = os.path.join( 1248 tempDir.path(), 'test_project_api_timefilters_updated.qgs') 1249 project.write(updated_path) 1250 project.read(updated_path) 1251 self.assertEqual(len(project.mapLayersByName('points')[ 1252 0].serverProperties().wmsDimensions()), 1) 1253 1254 layer = list(project.mapLayers().values())[0] 1255 layer.serverProperties().removeWmsDimension('date') 1256 layer.serverProperties().removeWmsDimension('time') 1257 self.assertEqual(len(project.mapLayersByName('points')[ 1258 0].serverProperties().wmsDimensions()), 0) 1259 self.assertTrue(layer.serverProperties().addWmsDimension( 1260 QgsVectorLayerServerProperties.WmsDimensionInfo('time', 'updated'))) 1261 self.assertTrue(layer.serverProperties().addWmsDimension( 1262 QgsVectorLayerServerProperties.WmsDimensionInfo('date', 'created'))) 1263 both_path = os.path.join( 1264 tempDir.path(), 'test_project_api_timefilters_both.qgs') 1265 project.write(both_path) 1266 project.read(both_path) 1267 self.assertEqual(len(project.mapLayersByName('points')[ 1268 0].serverProperties().wmsDimensions()), 2) 1269 1270 layer = list(project.mapLayers().values())[0] 1271 layer.serverProperties().removeWmsDimension('date') 1272 layer.serverProperties().removeWmsDimension('time') 1273 self.assertTrue(layer.serverProperties().addWmsDimension( 1274 QgsVectorLayerServerProperties.WmsDimensionInfo('date', 'begin', 'end'))) 1275 date_range_path = os.path.join( 1276 tempDir.path(), 'test_project_api_timefilters_date_range.qgs') 1277 project.write(date_range_path) 1278 project.read(date_range_path) 1279 self.assertEqual(len(project.mapLayersByName('points')[ 1280 0].serverProperties().wmsDimensions()), 1) 1281 1282 ''' 1283 Test data 1284 wkt_geom fid name created updated begin end 1285 Point (7.28848021144956881 44.79768920192042714) 3 bibiana 1286 Point (7.30355493642693343 44.82162158126364915) 2 bricherasio 2017-01-01 2019-01-01T01:01:01.000 2017-01-01 2019-01-01 1287 Point (7.22555186948937145 44.82015087638781381) 4 torre 2018-01-01 2021-01-01T01:01:01.000 2018-01-01 2021-01-01 1288 Point (7.2500747591236081 44.81342128741047048) 1 luserna 2019-01-01 2022-01-01T01:01:01.000 2020-01-01 2022-01-01 1289 Point (7.2500747591236081 44.81342128741047048) 5 villar 2010-01-01 2010-01-01T01:01:01.000 2010-01-01 2010-01-01 1290 ''' 1291 1292 # What to test: 1293 # interval-closed = date-time "/" date-time 1294 # interval-open-start = [".."] "/" date-time 1295 # interval-open-end = date-time "/" [".."] 1296 # interval = interval-closed / interval-open-start / interval-open-end 1297 # datetime = date-time / interval 1298 1299 def _date_tester(project_path, datetime, expected, unexpected): 1300 # Test "created" date field exact 1301 request = QgsBufferServerRequest( 1302 'http://server.qgis.org/wfs3/collections/points/items?datetime=%s' % datetime) 1303 response = QgsBufferServerResponse() 1304 project.read(project_path) 1305 self.server.handleRequest(request, response, project) 1306 body = bytes(response.body()).decode('utf8') 1307 # print(body) 1308 for exp in expected: 1309 self.assertTrue(exp in body) 1310 for unexp in unexpected: 1311 self.assertFalse(unexp in body) 1312 1313 def _interval(project_path, interval): 1314 project.read(project_path) 1315 layer = list(project.mapLayers().values())[0] 1316 return QgsServerApiUtils.temporalFilterExpression(layer, interval).expression() 1317 1318 # Bad request 1319 request = QgsBufferServerRequest( 1320 'http://server.qgis.org/wfs3/collections/points/items?datetime=bad timing!') 1321 response = QgsBufferServerResponse() 1322 project.read(created_path) 1323 self.server.handleRequest(request, response, project) 1324 self.assertEqual(response.statusCode(), 400) 1325 request = QgsBufferServerRequest( 1326 'http://server.qgis.org/wfs3/collections/points/items?datetime=2020-01-01/2010-01-01') 1327 self.server.handleRequest(request, response, project) 1328 self.assertEqual(response.statusCode(), 400) 1329 # empty 1330 request = QgsBufferServerRequest( 1331 'http://server.qgis.org/wfs3/collections/points/items?datetime=2020-01-01/2010-01-01') 1332 self.server.handleRequest(request, response, project) 1333 self.assertEqual(response.statusCode(), 400) 1334 1335 # Created (date type) 1336 self.assertEqualBrackets(_interval(created_path, '2017-01-01'), 1337 '( "created" IS NULL OR "created" = to_date( \'2017-01-01\' ) )') 1338 self.assertEqualBrackets(_interval(created_path, '../2017-01-01'), 1339 '( "created" IS NULL OR "created" <= to_date( \'2017-01-01\' ) )') 1340 self.assertEqualBrackets(_interval(created_path, '/2017-01-01'), 1341 '( "created" IS NULL OR "created" <= to_date( \'2017-01-01\' ) )') 1342 self.assertEqualBrackets(_interval(created_path, '2017-01-01/'), 1343 '( "created" IS NULL OR "created" >= to_date( \'2017-01-01\' ) )') 1344 self.assertEqualBrackets(_interval(created_path, '2017-01-01/..'), 1345 '( "created" IS NULL OR "created" >= to_date( \'2017-01-01\' ) )') 1346 self.assertEqualBrackets(_interval(created_path, '2017-01-01/2018-01-01'), 1347 '( "created" IS NULL OR ( to_date( \'2017-01-01\' ) <= "created" AND "created" <= to_date( \'2018-01-01\' ) ) )') 1348 1349 self.assertEqualBrackets(_interval(created_path, '2017-01-01T01:01:01'), 1350 '( "created" IS NULL OR "created" = to_date( \'2017-01-01\' ) )') 1351 self.assertEqualBrackets(_interval(created_path, '../2017-01-01T01:01:01'), 1352 '( "created" IS NULL OR "created" <= to_date( \'2017-01-01\' ) )') 1353 self.assertEqualBrackets(_interval(created_path, '/2017-01-01T01:01:01'), 1354 '( "created" IS NULL OR "created" <= to_date( \'2017-01-01\' ) )') 1355 self.assertEqualBrackets(_interval(created_path, '2017-01-01T01:01:01/'), 1356 '( "created" IS NULL OR "created" >= to_date( \'2017-01-01\' ) )') 1357 self.assertEqualBrackets(_interval(created_path, '2017-01-01T01:01:01/..'), 1358 '( "created" IS NULL OR "created" >= to_date( \'2017-01-01\' ) )') 1359 self.assertEqualBrackets(_interval(created_path, '2017-01-01T01:01:01/2018-01-01T01:01:01'), 1360 '( "created" IS NULL OR ( to_date( \'2017-01-01\' ) <= "created" AND "created" <= to_date( \'2018-01-01\' ) ) )') 1361 1362 # Updated (datetime type) 1363 self.assertEqualBrackets(_interval(updated_path, '2017-01-01'), 1364 '( "updated" IS NULL OR to_date( "updated" ) = to_date( \'2017-01-01\' ) )') 1365 self.assertEqualBrackets(_interval(updated_path, '/2017-01-01'), 1366 '( "updated" IS NULL OR to_date( "updated" ) <= to_date( \'2017-01-01\' ) )') 1367 self.assertEqualBrackets(_interval(updated_path, '../2017-01-01'), 1368 '( "updated" IS NULL OR to_date( "updated" ) <= to_date( \'2017-01-01\' ) )') 1369 self.assertEqualBrackets(_interval(updated_path, '2017-01-01/'), 1370 '( "updated" IS NULL OR to_date( "updated" ) >= to_date( \'2017-01-01\' ) )') 1371 self.assertEqualBrackets(_interval(updated_path, '2017-01-01/..'), 1372 '( "updated" IS NULL OR to_date( "updated" ) >= to_date( \'2017-01-01\' ) )') 1373 self.assertEqualBrackets(_interval(updated_path, '2017-01-01/2018-01-01'), 1374 '( "updated" IS NULL OR ( to_date( \'2017-01-01\' ) <= to_date( "updated" ) AND to_date( "updated" ) <= to_date( \'2018-01-01\' ) ) )') 1375 1376 self.assertEqualBrackets(_interval(updated_path, '2017-01-01T01:01:01'), 1377 '( "updated" IS NULL OR "updated" = to_datetime( \'2017-01-01T01:01:01\' ) )') 1378 self.assertEqualBrackets(_interval(updated_path, '../2017-01-01T01:01:01'), 1379 '( "updated" IS NULL OR "updated" <= to_datetime( \'2017-01-01T01:01:01\' ) )') 1380 self.assertEqualBrackets(_interval(updated_path, '/2017-01-01T01:01:01'), 1381 '( "updated" IS NULL OR "updated" <= to_datetime( \'2017-01-01T01:01:01\' ) )') 1382 self.assertEqualBrackets(_interval(updated_path, '2017-01-01T01:01:01/'), 1383 '( "updated" IS NULL OR "updated" >= to_datetime( \'2017-01-01T01:01:01\' ) )') 1384 self.assertEqualBrackets(_interval(updated_path, '2017-01-01T01:01:01/..'), 1385 '( "updated" IS NULL OR "updated" >= to_datetime( \'2017-01-01T01:01:01\' ) )') 1386 self.assertEqualBrackets(_interval(updated_path, '2017-01-01T01:01:01/2018-01-01T01:01:01'), 1387 '( "updated" IS NULL OR ( to_datetime( \'2017-01-01T01:01:01\' ) <= "updated" AND "updated" <= to_datetime( \'2018-01-01T01:01:01\' ) ) )') 1388 1389 # Created string (date type) 1390 self.assertEqualBrackets(_interval(created_string_path, '2017-01-01'), 1391 '( "created_string" IS NULL OR to_date( "created_string" ) = to_date( \'2017-01-01\' ) )') 1392 self.assertEqualBrackets(_interval(created_string_path, '../2017-01-01'), 1393 '( "created_string" IS NULL OR to_date( "created_string" ) <= to_date( \'2017-01-01\' ) )') 1394 self.assertEqualBrackets(_interval(created_string_path, '/2017-01-01'), 1395 '( "created_string" IS NULL OR to_date( "created_string" ) <= to_date( \'2017-01-01\' ) )') 1396 self.assertEqualBrackets(_interval(created_string_path, '2017-01-01/'), 1397 '( "created_string" IS NULL OR to_date( "created_string" ) >= to_date( \'2017-01-01\' ) )') 1398 self.assertEqualBrackets(_interval(created_string_path, '2017-01-01/..'), 1399 '( "created_string" IS NULL OR to_date( "created_string" ) >= to_date( \'2017-01-01\' ) )') 1400 self.assertEqualBrackets(_interval(created_string_path, '2017-01-01/2018-01-01'), 1401 '( "created_string" IS NULL OR ( to_date( \'2017-01-01\' ) <= to_date( "created_string" ) AND to_date( "created_string" ) <= to_date( \'2018-01-01\' ) ) )') 1402 1403 self.assertEqualBrackets(_interval(created_string_path, '2017-01-01T01:01:01'), 1404 '( "created_string" IS NULL OR to_date( "created_string" ) = to_date( \'2017-01-01\' ) )') 1405 self.assertEqualBrackets(_interval(created_string_path, '../2017-01-01T01:01:01'), 1406 '( "created_string" IS NULL OR to_date( "created_string" ) <= to_date( \'2017-01-01\' ) )') 1407 self.assertEqualBrackets(_interval(created_string_path, '/2017-01-01T01:01:01'), 1408 '( "created_string" IS NULL OR to_date( "created_string" ) <= to_date( \'2017-01-01\' ) )') 1409 self.assertEqualBrackets(_interval(created_string_path, '2017-01-01T01:01:01/'), 1410 '( "created_string" IS NULL OR to_date( "created_string" ) >= to_date( \'2017-01-01\' ) )') 1411 self.assertEqualBrackets(_interval(created_string_path, '2017-01-01T01:01:01/..'), 1412 '( "created_string" IS NULL OR to_date( "created_string" ) >= to_date( \'2017-01-01\' ) )') 1413 self.assertEqualBrackets(_interval(created_string_path, '2017-01-01T01:01:01/2018-01-01T01:01:01'), 1414 '( "created_string" IS NULL OR ( to_date( \'2017-01-01\' ) <= to_date( "created_string" ) AND to_date( "created_string" ) <= to_date( \'2018-01-01\' ) ) )') 1415 1416 # Updated string (datetime type) 1417 self.assertEqualBrackets(_interval(updated_string_path, '2017-01-01'), 1418 '( "updated_string" IS NULL OR to_date( "updated_string" ) = to_date( \'2017-01-01\' ) )') 1419 self.assertEqualBrackets(_interval(updated_string_path, '/2017-01-01'), 1420 '( "updated_string" IS NULL OR to_date( "updated_string" ) <= to_date( \'2017-01-01\' ) )') 1421 self.assertEqualBrackets(_interval(updated_string_path, '../2017-01-01'), 1422 '( "updated_string" IS NULL OR to_date( "updated_string" ) <= to_date( \'2017-01-01\' ) )') 1423 self.assertEqualBrackets(_interval(updated_string_path, '2017-01-01/'), 1424 '( "updated_string" IS NULL OR to_date( "updated_string" ) >= to_date( \'2017-01-01\' ) )') 1425 self.assertEqualBrackets(_interval(updated_string_path, '2017-01-01/..'), 1426 '( "updated_string" IS NULL OR to_date( "updated_string" ) >= to_date( \'2017-01-01\' ) )') 1427 self.assertEqualBrackets(_interval(updated_string_path, '2017-01-01/2018-01-01'), 1428 '( "updated_string" IS NULL OR ( to_date( \'2017-01-01\' ) <= to_date( "updated_string" ) AND to_date( "updated_string" ) <= to_date( \'2018-01-01\' ) ) )') 1429 1430 self.assertEqualBrackets(_interval(updated_string_path, '2017-01-01T01:01:01'), 1431 '( "updated_string" IS NULL OR to_datetime( "updated_string" ) = to_datetime( \'2017-01-01T01:01:01\' ) )') 1432 self.assertEqualBrackets(_interval(updated_string_path, '../2017-01-01T01:01:01'), 1433 '( "updated_string" IS NULL OR to_datetime( "updated_string" ) <= to_datetime( \'2017-01-01T01:01:01\' ) )') 1434 self.assertEqualBrackets(_interval(updated_string_path, '/2017-01-01T01:01:01'), 1435 '( "updated_string" IS NULL OR to_datetime( "updated_string" ) <= to_datetime( \'2017-01-01T01:01:01\' ) )') 1436 self.assertEqualBrackets(_interval(updated_string_path, '2017-01-01T01:01:01/'), 1437 '( "updated_string" IS NULL OR to_datetime( "updated_string" ) >= to_datetime( \'2017-01-01T01:01:01\' ) )') 1438 self.assertEqualBrackets(_interval(updated_string_path, '2017-01-01T01:01:01/..'), 1439 '( "updated_string" IS NULL OR to_datetime( "updated_string" ) >= to_datetime( \'2017-01-01T01:01:01\' ) )') 1440 self.assertEqualBrackets(_interval(updated_string_path, '2017-01-01T01:01:01/2018-01-01T01:01:01'), 1441 '( "updated_string" IS NULL OR ( to_datetime( \'2017-01-01T01:01:01\' ) <= to_datetime( "updated_string" ) AND to_datetime( "updated_string" ) <= to_datetime( \'2018-01-01T01:01:01\' ) ) )') 1442 1443 # Ranges 1444 self.assertEqualBrackets(_interval(date_range_path, '2010-01-01'), 1445 '( "begin" IS NULL OR "begin" <= to_date( \'2010-01-01\' ) ) AND ( "end" IS NULL OR to_date( \'2010-01-01\' ) <= "end" )') 1446 self.assertEqualBrackets(_interval(date_range_path, '../2010-01-01'), 1447 '( "begin" IS NULL OR "begin" <= to_date( \'2010-01-01\' ) )') 1448 self.assertEqualBrackets(_interval(date_range_path, '2010-01-01/..'), 1449 '( "end" IS NULL OR "end" >= to_date( \'2010-01-01\' ) )') 1450 # Overlap of ranges 1451 self.assertEqualBrackets(_interval(date_range_path, '2010-01-01/2020-09-12'), 1452 '( "begin" IS NULL OR "begin" <= to_date( \'2020-09-12\' ) ) AND ( "end" IS NULL OR "end" >= to_date( \'2010-01-01\' ) )') 1453 1454 ################################################################################## 1455 # Test "created" date field 1456 # Test exact 1457 _date_tester(created_path, '2017-01-01', 1458 ['bricherasio'], ['luserna', 'torre']) 1459 # Test datetime field exact (test that we can use a time on a date type field) 1460 _date_tester(created_path, '2017-01-01T01:01:01', 1461 ['bricherasio'], ['luserna', 'torre']) 1462 # Test exact no match 1463 _date_tester(created_path, '2000-05-06', [], 1464 ['luserna', 'bricherasio', 'torre']) 1465 1466 ################################################################################## 1467 # Test "updated" datetime field 1468 # Test exact 1469 _date_tester(updated_path, '2019-01-01T01:01:01', 1470 ['bricherasio'], ['luserna', 'torre']) 1471 # Test date field exact (test that we can also use a date on a datetime type field) 1472 _date_tester(updated_path, '2019-01-01', 1473 ['bricherasio'], ['luserna', 'torre']) 1474 # Test exact no match 1475 _date_tester(updated_path, '2017-01-01T05:05:05', 1476 [], ['luserna', 'bricherasio', 'torre']) 1477 1478 ################################################################################## 1479 # Test both 1480 # Test exact 1481 _date_tester(both_path, '2010-01-01T01:01:01', 1482 ['villar'], ['torre', 'bricherasio', 'luserna']) 1483 # Test date field exact (test that we can use a date on a datetime type field) 1484 _date_tester(both_path, '2010-01-01', 1485 ['villar'], ['luserna', 'bricherasio', 'torre']) 1486 # Test exact no match 1487 _date_tester(both_path, '2020-05-06T05:05:05', [], 1488 ['luserna', 'bricherasio', 'torre', 'villar']) 1489 1490 # Test intervals 1491 1492 ################################################################################## 1493 # Test "created" date field 1494 _date_tester(created_path, '2016-05-04/2018-05-06', 1495 ['bricherasio', 'torre'], ['luserna', 'villar']) 1496 _date_tester(created_path, '2016-05-04/..', 1497 ['bricherasio', 'torre', 'luserna'], ['villar']) 1498 _date_tester(created_path, '2016-05-04/', 1499 ['bricherasio', 'torre', 'luserna'], ['villar']) 1500 _date_tester(created_path, '2100-05-04/', [], 1501 ['luserna', 'bricherasio', 'torre', 'villar']) 1502 _date_tester(created_path, '2100-05-04/..', [], 1503 ['luserna', 'bricherasio', 'torre', 'villar']) 1504 _date_tester(created_path, '/2018-05-06', 1505 ['bricherasio', 'torre', 'villar'], ['luserna']) 1506 _date_tester(created_path, '../2018-05-06', 1507 ['bricherasio', 'torre', 'villar'], ['luserna']) 1508 1509 # Test datetimes on "created" date field 1510 _date_tester(created_path, '2016-05-04T01:01:01/2018-05-06T01:01:01', ['bricherasio', 'torre'], 1511 ['luserna', 'villar']) 1512 _date_tester(created_path, '2016-05-04T01:01:01/..', 1513 ['bricherasio', 'torre', 'luserna'], ['villar']) 1514 _date_tester(created_path, '2016-05-04T01:01:01/', 1515 ['bricherasio', 'torre', 'luserna'], ['villar']) 1516 _date_tester(created_path, '2100-05-04T01:01:01/', [], 1517 ['luserna', 'bricherasio', 'torre', 'villar']) 1518 _date_tester(created_path, '2100-05-04T01:01:01/..', [], 1519 ['luserna', 'bricherasio', 'torre', 'villar']) 1520 _date_tester(created_path, '/2018-05-06T01:01:01', 1521 ['bricherasio', 'torre', 'villar'], ['luserna']) 1522 _date_tester(created_path, '../2018-05-06T01:01:01', 1523 ['bricherasio', 'torre', 'villar'], ['luserna']) 1524 1525 ################################################################################## 1526 # Test "updated" date field 1527 _date_tester(updated_path, '2020-05-04/2022-12-31', 1528 ['torre', 'luserna'], ['bricherasio', 'villar']) 1529 _date_tester(updated_path, '2020-05-04/..', 1530 ['torre', 'luserna'], ['bricherasio', 'villar']) 1531 _date_tester(updated_path, '2020-05-04/', 1532 ['torre', 'luserna'], ['bricherasio', 'villar']) 1533 _date_tester(updated_path, '2019-01-01/', 1534 ['torre', 'luserna', 'bricherasio'], ['villar']) 1535 _date_tester(updated_path, '2019-01-01/..', 1536 ['torre', 'luserna', 'bricherasio'], ['villar']) 1537 _date_tester(updated_path, '/2020-02-02', 1538 ['villar', 'bricherasio'], ['torre', 'luserna']) 1539 _date_tester(updated_path, '../2020-02-02', 1540 ['villar', 'bricherasio'], ['torre', 'luserna']) 1541 1542 # Test datetimes on "updated" datetime field 1543 _date_tester(updated_path, '2020-05-04T01:01:01/2022-12-31T01:01:01', ['torre', 'luserna'], 1544 ['bricherasio', 'villar']) 1545 _date_tester(updated_path, '2020-05-04T01:01:01/..', 1546 ['torre', 'luserna'], ['bricherasio', 'villar']) 1547 _date_tester(updated_path, '2020-05-04T01:01:01/', 1548 ['torre', 'luserna'], ['bricherasio', 'villar']) 1549 _date_tester(updated_path, '2019-01-01T01:01:01/', 1550 ['torre', 'luserna', 'bricherasio'], ['villar']) 1551 _date_tester(updated_path, '2019-01-01T01:01:01/..', 1552 ['torre', 'luserna', 'bricherasio'], ['villar']) 1553 _date_tester(updated_path, '/2020-02-02T01:01:01', 1554 ['villar', 'bricherasio'], ['torre', 'luserna']) 1555 _date_tester(updated_path, '../2020-02-02T01:01:01', 1556 ['villar', 'bricherasio'], ['torre', 'luserna']) 1557 1558 ################################################################################## 1559 # Test both 1560 _date_tester(both_path, '2010-01-01', 1561 ['villar'], ['luserna', 'bricherasio']) 1562 _date_tester(both_path, '2010-01-01/2010-01-01', 1563 ['villar'], ['luserna', 'bricherasio']) 1564 _date_tester(both_path, '2017-01-01/2021-01-01', 1565 ['torre', 'bricherasio'], ['luserna', 'villar']) 1566 _date_tester(both_path, '../2021-01-01', 1567 ['torre', 'bricherasio', 'villar'], ['luserna']) 1568 _date_tester(both_path, '2019-01-01/..', 1569 ['luserna'], ['torre', 'bricherasio', 'villar']) 1570 1571 ################################################################################## 1572 # Test none path (should take the first date/datetime field, that is "created") 1573 1574 _date_tester(none_path, '2016-05-04/2018-05-06', 1575 ['bricherasio', 'torre'], ['luserna', 'villar']) 1576 _date_tester(none_path, '2016-05-04/..', 1577 ['bricherasio', 'torre', 'luserna'], ['villar']) 1578 _date_tester(none_path, '2016-05-04/', 1579 ['bricherasio', 'torre', 'luserna'], ['villar']) 1580 _date_tester(none_path, '2100-05-04/', [], 1581 ['luserna', 'bricherasio', 'torre', 'villar']) 1582 _date_tester(none_path, '2100-05-04/..', [], 1583 ['luserna', 'bricherasio', 'torre', 'villar']) 1584 _date_tester(none_path, '/2018-05-06', 1585 ['bricherasio', 'torre', 'villar'], ['luserna']) 1586 _date_tester(none_path, '../2018-05-06', 1587 ['bricherasio', 'torre', 'villar'], ['luserna']) 1588 1589 # Test datetimes on "created" date field 1590 _date_tester(none_path, '2016-05-04T01:01:01/2018-05-06T01:01:01', ['bricherasio', 'torre'], 1591 ['luserna', 'villar']) 1592 _date_tester(none_path, '2016-05-04T01:01:01/..', 1593 ['bricherasio', 'torre', 'luserna'], ['villar']) 1594 _date_tester(none_path, '2016-05-04T01:01:01/', 1595 ['bricherasio', 'torre', 'luserna'], ['villar']) 1596 _date_tester(none_path, '2100-05-04T01:01:01/', [], 1597 ['luserna', 'bricherasio', 'torre', 'villar']) 1598 _date_tester(none_path, '2100-05-04T01:01:01/..', [], 1599 ['luserna', 'bricherasio', 'torre', 'villar']) 1600 _date_tester(none_path, '/2018-05-06T01:01:01', 1601 ['bricherasio', 'torre', 'villar'], ['luserna']) 1602 _date_tester(none_path, '../2018-05-06T01:01:01', 1603 ['bricherasio', 'torre', 'villar'], ['luserna']) 1604 1605 ##################################################################################################### 1606 # Test ranges 1607 _date_tester(date_range_path, '2000-05-05T01:01:01', [], 1608 ['bricherasio', 'villar', 'luserna', 'torre']) 1609 _date_tester(date_range_path, '2020-05-05T01:01:01', 1610 ['luserna', 'torre'], ['bricherasio', 'villar']) 1611 _date_tester(date_range_path, '../2000-05-05T01:01:01', [], 1612 ['luserna', 'torre', 'bricherasio', 'villar']) 1613 _date_tester(date_range_path, '../2017-05-05T01:01:01', 1614 ['bricherasio', 'villar'], ['luserna', 'torre']) 1615 _date_tester(date_range_path, '../2050-05-05T01:01:01', 1616 ['bricherasio', 'villar', 'luserna', 'torre'], []) 1617 _date_tester(date_range_path, '2020-05-05T01:01:01/', 1618 ['luserna', 'torre'], ['bricherasio', 'villar']) 1619 1620 _date_tester(date_range_path, '2000-05-05', [], 1621 ['bricherasio', 'villar', 'luserna', 'torre']) 1622 _date_tester(date_range_path, '2020-05-05', 1623 ['luserna', 'torre'], ['bricherasio', 'villar']) 1624 _date_tester(date_range_path, '../2000-05-05', [], 1625 ['luserna', 'torre', 'bricherasio', 'villar']) 1626 _date_tester(date_range_path, '../2017-05-05', 1627 ['bricherasio', 'villar'], ['luserna', 'torre']) 1628 _date_tester(date_range_path, '../2050-05-05', 1629 ['bricherasio', 'villar', 'luserna', 'torre'], []) 1630 _date_tester(date_range_path, '2020-05-05/', 1631 ['luserna', 'torre'], ['bricherasio', 'villar']) 1632 1633 # Test bad requests 1634 request = QgsBufferServerRequest( 1635 'http://server.qgis.org/wfs3/collections/points/items?datetime=bad timing!') 1636 response = QgsBufferServerResponse() 1637 project.read(created_path) 1638 self.server.handleRequest(request, response, project) 1639 self.assertEqual(response.statusCode(), 400) 1640 1641 1642class Handler1(QgsServerOgcApiHandler): 1643 1644 def path(self): 1645 return QtCore.QRegularExpression("/handlerone") 1646 1647 def operationId(self): 1648 return "handlerOne" 1649 1650 def summary(self): 1651 return "First of its name" 1652 1653 def description(self): 1654 return "The first handler ever" 1655 1656 def linkTitle(self): 1657 return "Handler One Link Title" 1658 1659 def linkType(self): 1660 return QgsServerOgcApi.data 1661 1662 def handleRequest(self, context): 1663 """Simple mirror: returns the parameters""" 1664 1665 params = self.values(context) 1666 self.write(params, context) 1667 1668 def parameters(self, context): 1669 return [ 1670 QgsServerQueryStringParameter('value1', True, QgsServerQueryStringParameter.Type.Double, 'a double value')] 1671 1672 1673class Handler2(QgsServerOgcApiHandler): 1674 1675 def path(self): 1676 return QtCore.QRegularExpression(r"/handlertwo/(?P<code1>\d{2})/(\d{3})") 1677 1678 def operationId(self): 1679 return "handlerTwo" 1680 1681 def summary(self): 1682 return "Second of its name" 1683 1684 def description(self): 1685 return "The second handler ever" 1686 1687 def linkTitle(self): 1688 return "Handler Two Link Title" 1689 1690 def linkType(self): 1691 return QgsServerOgcApi.data 1692 1693 def handleRequest(self, context): 1694 """Simple mirror: returns the parameters""" 1695 1696 params = self.values(context) 1697 self.write(params, context) 1698 1699 def parameters(self, context): 1700 return [ 1701 QgsServerQueryStringParameter( 1702 'value1', True, QgsServerQueryStringParameter.Type.Double, 'a double value'), 1703 QgsServerQueryStringParameter('value2', False, QgsServerQueryStringParameter.Type.String, 1704 'a string value'), ] 1705 1706 1707class Handler3(QgsServerOgcApiHandler): 1708 """Custom content types: only accept JSON""" 1709 1710 templatePathOverride = None 1711 1712 def __init__(self): 1713 super(Handler3, self).__init__() 1714 self.setContentTypes([QgsServerOgcApi.JSON]) 1715 1716 def path(self): 1717 return QtCore.QRegularExpression(r"/handlerthree") 1718 1719 def operationId(self): 1720 return "handlerThree" 1721 1722 def summary(self): 1723 return "Third of its name" 1724 1725 def description(self): 1726 return "The third handler ever" 1727 1728 def linkTitle(self): 1729 return "Handler Three Link Title" 1730 1731 def linkType(self): 1732 return QgsServerOgcApi.data 1733 1734 def handleRequest(self, context): 1735 """Simple mirror: returns the parameters""" 1736 1737 params = self.values(context) 1738 self.write(params, context) 1739 1740 def parameters(self, context): 1741 return [ 1742 QgsServerQueryStringParameter('value1', True, QgsServerQueryStringParameter.Type.Double, 'a double value')] 1743 1744 def templatePath(self, context): 1745 if self.templatePathOverride is None: 1746 return super(Handler3, self).templatePath(context) 1747 else: 1748 return self.templatePathOverride 1749 1750 1751class Handler4(QgsServerOgcApiHandler): 1752 1753 def path(self): 1754 return QtCore.QRegularExpression("/(?P<tilemapid>[^/]+)") 1755 1756 def operationId(self): 1757 return "handler4" 1758 1759 def summary(self): 1760 return "Fourth of its name" 1761 1762 def description(self): 1763 return "The fourth handler ever" 1764 1765 def linkTitle(self): 1766 return "Handler Four Link Title" 1767 1768 def linkType(self): 1769 return QgsServerOgcApi.data 1770 1771 def handleRequest(self, context): 1772 """Simple mirror: returns the parameters""" 1773 1774 self.params = self.values(context) 1775 self.write(self.params, context) 1776 1777 def parameters(self, context): 1778 return [] 1779 1780 1781class HandlerException(QgsServerOgcApiHandler): 1782 1783 def __init__(self): 1784 super().__init__() 1785 self.__exception = None 1786 1787 def setException(self, exception): 1788 self.__exception = exception 1789 1790 def path(self): 1791 return QtCore.QRegularExpression("/handlerexception") 1792 1793 def operationId(self): 1794 return "handlerException" 1795 1796 def summary(self): 1797 return "Trigger an exception" 1798 1799 def description(self): 1800 return "Trigger an exception" 1801 1802 def linkTitle(self): 1803 return "Trigger an exception Title" 1804 1805 def linkType(self): 1806 return QgsServerOgcApi.data 1807 1808 def handleRequest(self, context): 1809 """Triggers an exception""" 1810 raise self.__exception 1811 1812 def parameters(self, context): 1813 return [ 1814 QgsServerQueryStringParameter('value1', True, QgsServerQueryStringParameter.Type.Double, 'a double value')] 1815 1816 1817class QgsServerOgcAPITest(QgsServerAPITestBase): 1818 """ QGIS OGC API server tests""" 1819 1820 def testOgcApi(self): 1821 """Test OGC API""" 1822 1823 api = QgsServerOgcApi(self.server.serverInterface(), 1824 '/api1', 'apione', 'an api', '1.1') 1825 self.assertEqual(api.name(), 'apione') 1826 self.assertEqual(api.description(), 'an api') 1827 self.assertEqual(api.version(), '1.1') 1828 self.assertEqual(api.rootPath(), '/api1') 1829 url = 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=-1' 1830 self.assertEqual(api.sanitizeUrl(QtCore.QUrl(url)).toString(), 1831 'http://server.qgis.org/wfs3/collections/testlayer \xe8\xe9/items?limit=-1') 1832 self.assertEqual(api.sanitizeUrl(QtCore.QUrl('/path//double//slashes//#fr')).toString(), 1833 '/path/double/slashes#fr') 1834 self.assertEqual(api.relToString(QgsServerOgcApi.data), 'data') 1835 self.assertEqual(api.relToString( 1836 QgsServerOgcApi.alternate), 'alternate') 1837 self.assertEqual(api.contentTypeToString(QgsServerOgcApi.JSON), 'JSON') 1838 self.assertEqual(api.contentTypeToStdString( 1839 QgsServerOgcApi.JSON), 'JSON') 1840 self.assertEqual(api.contentTypeToExtension( 1841 QgsServerOgcApi.JSON), 'json') 1842 self.assertEqual(api.contentTypeToExtension( 1843 QgsServerOgcApi.GEOJSON), 'geojson') 1844 1845 def testOgcApiHandler(self): 1846 """Test OGC API Handler""" 1847 1848 project = QgsProject() 1849 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 1850 request = QgsBufferServerRequest( 1851 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=-1') 1852 response = QgsBufferServerResponse() 1853 1854 ctx = QgsServerApiContext( 1855 '/services/api1', request, response, project, self.server.serverInterface()) 1856 h = Handler1() 1857 self.assertTrue(h.staticPath(ctx).endswith( 1858 '/resources/server/api/ogc/static')) 1859 self.assertEqual(h.path(), QtCore.QRegularExpression("/handlerone")) 1860 self.assertEqual(h.description(), 'The first handler ever') 1861 self.assertEqual(h.operationId(), 'handlerOne') 1862 self.assertEqual(h.summary(), 'First of its name') 1863 self.assertEqual(h.linkTitle(), 'Handler One Link Title') 1864 self.assertEqual(h.linkType(), QgsServerOgcApi.data) 1865 with self.assertRaises(QgsServerApiBadRequestException) as ex: 1866 h.handleRequest(ctx) 1867 self.assertEqual(str(ex.exception), 1868 'Missing required argument: \'value1\'') 1869 1870 r = ctx.response() 1871 self.assertEqual(r.data(), '') 1872 1873 with self.assertRaises(QgsServerApiBadRequestException) as ex: 1874 h.values(ctx) 1875 self.assertEqual(str(ex.exception), 1876 'Missing required argument: \'value1\'') 1877 1878 # Add handler to API and test for /api2 1879 ctx = QgsServerApiContext( 1880 '/services/api2', request, response, project, self.server.serverInterface()) 1881 api = QgsServerOgcApi(self.server.serverInterface(), 1882 '/api2', 'apitwo', 'a second api', '1.2') 1883 api.registerHandler(h) 1884 # Add a second handler (will be tested later) 1885 h2 = Handler2() 1886 api.registerHandler(h2) 1887 1888 ctx.request().setUrl(QtCore.QUrl('http://www.qgis.org/services/api1')) 1889 with self.assertRaises(QgsServerApiBadRequestException) as ex: 1890 api.executeRequest(ctx) 1891 self.assertEqual( 1892 str(ex.exception), 'Requested URI does not match any registered API handler') 1893 1894 ctx.request().setUrl(QtCore.QUrl('http://www.qgis.org/services/api2')) 1895 with self.assertRaises(QgsServerApiBadRequestException) as ex: 1896 api.executeRequest(ctx) 1897 self.assertEqual( 1898 str(ex.exception), 'Requested URI does not match any registered API handler') 1899 1900 ctx.request().setUrl(QtCore.QUrl('http://www.qgis.org/services/api2/handlerone')) 1901 with self.assertRaises(QgsServerApiBadRequestException) as ex: 1902 api.executeRequest(ctx) 1903 self.assertEqual(str(ex.exception), 1904 'Missing required argument: \'value1\'') 1905 1906 ctx.request().setUrl(QtCore.QUrl( 1907 'http://www.qgis.org/services/api2/handlerone?value1=not+a+double')) 1908 with self.assertRaises(QgsServerApiBadRequestException) as ex: 1909 api.executeRequest(ctx) 1910 self.assertEqual( 1911 str(ex.exception), 'Argument \'value1\' could not be converted to Double') 1912 1913 ctx.request().setUrl(QtCore.QUrl( 1914 'http://www.qgis.org/services/api2/handlerone?value1=1.2345')) 1915 params = h.values(ctx) 1916 self.assertEqual(params, {'value1': 1.2345}) 1917 api.executeRequest(ctx) 1918 self.assertEqual(json.loads(bytes(ctx.response().data()))[ 1919 'value1'], 1.2345) 1920 1921 # Test path fragments extraction 1922 ctx.request().setUrl(QtCore.QUrl( 1923 'http://www.qgis.org/services/api2/handlertwo/00/555?value1=1.2345')) 1924 params = h2.values(ctx) 1925 self.assertEqual( 1926 params, {'code1': '00', 'value1': 1.2345, 'value2': None}) 1927 1928 # Test string encoding 1929 ctx.request().setUrl( 1930 QtCore.QUrl('http://www.qgis.org/services/api2/handlertwo/00/555?value1=1.2345&value2=a%2Fstring%20some')) 1931 params = h2.values(ctx) 1932 self.assertEqual( 1933 params, {'code1': '00', 'value1': 1.2345, 'value2': 'a/string some'}) 1934 1935 # Test links 1936 self.assertEqual(h2.href(ctx), 1937 'http://www.qgis.org/services/api2/handlertwo/00/555?value1=1.2345&value2=a%2Fstring%20some') 1938 self.assertEqual(h2.href(ctx, '/extra'), 1939 'http://www.qgis.org/services/api2/handlertwo/00/555/extra?value1=1.2345&value2=a%2Fstring%20some') 1940 self.assertEqual(h2.href(ctx, '/extra', 'json'), 1941 'http://www.qgis.org/services/api2/handlertwo/00/555/extra.json?value1=1.2345&value2=a%2Fstring%20some') 1942 1943 # Test template path 1944 self.assertTrue( 1945 h2.templatePath(ctx).endswith('/resources/server/api/ogc/templates/services/api2/handlerTwo.html')) 1946 1947 del(project) 1948 1949 def testOgcApiHandlerContentType(self): 1950 """Test OGC API Handler content types""" 1951 1952 project = QgsProject() 1953 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 1954 request = QgsBufferServerRequest( 1955 'http://server.qgis.org/api3/handlerthree?value1=9.5') 1956 response = QgsBufferServerResponse() 1957 1958 # Add handler to API and test for /api3 1959 ctx = QgsServerApiContext( 1960 '/services/api3', request, response, project, self.server.serverInterface()) 1961 api = QgsServerOgcApi(self.server.serverInterface(), 1962 '/api3', 'apithree', 'a third api', '1.2') 1963 h3 = Handler3() 1964 api.registerHandler(h3) 1965 1966 ctx = QgsServerApiContext( 1967 '/services/api3/', request, response, project, self.server.serverInterface()) 1968 api.executeRequest(ctx) 1969 self.assertEqual(json.loads( 1970 bytes(ctx.response().data()))['value1'], 9.5) 1971 1972 # Call HTML 1973 ctx.request().setUrl(QtCore.QUrl( 1974 'http://server.qgis.org/api3/handlerthree.html?value1=9.5')) 1975 with self.assertRaises(QgsServerApiBadRequestException) as ex: 1976 api.executeRequest(ctx) 1977 self.assertEqual(str(ex.exception), 'Unsupported Content-Type: HTML') 1978 1979 h3.setContentTypes([QgsServerOgcApi.HTML]) 1980 with self.assertRaises(QgsServerApiBadRequestException) as ex: 1981 api.executeRequest(ctx) 1982 self.assertEqual(str(ex.exception), 1983 'Template not found: handlerThree.html') 1984 1985 # Define a template path 1986 tmpDir = QtCore.QTemporaryDir() 1987 with open(tmpDir.path() + '/handlerThree.html', 'w+') as f: 1988 f.write("Hello world") 1989 h3.templatePathOverride = tmpDir.path() + '/handlerThree.html' 1990 ctx.response().clear() 1991 api.executeRequest(ctx) 1992 self.assertEqual(bytes(ctx.response().data()), b"Hello world") 1993 1994 req = QgsBufferServerRequest( 1995 'http://localhost:8000/project/7ecb/wfs3/collections/zg.grundnutzung.html') 1996 self.assertEqual(h3.contentTypeFromRequest(req), QgsServerOgcApi.HTML) 1997 1998 del(project) 1999 2000 def testOgcApiHandlerException(self): 2001 """Test OGC API Handler exception""" 2002 2003 project = QgsProject() 2004 project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs') 2005 request = QgsBufferServerRequest('') 2006 response = QgsBufferServerResponse() 2007 2008 ctx = QgsServerApiContext( 2009 '/services/apiexception', request, response, project, self.server.serverInterface()) 2010 h = HandlerException() 2011 2012 api = QgsServerOgcApi(self.server.serverInterface(), 2013 '/apiexception', 'apiexception', 'an api with exception', '1.2') 2014 api.registerHandler(h) 2015 2016 h.setException(Exception("UTF-8 Exception 1 $ù~à^£")) 2017 ctx.request().setUrl(QtCore.QUrl('http://www.qgis.org/handlerexception')) 2018 with self.assertRaises(QgsServerApiBadRequestException) as ex: 2019 api.executeRequest(ctx) 2020 self.assertEqual( 2021 str(ex.exception), "UTF-8 Exception 1 $ù~à^£") 2022 2023 h.setException(QgsServerApiBadRequestException("UTF-8 Exception 2 $ù~à^£")) 2024 ctx.request().setUrl(QtCore.QUrl('http://www.qgis.org/handlerexception')) 2025 with self.assertRaises(QgsServerApiBadRequestException) as ex: 2026 api.executeRequest(ctx) 2027 self.assertEqual( 2028 str(ex.exception), "UTF-8 Exception 2 $ù~à^£") 2029 2030 del(project) 2031 2032 def test_path_capture(self): 2033 """Test issue GH #45439""" 2034 2035 api = QgsServerOgcApi(self.server.serverInterface(), 2036 '/api4', 'apifour', 'a fourth api', '1.2') 2037 2038 h4 = Handler4() 2039 api.registerHandler(h4) 2040 2041 request = QgsBufferServerRequest( 2042 'http://localhost:19876/api4/france_parts.json?MAP=france_parts') 2043 response = QgsBufferServerResponse() 2044 2045 server = QgsServer() 2046 iface = server.serverInterface() 2047 iface.serviceRegistry().registerApi(api) 2048 2049 server.handleRequest(request, response) 2050 2051 self.assertEqual(h4.params, {'tilemapid': 'france_parts.json'}) 2052 2053 ctx = QgsServerApiContext(api.rootPath(), request, response, None, iface) 2054 self.assertEqual(h4.href(ctx), 'http://localhost:19876/api4/france_parts?MAP=france_parts') 2055 2056 2057if __name__ == '__main__': 2058 unittest.main() 2059