Skip to content

Commit 2023f65

Browse files
committed
Re-enable api tests
1 parent 1ff693b commit 2023f65

File tree

4 files changed

+135
-29
lines changed

4 files changed

+135
-29
lines changed

.github/workflows/main.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ jobs:
1818
- name: Start demo database
1919
run: |
2020
cd qwc-docker
21-
cp pg_service.conf ~/.pg_service.conf
22-
sed -i 's|host=qwc-postgis|host=localhost|g' ~/.pg_service.conf
23-
sed -i 's|port=5432|port=5439|g' ~/.pg_service.conf
21+
cp pg_service.conf pg_service_local.conf
22+
sed -i 's|host=qwc-postgis|host=localhost|g' pg_service_local.conf
23+
sed -i 's|port=5432|port=5439|g' pg_service_local.conf
2424
sed -Ei "s|^(\s*POSTGRES_PASSWORD:).*$|\1 'waej7WuoOoth0wor'|" docker-compose-example.yml
2525
docker compose -f docker-compose-example.yml up -d qwc-postgis qwc-config-db-migrate qwc-qgis-server
2626
@@ -33,7 +33,8 @@ jobs:
3333
run: |
3434
python -m pip install --upgrade pip
3535
python -m pip install -r requirements.txt
36-
PYTHONPATH=$PWD/src python3 test.py
36+
python tests/patch_permissions.py qwc-docker/volumes/config/default/permissions.json
37+
PGSERVICEFILE=$PWD/qwc-docker/pg_service_local.conf PYTHONPATH=$PWD/src CONFIG_PATH=$PWD/qwc-docker/volumes/config/ python3 test.py
3738
3839
- name: Validate schema
3940
run: |

test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import unittest
22

3-
# from tests.api_tests import *
3+
from tests.api_tests import *
44
from tests.feature_validation_tests import *
55
# from tests.feature_validation_tests_somap import *
66

tests/api_tests.py

Lines changed: 100 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,82 @@
1+
import os
12
import unittest
23
from unittest.mock import patch
4+
from functools import wraps
35

46
from flask import Response, json
57
from flask.testing import FlaskClient
68
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
722

823
import 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

1165
class 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

Comments
 (0)