FlaskTester 1.2__tar.gz → 1.4__tar.gz
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.
- {FlaskTester-1.2 → FlaskTester-1.4}/FlaskTester.egg-info/PKG-INFO +38 -15
- {FlaskTester-1.2 → FlaskTester-1.4}/FlaskTester.egg-info/SOURCES.txt +1 -0
- {FlaskTester-1.2 → FlaskTester-1.4}/FlaskTester.egg-info/requires.txt +1 -0
- {FlaskTester-1.2 → FlaskTester-1.4}/FlaskTester.py +123 -71
- FlaskTester-1.4/MANIFEST.in +1 -0
- {FlaskTester-1.2 → FlaskTester-1.4}/PKG-INFO +38 -15
- {FlaskTester-1.2 → FlaskTester-1.4}/README.md +36 -14
- {FlaskTester-1.2 → FlaskTester-1.4}/pyproject.toml +2 -2
- {FlaskTester-1.2 → FlaskTester-1.4}/tests/test.py +35 -15
- {FlaskTester-1.2 → FlaskTester-1.4}/FlaskTester.egg-info/dependency_links.txt +0 -0
- {FlaskTester-1.2 → FlaskTester-1.4}/FlaskTester.egg-info/top_level.txt +0 -0
- {FlaskTester-1.2 → FlaskTester-1.4}/LICENSE +0 -0
- {FlaskTester-1.2 → FlaskTester-1.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: FlaskTester
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4
|
|
4
4
|
Summary: Pytest fixtures for Flask internal and external authenticated tests
|
|
5
5
|
Author-email: Fabien Coelho <flask.tester@coelho.net>
|
|
6
6
|
License: CC0
|
|
@@ -39,6 +39,7 @@ Requires-Dist: sphinx; extra == "doc"
|
|
|
39
39
|
Requires-Dist: sphinx_rtd_theme; extra == "doc"
|
|
40
40
|
Requires-Dist: sphinx-autoapi; extra == "doc"
|
|
41
41
|
Requires-Dist: sphinx-lint; extra == "doc"
|
|
42
|
+
Requires-Dist: myst_parser; extra == "doc"
|
|
42
43
|
|
|
43
44
|
# FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
|
|
44
45
|
|
|
@@ -64,37 +65,48 @@ achieved through environment variables.
|
|
|
64
65
|
Install package with `pip install FlaskTester` or equivalent.
|
|
65
66
|
|
|
66
67
|
The following test creates a local fixture with 2 users identified by a
|
|
67
|
-
password, and retrieves
|
|
68
|
+
password, and retrieves tokens for both users using a `/login` route
|
|
68
69
|
provided by the application.
|
|
69
|
-
It then proceeds to run
|
|
70
|
+
It then proceeds to run authenticated requests against the `/admin` route.
|
|
70
71
|
|
|
71
72
|
```python
|
|
72
|
-
import secret
|
|
73
73
|
import pytest
|
|
74
74
|
from FlaskTester import ft_authenticator, ft_client
|
|
75
|
+
import secret
|
|
75
76
|
|
|
76
77
|
@pytest.fixture
|
|
77
78
|
def app(ft_client):
|
|
78
79
|
# add test passwords for Calvin and Hobbes (must be consistent with app!)
|
|
79
80
|
ft_client.setPass("calvin", secret.PASSES["calvin"])
|
|
80
81
|
ft_client.setPass("hobbes", secret.PASSES["hobbes"])
|
|
81
|
-
# get
|
|
82
|
-
res = ft_client.get("/
|
|
82
|
+
# get user tokens, assume json result {"token": "<token-value>"}
|
|
83
|
+
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
83
84
|
assert res.is_json
|
|
84
85
|
ft_client.setToken("calvin", res.json["token"])
|
|
86
|
+
res = ft_client.post("/login", login="hobbes", auth="param", status=201)
|
|
87
|
+
assert res.is_json
|
|
88
|
+
ft_client.setToken("hobbes", res.json["token"])
|
|
85
89
|
# return working client
|
|
86
90
|
yield ft_client
|
|
87
91
|
|
|
88
|
-
def
|
|
92
|
+
def test_app_admin(app):
|
|
93
|
+
# try all authentication schemes for calvin
|
|
89
94
|
app.get("/admin", login="calvin", auth="bearer", status=200)
|
|
90
95
|
app.get("/admin", login="calvin", auth="basic", status=200)
|
|
96
|
+
app.get("/admin", login="calvin", auth="param", status=200)
|
|
97
|
+
# try all authentication schemes for hobbes
|
|
98
|
+
res = app.get("/admin", login="hobbes", auth="bearer", status=403)
|
|
99
|
+
assert 'not in group "ADMIN"' in res.text
|
|
91
100
|
res = app.get("/admin", login="hobbes", auth="basic", status=403)
|
|
92
101
|
assert 'not in group "ADMIN"' in res.text
|
|
102
|
+
res = app.get("/admin", login="hobbes", auth="param", status=403)
|
|
103
|
+
assert 'not in group "ADMIN"' in res.text
|
|
93
104
|
```
|
|
94
105
|
|
|
95
106
|
This can be run against a (local) server:
|
|
96
107
|
|
|
97
108
|
```shell
|
|
109
|
+
export TEST_SEED="some-random-data" # shared seed
|
|
98
110
|
flask --app app:app run & # start flask app
|
|
99
111
|
pid=$! # keep pid
|
|
100
112
|
export FLASK_TESTER_URL="http://localhost:5000" # set app local url
|
|
@@ -213,23 +225,29 @@ The implementation of these fixtures is based on five classes plus exceptions:
|
|
|
213
225
|
|
|
214
226
|
## License
|
|
215
227
|
|
|
216
|
-
This code is
|
|
228
|
+
This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
|
|
229
|
+
|
|
230
|
+
All software has bug, this is software, hence… Beware that you may lose your
|
|
231
|
+
hairs or your friends because of it. If you like it, feel free to send a
|
|
232
|
+
postcard to the author.
|
|
233
|
+
|
|
234
|
+
## Versions
|
|
217
235
|
|
|
218
236
|
Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
|
|
219
237
|
[sources](https://github.com/zx80/flask-tester) are available on GitHub,
|
|
220
238
|
see also the [documentation](https://zx80.github.io/flask-tester/),
|
|
221
239
|
please report any [issues](https://github.com/zx80/flask-tester/issues).
|
|
222
240
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
- [Flask Testing](https://github.com/jarus/flask-testing) an unmaintained
|
|
226
|
-
old-style unit test for Flask 1.x, without authentication help.
|
|
241
|
+
### 1.4 on 2024-03-19
|
|
227
242
|
|
|
228
|
-
|
|
243
|
+
Test expected assert failures.
|
|
244
|
+
Improved API documentation.
|
|
245
|
+
Keep first found app.
|
|
229
246
|
|
|
230
|
-
|
|
247
|
+
### 1.3 on 2024-03-16
|
|
231
248
|
|
|
232
|
-
|
|
249
|
+
Generate API documentation.
|
|
250
|
+
Cleaner code.
|
|
233
251
|
|
|
234
252
|
### 1.2 on 2024-03-15
|
|
235
253
|
|
|
@@ -255,3 +273,8 @@ Working coverage tests.
|
|
|
255
273
|
### 0.9 on 2024-03-11
|
|
256
274
|
|
|
257
275
|
Initial revision extracted from a separate project.
|
|
276
|
+
|
|
277
|
+
## See Also, or Not
|
|
278
|
+
|
|
279
|
+
- [Flask Testing](https://github.com/jarus/flask-testing) an unmaintained
|
|
280
|
+
old-style unit test for Flask 1.x, without authentication help.
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
"""FlaskTester - Pytest fixtures for Flask authenticated internal and external tests.
|
|
2
|
+
|
|
3
|
+
Pytest: PYTEST_DONT_REWRITE
|
|
4
|
+
"""
|
|
5
|
+
|
|
1
6
|
import os
|
|
2
7
|
import io
|
|
3
8
|
import re
|
|
@@ -10,10 +15,12 @@ log = logging.getLogger("flask_tester")
|
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class FlaskTesterError(BaseException):
|
|
18
|
+
"""Base exception for FlaskTester package."""
|
|
13
19
|
pass
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
class AuthError(FlaskTesterError):
|
|
23
|
+
"""Authenticator Exception."""
|
|
17
24
|
pass
|
|
18
25
|
|
|
19
26
|
|
|
@@ -21,13 +28,33 @@ class Authenticator:
|
|
|
21
28
|
"""Manage authentication for test requests.
|
|
22
29
|
|
|
23
30
|
Supported schemes:
|
|
31
|
+
|
|
24
32
|
- ``basic``: HTTP Basic Authentication
|
|
25
33
|
- ``param``: password with HTTP or JSON parameters
|
|
26
|
-
- ``bearer``: token in ``Authorization``
|
|
34
|
+
- ``bearer``: token in ``Authorization`` *bearer* header
|
|
27
35
|
- ``header``: token in a header
|
|
28
36
|
- ``cookie``: token in a cookie
|
|
29
37
|
- ``tparam``: token in a parameter
|
|
30
38
|
- ``fake``: fake scheme, login directly passed as a parameter
|
|
39
|
+
|
|
40
|
+
Constructor parameters:
|
|
41
|
+
|
|
42
|
+
- ``allow``: list of allowed schemes.
|
|
43
|
+
default is ``["bearer", "basic", "param"]``
|
|
44
|
+
- ``user``: parameter for user on ``param`` password authentication,
|
|
45
|
+
default is ``USER``
|
|
46
|
+
- ``pwd``: parameter for password on ``param`` password authentication,
|
|
47
|
+
default is ``PASS``
|
|
48
|
+
- ``login``: parameter for user on ``fake`` authentication,
|
|
49
|
+
default is ``LOGIN``
|
|
50
|
+
- ``bearer``: name of bearer scheme for token,
|
|
51
|
+
default is ``Bearer``
|
|
52
|
+
- ``header``: name of header for token,
|
|
53
|
+
default is ``Auth``
|
|
54
|
+
- ``cookie``: name of cookie for token,
|
|
55
|
+
default is ``auth``
|
|
56
|
+
- ``tparam``: name of parameter for token,
|
|
57
|
+
default is ``AUTH``
|
|
31
58
|
"""
|
|
32
59
|
|
|
33
60
|
_TOKEN_SCHEMES = {"bearer", "header", "cookie", "tparam"}
|
|
@@ -50,25 +77,6 @@ class Authenticator:
|
|
|
50
77
|
cookie: str = "auth",
|
|
51
78
|
tparam: str = "AUTH",
|
|
52
79
|
):
|
|
53
|
-
"""Constructor parameters:
|
|
54
|
-
|
|
55
|
-
- ``allow``: list of allowed schemes.
|
|
56
|
-
default is ``["bearer", "basic", "param"]``
|
|
57
|
-
- ``user``: parameter for user on ``param`` password authentication,
|
|
58
|
-
default is ``USER``
|
|
59
|
-
- ``pwd``: parameter for password on ``param`` password authentication,
|
|
60
|
-
default is ``PASS``
|
|
61
|
-
- ``login``: parameter for user on ``fake`` authentication,
|
|
62
|
-
default is ``LOGIN``
|
|
63
|
-
- ``bearer``: name of bearer scheme for token,
|
|
64
|
-
default is ``Bearer``
|
|
65
|
-
- ``header``: name of header for token,
|
|
66
|
-
default is ``Auth``
|
|
67
|
-
- ``cookie``: name of cookie for token,
|
|
68
|
-
default is ``auth``
|
|
69
|
-
- ``tparam``: name of parameter for token,
|
|
70
|
-
default is ``AUTH``
|
|
71
|
-
"""
|
|
72
80
|
|
|
73
81
|
self._has_pass, self._has_token = False, False
|
|
74
82
|
for auth in allow:
|
|
@@ -199,20 +207,21 @@ class RequestFlaskResponse:
|
|
|
199
207
|
This only work for simple responses.
|
|
200
208
|
|
|
201
209
|
Available attributes:
|
|
202
|
-
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
210
|
+
|
|
211
|
+
- ``status_code``: integer status code
|
|
212
|
+
- ``data``: body as bytes
|
|
213
|
+
- ``text``: body as a string
|
|
214
|
+
- ``headers``: dict of headers and their values
|
|
215
|
+
- ``cookies``: dict of cookies
|
|
216
|
+
- ``json``: JSON-converted body, or None
|
|
217
|
+
- ``is_json``: whether body was in JSON
|
|
218
|
+
|
|
219
|
+
Constructor parameter:
|
|
220
|
+
|
|
221
|
+
- ``response``: from request.
|
|
209
222
|
"""
|
|
210
223
|
|
|
211
224
|
def __init__(self, response):
|
|
212
|
-
"""Constructor parameter
|
|
213
|
-
|
|
214
|
-
- ``response`` from request.
|
|
215
|
-
"""
|
|
216
225
|
|
|
217
226
|
self._response = response
|
|
218
227
|
self.status_code = response.status_code
|
|
@@ -229,23 +238,24 @@ class RequestFlaskResponse:
|
|
|
229
238
|
|
|
230
239
|
|
|
231
240
|
class Client:
|
|
232
|
-
"""Common class for flask authenticated testing.
|
|
241
|
+
"""Common class for flask authenticated testing.
|
|
233
242
|
|
|
234
|
-
|
|
235
|
-
"""Constructor parameters.
|
|
243
|
+
Constructor parameters:
|
|
236
244
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
245
|
+
- ``auth`` authenticator
|
|
246
|
+
- ``default_login`` if not ``login`` is set.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def __init__(self, auth: Authenticator, default_login: str|None = None):
|
|
240
250
|
self._auth = auth
|
|
241
251
|
self._default_login = default_login
|
|
242
252
|
|
|
243
253
|
def setToken(self, login: str, token: str|None):
|
|
244
|
-
"""Associate a token to a login, None to remove."""
|
|
254
|
+
"""Associate a token to a login, *None* to remove."""
|
|
245
255
|
self._auth.setToken(login, token)
|
|
246
256
|
|
|
247
257
|
def setPass(self, login: str, password: str|None):
|
|
248
|
-
"""Associate a password to a login, None to remove."""
|
|
258
|
+
"""Associate a password to a login, *None* to remove."""
|
|
249
259
|
self._auth.setPass(login, password)
|
|
250
260
|
|
|
251
261
|
def _request(self, method: str, path: str, **kwargs):
|
|
@@ -256,14 +266,16 @@ class Client:
|
|
|
256
266
|
"""Run a possibly authenticated HTTP request.
|
|
257
267
|
|
|
258
268
|
Mandatory parameters:
|
|
259
|
-
|
|
260
|
-
-
|
|
269
|
+
|
|
270
|
+
- ``method``: HTTP method ("GET", "POST", "PATCH", "DELETE"…)
|
|
271
|
+
- ``path``: local path under the base URL
|
|
261
272
|
|
|
262
273
|
Optional parameters:
|
|
263
|
-
|
|
264
|
-
-
|
|
265
|
-
-
|
|
266
|
-
-
|
|
274
|
+
|
|
275
|
+
- ``status``: expected HTTP status, *None* to skip status check
|
|
276
|
+
- ``login``: authenticated user, use **explicit** *None* to skip
|
|
277
|
+
- ``auth``: authentication scheme to use
|
|
278
|
+
- ``**kwargs``: more request parameters (headers, data, json…)
|
|
267
279
|
"""
|
|
268
280
|
|
|
269
281
|
if "login" in kwargs:
|
|
@@ -276,9 +288,9 @@ class Client:
|
|
|
276
288
|
res = self._request(method, path, **kwargs) # type: ignore
|
|
277
289
|
|
|
278
290
|
if status is not None:
|
|
279
|
-
if res.status_code != status: #
|
|
291
|
+
if res.status_code != status: # show error before aborting
|
|
280
292
|
log.error(f"bad {status} result: {res.status_code} {res.text[:512]}")
|
|
281
|
-
assert res.status_code == status
|
|
293
|
+
assert res.status_code == status, f"unexpected status {res.status_code}, expecting {status}"
|
|
282
294
|
|
|
283
295
|
return res
|
|
284
296
|
|
|
@@ -306,14 +318,16 @@ class Client:
|
|
|
306
318
|
"""Run a query and check the response.
|
|
307
319
|
|
|
308
320
|
Mandatory parameters:
|
|
309
|
-
|
|
310
|
-
-
|
|
311
|
-
-
|
|
321
|
+
|
|
322
|
+
- ``method``: HTTP method ("GET", "POST", "PATCH", "DELETE"…)
|
|
323
|
+
- ``path``: local path under the base URL
|
|
324
|
+
- ``status``: expected HTTP status
|
|
312
325
|
|
|
313
326
|
Optional parameters:
|
|
314
|
-
|
|
315
|
-
-
|
|
316
|
-
-
|
|
327
|
+
|
|
328
|
+
- ``content``: regular expression in response body
|
|
329
|
+
- ``login``: authenticated user, use explicit *None* to skip
|
|
330
|
+
- ``**kwargs``: more request parameters (headers, data, json…)
|
|
317
331
|
"""
|
|
318
332
|
|
|
319
333
|
# get HTTP response
|
|
@@ -321,23 +335,24 @@ class Client:
|
|
|
321
335
|
|
|
322
336
|
# check contents
|
|
323
337
|
if content is not None:
|
|
324
|
-
if not re.search(content, res.text, re.DOTALL):
|
|
338
|
+
if not re.search(content, res.text, re.DOTALL):
|
|
325
339
|
log.error(f"cannot find {content} in {res.text}")
|
|
326
|
-
assert False, "content not found"
|
|
340
|
+
assert False, f"expected content {content} not found in {res.text}"
|
|
327
341
|
|
|
328
342
|
return res
|
|
329
343
|
|
|
330
344
|
|
|
331
345
|
class RequestClient(Client):
|
|
332
|
-
"""Request-based test provider.
|
|
346
|
+
"""Request-based test provider.
|
|
333
347
|
|
|
334
|
-
|
|
335
|
-
"""Constructor parameters.
|
|
348
|
+
Constructor parameters:
|
|
336
349
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
350
|
+
- ``auth`` authenticator
|
|
351
|
+
- ``base_url`` target server
|
|
352
|
+
- ``default_login`` if not ``login`` is set.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
def __init__(self, auth: Authenticator, base_url: str, default_login=None):
|
|
341
356
|
super().__init__(auth, default_login)
|
|
342
357
|
self._base_url = base_url
|
|
343
358
|
# reuse connections, otherwise it is too slow…
|
|
@@ -374,15 +389,16 @@ class RequestClient(Client):
|
|
|
374
389
|
|
|
375
390
|
|
|
376
391
|
class FlaskClient(Client):
|
|
377
|
-
"""Flask-based test provider.
|
|
392
|
+
"""Flask-based test provider.
|
|
378
393
|
|
|
379
|
-
|
|
380
|
-
"""Constructor parameters.
|
|
394
|
+
Constructor parameters:
|
|
381
395
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
396
|
+
- ``auth`` authenticator
|
|
397
|
+
- ``client`` Flask ``test_client``
|
|
398
|
+
- ``default_login`` if not ``login`` is set.
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
def __init__(self, auth: Authenticator, client, default_login=None):
|
|
386
402
|
super().__init__(auth, default_login)
|
|
387
403
|
self._client = client
|
|
388
404
|
|
|
@@ -393,7 +409,31 @@ class FlaskClient(Client):
|
|
|
393
409
|
|
|
394
410
|
@pytest.fixture
|
|
395
411
|
def ft_authenticator():
|
|
396
|
-
"""Pytest Fixture: ft_authenticator.
|
|
412
|
+
"""Pytest Fixture: ft_authenticator.
|
|
413
|
+
|
|
414
|
+
Environment variables:
|
|
415
|
+
|
|
416
|
+
- ``FLASK_TESTER_LOG_LEVEL``: package log level in
|
|
417
|
+
``DEBUG INFO WARNING ERROR CRITICAL NOSET``.
|
|
418
|
+
- ``FLASK_TESTER_ALLOW``: allowed space-separated authentication schemes, in
|
|
419
|
+
``basic param bearer header cookie tparam fake``.
|
|
420
|
+
Default is ``bearer basic param``.
|
|
421
|
+
- ``FLASK_TESTER_USER``: user login parameter for ``param`` authentication.
|
|
422
|
+
Default is ``USER``.
|
|
423
|
+
- ``FLASK_TESTER_PASS``: user password parameter for ``param`` authentication.
|
|
424
|
+
Default is ``PASS``.
|
|
425
|
+
- ``FLASK_TESTER_LOGIN``: user login parameter for ``fake`` authentication.
|
|
426
|
+
Default is ``LOGIN``.
|
|
427
|
+
- ``FLASK_TESTER_BEARER``: bearer name for *token* authentication.
|
|
428
|
+
Default is ``Bearer``.
|
|
429
|
+
- ``FLASK_TESTER_HEADER``: header name for *token* authentication.
|
|
430
|
+
Default is ``Auth``.
|
|
431
|
+
- ``FLASK_TESTER_COOKIE``: cookie name for *token* authentication.
|
|
432
|
+
Default is ``auth``.
|
|
433
|
+
- ``FLASK_TESTER_TPARAM``: parameter for *token* authentication.
|
|
434
|
+
Default is ``AUTH``.
|
|
435
|
+
- ``FLASK_TESTER_AUTH``: initial comma-separated list of *login:password*.
|
|
436
|
+
"""
|
|
397
437
|
|
|
398
438
|
level = os.environ.get("FLASK_TESTER_LOG_LEVEL", "NOTSET")
|
|
399
439
|
log.setLevel(logging.DEBUG if level == "DEBUG" else
|
|
@@ -425,7 +465,18 @@ def ft_authenticator():
|
|
|
425
465
|
|
|
426
466
|
@pytest.fixture
|
|
427
467
|
def ft_client(ft_authenticator):
|
|
428
|
-
"""Pytest Fixture: ft_client.
|
|
468
|
+
"""Pytest Fixture: ft_client.
|
|
469
|
+
|
|
470
|
+
Target environment variable, one **must** be defined:
|
|
471
|
+
|
|
472
|
+
- ``FLASK_TESTER_URL``: application HTTP base URL.
|
|
473
|
+
- ``FLASK_TESTER_APP``: Flask application, eg ``app:create_app``.
|
|
474
|
+
|
|
475
|
+
Other environment variable:
|
|
476
|
+
|
|
477
|
+
- ``FLASK_TESTER_DEFAULT``: Default client login, default is *None* for no
|
|
478
|
+
default.
|
|
479
|
+
"""
|
|
429
480
|
|
|
430
481
|
default_login = os.environ.get("FLASK_TESTER_DEFAULT", None)
|
|
431
482
|
client: Client
|
|
@@ -450,6 +501,7 @@ def ft_client(ft_authenticator):
|
|
|
450
501
|
app = getattr(pkg, name)
|
|
451
502
|
if callable(app) and not hasattr(app, "test_client"):
|
|
452
503
|
app = app()
|
|
504
|
+
break
|
|
453
505
|
if not app: # pragma: no cover
|
|
454
506
|
raise FlaskTesterError(f"cannot find Flask app in {pkg_name}")
|
|
455
507
|
client = FlaskClient(ft_authenticator, app.test_client(), default_login)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exclude docs/* */Makefile
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: FlaskTester
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4
|
|
4
4
|
Summary: Pytest fixtures for Flask internal and external authenticated tests
|
|
5
5
|
Author-email: Fabien Coelho <flask.tester@coelho.net>
|
|
6
6
|
License: CC0
|
|
@@ -39,6 +39,7 @@ Requires-Dist: sphinx; extra == "doc"
|
|
|
39
39
|
Requires-Dist: sphinx_rtd_theme; extra == "doc"
|
|
40
40
|
Requires-Dist: sphinx-autoapi; extra == "doc"
|
|
41
41
|
Requires-Dist: sphinx-lint; extra == "doc"
|
|
42
|
+
Requires-Dist: myst_parser; extra == "doc"
|
|
42
43
|
|
|
43
44
|
# FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
|
|
44
45
|
|
|
@@ -64,37 +65,48 @@ achieved through environment variables.
|
|
|
64
65
|
Install package with `pip install FlaskTester` or equivalent.
|
|
65
66
|
|
|
66
67
|
The following test creates a local fixture with 2 users identified by a
|
|
67
|
-
password, and retrieves
|
|
68
|
+
password, and retrieves tokens for both users using a `/login` route
|
|
68
69
|
provided by the application.
|
|
69
|
-
It then proceeds to run
|
|
70
|
+
It then proceeds to run authenticated requests against the `/admin` route.
|
|
70
71
|
|
|
71
72
|
```python
|
|
72
|
-
import secret
|
|
73
73
|
import pytest
|
|
74
74
|
from FlaskTester import ft_authenticator, ft_client
|
|
75
|
+
import secret
|
|
75
76
|
|
|
76
77
|
@pytest.fixture
|
|
77
78
|
def app(ft_client):
|
|
78
79
|
# add test passwords for Calvin and Hobbes (must be consistent with app!)
|
|
79
80
|
ft_client.setPass("calvin", secret.PASSES["calvin"])
|
|
80
81
|
ft_client.setPass("hobbes", secret.PASSES["hobbes"])
|
|
81
|
-
# get
|
|
82
|
-
res = ft_client.get("/
|
|
82
|
+
# get user tokens, assume json result {"token": "<token-value>"}
|
|
83
|
+
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
83
84
|
assert res.is_json
|
|
84
85
|
ft_client.setToken("calvin", res.json["token"])
|
|
86
|
+
res = ft_client.post("/login", login="hobbes", auth="param", status=201)
|
|
87
|
+
assert res.is_json
|
|
88
|
+
ft_client.setToken("hobbes", res.json["token"])
|
|
85
89
|
# return working client
|
|
86
90
|
yield ft_client
|
|
87
91
|
|
|
88
|
-
def
|
|
92
|
+
def test_app_admin(app):
|
|
93
|
+
# try all authentication schemes for calvin
|
|
89
94
|
app.get("/admin", login="calvin", auth="bearer", status=200)
|
|
90
95
|
app.get("/admin", login="calvin", auth="basic", status=200)
|
|
96
|
+
app.get("/admin", login="calvin", auth="param", status=200)
|
|
97
|
+
# try all authentication schemes for hobbes
|
|
98
|
+
res = app.get("/admin", login="hobbes", auth="bearer", status=403)
|
|
99
|
+
assert 'not in group "ADMIN"' in res.text
|
|
91
100
|
res = app.get("/admin", login="hobbes", auth="basic", status=403)
|
|
92
101
|
assert 'not in group "ADMIN"' in res.text
|
|
102
|
+
res = app.get("/admin", login="hobbes", auth="param", status=403)
|
|
103
|
+
assert 'not in group "ADMIN"' in res.text
|
|
93
104
|
```
|
|
94
105
|
|
|
95
106
|
This can be run against a (local) server:
|
|
96
107
|
|
|
97
108
|
```shell
|
|
109
|
+
export TEST_SEED="some-random-data" # shared seed
|
|
98
110
|
flask --app app:app run & # start flask app
|
|
99
111
|
pid=$! # keep pid
|
|
100
112
|
export FLASK_TESTER_URL="http://localhost:5000" # set app local url
|
|
@@ -213,23 +225,29 @@ The implementation of these fixtures is based on five classes plus exceptions:
|
|
|
213
225
|
|
|
214
226
|
## License
|
|
215
227
|
|
|
216
|
-
This code is
|
|
228
|
+
This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
|
|
229
|
+
|
|
230
|
+
All software has bug, this is software, hence… Beware that you may lose your
|
|
231
|
+
hairs or your friends because of it. If you like it, feel free to send a
|
|
232
|
+
postcard to the author.
|
|
233
|
+
|
|
234
|
+
## Versions
|
|
217
235
|
|
|
218
236
|
Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
|
|
219
237
|
[sources](https://github.com/zx80/flask-tester) are available on GitHub,
|
|
220
238
|
see also the [documentation](https://zx80.github.io/flask-tester/),
|
|
221
239
|
please report any [issues](https://github.com/zx80/flask-tester/issues).
|
|
222
240
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
- [Flask Testing](https://github.com/jarus/flask-testing) an unmaintained
|
|
226
|
-
old-style unit test for Flask 1.x, without authentication help.
|
|
241
|
+
### 1.4 on 2024-03-19
|
|
227
242
|
|
|
228
|
-
|
|
243
|
+
Test expected assert failures.
|
|
244
|
+
Improved API documentation.
|
|
245
|
+
Keep first found app.
|
|
229
246
|
|
|
230
|
-
|
|
247
|
+
### 1.3 on 2024-03-16
|
|
231
248
|
|
|
232
|
-
|
|
249
|
+
Generate API documentation.
|
|
250
|
+
Cleaner code.
|
|
233
251
|
|
|
234
252
|
### 1.2 on 2024-03-15
|
|
235
253
|
|
|
@@ -255,3 +273,8 @@ Working coverage tests.
|
|
|
255
273
|
### 0.9 on 2024-03-11
|
|
256
274
|
|
|
257
275
|
Initial revision extracted from a separate project.
|
|
276
|
+
|
|
277
|
+
## See Also, or Not
|
|
278
|
+
|
|
279
|
+
- [Flask Testing](https://github.com/jarus/flask-testing) an unmaintained
|
|
280
|
+
old-style unit test for Flask 1.x, without authentication help.
|
|
@@ -22,37 +22,48 @@ achieved through environment variables.
|
|
|
22
22
|
Install package with `pip install FlaskTester` or equivalent.
|
|
23
23
|
|
|
24
24
|
The following test creates a local fixture with 2 users identified by a
|
|
25
|
-
password, and retrieves
|
|
25
|
+
password, and retrieves tokens for both users using a `/login` route
|
|
26
26
|
provided by the application.
|
|
27
|
-
It then proceeds to run
|
|
27
|
+
It then proceeds to run authenticated requests against the `/admin` route.
|
|
28
28
|
|
|
29
29
|
```python
|
|
30
|
-
import secret
|
|
31
30
|
import pytest
|
|
32
31
|
from FlaskTester import ft_authenticator, ft_client
|
|
32
|
+
import secret
|
|
33
33
|
|
|
34
34
|
@pytest.fixture
|
|
35
35
|
def app(ft_client):
|
|
36
36
|
# add test passwords for Calvin and Hobbes (must be consistent with app!)
|
|
37
37
|
ft_client.setPass("calvin", secret.PASSES["calvin"])
|
|
38
38
|
ft_client.setPass("hobbes", secret.PASSES["hobbes"])
|
|
39
|
-
# get
|
|
40
|
-
res = ft_client.get("/
|
|
39
|
+
# get user tokens, assume json result {"token": "<token-value>"}
|
|
40
|
+
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
41
41
|
assert res.is_json
|
|
42
42
|
ft_client.setToken("calvin", res.json["token"])
|
|
43
|
+
res = ft_client.post("/login", login="hobbes", auth="param", status=201)
|
|
44
|
+
assert res.is_json
|
|
45
|
+
ft_client.setToken("hobbes", res.json["token"])
|
|
43
46
|
# return working client
|
|
44
47
|
yield ft_client
|
|
45
48
|
|
|
46
|
-
def
|
|
49
|
+
def test_app_admin(app):
|
|
50
|
+
# try all authentication schemes for calvin
|
|
47
51
|
app.get("/admin", login="calvin", auth="bearer", status=200)
|
|
48
52
|
app.get("/admin", login="calvin", auth="basic", status=200)
|
|
53
|
+
app.get("/admin", login="calvin", auth="param", status=200)
|
|
54
|
+
# try all authentication schemes for hobbes
|
|
55
|
+
res = app.get("/admin", login="hobbes", auth="bearer", status=403)
|
|
56
|
+
assert 'not in group "ADMIN"' in res.text
|
|
49
57
|
res = app.get("/admin", login="hobbes", auth="basic", status=403)
|
|
50
58
|
assert 'not in group "ADMIN"' in res.text
|
|
59
|
+
res = app.get("/admin", login="hobbes", auth="param", status=403)
|
|
60
|
+
assert 'not in group "ADMIN"' in res.text
|
|
51
61
|
```
|
|
52
62
|
|
|
53
63
|
This can be run against a (local) server:
|
|
54
64
|
|
|
55
65
|
```shell
|
|
66
|
+
export TEST_SEED="some-random-data" # shared seed
|
|
56
67
|
flask --app app:app run & # start flask app
|
|
57
68
|
pid=$! # keep pid
|
|
58
69
|
export FLASK_TESTER_URL="http://localhost:5000" # set app local url
|
|
@@ -171,23 +182,29 @@ The implementation of these fixtures is based on five classes plus exceptions:
|
|
|
171
182
|
|
|
172
183
|
## License
|
|
173
184
|
|
|
174
|
-
This code is
|
|
185
|
+
This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
|
|
186
|
+
|
|
187
|
+
All software has bug, this is software, hence… Beware that you may lose your
|
|
188
|
+
hairs or your friends because of it. If you like it, feel free to send a
|
|
189
|
+
postcard to the author.
|
|
190
|
+
|
|
191
|
+
## Versions
|
|
175
192
|
|
|
176
193
|
Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
|
|
177
194
|
[sources](https://github.com/zx80/flask-tester) are available on GitHub,
|
|
178
195
|
see also the [documentation](https://zx80.github.io/flask-tester/),
|
|
179
196
|
please report any [issues](https://github.com/zx80/flask-tester/issues).
|
|
180
197
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
- [Flask Testing](https://github.com/jarus/flask-testing) an unmaintained
|
|
184
|
-
old-style unit test for Flask 1.x, without authentication help.
|
|
198
|
+
### 1.4 on 2024-03-19
|
|
185
199
|
|
|
186
|
-
|
|
200
|
+
Test expected assert failures.
|
|
201
|
+
Improved API documentation.
|
|
202
|
+
Keep first found app.
|
|
187
203
|
|
|
188
|
-
|
|
204
|
+
### 1.3 on 2024-03-16
|
|
189
205
|
|
|
190
|
-
|
|
206
|
+
Generate API documentation.
|
|
207
|
+
Cleaner code.
|
|
191
208
|
|
|
192
209
|
### 1.2 on 2024-03-15
|
|
193
210
|
|
|
@@ -213,3 +230,8 @@ Working coverage tests.
|
|
|
213
230
|
### 0.9 on 2024-03-11
|
|
214
231
|
|
|
215
232
|
Initial revision extracted from a separate project.
|
|
233
|
+
|
|
234
|
+
## See Also, or Not
|
|
235
|
+
|
|
236
|
+
- [Flask Testing](https://github.com/jarus/flask-testing) an unmaintained
|
|
237
|
+
old-style unit test for Flask 1.x, without authentication help.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "FlaskTester"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4"
|
|
8
8
|
authors = [ { name = "Fabien Coelho", email = "flask.tester@coelho.net" } ]
|
|
9
9
|
description = "Pytest fixtures for Flask internal and external authenticated tests"
|
|
10
10
|
readme = "README.md"
|
|
@@ -35,7 +35,7 @@ package = "https://pypi.org/project/FlaskTester/"
|
|
|
35
35
|
# various dev tools
|
|
36
36
|
dev = ["mypy", "pyright", "ruff", "coverage", "pymarkdownlnt!=0.9.5", "build", "twine", "wheel", "types-flask", "types-requests", "FlaskSimpleAuth", "passlib"]
|
|
37
37
|
# documentation generation
|
|
38
|
-
doc = ["sphinx", "sphinx_rtd_theme", "sphinx-autoapi", "sphinx-lint"]
|
|
38
|
+
doc = ["sphinx", "sphinx_rtd_theme", "sphinx-autoapi", "sphinx-lint", "myst_parser"]
|
|
39
39
|
|
|
40
40
|
[tool.setuptools]
|
|
41
41
|
py-modules = [ "FlaskTester" ]
|
|
@@ -27,18 +27,28 @@ def app(ft_client):
|
|
|
27
27
|
# add test passwords for Calvin and Hobbes (must be consistent with app!)
|
|
28
28
|
ft_client.setPass("calvin", secret.PASSES["calvin"])
|
|
29
29
|
ft_client.setPass("hobbes", secret.PASSES["hobbes"])
|
|
30
|
-
# get
|
|
31
|
-
res = ft_client.get("/
|
|
30
|
+
# get user tokens, assume json result {"token": "<token-value>"}
|
|
31
|
+
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
32
32
|
assert res.is_json
|
|
33
33
|
ft_client.setToken("calvin", res.json["token"])
|
|
34
|
+
res = ft_client.post("/login", login="hobbes", auth="param", status=201)
|
|
35
|
+
assert res.is_json
|
|
36
|
+
ft_client.setToken("hobbes", res.json["token"])
|
|
34
37
|
# return working client
|
|
35
38
|
yield ft_client
|
|
36
39
|
|
|
37
|
-
def
|
|
40
|
+
def test_app_admin(app):
|
|
41
|
+
# try all authentication schemes for calvin
|
|
38
42
|
app.get("/admin", login="calvin", auth="bearer", status=200)
|
|
39
43
|
app.get("/admin", login="calvin", auth="basic", status=200)
|
|
44
|
+
app.get("/admin", login="calvin", auth="param", status=200)
|
|
45
|
+
# try all authentication schemes for hobbes
|
|
46
|
+
res = app.get("/admin", login="hobbes", auth="bearer", status=403)
|
|
47
|
+
assert 'not in group "ADMIN"' in res.text
|
|
40
48
|
res = app.get("/admin", login="hobbes", auth="basic", status=403)
|
|
41
49
|
assert 'not in group "ADMIN"' in res.text
|
|
50
|
+
res = app.get("/admin", login="hobbes", auth="param", status=403)
|
|
51
|
+
assert 'not in group "ADMIN"' in res.text
|
|
42
52
|
|
|
43
53
|
@pytest.fixture
|
|
44
54
|
def api(ft_client):
|
|
@@ -50,13 +60,13 @@ def api(ft_client):
|
|
|
50
60
|
ft_client.setPass("moe", "bad password")
|
|
51
61
|
ft_client.setToken("moe", "bad token")
|
|
52
62
|
# get valid tokens using password authn
|
|
53
|
-
res = ft_client.get("/
|
|
63
|
+
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
54
64
|
assert res.json["user"] == "calvin"
|
|
55
65
|
ft_client._auth.setToken("calvin", res.json["token"])
|
|
56
|
-
res = ft_client.post("/
|
|
66
|
+
res = ft_client.post("/login", login="hobbes", auth="param", status=201, data={})
|
|
57
67
|
assert res.json["user"] == "hobbes"
|
|
58
68
|
ft_client._auth.setToken("hobbes", res.json["token"])
|
|
59
|
-
res = ft_client.post("/
|
|
69
|
+
res = ft_client.post("/login", login="susie", auth="param", status=201, json={})
|
|
60
70
|
assert res.json["user"] == "susie"
|
|
61
71
|
ft_client._auth.setToken("susie", res.json["token"])
|
|
62
72
|
# check that token auth is ok
|
|
@@ -86,12 +96,22 @@ def test_admin(api):
|
|
|
86
96
|
api.check("GET", "/admin", 403, login="hobbes", auth=auth)
|
|
87
97
|
api.check("GET", "/admin", 401, login="moe", auth=auth)
|
|
88
98
|
api.check("GET", "/admin", 401, login=None, auth=auth)
|
|
99
|
+
try:
|
|
100
|
+
api.check("GET", "/admin", status=599, login="calvin", auth=auth)
|
|
101
|
+
pytest.fail("assert on status must fail") # pragma: no cover
|
|
102
|
+
except AssertionError as e:
|
|
103
|
+
assert "200" in str(e)
|
|
104
|
+
try:
|
|
105
|
+
api.check("GET", "/admin", status=200, login="calvin", auth=auth, content="NOT THERE")
|
|
106
|
+
pytest.fail("assert on content must fail") # pragma: no cover
|
|
107
|
+
except AssertionError as e:
|
|
108
|
+
assert "NOT THERE" in str(e)
|
|
89
109
|
|
|
90
110
|
def test_errors(api):
|
|
91
111
|
for scheme in ("header", "cookie", "fake", "tparam", "unexpected"):
|
|
92
112
|
try:
|
|
93
|
-
api.get("/
|
|
94
|
-
|
|
113
|
+
api.get("/login", login="calvin", auth=scheme)
|
|
114
|
+
pytest.fail("must raise an exception") # pragma: no cover
|
|
95
115
|
except ft.AuthError as e:
|
|
96
116
|
assert True, "expected error"
|
|
97
117
|
|
|
@@ -128,13 +148,13 @@ def test_authenticator_token():
|
|
|
128
148
|
try:
|
|
129
149
|
kwargs = {}
|
|
130
150
|
auth.setAuth("dad", kwargs)
|
|
131
|
-
|
|
151
|
+
pytest.fail("must raise an error") # pragma: no cover
|
|
132
152
|
except ft.FlaskTesterError:
|
|
133
153
|
assert True, "error raised"
|
|
134
154
|
# rosalyn as a password, but no password carrier is allowed
|
|
135
155
|
try:
|
|
136
156
|
auth.setPass("rosalyn", "rsln-pass")
|
|
137
|
-
|
|
157
|
+
pytest.fail("must raise an error") # pragma: no cover
|
|
138
158
|
except ft.AuthError:
|
|
139
159
|
assert True, "error raised"
|
|
140
160
|
# force to trigger later errors
|
|
@@ -143,7 +163,7 @@ def test_authenticator_token():
|
|
|
143
163
|
try:
|
|
144
164
|
kwargs={}
|
|
145
165
|
auth.setAuth("rosalyn", kwargs)
|
|
146
|
-
|
|
166
|
+
pytest.fail("must raise an error") # pragma: no cover
|
|
147
167
|
except ft.FlaskTesterError:
|
|
148
168
|
assert True, "error raised"
|
|
149
169
|
|
|
@@ -172,7 +192,7 @@ def test_authenticator_password():
|
|
|
172
192
|
# susie as a token, but no token carrier is allowed
|
|
173
193
|
try:
|
|
174
194
|
auth.setToken("susie", "ss-token")
|
|
175
|
-
|
|
195
|
+
pytest.fail("must raise an error") # pragma: no cover
|
|
176
196
|
except ft.FlaskTesterError:
|
|
177
197
|
assert True, "error raised"
|
|
178
198
|
# force to trigger later error
|
|
@@ -181,7 +201,7 @@ def test_authenticator_password():
|
|
|
181
201
|
try:
|
|
182
202
|
kwargs={}
|
|
183
203
|
auth.setAuth("susie", kwargs)
|
|
184
|
-
|
|
204
|
+
pytest.fail("must raise an error") # pragma: no cover
|
|
185
205
|
except ft.FlaskTesterError:
|
|
186
206
|
assert True, "error raised"
|
|
187
207
|
|
|
@@ -214,9 +234,9 @@ def test_client():
|
|
|
214
234
|
client = ft.Client(ft.Authenticator())
|
|
215
235
|
try:
|
|
216
236
|
client._request("GET", "/")
|
|
217
|
-
|
|
237
|
+
pytest.fail("must raise an error") # pragma: no cover
|
|
218
238
|
except NotImplementedError:
|
|
219
|
-
assert True, "error raised"
|
|
239
|
+
assert True, "expected error raised"
|
|
220
240
|
|
|
221
241
|
def test_request_client():
|
|
222
242
|
httpd = htsv.HTTPServer(("", 8888), htsv.SimpleHTTPRequestHandler)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|