Skip to content

Commit c379f22

Browse files
totycrotomkralidis
andauthored
Admin tests without live server (#1888)
* Don't run admin live tests in CI * Add exemplary test to demonstrate admin api test style without live server * Also add resource admin test without live server * Fix a typo * Update test_admin_api.py --------- Co-authored-by: Tom Kralidis <tomkralidis@gmail.com>
1 parent 125a1b4 commit c379f22

3 files changed

Lines changed: 274 additions & 128 deletions

File tree

.github/workflows/main.yml

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -150,45 +150,6 @@ jobs:
150150
pytest tests/test_util.py
151151
pytest tests/test_xarray_netcdf_provider.py
152152
pytest tests/test_xarray_zarr_provider.py
153-
- name: failed tests 🚩
154-
if: ${{ failure() }}
155-
run: |
156-
pip3 list -v
157-
158-
admin:
159-
runs-on: ubuntu-22.04
160-
strategy:
161-
matrix:
162-
include:
163-
- python-version: '3.10'
164-
env:
165-
PYGEOAPI_CONFIG: "tests/pygeoapi-test-config-admin.yml"
166-
PYGEOAPI_OPENAPI: "tests/pygeoapi-test-openapi-admin.yml"
167-
steps:
168-
- uses: actions/checkout@v2
169-
- uses: actions/setup-python@v2
170-
name: Setup Python ${{ matrix.python-version }}
171-
with:
172-
python-version: ${{ matrix.python-version }}
173-
- uses: awalsh128/cache-apt-pkgs-action@latest
174-
with:
175-
packages: gunicorn python3-gevent
176-
version: 1.0
177-
- name: Install requirements 📦
178-
run: |
179-
pip3 install -r requirements.txt
180-
pip3 install -r requirements-dev.txt
181-
pip3 install -r requirements-admin.txt
182-
python3 setup.py install
183-
- name: Run pygeoapi with admin API ⚙️
184-
run: |
185-
pygeoapi openapi generate ${PYGEOAPI_CONFIG} --output-file ${PYGEOAPI_OPENAPI}
186-
gunicorn --bind 0.0.0.0:5000 \
187-
--reload \
188-
--reload-extra-file ${PYGEOAPI_CONFIG} \
189-
pygeoapi.flask_app:APP &
190-
- name: run integration tests ⚙️
191-
run: |
192153
pytest tests/test_admin_api.py
193154
- name: failed tests 🚩
194155
if: ${{ failure() }}

tests/test_admin_api.py

Lines changed: 104 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
#
33
# Authors: Tom Kralidis <tomkralidis@gmail.com>
44
# Authors: Benjamin Webb <benjamin.miller.webb@gmail.com>
5+
# Authors: Bernhard Mallinger <bernhard.mallinger@eox.at>
56
#
67
# Copyright (c) 2024 Tom Kralidis
78
# Copyright (c) 2023 Benjamin Webb
9+
# Copyright (c) 2024 Bernhard Mallinger
810
#
911
# Permission is hereby granted, free of charge, to any person
1012
# obtaining a copy of this software and associated documentation
@@ -30,137 +32,150 @@
3032
# =================================================================
3133

3234
from datetime import datetime
33-
import time
34-
import unittest
35+
import json
36+
import os
3537

3638
from pathlib import Path
37-
from requests import Session
39+
import pytest
3840

3941
from pygeoapi.util import yaml_load
42+
from pygeoapi.admin import (
43+
Admin, delete_resource, get_config_, get_resource,
44+
get_resources, patch_config, patch_resource, post_resource,
45+
put_config, put_resource)
46+
47+
from tests.util import mock_api_request
4048

4149
THISDIR = Path(__file__).resolve().parent
4250

4351

44-
class APITest(unittest.TestCase):
45-
def setUp(self):
46-
"""setup test fixtures, etc."""
52+
@pytest.fixture()
53+
def admin_config_path(tmp_path, monkeypatch):
54+
# create a temporary config file because the test will modify it in place
55+
config_path = tmp_path / "config.yml"
56+
config_path.write_text(
57+
(Path(THISDIR) / "pygeoapi-test-config-admin.yml").read_text()
58+
)
59+
60+
# get_config() reads the config directly, so we need to patch os.environ
61+
monkeypatch.setitem(os.environ, "PYGEOAPI_CONFIG", str(config_path))
62+
63+
return config_path
4764

48-
self.admin_endpoint = 'http://localhost:5000/admin/config'
49-
self.http = Session()
50-
self.http.headers.update({
51-
'Content-type': 'application/json',
52-
'Accept': 'application/json'
53-
})
5465

55-
def tearDown(self):
56-
"""return to pristine state"""
66+
def reload_api(config_path, monkeypatch, openapi):
67+
# initialize admin api with current config contents
68+
with config_path.open() as config_handle:
69+
admin = Admin(yaml_load(config_handle), openapi)
5770

58-
pass
71+
# the config paths are set on a class level, so they are set before
72+
# we can patch os.environ and need to patch them directly
73+
monkeypatch.setattr(admin, "PYGEOAPI_CONFIG", str(config_path))
74+
openapi_filename = str(config_path).replace("config.yml", "openapi.yml")
75+
monkeypatch.setattr(admin, "PYGEOAPI_OPENAPI", openapi_filename)
5976

60-
def test_admin(self):
77+
return admin
6178

62-
url = f'{self.admin_endpoint}'
63-
content = self.http.get(url).json()
6479

65-
keys = ['logging', 'metadata', 'resources', 'server']
66-
self.assertEqual(sorted(content.keys()), keys)
80+
def test_admin(monkeypatch, admin_config_path, openapi):
6781

68-
# PUT configuration
69-
with get_abspath('admin-put.json').open() as fh:
70-
put = fh.read()
71-
response = self.http.put(url, data=put)
72-
self.assertEqual(response.status_code, 204)
82+
admin_api = reload_api(admin_config_path, monkeypatch, openapi)
7383

74-
# NOTE: we sleep 5 between CRUD requests so as to let gunicorn
75-
# restart with the refreshed configuration
76-
time.sleep(5)
84+
req = mock_api_request()
85+
headers, status_code, content = get_config_(admin_api, req)
7786

78-
content = self.http.get(url).json()
79-
self.assertEqual(content['logging']['level'], 'INFO')
87+
keys = {'logging', 'metadata', 'resources', 'server'}
88+
assert set(json.loads(content).keys()) == keys
8089

81-
# PATCH configuration
82-
with get_abspath('admin-patch.json').open() as fh:
83-
patch = fh.read()
90+
# PUT configuration
91+
with get_abspath('admin-put.json').open() as fh:
92+
put = fh.read()
93+
req = mock_api_request(data=put)
94+
headers, status_code, content = put_config(admin_api, req)
95+
assert status_code == 204
8496

85-
response = self.http.patch(url, data=patch)
86-
self.assertEqual(response.status_code, 204)
97+
admin_api = reload_api(admin_config_path, monkeypatch, openapi)
8798

88-
time.sleep(5)
99+
req = mock_api_request()
100+
headers, status_code, content = get_config_(admin_api, req)
101+
assert json.loads(content)['logging']['level'] == 'INFO'
89102

90-
content = self.http.get(url).json()
91-
self.assertEqual(content['logging']['level'], 'DEBUG')
103+
# PATCH configuration
104+
with get_abspath('admin-patch.json').open() as fh:
105+
patch = fh.read()
92106

93-
def test_resources_crud(self):
107+
req = mock_api_request(data=patch)
108+
headers, status_code, content = patch_config(admin_api, req)
109+
assert status_code == 204
94110

95-
url = f'{self.admin_endpoint}/resources'
96-
content = self.http.get(url).json()
97-
self.assertEqual(len(content.keys()), 1)
111+
admin_api = reload_api(admin_config_path, monkeypatch, openapi)
98112

99-
# POST a new resource
100-
with get_abspath('resource-post.json').open() as fh:
101-
post_data = fh.read()
113+
assert json.loads(content)['logging']['level'] == 'DEBUG'
102114

103-
response = self.http.post(url, data=post_data)
104-
self.assertEqual(response.status_code, 201)
105-
self.assertEqual(response.text,
106-
'Location: /admin/config/resources/data2')
107115

108-
# NOTE: we sleep 5 between CRUD requests so as to let gunicorn
109-
# restart with the refreshed configuration
110-
time.sleep(5)
116+
def test_resources_crud(monkeypatch, admin_config_path, openapi):
117+
admin_api = reload_api(admin_config_path, monkeypatch, openapi)
111118

112-
content = self.http.get(url).json()
113-
self.assertEqual(len(content.keys()), 2)
119+
empty_req = mock_api_request()
120+
headers, status_code, content = get_resources(admin_api, empty_req)
121+
assert len(json.loads(content).keys()) == 1
114122

115-
with get_abspath('../../pygeoapi-test-config-admin.yml').open() as fh:
116-
d = yaml_load(fh)
117-
temporal_extent_begin = d['resources']['data2']['extents']['temporal']['begin'] # noqa
118-
self.assertIsInstance(temporal_extent_begin, datetime)
123+
# POST a new resource
124+
with get_abspath('resource-post.json').open() as fh:
125+
post_data = fh.read()
119126

120-
# PUT an existing resource
121-
url = f'{self.admin_endpoint}/resources/data2'
122-
with get_abspath('resource-put.json').open() as fh:
123-
post_data = fh.read()
127+
req = mock_api_request(data=post_data)
128+
headers, status_code, content = post_resource(admin_api, req)
129+
assert status_code == 201
130+
assert content == 'Location: //data2'
124131

125-
response = self.http.put(url, data=post_data)
126-
self.assertEqual(response.status_code, 204)
132+
admin_api = reload_api(admin_config_path, monkeypatch, openapi)
127133

128-
time.sleep(5)
134+
headers, status_code, content = get_resources(admin_api, empty_req)
135+
assert len(json.loads(content).keys()) == 2
129136

130-
content = self.http.get(url).json()
131-
self.assertEqual(content['title']['en'],
132-
'Data assets, updated by HTTP PUT')
137+
d = yaml_load(admin_config_path.read_text())
138+
temporal_extent_begin = d['resources']['data2']['extents']['temporal']['begin'] # noqa
139+
assert isinstance(temporal_extent_begin, datetime)
133140

134-
# PATCH an existing resource
135-
url = f'{self.admin_endpoint}/resources/data2'
136-
with get_abspath('resource-patch.json').open() as fh:
137-
post_data = fh.read()
141+
# PUT an existing resource
142+
with get_abspath('resource-put.json').open() as fh:
143+
post_data = fh.read()
138144

139-
response = self.http.patch(url, data=post_data)
140-
self.assertEqual(response.status_code, 204)
145+
req = mock_api_request(data=post_data)
146+
headers, status_code, content = put_resource(admin_api, req, 'data2')
147+
assert status_code == 204
141148

142-
time.sleep(5)
149+
headers, status_code, content = get_resource(admin_api, empty_req, 'data2')
150+
assert (
151+
json.loads(content)['title']['en'] ==
152+
'Data assets, updated by HTTP PUT'
153+
)
143154

144-
content = self.http.get(url).json()
145-
self.assertEqual(content['title']['en'],
146-
'Data assets, updated by HTTP PATCH')
155+
# PATCH an existing resource
156+
with get_abspath('resource-patch.json').open() as fh:
157+
post_data = fh.read()
147158

148-
# DELETE an existing new resource
149-
response = self.http.delete(url)
150-
self.assertEqual(response.status_code, 204)
159+
req = mock_api_request(data=post_data)
160+
headers, status_code, content = patch_resource(admin_api, req, 'data2')
161+
assert status_code == 204
151162

152-
time.sleep(5)
163+
headers, status_code, content = get_resource(admin_api, empty_req, 'data2')
164+
assert (
165+
json.loads(content)['title']['en'] ==
166+
'Data assets, updated by HTTP PATCH'
167+
)
153168

154-
url = f'{self.admin_endpoint}/resources'
155-
content = self.http.get(url).json()
156-
self.assertEqual(len(content.keys()), 1)
169+
# DELETE an existing new resource
170+
headers, status_code, content = \
171+
delete_resource(admin_api, empty_req, 'data2')
172+
assert status_code == 204
173+
174+
headers, status_code, content = get_resources(admin_api, empty_req)
175+
assert len(json.loads(content).keys()) == 1
157176

158177

159178
def get_abspath(filepath):
160179
"""helper function absolute file access"""
161180

162181
return Path(THISDIR) / 'data' / 'admin' / filepath
163-
164-
165-
if __name__ == '__main__':
166-
unittest.main()

0 commit comments

Comments
 (0)