1
+ import os
1
2
import unittest
2
3
from unittest .mock import patch
4
+ from functools import wraps
3
5
4
6
from flask import Response , json
5
7
from flask .testing import FlaskClient
6
8
from flask_jwt_extended import JWTManager , create_access_token
9
+ from qwc_services_core .database import DatabaseEngine
10
+ from contextlib import contextmanager
11
+
12
+ # Monkey-patch DatabaseEngine.db_engine to ensure always the same engine is returned
13
+ original_db_engine = DatabaseEngine .db_engine
14
+ db_engines = {}
15
+
16
+ def make_test_engine (self , conn_str ):
17
+ if not conn_str in db_engines :
18
+ db_engines [conn_str ] = original_db_engine (self , conn_str )
19
+ return db_engines [conn_str ]
20
+
21
+ DatabaseEngine .db_engine = make_test_engine
7
22
8
23
import server
9
24
25
+ JWTManager (server .app )
26
+
27
+
28
+ # Rollback DB to initial state after each test
29
+ db_engine = DatabaseEngine ()
30
+ db = db_engine .geo_db ()
31
+
32
+ def patch_db_begin (outer_conn ):
33
+ @contextmanager
34
+ def fake_begin ():
35
+ yield outer_conn
36
+ db .begin = fake_begin
37
+
38
+ def patch_db_connect (outer_conn ):
39
+ @contextmanager
40
+ def fake_connect ():
41
+ yield outer_conn
42
+ db .connect = fake_connect
43
+
44
+ def with_rollback (fn ):
45
+ @wraps (fn )
46
+ def wrapper (* args , ** kwargs ):
47
+ conn = db .connect ()
48
+ trans = conn .begin ()
49
+
50
+ original_begin = db .begin
51
+ patch_db_begin (conn )
52
+ original_connect = db .connect
53
+ patch_db_connect (conn )
54
+
55
+ try :
56
+ return fn (* args , ** kwargs )
57
+ finally :
58
+ db .begin = original_begin
59
+ db .connect = original_connect
60
+ trans .rollback ()
61
+ conn .close ()
62
+ return wrapper
63
+
10
64
11
65
class ApiTestCase (unittest .TestCase ):
12
66
"""Test case for server API"""
13
67
14
68
def setUp (self ):
15
69
server .app .testing = True
16
70
self .app = FlaskClient (server .app , Response )
17
- JWTManager (server .app )
18
- self .dataset = 'test_polygons'
19
- self .dataset_read_only = 'test_points'
71
+ self .dataset = 'qwc_demo.edit_polygons'
72
+ self .dataset_read_only = 'qwc_demo.edit_points'
20
73
21
74
def tearDown (self ):
22
75
pass
23
76
24
77
def jwtHeader (self ):
25
78
with server .app .test_request_context ():
26
- access_token = create_access_token ('test ' )
79
+ access_token = create_access_token ('admin ' )
27
80
return {'Authorization' : 'Bearer {}' .format (access_token )}
28
81
29
82
def get (self , url ):
@@ -94,15 +147,15 @@ def check_feature(self, feature, has_crs=True):
94
147
crs = {
95
148
'type' : 'name' ,
96
149
'properties' : {
97
- 'name' : 'urn:ogc:def:crs:EPSG::2056 '
150
+ 'name' : 'urn:ogc:def:crs:EPSG::3857 '
98
151
}
99
152
}
100
153
self .assertEqual (crs , feature ['crs' ])
101
154
else :
102
155
self .assertNotIn ('crs' , feature )
103
156
104
157
# check for surplus properties
105
- geo_json_feature_keys = ['type' , 'id' , 'geometry' , 'properties' , 'crs' ]
158
+ geo_json_feature_keys = ['type' , 'id' , 'geometry' , 'properties' , 'crs' , 'bbox' ]
106
159
for key in feature .keys ():
107
160
self .assertIn (key , geo_json_feature_keys ,
108
161
"Invalid property for GeoJSON Feature" )
@@ -117,12 +170,18 @@ def build_poly_feature(self):
117
170
},
118
171
'properties' : {
119
172
'name' : 'Test' ,
120
- 'beschreibung' : 'Test Polygon'
173
+ 'description' : 'Test Polygon' ,
174
+ 'num' : 1 ,
175
+ 'value' : 3.14 ,
176
+ 'type' : 0 ,
177
+ 'amount' : 1.23 ,
178
+ 'validated' : False ,
179
+ 'datetime' : '2025-01-01T12:34:56'
121
180
},
122
181
'crs' : {
123
182
'type' : 'name' ,
124
183
'properties' : {
125
- 'name' : 'urn:ogc:def:crs:EPSG::2056 '
184
+ 'name' : 'urn:ogc:def:crs:EPSG::3857 '
126
185
}
127
186
}
128
187
}
@@ -137,18 +196,18 @@ def build_point_feature(self):
137
196
},
138
197
'properties' : {
139
198
'name' : 'Test' ,
140
- 'beschreibung ' : 'Test Punkt '
199
+ 'description ' : 'Test Point '
141
200
},
142
201
'crs' : {
143
202
'type' : 'name' ,
144
203
'properties' : {
145
- 'name' : 'urn:ogc:def:crs:EPSG::2056 '
204
+ 'name' : 'urn:ogc:def:crs:EPSG::3857 '
146
205
}
147
206
}
148
207
}
149
208
150
209
# index
151
-
210
+ @ with_rollback
152
211
def test_index (self ):
153
212
# without bbox
154
213
status_code , json_data = self .get ("/%s/" % self .dataset )
@@ -161,14 +220,14 @@ def test_index(self):
161
220
crs = {
162
221
'type' : 'name' ,
163
222
'properties' : {
164
- 'name' : 'urn:ogc:def:crs:EPSG::2056 '
223
+ 'name' : 'urn:ogc:def:crs:EPSG::3857 '
165
224
}
166
225
}
167
226
self .assertEqual (crs , json_data ['crs' ])
168
227
no_bbox_count = len (json_data ['features' ])
169
228
170
229
# with bbox
171
- bbox = '1288647,-4658384,1501913,-4538362 '
230
+ bbox = '950800,6003900,950850,6003950 '
172
231
status_code , json_data = self .get ("/%s/?bbox=%s" % (self .dataset , bbox ))
173
232
self .assertEqual (200 , status_code , "Status code is not OK" )
174
233
self .assertEqual ('FeatureCollection' , json_data ['type' ])
@@ -179,15 +238,16 @@ def test_index(self):
179
238
crs = {
180
239
'type' : 'name' ,
181
240
'properties' : {
182
- 'name' : 'urn:ogc:def:crs:EPSG::2056 '
241
+ 'name' : 'urn:ogc:def:crs:EPSG::3857 '
183
242
}
184
243
}
185
244
self .assertEqual (crs , json_data ['crs' ])
186
245
self .assertGreaterEqual (no_bbox_count , len (json_data ['features' ]),
187
246
"Too many features within bbox." )
188
247
248
+ @with_rollback
189
249
def test_index_read_only (self ):
190
- bbox = '1358925,-4604991,1431179,-4569265 '
250
+ bbox = '950750,6003950,950760,6003960 '
191
251
status_code , json_data = self .get ("/%s/?bbox=%s" %
192
252
(self .dataset_read_only , bbox ))
193
253
self .assertEqual (200 , status_code , "Status code is not OK" )
@@ -199,25 +259,28 @@ def test_index_read_only(self):
199
259
crs = {
200
260
'type' : 'name' ,
201
261
'properties' : {
202
- 'name' : 'urn:ogc:def:crs:EPSG::2056 '
262
+ 'name' : 'urn:ogc:def:crs:EPSG::3857 '
203
263
}
204
264
}
205
265
self .assertEqual (crs , json_data ['crs' ])
206
266
267
+ @with_rollback
207
268
def test_index_invalid_dataset (self ):
208
269
status_code , json_data = self .get ('/invalid_dataset/' )
209
270
self .assertEqual (404 , status_code , "Status code is not Not Found" )
210
271
self .assertEqual ('Dataset not found or permission error' , json_data ['message' ],
211
272
"Message does not match" )
212
273
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
213
274
275
+ @with_rollback
214
276
def test_index_empty_bbox (self ):
215
277
status_code , json_data = self .get ("/%s/?bbox=" % self .dataset )
216
278
self .assertEqual (400 , status_code , "Status code is not Bad Request" )
217
279
self .assertEqual ('Invalid bounding box' , json_data ['message' ],
218
280
"Message does not match" )
219
281
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
220
282
283
+ @with_rollback
221
284
def test_index_invalid_bbox (self ):
222
285
test_bboxes = [
223
286
'test' , # string
@@ -237,33 +300,37 @@ def test_index_invalid_bbox(self):
237
300
"Message does not match (bbox='%s')" % bbox )
238
301
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
239
302
303
+ @with_rollback
240
304
def test_index_equal_coords_bbox (self ):
241
305
bbox = '2606900,1228600,2606900,1228600'
242
306
status_code , json_data = self .get ("/%s/?bbox=%s" % (self .dataset , bbox ))
243
307
self .assertEqual (200 , status_code , "Status code is not OK" )
244
308
self .assertEqual ('FeatureCollection' , json_data ['type' ])
245
309
246
310
# show
247
-
311
+ @ with_rollback
248
312
def test_show (self ):
249
313
status_code , json_data = self .get ("/%s/1" % self .dataset )
250
314
self .assertEqual (200 , status_code , "Status code is not OK" )
251
315
self .check_feature (json_data )
252
316
self .assertEqual (1 , json_data ['id' ], "ID does not match" )
253
317
318
+ @with_rollback
254
319
def test_show_read_only (self ):
255
320
status_code , json_data = self .get ("/%s/1" % self .dataset_read_only )
256
321
self .assertEqual (200 , status_code , "Status code is not OK" )
257
322
self .check_feature (json_data )
258
323
self .assertEqual (1 , json_data ['id' ], "ID does not match" )
259
324
325
+ @with_rollback
260
326
def test_show_invalid_dataset (self ):
261
327
status_code , json_data = self .get ('/test/1' )
262
328
self .assertEqual (404 , status_code , "Status code is not Not Found" )
263
329
self .assertEqual ('Dataset not found or permission error' , json_data ['message' ],
264
330
"Message does not match" )
265
331
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
266
332
333
+ @with_rollback
267
334
def test_show_invalid_id (self ):
268
335
status_code , json_data = self .get ("/%s/999999" % self .dataset )
269
336
self .assertEqual (404 , status_code , "Status code is not Not Found" )
@@ -273,6 +340,7 @@ def test_show_invalid_id(self):
273
340
274
341
# create
275
342
343
+ @with_rollback
276
344
def test_create (self ):
277
345
input_feature = self .build_poly_feature ()
278
346
status_code , json_data = self .post ("/%s/" % self .dataset , input_feature )
@@ -290,16 +358,18 @@ def test_create(self):
290
358
self .assertEqual (200 , status_code , "Status code is not OK" )
291
359
self .assertEqual (feature , json_data )
292
360
361
+ @with_rollback
293
362
def test_create_read_only (self ):
294
363
input_feature = self .build_point_feature ()
295
364
status_code , json_data = self .post ("/%s/" % self .dataset_read_only ,
296
365
input_feature )
297
366
self .assertEqual (405 , status_code ,
298
367
"Status code is not Method Not Allowed" )
299
- self .assertEqual ('Dataset not writable ' , json_data ['message' ],
368
+ self .assertEqual ('Dataset not creatable ' , json_data ['message' ],
300
369
"Message does not match" )
301
370
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
302
371
372
+ @with_rollback
303
373
def test_create_invalid_dataset (self ):
304
374
input_feature = self .build_poly_feature ()
305
375
status_code , json_data = self .post ('/invalid_dataset/' , input_feature )
@@ -309,7 +379,7 @@ def test_create_invalid_dataset(self):
309
379
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
310
380
311
381
# update
312
-
382
+ @ with_rollback
313
383
def test_update (self ):
314
384
input_feature = self .build_poly_feature ()
315
385
status_code , json_data = self .put ("/%s/1" % self .dataset , input_feature )
@@ -328,16 +398,18 @@ def test_update(self):
328
398
self .assertEqual (200 , status_code , "Status code is not OK" )
329
399
self .assertEqual (feature , json_data )
330
400
401
+ @with_rollback
331
402
def test_update_read_only (self ):
332
403
input_feature = self .build_point_feature ()
333
404
status_code , json_data = self .put ("/%s/1" % self .dataset_read_only ,
334
405
input_feature )
335
406
self .assertEqual (405 , status_code ,
336
407
"Status code is not Method Not Allowed" )
337
- self .assertEqual ('Dataset not writable ' , json_data ['message' ],
408
+ self .assertEqual ('Dataset not updatable ' , json_data ['message' ],
338
409
"Message does not match" )
339
410
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
340
411
412
+ @with_rollback
341
413
def test_update_invalid_dataset (self ):
342
414
input_feature = self .build_poly_feature ()
343
415
status_code , json_data = self .put ('/invalid_dataset/1' , input_feature )
@@ -346,6 +418,7 @@ def test_update_invalid_dataset(self):
346
418
"Message does not match" )
347
419
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
348
420
421
+ @with_rollback
349
422
def test_update_invalid_id (self ):
350
423
input_feature = self .build_poly_feature ()
351
424
status_code , json_data = self .put (
@@ -356,36 +429,39 @@ def test_update_invalid_id(self):
356
429
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
357
430
358
431
# destroy
359
-
432
+ @ with_rollback
360
433
def test_destroy (self ):
361
- status_code , json_data = self .delete ("/%s/2 " % self .dataset )
434
+ status_code , json_data = self .delete ("/%s/1 " % self .dataset )
362
435
self .assertEqual (200 , status_code , "Status code is not OK" )
363
436
self .assertEqual ('Dataset feature deleted' , json_data ['message' ],
364
437
"Message does not match" )
365
438
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
366
439
367
440
# check that feature has been deleted
368
- status_code , json_data = self .get ("/%s/2 " % self .dataset )
441
+ status_code , json_data = self .get ("/%s/1 " % self .dataset )
369
442
self .assertEqual (404 , status_code , "Status code is not Not Found" )
370
443
self .assertEqual ('Feature not found' , json_data ['message' ],
371
444
"Message does not match" )
372
445
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
373
446
447
+ @with_rollback
374
448
def test_destroy_read_only (self ):
375
449
status_code , json_data = self .delete ("/%s/2" % self .dataset_read_only )
376
450
self .assertEqual (405 , status_code ,
377
451
"Status code is not Method Not Allowed" )
378
- self .assertEqual ('Dataset not writable ' , json_data ['message' ],
452
+ self .assertEqual ('Dataset not deletable ' , json_data ['message' ],
379
453
"Message does not match" )
380
454
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
381
455
456
+ @with_rollback
382
457
def test_destroy_invalid_dataset (self ):
383
458
status_code , json_data = self .delete ('/test/1' )
384
459
self .assertEqual (404 , status_code , "Status code is not Not Found" )
385
460
self .assertEqual ('Dataset not found or permission error' , json_data ['message' ],
386
461
"Message does not match" )
387
462
self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
388
463
464
+ @with_rollback
389
465
def test_destroy_invalid_id (self ):
390
466
status_code , json_data = self .delete (
391
467
"/%s/999999" % self .dataset )
0 commit comments