spltz-viur-testing 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spltz_viur_testing-0.1.0.dist-info/METADATA +308 -0
- spltz_viur_testing-0.1.0.dist-info/RECORD +14 -0
- spltz_viur_testing-0.1.0.dist-info/WHEEL +5 -0
- spltz_viur_testing-0.1.0.dist-info/licenses/LICENSE +21 -0
- spltz_viur_testing-0.1.0.dist-info/top_level.txt +1 -0
- viur/testing/__init__.py +321 -0
- viur/testing/_test/__init__.py +144 -0
- viur/testing/_test/config.py +350 -0
- viur/testing/activation.py +330 -0
- viur/testing/banner.py +145 -0
- viur/testing/constants.py +40 -0
- viur/testing/protection.py +39 -0
- viur/testing/runner.py +202 -0
- viur/testing/validator.py +121 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spltz-viur-testing
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Safe test-mode for ViUR core projects — Playwright e2e ready.
|
|
5
|
+
Author: Andreas H. Kelch
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright © 2026 Andreas H. Kelch
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/sprengplatz/viur-testing
|
|
29
|
+
Project-URL: Documentation, https://sprengplatz.github.io/viur-testing/
|
|
30
|
+
Project-URL: Repository, https://github.com/sprengplatz/viur-testing.git
|
|
31
|
+
Project-URL: Bug Tracker, https://github.com/sprengplatz/viur-testing/issues
|
|
32
|
+
Project-URL: Changelog, https://github.com/sprengplatz/viur-testing/blob/main/CHANGELOG.md
|
|
33
|
+
Keywords: viur,e2e,testing,playwright
|
|
34
|
+
Classifier: Development Status :: 4 - Beta
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
40
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
41
|
+
Requires-Python: >=3.12
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: viur-core<4,>=3.7
|
|
45
|
+
Provides-Extra: test
|
|
46
|
+
Requires-Dist: pytest~=8.0; extra == "test"
|
|
47
|
+
Requires-Dist: pytest-cov~=5.0; extra == "test"
|
|
48
|
+
Requires-Dist: coverage[toml]~=7.0; extra == "test"
|
|
49
|
+
Requires-Dist: spltz-viur-light-mock>=0.1; extra == "test"
|
|
50
|
+
Provides-Extra: docs
|
|
51
|
+
Requires-Dist: mkdocs-material~=9.5; extra == "docs"
|
|
52
|
+
Requires-Dist: mkdocstrings[python]~=0.26; extra == "docs"
|
|
53
|
+
Provides-Extra: dev
|
|
54
|
+
Requires-Dist: spltz-viur-testing[docs,test]; extra == "dev"
|
|
55
|
+
Requires-Dist: build~=1.2; extra == "dev"
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
|
|
58
|
+
# viur-testing
|
|
59
|
+
|
|
60
|
+
[](https://github.com/sprengplatz/viur-testing/actions/workflows/test.yml)
|
|
61
|
+
[](https://sprengplatz.github.io/viur-testing/)
|
|
62
|
+
[](LICENSE)
|
|
63
|
+
|
|
64
|
+
Safe test-mode for ViUR core projects — primarily for Playwright
|
|
65
|
+
end-to-end tests. Swaps the default datastore database out for a
|
|
66
|
+
dedicated named database (default: `viur-tests`), with a bilateral
|
|
67
|
+
handshake that refuses to let any test or endpoint run unless both the
|
|
68
|
+
server **and** the runner agree they are talking to the test instance.
|
|
69
|
+
|
|
70
|
+
## Bilateral guarantee in six lines
|
|
71
|
+
|
|
72
|
+
1. **`activate()` refuses outside `conf.instance.is_dev_server`** —
|
|
73
|
+
read from viur-core's canonical flag.
|
|
74
|
+
2. **Synchronous datastore probe** in the target database has to
|
|
75
|
+
succeed before the client swap is applied.
|
|
76
|
+
3. **`TestModule` *and* `ConfigModule` both refuse to instantiate**
|
|
77
|
+
outside the dev server *or* without a prior `activate()` call —
|
|
78
|
+
both `__init__`s raise in either case. Even a host that bypasses
|
|
79
|
+
the `TestModule` container and mounts `ConfigModule` directly is
|
|
80
|
+
subject to the same checks, so a forgotten activate or a stray
|
|
81
|
+
production mount fails loudly at boot.
|
|
82
|
+
4. **Per-request token validator** (`X-Viur-Test-Token`) blocks every
|
|
83
|
+
request that does not carry the session token, except the two
|
|
84
|
+
bootstrap endpoints.
|
|
85
|
+
5. **`protect()` installs a production-side header guard** that 403s
|
|
86
|
+
any request carrying the test token header outside dev, regardless
|
|
87
|
+
of its value. Installed in every environment.
|
|
88
|
+
6. **Runner preflight** calls `/json/_test/config/status` and refuses to run
|
|
89
|
+
any test if the server's reply (database, project_id, token hash)
|
|
90
|
+
does not match.
|
|
91
|
+
|
|
92
|
+
The session token is stored only in the test database itself
|
|
93
|
+
(kind `viur-tests`, entity `auth-token`) — never on disk. App Engine
|
|
94
|
+
file-system writes are not needed.
|
|
95
|
+
|
|
96
|
+
## Module layout
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
/_test/ TestModule (container, refuses outside dev mode)
|
|
100
|
+
/_test/config/status ConfigModule.status — issues/returns token
|
|
101
|
+
/_test/config/finish ConfigModule.finish — deletes token entity
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Future test flavours (load test, integration helpers, …) go in as
|
|
105
|
+
sibling submodules under the same `_test` container.
|
|
106
|
+
|
|
107
|
+
## Requirements
|
|
108
|
+
|
|
109
|
+
- Python ≥ 3.12
|
|
110
|
+
- viur-core ≥ 3.7, < 4
|
|
111
|
+
- A named Datastore database (default name: `viur-tests`) created in
|
|
112
|
+
your GCP project alongside `(default)`.
|
|
113
|
+
|
|
114
|
+
## Install
|
|
115
|
+
|
|
116
|
+
The PyPI distribution name is `spltz-viur-testing` (experimental
|
|
117
|
+
prefix); the Python import path stays `viur.testing`:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
pip install spltz-viur-testing
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import viur.testing
|
|
125
|
+
viur.testing.setup()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Server-side wiring
|
|
129
|
+
|
|
130
|
+
Two one-liners. `main.py` — **as the first lines, before any
|
|
131
|
+
`viur.core` import**:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
import viur.testing
|
|
135
|
+
viur.testing.setup()
|
|
136
|
+
|
|
137
|
+
# Only now may viur.core be imported by your own code.
|
|
138
|
+
from viur.core import setup as core_setup
|
|
139
|
+
import modules
|
|
140
|
+
import render
|
|
141
|
+
|
|
142
|
+
core_setup(modules, render)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`viur.testing.setup()` checks the `VIUR_TESTING_ENABLE` env var; if
|
|
146
|
+
truthy it calls `activate()` (datastore client swap + key-factory
|
|
147
|
+
patch + closed-system whitelist + state priming + validator install)
|
|
148
|
+
and always installs the production header guard via `protect()`.
|
|
149
|
+
|
|
150
|
+
In `modules/__init__.py` register the test endpoints — idempotent and
|
|
151
|
+
safe to leave in place for production deployments (no-op when test
|
|
152
|
+
mode isn't armed):
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# modules/__init__.py
|
|
156
|
+
import viur.testing
|
|
157
|
+
viur.testing.register_modules(globals())
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
That exposes `POST /json/_test/config/status` and `POST /json/_test/config/finish`.
|
|
161
|
+
Both endpoints re-verify `conf.instance.is_dev_server` and the active
|
|
162
|
+
datastore database before performing any work — defence in depth on
|
|
163
|
+
top of the `TestModule.__init__` guard.
|
|
164
|
+
|
|
165
|
+
If you need more control, the two functions wrap underlying primitives
|
|
166
|
+
you can call yourself: `viur.testing.activate(database=...)`,
|
|
167
|
+
`viur.testing.protect()`, plus direct mounting via
|
|
168
|
+
`from viur.testing._test import TestModule`.
|
|
169
|
+
|
|
170
|
+
## Running the dev server with test mode
|
|
171
|
+
|
|
172
|
+
Toggle test mode at boot by setting the env var that `setup()` reads:
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
VIUR_TESTING_ENABLE=1 viur run
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Without the env var, `setup()` skips `activate()` and the process boots
|
|
179
|
+
against the default database as if the package were not installed.
|
|
180
|
+
|
|
181
|
+
When test mode is active, the dev-server boot banner gains two extra
|
|
182
|
+
lines — `database = …` and `namespace = …` — so the running slice is
|
|
183
|
+
visible at a glance. The namespace line is rendered unconditionally;
|
|
184
|
+
without `VIUR_TESTING_NAMESPACE` it falls back to `(default)`, making
|
|
185
|
+
it obvious that test mode is armed but namespace isolation is **not**
|
|
186
|
+
in effect:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
# With VIUR_TESTING_NAMESPACE=alice
|
|
190
|
+
################## LOCAL DEVELOPMENT SERVER IS UP AND RUNNING ##################
|
|
191
|
+
# project = my-viur-project #
|
|
192
|
+
# python = 3.13.0 #
|
|
193
|
+
# viur = 3.8.25 #
|
|
194
|
+
# database = viur-tests #
|
|
195
|
+
# namespace = alice #
|
|
196
|
+
################################################################################
|
|
197
|
+
|
|
198
|
+
# Without VIUR_TESTING_NAMESPACE (or with empty value)
|
|
199
|
+
################## LOCAL DEVELOPMENT SERVER IS UP AND RUNNING ##################
|
|
200
|
+
# project = my-viur-project #
|
|
201
|
+
# python = 3.13.0 #
|
|
202
|
+
# viur = 3.8.25 #
|
|
203
|
+
# database = viur-tests #
|
|
204
|
+
# namespace = (default) #
|
|
205
|
+
################################################################################
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Concurrency: sharing one test database across multiple testers
|
|
209
|
+
|
|
210
|
+
The `viur-tests` database is a shared GCP resource. If two engineers
|
|
211
|
+
both boot a dev server with the same database and run tests at the
|
|
212
|
+
same time, their entities will collide — Person A's seed wipes
|
|
213
|
+
Person B's user, Person B's test queries find leftovers from Person A.
|
|
214
|
+
|
|
215
|
+
The fix is the optional `namespace` argument. ViUR-testing passes it
|
|
216
|
+
to `google.cloud.datastore.Client(database=…, namespace=…)` and rewires
|
|
217
|
+
`viur.core.db.Key` so every read and write in the process is scoped
|
|
218
|
+
to that namespace. Different namespaces in the same database are
|
|
219
|
+
fully isolated — no separate DB provisioning needed.
|
|
220
|
+
|
|
221
|
+
Boot each dev server with its own namespace:
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
# Alice's machine
|
|
225
|
+
VIUR_TESTING_ENABLE=1 VIUR_TESTING_NAMESPACE=alice viur run
|
|
226
|
+
|
|
227
|
+
# Bob's machine
|
|
228
|
+
VIUR_TESTING_ENABLE=1 VIUR_TESTING_NAMESPACE=bob viur run
|
|
229
|
+
|
|
230
|
+
# CI for PR #42
|
|
231
|
+
VIUR_TESTING_ENABLE=1 VIUR_TESTING_NAMESPACE=ci-pr-42 viur run
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The runner-side `require_test_mode` can assert the expected namespace
|
|
235
|
+
to fail fast when somebody points at the wrong slice::
|
|
236
|
+
|
|
237
|
+
from viur.testing import require_test_mode
|
|
238
|
+
|
|
239
|
+
status = require_test_mode(
|
|
240
|
+
"http://localhost:8080",
|
|
241
|
+
expected_namespace="alice", # omit to skip; pass None for default
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
`VIUR_TESTING_NAMESPACE` may be empty or unset — both mean "no
|
|
245
|
+
namespace, use the Datastore default". This is the existing behaviour
|
|
246
|
+
when no namespaces are needed (e.g. single-developer setup).
|
|
247
|
+
|
|
248
|
+
## Runner-side wiring (pytest + Playwright)
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
# tests/conftest.py
|
|
252
|
+
import pytest
|
|
253
|
+
from viur.testing import require_test_mode, finish
|
|
254
|
+
|
|
255
|
+
_BASE_URL = "http://localhost:8080"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@pytest.fixture(scope="session")
|
|
259
|
+
def test_session():
|
|
260
|
+
status = require_test_mode(_BASE_URL)
|
|
261
|
+
yield status
|
|
262
|
+
finish(_BASE_URL, status.token)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
If the server is not in test mode, points at the wrong database, or
|
|
266
|
+
the token hash does not match the returned token, `require_test_mode`
|
|
267
|
+
raises `TestModePreflightError` and no test ever runs.
|
|
268
|
+
|
|
269
|
+
For Playwright, inject the token as a default header on the browser
|
|
270
|
+
context:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
@pytest.fixture
|
|
274
|
+
def context(browser, test_session):
|
|
275
|
+
return browser.new_context(
|
|
276
|
+
extra_http_headers={"X-Viur-Test-Token": test_session.token},
|
|
277
|
+
)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Naming
|
|
281
|
+
|
|
282
|
+
The Python package keeps its repository name `viur-testing` for stability,
|
|
283
|
+
but everything inside it speaks the generic *test* vocabulary so future
|
|
284
|
+
test flavours can be added under the same `TestModule` umbrella
|
|
285
|
+
without churn. The `_test` URL prefix's leading underscore signals
|
|
286
|
+
"system-internal, not for production callers".
|
|
287
|
+
|
|
288
|
+
## Development
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
git clone git@github.com:sprengplatz/viur-testing.git
|
|
292
|
+
cd viur-testing
|
|
293
|
+
pip install -e ".[dev]"
|
|
294
|
+
pytest # 100% coverage required
|
|
295
|
+
mkdocs serve # docs at http://localhost:8000
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Documentation
|
|
299
|
+
|
|
300
|
+
See [sprengplatz.github.io/viur-testing](https://sprengplatz.github.io/viur-testing/).
|
|
301
|
+
|
|
302
|
+
## Changelog
|
|
303
|
+
|
|
304
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
spltz_viur_testing-0.1.0.dist-info/licenses/LICENSE,sha256=xbdB0-3osMyjq6qSugjP0wbJx20IIMB2w_qj-HhvnXE,1072
|
|
2
|
+
viur/testing/__init__.py,sha256=0olAfnZdCkYJD5fUzKeB6kpdm_bu_YcM5_l2FD2ogQQ,12639
|
|
3
|
+
viur/testing/activation.py,sha256=x7Uo15BjSfbOuzEoYO6Gtj-QHKzH4oLinzUhk2wK0KY,14408
|
|
4
|
+
viur/testing/banner.py,sha256=LDzRLZ2ZLBE4x3Cn3VDcBR7rH7FABgVJDxxnO8ZP7xY,5934
|
|
5
|
+
viur/testing/constants.py,sha256=8n9vqS9k6LYbh7LbRgi4UGZrqmoc9djmd5idpbfNCB8,1561
|
|
6
|
+
viur/testing/protection.py,sha256=zfljUGeM-OGBoiJHSsEi5flIHDAajopNll9_EQa74OQ,1664
|
|
7
|
+
viur/testing/runner.py,sha256=Cd-vzM2z5MD-HeiOppVwFTNp92CGoxHPGLVrBWnouFM,7383
|
|
8
|
+
viur/testing/validator.py,sha256=bkUvBKB0ZmPlpznuRCd8MHZdPM9-oauxUxIaMQjaP5w,4810
|
|
9
|
+
viur/testing/_test/__init__.py,sha256=HFEiBlWmuY4akpf55kqGXv7L7Qo4uvHc0wDg-ermvtc,5990
|
|
10
|
+
viur/testing/_test/config.py,sha256=vG2nRIt174Y3w9W6zTZ55lc6m2FhGnhB3YU7WR5BOkQ,13126
|
|
11
|
+
spltz_viur_testing-0.1.0.dist-info/METADATA,sha256=lAcjL57ksq4reeFJb338nb_9Q2VKzEODX0pT_neI2_k,11905
|
|
12
|
+
spltz_viur_testing-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
13
|
+
spltz_viur_testing-0.1.0.dist-info/top_level.txt,sha256=UpXSoS_clIJYncMZKAppy3WNRScHSjarTabKBOa8OeI,5
|
|
14
|
+
spltz_viur_testing-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Andreas H. Kelch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
viur
|
viur/testing/__init__.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
viur-testing — safe test-mode for viur-core projects.
|
|
3
|
+
|
|
4
|
+
The package implements a bilateral guarantee for end-to-end tests
|
|
5
|
+
(typically Playwright): the server refuses to boot unless it is wired
|
|
6
|
+
to a dedicated test database, and the runner refuses to start tests
|
|
7
|
+
unless the server confirms that state back. The session token lives
|
|
8
|
+
only in the test database itself — no on-disk handoff.
|
|
9
|
+
|
|
10
|
+
Server side
|
|
11
|
+
~~~~~~~~~~~
|
|
12
|
+
|
|
13
|
+
- :func:`activate` swaps the datastore client to the target test
|
|
14
|
+
database, runs a synchronous probe roundtrip, primes the in-process
|
|
15
|
+
state on :class:`~viur.testing._test.config.ConfigModule`, and installs
|
|
16
|
+
the request validator. Must be called before any ``viur.core``
|
|
17
|
+
import.
|
|
18
|
+
- :class:`~viur.testing._test.TestModule` is the host-mountable container
|
|
19
|
+
under ``/_test``. It refuses to instantiate outside a local dev
|
|
20
|
+
server — the structural last line of defence against an accidental
|
|
21
|
+
production mount. It carries
|
|
22
|
+
:class:`~viur.testing._test.config.ConfigModule` as a submodule under
|
|
23
|
+
``config``, exposing ``POST /json/_test/config/status`` and
|
|
24
|
+
``POST /json/_test/config/finish``.
|
|
25
|
+
- :class:`~viur.testing.validator.TokenValidator` rejects every
|
|
26
|
+
non-bootstrap request that lacks the matching ``X-Viur-Test-Token``
|
|
27
|
+
header.
|
|
28
|
+
|
|
29
|
+
Runner side
|
|
30
|
+
~~~~~~~~~~~
|
|
31
|
+
|
|
32
|
+
- :func:`require_test_mode` is the preflight check.
|
|
33
|
+
- :func:`finish` tells the server to drop the token entity.
|
|
34
|
+
|
|
35
|
+
Importing the heavy classes
|
|
36
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
37
|
+
|
|
38
|
+
:class:`TestModule`, :class:`ConfigModule`, :class:`TokenValidator` and
|
|
39
|
+
:class:`ProductionGuardValidator` all inherit from viur-core base
|
|
40
|
+
classes at class-definition time — importing any of them triggers the
|
|
41
|
+
full ``viur.core/__init__.py`` chain, which loads
|
|
42
|
+
``viur.core.db.transport`` and instantiates the default
|
|
43
|
+
``datastore.Client``. That is exactly what :func:`activate` is trying
|
|
44
|
+
to swap *before* it ever runs.
|
|
45
|
+
|
|
46
|
+
This top-level package therefore deliberately does **not** re-export
|
|
47
|
+
those classes. Import them from their concrete submodules, and only
|
|
48
|
+
*after* :func:`activate` has finished:
|
|
49
|
+
|
|
50
|
+
- :class:`~viur.testing._test.TestModule` →
|
|
51
|
+
``from viur.testing._test import TestModule``
|
|
52
|
+
- :class:`~viur.testing.validator.TokenValidator`,
|
|
53
|
+
:class:`~viur.testing.validator.ProductionGuardValidator` →
|
|
54
|
+
used internally by :func:`activate` / :func:`protect`; the host
|
|
55
|
+
rarely needs to touch them.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
import os as _os
|
|
59
|
+
|
|
60
|
+
from .activation import activate
|
|
61
|
+
from .constants import DEFAULT_DATABASE, TOKEN_HEADER
|
|
62
|
+
from .protection import protect
|
|
63
|
+
from .runner import ServerStatus, TestModePreflightError, finish, require_test_mode
|
|
64
|
+
|
|
65
|
+
__version__ = "0.1.0"
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"DEFAULT_DATABASE",
|
|
69
|
+
"ServerStatus",
|
|
70
|
+
"TOKEN_HEADER",
|
|
71
|
+
"TestModePreflightError",
|
|
72
|
+
"activate",
|
|
73
|
+
"finish",
|
|
74
|
+
"protect",
|
|
75
|
+
"register_finish_hook",
|
|
76
|
+
"register_modules",
|
|
77
|
+
"register_status_hook",
|
|
78
|
+
"register_test_submodule",
|
|
79
|
+
"require_test_mode",
|
|
80
|
+
"setup",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def register_status_hook(hook) -> None:
|
|
85
|
+
"""Register a project callback that runs inside ``/_test/config/status``.
|
|
86
|
+
|
|
87
|
+
The hook is invoked after the session token has been issued and
|
|
88
|
+
in-process state primed; if it returns a dict, the entries are
|
|
89
|
+
merged into the response payload. Use this to inject
|
|
90
|
+
project-specific configuration that the e2e runner needs to know
|
|
91
|
+
about (feature flags, generated IDs, seed data references, …).
|
|
92
|
+
|
|
93
|
+
Thin wrapper around
|
|
94
|
+
:meth:`viur.testing._test.config.ConfigModule.register_status_hook`.
|
|
95
|
+
Typical wiring lives in ``deploy/test/__init__.py``.
|
|
96
|
+
|
|
97
|
+
:param hook: ``() -> dict | None`` callable.
|
|
98
|
+
"""
|
|
99
|
+
from ._test.config import ConfigModule # noqa: PLC0415
|
|
100
|
+
|
|
101
|
+
ConfigModule.register_status_hook(hook)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def register_finish_hook(hook) -> None:
|
|
105
|
+
"""Register a project callback that runs inside ``/_test/config/finish``.
|
|
106
|
+
|
|
107
|
+
Same shape as :func:`register_status_hook`: optional dict return
|
|
108
|
+
is merged into the finish response. Useful for cleanup
|
|
109
|
+
confirmation, summary info, or project-specific shutdown hooks.
|
|
110
|
+
|
|
111
|
+
:param hook: ``() -> dict | None`` callable.
|
|
112
|
+
"""
|
|
113
|
+
from ._test.config import ConfigModule # noqa: PLC0415
|
|
114
|
+
|
|
115
|
+
ConfigModule.register_finish_hook(hook)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def register_test_submodule(name: str, module_cls: type) -> None:
|
|
119
|
+
"""Mount a host-provided submodule under ``/_test/<name>/...``.
|
|
120
|
+
|
|
121
|
+
Use this to attach project-specific test fixtures (setup,
|
|
122
|
+
teardown, seed data, …) to the same ``/_test`` container that
|
|
123
|
+
carries the built-in :class:`~viur.testing._test.config.ConfigModule`.
|
|
124
|
+
|
|
125
|
+
Recommended convention: one submodule per e2e spec file, named
|
|
126
|
+
after the spec — ``tests/auth/userLogin.spec.ts`` ↔ ``/_test/userLogin/``
|
|
127
|
+
with methods ``setup`` and ``teardown``. This keeps test fixtures
|
|
128
|
+
co-located with the tests that need them.
|
|
129
|
+
|
|
130
|
+
The registration is stored on :class:`~viur.testing._test.TestModule`
|
|
131
|
+
and consumed at mount time. Call this **before** ``viur.core.setup()``
|
|
132
|
+
runs — typically right after ``register_modules`` in your host's
|
|
133
|
+
``modules/__init__.py``.
|
|
134
|
+
|
|
135
|
+
Production-safe: if test mode is not armed (``VIUR_TESTING_ENABLE``
|
|
136
|
+
unset), ``TestModule`` is never mounted and the registration has
|
|
137
|
+
no observable effect.
|
|
138
|
+
|
|
139
|
+
:param name: URL segment under ``/_test/``. Must not collide with
|
|
140
|
+
viur-testing's reserved names (currently ``config``).
|
|
141
|
+
:param module_cls: A ``viur.core.Module`` subclass with the
|
|
142
|
+
endpoints the test needs.
|
|
143
|
+
:raises ValueError: when ``name`` is reserved or empty.
|
|
144
|
+
"""
|
|
145
|
+
from ._test import TestModule # noqa: PLC0415
|
|
146
|
+
|
|
147
|
+
TestModule.register_submodule(name, module_cls)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def setup(
|
|
151
|
+
*,
|
|
152
|
+
enable_env_var: str = "VIUR_TESTING_ENABLE",
|
|
153
|
+
database: str = DEFAULT_DATABASE,
|
|
154
|
+
namespace: str | None = None,
|
|
155
|
+
namespace_env_var: str = "VIUR_TESTING_NAMESPACE",
|
|
156
|
+
api_dir: str | None = "testing",
|
|
157
|
+
) -> None:
|
|
158
|
+
"""One-call host-side wiring for ``main.py``.
|
|
159
|
+
|
|
160
|
+
Must be the **first** line of code in ``main.py`` — before any
|
|
161
|
+
``from viur.core ...`` import. Internally:
|
|
162
|
+
|
|
163
|
+
1. Reads ``os.environ[enable_env_var]`` (default
|
|
164
|
+
``VIUR_TESTING_ENABLE``). If truthy, calls :func:`activate`
|
|
165
|
+
which swaps the datastore client to ``database`` (default
|
|
166
|
+
``viur-tests``) and the optional ``namespace``, runs the probe
|
|
167
|
+
and installs the request validator.
|
|
168
|
+
2. Calls :func:`protect` unconditionally to install the
|
|
169
|
+
production header guard.
|
|
170
|
+
|
|
171
|
+
Namespace resolution: if ``namespace`` is not given to this call,
|
|
172
|
+
``os.environ[namespace_env_var]`` is consulted. An empty string is
|
|
173
|
+
treated as "no namespace" — the host can clear an inherited env
|
|
174
|
+
var by exporting ``VIUR_TESTING_NAMESPACE=``. This makes it easy
|
|
175
|
+
to give different testers their own slice of the same
|
|
176
|
+
``viur-tests`` database without changing ``main.py``::
|
|
177
|
+
|
|
178
|
+
$ VIUR_TESTING_ENABLE=1 VIUR_TESTING_NAMESPACE=ak viur run
|
|
179
|
+
|
|
180
|
+
Use the env var, **not** ``conf.instance.is_dev_server``, as the
|
|
181
|
+
gate — reading ``conf.instance`` triggers the full ``viur.core``
|
|
182
|
+
import chain (including ``viur.core.db.transport``), which would
|
|
183
|
+
leave :func:`activate` with no chance to patch the singleton.
|
|
184
|
+
|
|
185
|
+
Typical host wiring::
|
|
186
|
+
|
|
187
|
+
# main.py
|
|
188
|
+
import viur.testing
|
|
189
|
+
viur.testing.setup()
|
|
190
|
+
|
|
191
|
+
from viur.core import setup as core_setup
|
|
192
|
+
import modules, render
|
|
193
|
+
app = core_setup(modules, render)
|
|
194
|
+
|
|
195
|
+
:param enable_env_var: Name of the env var that gates
|
|
196
|
+
:func:`activate`. Default ``VIUR_TESTING_ENABLE``.
|
|
197
|
+
:param database: Name of the test database to swap to. Default
|
|
198
|
+
``viur-tests``.
|
|
199
|
+
:param namespace: Optional Datastore namespace. When ``None``, the
|
|
200
|
+
env var named in ``namespace_env_var`` is consulted as
|
|
201
|
+
fallback.
|
|
202
|
+
:param namespace_env_var: Name of the env var to read when
|
|
203
|
+
``namespace`` is not given. Default ``VIUR_TESTING_NAMESPACE``.
|
|
204
|
+
:param api_dir: Name of the wrapper directory (relative to the
|
|
205
|
+
caller's parent dir) that contains an ``api/`` subfolder
|
|
206
|
+
with the project test API package. ``setup()`` loads
|
|
207
|
+
``<api_dir>/api/__init__.py`` and registers it as the
|
|
208
|
+
top-level Python package ``api`` via ``importlib`` — no
|
|
209
|
+
``sys.path`` manipulation, no sibling-directory exposure.
|
|
210
|
+
|
|
211
|
+
Default: ``"testing"`` — resolves to
|
|
212
|
+
``<dirname(main.py)>/../testing/api/`` and matches the
|
|
213
|
+
convention ``testing/api/`` (backend fixtures) +
|
|
214
|
+
``testing/e2e/`` (Playwright). Pass any other string to
|
|
215
|
+
relocate the wrapper, or ``None`` to skip the project
|
|
216
|
+
test API entirely.
|
|
217
|
+
|
|
218
|
+
If the resolved ``__init__.py`` does not exist, a one-line
|
|
219
|
+
info message is printed and setup continues — that helps
|
|
220
|
+
spot misconfigurations early (you'd otherwise see mysterious
|
|
221
|
+
``404 /_test/<spec>/setup`` errors from the runner side).
|
|
222
|
+
"""
|
|
223
|
+
if _os.environ.get(enable_env_var):
|
|
224
|
+
if namespace is None:
|
|
225
|
+
namespace = _os.environ.get(namespace_env_var) or None
|
|
226
|
+
activate(database=database, namespace=namespace)
|
|
227
|
+
if api_dir is not None:
|
|
228
|
+
_load_project_api(api_dir)
|
|
229
|
+
protect()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _load_project_api(api_dir: str, caller_file: str | None = None) -> None:
|
|
233
|
+
"""Resolve ``<caller_parent>/<api_dir>/api/__init__.py`` and
|
|
234
|
+
register it as top-level Python package ``api``.
|
|
235
|
+
|
|
236
|
+
The relative path is anchored at the host's ``main.py``; when
|
|
237
|
+
invoked from :func:`setup` we walk the stack via
|
|
238
|
+
``inspect.stack`` to find that file. Tests can pass
|
|
239
|
+
``caller_file`` directly to bypass the stack walk.
|
|
240
|
+
|
|
241
|
+
:param api_dir: Wrapper directory name (e.g. ``"testing"``).
|
|
242
|
+
:param caller_file: Override for the host file used to anchor the
|
|
243
|
+
relative path. When ``None``, walks the call stack.
|
|
244
|
+
"""
|
|
245
|
+
if caller_file is None:
|
|
246
|
+
import inspect # noqa: PLC0415
|
|
247
|
+
# stack[0]=this fn, stack[1]=setup, stack[2]=caller of setup
|
|
248
|
+
caller_file = inspect.stack()[2].filename
|
|
249
|
+
api_init = _os.path.abspath(_os.path.join(
|
|
250
|
+
_os.path.dirname(caller_file), "..", api_dir, "api", "__init__.py",
|
|
251
|
+
))
|
|
252
|
+
_load_api_package(api_init)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _load_api_package(api_init: str) -> None:
|
|
256
|
+
"""Register the package at ``api_init`` as the top-level Python
|
|
257
|
+
package ``api``.
|
|
258
|
+
|
|
259
|
+
``api_init`` is the absolute path to an ``__init__.py``. Uses
|
|
260
|
+
``importlib.util.spec_from_file_location`` so only the one
|
|
261
|
+
package is exposed — ``sys.path`` is left untouched. If the file
|
|
262
|
+
is missing, prints a clear info line and returns; the rest of
|
|
263
|
+
test-mode setup keeps running.
|
|
264
|
+
"""
|
|
265
|
+
import importlib.util # noqa: PLC0415
|
|
266
|
+
import sys # noqa: PLC0415
|
|
267
|
+
|
|
268
|
+
if not _os.path.isfile(api_init):
|
|
269
|
+
print(
|
|
270
|
+
f"[viur-testing] no api package found at {api_init!r} — "
|
|
271
|
+
"project-specific test fixtures will not be loaded. "
|
|
272
|
+
"Pass `api_dir=<wrapper>` to viur.testing.setup() pointing "
|
|
273
|
+
"at a wrapper directory that contains an api/ subfolder.",
|
|
274
|
+
)
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
spec = importlib.util.spec_from_file_location(
|
|
278
|
+
"api", api_init,
|
|
279
|
+
submodule_search_locations=[_os.path.dirname(api_init)],
|
|
280
|
+
)
|
|
281
|
+
if spec is None or spec.loader is None: # pragma: no cover — only happens on a corrupted file system
|
|
282
|
+
print(f"[viur-testing] could not build import spec for {api_init!r}")
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
module = importlib.util.module_from_spec(spec)
|
|
286
|
+
sys.modules["api"] = module
|
|
287
|
+
spec.loader.exec_module(module)
|
|
288
|
+
print(f"[viur-testing] loaded project api package from {api_init!r}")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def register_modules(target: dict) -> None:
|
|
292
|
+
"""Inject :class:`~viur.testing._test.TestModule` into the host's
|
|
293
|
+
``modules/`` namespace if test mode is active.
|
|
294
|
+
|
|
295
|
+
Call from ``modules/__init__.py`` after the auto-discovery loop —
|
|
296
|
+
typically::
|
|
297
|
+
|
|
298
|
+
# modules/__init__.py
|
|
299
|
+
from viur.testing import register_modules
|
|
300
|
+
register_modules(globals())
|
|
301
|
+
|
|
302
|
+
Idempotent: if :func:`activate` has not run (test mode not armed)
|
|
303
|
+
this is a no-op, so the same line stays in place for both dev and
|
|
304
|
+
production deployments.
|
|
305
|
+
|
|
306
|
+
``TestModule`` is registered as a **class**, not an instance, so
|
|
307
|
+
viur-core's ``__build_app`` (which scans ``vars(modules)`` for
|
|
308
|
+
Module subclasses) picks it up and routes ``/_test/config/*``
|
|
309
|
+
through it.
|
|
310
|
+
|
|
311
|
+
:param target: The ``modules/__init__.py`` global namespace dict,
|
|
312
|
+
typically ``globals()``.
|
|
313
|
+
"""
|
|
314
|
+
from ._test.config import ConfigModule # noqa: PLC0415
|
|
315
|
+
|
|
316
|
+
if not ConfigModule.is_active():
|
|
317
|
+
return # test mode not armed — nothing to mount
|
|
318
|
+
|
|
319
|
+
from ._test import TestModule # noqa: PLC0415
|
|
320
|
+
|
|
321
|
+
target["_test"] = TestModule
|