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