1+ import os
12import unittest
23from unittest .mock import patch
4+ from functools import wraps
35
46from flask import Response , json
57from flask .testing import FlaskClient
68from 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
722
823import server
924
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+
1064
1165class ApiTestCase (unittest .TestCase ):
1266 """Test case for server API"""
1367
1468 def setUp (self ):
1569 server .app .testing = True
1670 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'
2073
2174 def tearDown (self ):
2275 pass
2376
2477 def jwtHeader (self ):
2578 with server .app .test_request_context ():
26- access_token = create_access_token ('test ' )
79+ access_token = create_access_token ('admin ' )
2780 return {'Authorization' : 'Bearer {}' .format (access_token )}
2881
2982 def get (self , url ):
@@ -94,15 +147,15 @@ def check_feature(self, feature, has_crs=True):
94147 crs = {
95148 'type' : 'name' ,
96149 'properties' : {
97- 'name' : 'urn:ogc:def:crs:EPSG::2056 '
150+ 'name' : 'urn:ogc:def:crs:EPSG::3857 '
98151 }
99152 }
100153 self .assertEqual (crs , feature ['crs' ])
101154 else :
102155 self .assertNotIn ('crs' , feature )
103156
104157 # 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' ]
106159 for key in feature .keys ():
107160 self .assertIn (key , geo_json_feature_keys ,
108161 "Invalid property for GeoJSON Feature" )
@@ -117,12 +170,18 @@ def build_poly_feature(self):
117170 },
118171 'properties' : {
119172 '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'
121180 },
122181 'crs' : {
123182 'type' : 'name' ,
124183 'properties' : {
125- 'name' : 'urn:ogc:def:crs:EPSG::2056 '
184+ 'name' : 'urn:ogc:def:crs:EPSG::3857 '
126185 }
127186 }
128187 }
@@ -137,18 +196,18 @@ def build_point_feature(self):
137196 },
138197 'properties' : {
139198 'name' : 'Test' ,
140- 'beschreibung ' : 'Test Punkt '
199+ 'description ' : 'Test Point '
141200 },
142201 'crs' : {
143202 'type' : 'name' ,
144203 'properties' : {
145- 'name' : 'urn:ogc:def:crs:EPSG::2056 '
204+ 'name' : 'urn:ogc:def:crs:EPSG::3857 '
146205 }
147206 }
148207 }
149208
150209 # index
151-
210+ @ with_rollback
152211 def test_index (self ):
153212 # without bbox
154213 status_code , json_data = self .get ("/%s/" % self .dataset )
@@ -161,14 +220,14 @@ def test_index(self):
161220 crs = {
162221 'type' : 'name' ,
163222 'properties' : {
164- 'name' : 'urn:ogc:def:crs:EPSG::2056 '
223+ 'name' : 'urn:ogc:def:crs:EPSG::3857 '
165224 }
166225 }
167226 self .assertEqual (crs , json_data ['crs' ])
168227 no_bbox_count = len (json_data ['features' ])
169228
170229 # with bbox
171- bbox = '1288647,-4658384,1501913,-4538362 '
230+ bbox = '950800,6003900,950850,6003950 '
172231 status_code , json_data = self .get ("/%s/?bbox=%s" % (self .dataset , bbox ))
173232 self .assertEqual (200 , status_code , "Status code is not OK" )
174233 self .assertEqual ('FeatureCollection' , json_data ['type' ])
@@ -179,15 +238,16 @@ def test_index(self):
179238 crs = {
180239 'type' : 'name' ,
181240 'properties' : {
182- 'name' : 'urn:ogc:def:crs:EPSG::2056 '
241+ 'name' : 'urn:ogc:def:crs:EPSG::3857 '
183242 }
184243 }
185244 self .assertEqual (crs , json_data ['crs' ])
186245 self .assertGreaterEqual (no_bbox_count , len (json_data ['features' ]),
187246 "Too many features within bbox." )
188247
248+ @with_rollback
189249 def test_index_read_only (self ):
190- bbox = '1358925,-4604991,1431179,-4569265 '
250+ bbox = '950750,6003950,950760,6003960 '
191251 status_code , json_data = self .get ("/%s/?bbox=%s" %
192252 (self .dataset_read_only , bbox ))
193253 self .assertEqual (200 , status_code , "Status code is not OK" )
@@ -199,25 +259,28 @@ def test_index_read_only(self):
199259 crs = {
200260 'type' : 'name' ,
201261 'properties' : {
202- 'name' : 'urn:ogc:def:crs:EPSG::2056 '
262+ 'name' : 'urn:ogc:def:crs:EPSG::3857 '
203263 }
204264 }
205265 self .assertEqual (crs , json_data ['crs' ])
206266
267+ @with_rollback
207268 def test_index_invalid_dataset (self ):
208269 status_code , json_data = self .get ('/invalid_dataset/' )
209270 self .assertEqual (404 , status_code , "Status code is not Not Found" )
210271 self .assertEqual ('Dataset not found or permission error' , json_data ['message' ],
211272 "Message does not match" )
212273 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
213274
275+ @with_rollback
214276 def test_index_empty_bbox (self ):
215277 status_code , json_data = self .get ("/%s/?bbox=" % self .dataset )
216278 self .assertEqual (400 , status_code , "Status code is not Bad Request" )
217279 self .assertEqual ('Invalid bounding box' , json_data ['message' ],
218280 "Message does not match" )
219281 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
220282
283+ @with_rollback
221284 def test_index_invalid_bbox (self ):
222285 test_bboxes = [
223286 'test' , # string
@@ -237,33 +300,37 @@ def test_index_invalid_bbox(self):
237300 "Message does not match (bbox='%s')" % bbox )
238301 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
239302
303+ @with_rollback
240304 def test_index_equal_coords_bbox (self ):
241305 bbox = '2606900,1228600,2606900,1228600'
242306 status_code , json_data = self .get ("/%s/?bbox=%s" % (self .dataset , bbox ))
243307 self .assertEqual (200 , status_code , "Status code is not OK" )
244308 self .assertEqual ('FeatureCollection' , json_data ['type' ])
245309
246310 # show
247-
311+ @ with_rollback
248312 def test_show (self ):
249313 status_code , json_data = self .get ("/%s/1" % self .dataset )
250314 self .assertEqual (200 , status_code , "Status code is not OK" )
251315 self .check_feature (json_data )
252316 self .assertEqual (1 , json_data ['id' ], "ID does not match" )
253317
318+ @with_rollback
254319 def test_show_read_only (self ):
255320 status_code , json_data = self .get ("/%s/1" % self .dataset_read_only )
256321 self .assertEqual (200 , status_code , "Status code is not OK" )
257322 self .check_feature (json_data )
258323 self .assertEqual (1 , json_data ['id' ], "ID does not match" )
259324
325+ @with_rollback
260326 def test_show_invalid_dataset (self ):
261327 status_code , json_data = self .get ('/test/1' )
262328 self .assertEqual (404 , status_code , "Status code is not Not Found" )
263329 self .assertEqual ('Dataset not found or permission error' , json_data ['message' ],
264330 "Message does not match" )
265331 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
266332
333+ @with_rollback
267334 def test_show_invalid_id (self ):
268335 status_code , json_data = self .get ("/%s/999999" % self .dataset )
269336 self .assertEqual (404 , status_code , "Status code is not Not Found" )
@@ -273,6 +340,7 @@ def test_show_invalid_id(self):
273340
274341 # create
275342
343+ @with_rollback
276344 def test_create (self ):
277345 input_feature = self .build_poly_feature ()
278346 status_code , json_data = self .post ("/%s/" % self .dataset , input_feature )
@@ -290,16 +358,18 @@ def test_create(self):
290358 self .assertEqual (200 , status_code , "Status code is not OK" )
291359 self .assertEqual (feature , json_data )
292360
361+ @with_rollback
293362 def test_create_read_only (self ):
294363 input_feature = self .build_point_feature ()
295364 status_code , json_data = self .post ("/%s/" % self .dataset_read_only ,
296365 input_feature )
297366 self .assertEqual (405 , status_code ,
298367 "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' ],
300369 "Message does not match" )
301370 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
302371
372+ @with_rollback
303373 def test_create_invalid_dataset (self ):
304374 input_feature = self .build_poly_feature ()
305375 status_code , json_data = self .post ('/invalid_dataset/' , input_feature )
@@ -309,7 +379,7 @@ def test_create_invalid_dataset(self):
309379 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
310380
311381 # update
312-
382+ @ with_rollback
313383 def test_update (self ):
314384 input_feature = self .build_poly_feature ()
315385 status_code , json_data = self .put ("/%s/1" % self .dataset , input_feature )
@@ -328,16 +398,18 @@ def test_update(self):
328398 self .assertEqual (200 , status_code , "Status code is not OK" )
329399 self .assertEqual (feature , json_data )
330400
401+ @with_rollback
331402 def test_update_read_only (self ):
332403 input_feature = self .build_point_feature ()
333404 status_code , json_data = self .put ("/%s/1" % self .dataset_read_only ,
334405 input_feature )
335406 self .assertEqual (405 , status_code ,
336407 "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' ],
338409 "Message does not match" )
339410 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
340411
412+ @with_rollback
341413 def test_update_invalid_dataset (self ):
342414 input_feature = self .build_poly_feature ()
343415 status_code , json_data = self .put ('/invalid_dataset/1' , input_feature )
@@ -346,6 +418,7 @@ def test_update_invalid_dataset(self):
346418 "Message does not match" )
347419 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
348420
421+ @with_rollback
349422 def test_update_invalid_id (self ):
350423 input_feature = self .build_poly_feature ()
351424 status_code , json_data = self .put (
@@ -356,36 +429,39 @@ def test_update_invalid_id(self):
356429 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
357430
358431 # destroy
359-
432+ @ with_rollback
360433 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 )
362435 self .assertEqual (200 , status_code , "Status code is not OK" )
363436 self .assertEqual ('Dataset feature deleted' , json_data ['message' ],
364437 "Message does not match" )
365438 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
366439
367440 # 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 )
369442 self .assertEqual (404 , status_code , "Status code is not Not Found" )
370443 self .assertEqual ('Feature not found' , json_data ['message' ],
371444 "Message does not match" )
372445 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
373446
447+ @with_rollback
374448 def test_destroy_read_only (self ):
375449 status_code , json_data = self .delete ("/%s/2" % self .dataset_read_only )
376450 self .assertEqual (405 , status_code ,
377451 "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' ],
379453 "Message does not match" )
380454 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
381455
456+ @with_rollback
382457 def test_destroy_invalid_dataset (self ):
383458 status_code , json_data = self .delete ('/test/1' )
384459 self .assertEqual (404 , status_code , "Status code is not Not Found" )
385460 self .assertEqual ('Dataset not found or permission error' , json_data ['message' ],
386461 "Message does not match" )
387462 self .assertNotIn ('type' , json_data , "GeoJSON Type present" )
388463
464+ @with_rollback
389465 def test_destroy_invalid_id (self ):
390466 status_code , json_data = self .delete (
391467 "/%s/999999" % self .dataset )
0 commit comments