FlaskTester 3.3__tar.gz → 3.5__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-3.5/FlaskTester.egg-info/PKG-INFO +146 -0
- {FlaskTester-3.3 → FlaskTester-3.5}/FlaskTester.egg-info/requires.txt +1 -1
- {FlaskTester-3.3 → FlaskTester-3.5}/FlaskTester.py +93 -74
- FlaskTester-3.5/PKG-INFO +146 -0
- FlaskTester-3.5/README.md +103 -0
- {FlaskTester-3.3 → FlaskTester-3.5}/pyproject.toml +2 -2
- {FlaskTester-3.3 → FlaskTester-3.5}/tests/test.py +39 -27
- FlaskTester-3.3/FlaskTester.egg-info/PKG-INFO +0 -315
- FlaskTester-3.3/PKG-INFO +0 -315
- FlaskTester-3.3/README.md +0 -272
- {FlaskTester-3.3 → FlaskTester-3.5}/FlaskTester.egg-info/SOURCES.txt +0 -0
- {FlaskTester-3.3 → FlaskTester-3.5}/FlaskTester.egg-info/dependency_links.txt +0 -0
- {FlaskTester-3.3 → FlaskTester-3.5}/FlaskTester.egg-info/top_level.txt +0 -0
- {FlaskTester-3.3 → FlaskTester-3.5}/LICENSE +0 -0
- {FlaskTester-3.3 → FlaskTester-3.5}/MANIFEST.in +0 -0
- {FlaskTester-3.3 → FlaskTester-3.5}/setup.cfg +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: FlaskTester
|
|
3
|
+
Version: 3.5
|
|
4
|
+
Summary: Pytest fixtures for Flask internal and external authenticated tests
|
|
5
|
+
Author-email: Fabien Coelho <flask.tester@coelho.net>
|
|
6
|
+
License: CC0
|
|
7
|
+
Project-URL: repository, https://github.com/zx80/flask-tester
|
|
8
|
+
Project-URL: documentation, https://zx80.github.io/flask-tester/
|
|
9
|
+
Project-URL: issues, https://github.com/zx80/flask-tester/issues
|
|
10
|
+
Project-URL: package, https://pypi.org/project/FlaskTester/
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Framework :: Flask
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: requests
|
|
22
|
+
Requires-Dist: flask
|
|
23
|
+
Requires-Dist: pytest
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy; extra == "dev"
|
|
26
|
+
Requires-Dist: pyright; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff; extra == "dev"
|
|
28
|
+
Requires-Dist: coverage; extra == "dev"
|
|
29
|
+
Requires-Dist: pymarkdownlnt!=0.9.5; extra == "dev"
|
|
30
|
+
Requires-Dist: build; extra == "dev"
|
|
31
|
+
Requires-Dist: twine; extra == "dev"
|
|
32
|
+
Requires-Dist: wheel; extra == "dev"
|
|
33
|
+
Requires-Dist: types-flask; extra == "dev"
|
|
34
|
+
Requires-Dist: types-requests; extra == "dev"
|
|
35
|
+
Requires-Dist: FlaskSimpleAuth>=30.0; extra == "dev"
|
|
36
|
+
Requires-Dist: passlib; extra == "dev"
|
|
37
|
+
Provides-Extra: doc
|
|
38
|
+
Requires-Dist: sphinx; extra == "doc"
|
|
39
|
+
Requires-Dist: sphinx_rtd_theme; extra == "doc"
|
|
40
|
+
Requires-Dist: sphinx-autoapi; extra == "doc"
|
|
41
|
+
Requires-Dist: sphinx-lint; extra == "doc"
|
|
42
|
+
Requires-Dist: myst_parser; extra == "doc"
|
|
43
|
+
|
|
44
|
+
# FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
|
|
45
|
+
|
|
46
|
+
This package allows to run authenticated tests against a Flask application,
|
|
47
|
+
either with internal Flask tests (aka `test_client`) or external tests (with
|
|
48
|
+
`requests` which performs actual HTTP requests), including password and token
|
|
49
|
+
authentication and per-user cookies.
|
|
50
|
+
|
|
51
|
+
Only one set of tests is needed, switching from internal to external is
|
|
52
|
+
achieved through environment variables.
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+

|
|
56
|
+

|
|
57
|
+

|
|
58
|
+

|
|
59
|
+

|
|
60
|
+

|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
Install package with `pip install FlaskTester` or equivalent.
|
|
66
|
+
|
|
67
|
+
The following test creates a local fixture with 2 users identified by a
|
|
68
|
+
password, and retrieves tokens for both users using a `/login` route
|
|
69
|
+
provided by the application.
|
|
70
|
+
It then proceeds to run authenticated requests against the `/admin` route.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import pytest
|
|
74
|
+
from FlaskTester import ft_authenticator, ft_client
|
|
75
|
+
import secret
|
|
76
|
+
|
|
77
|
+
@pytest.fixture
|
|
78
|
+
def app(ft_client):
|
|
79
|
+
# add test passwords for Calvin and Hobbes (must be consistent with app!)
|
|
80
|
+
ft_client.setPass("calvin", secret.PASSES["calvin"])
|
|
81
|
+
ft_client.setPass("hobbes", secret.PASSES["hobbes"])
|
|
82
|
+
# get user tokens, assume json result {"token": "<token-value>"}
|
|
83
|
+
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
84
|
+
assert res.is_json
|
|
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"])
|
|
89
|
+
# also set a cookie
|
|
90
|
+
ft_client.setCookie("hobbes", "lang", "fr")
|
|
91
|
+
ft_client.setCookie("calvin", "lang", "en")
|
|
92
|
+
# return working client
|
|
93
|
+
yield ft_client
|
|
94
|
+
|
|
95
|
+
def test_app_admin(app):
|
|
96
|
+
app.get("/admin", login=None, status=401)
|
|
97
|
+
for auth in ["bearer", "basic", "param"]:
|
|
98
|
+
res = app.get("/admin", login="calvin", auth=auth, status=200)
|
|
99
|
+
assert res.json["user"] == "calvin" and res.json["isadmin"]
|
|
100
|
+
res = app.get("/admin", login="hobbes", auth=auth, status=403)
|
|
101
|
+
assert 'not in group "ADMIN"' in res.text
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This can be run against a (local) server:
|
|
105
|
+
|
|
106
|
+
```shell
|
|
107
|
+
export TEST_SEED="some-random-data" # shared test seed
|
|
108
|
+
flask --app app:app run & # start flask app
|
|
109
|
+
pid=$! # keep pid
|
|
110
|
+
export FLASK_TESTER_URL="http://localhost:5000" # set app local url
|
|
111
|
+
pytest test.py # run external tests
|
|
112
|
+
kill $pid # stop app with pid
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Or locally with the Flask internal test infrastructure:
|
|
116
|
+
|
|
117
|
+
```shell
|
|
118
|
+
export FLASK_TESTER_APP="app:app" # set app module
|
|
119
|
+
pytest test.py # run internal tests
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The above test runs with [`tests/app.py`](tests/app.py)
|
|
123
|
+
[Flask](https://flask.palletsprojects.com/)
|
|
124
|
+
REST application back-end with password and token authentication based on
|
|
125
|
+
[FlaskSimpleAuth](https://pypi.org/project/FlaskSimpleAuth/).
|
|
126
|
+
The code uses _25_ lines of Python for implementing
|
|
127
|
+
password (basic and parameters) and token authentications,
|
|
128
|
+
admin group authorization, and routes for
|
|
129
|
+
token generation (2), identity tests (2) and an incredible open cookie-based
|
|
130
|
+
translation service.
|
|
131
|
+
|
|
132
|
+
See the [documentation](https://zx80.github.io/flask-tester/).
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
|
|
137
|
+
|
|
138
|
+
All software has bug, this is software, hence… Beware that you may lose your
|
|
139
|
+
hairs or your friends because of it. If you like it, feel free to send a
|
|
140
|
+
postcard to the author.
|
|
141
|
+
|
|
142
|
+
## Versions
|
|
143
|
+
|
|
144
|
+
Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
|
|
145
|
+
[sources](https://github.com/zx80/flask-tester) are available on GitHub,
|
|
146
|
+
please report any [issues](https://github.com/zx80/flask-tester/issues).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""FlaskTester - Pytest fixtures for Flask authenticated internal and external tests.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
PYTEST_DONT_REWRITE: local assertions are really that, not pytest assertions.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import os
|
|
@@ -24,38 +24,56 @@ class AuthError(FlaskTesterError):
|
|
|
24
24
|
pass
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class _AssertError(FlaskTesterError):
|
|
28
|
+
"""User assertion Exception, only for internal testing."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _raiseError(msg: str):
|
|
33
|
+
"""Undocumented switch for FlaskTester own tests."""
|
|
34
|
+
log.error(msg)
|
|
35
|
+
if "FLASK_TESTER_TESTING" in os.environ:
|
|
36
|
+
raise _AssertError(msg)
|
|
37
|
+
else: # pragma: no cover # cannot cover an expected pytest failure!
|
|
38
|
+
pytest.fail(msg)
|
|
39
|
+
|
|
40
|
+
|
|
27
41
|
class Authenticator:
|
|
28
|
-
"""Manage
|
|
42
|
+
"""Manage HTTP request authentication.
|
|
29
43
|
|
|
30
44
|
Supported schemes:
|
|
31
45
|
|
|
32
|
-
- ``basic``: HTTP Basic Authentication
|
|
33
|
-
- ``param``: password with HTTP or JSON parameters
|
|
34
|
-
- ``bearer``: token in ``Authorization`` *bearer* header
|
|
35
|
-
- ``header``: token in a header
|
|
36
|
-
- ``cookie``: token in a cookie
|
|
37
|
-
- ``tparam``: token in a parameter
|
|
38
|
-
- ``fake``: fake scheme, login directly passed as a parameter
|
|
39
|
-
- ``none``: no authentication, only cookies
|
|
46
|
+
- ``basic``: HTTP Basic Authentication.
|
|
47
|
+
- ``param``: password with HTTP or JSON parameters.
|
|
48
|
+
- ``bearer``: token in ``Authorization`` *bearer* header.
|
|
49
|
+
- ``header``: token in a header.
|
|
50
|
+
- ``cookie``: token in a cookie.
|
|
51
|
+
- ``tparam``: token in a parameter.
|
|
52
|
+
- ``fake``: fake scheme, login directly passed as a parameter.
|
|
53
|
+
- ``none``: no authentication, only cookies.
|
|
40
54
|
|
|
41
55
|
Constructor parameters:
|
|
42
56
|
|
|
43
57
|
- ``allow``: list of allowed schemes.
|
|
44
|
-
default is ``["bearer", "basic", "param", "none"]
|
|
58
|
+
default is ``["bearer", "basic", "param", "none"]``.
|
|
45
59
|
- ``user``: parameter for user on ``param`` password authentication,
|
|
46
|
-
default is ``USER
|
|
60
|
+
default is ``USER``.
|
|
47
61
|
- ``pwd``: parameter for password on ``param`` password authentication,
|
|
48
|
-
default is ``PASS
|
|
62
|
+
default is ``PASS``.
|
|
49
63
|
- ``login``: parameter for user on ``fake`` authentication,
|
|
50
|
-
default is ``LOGIN
|
|
64
|
+
default is ``LOGIN``.
|
|
51
65
|
- ``bearer``: name of bearer scheme for token,
|
|
52
|
-
default is ``Bearer
|
|
66
|
+
default is ``Bearer``.
|
|
53
67
|
- ``header``: name of header for token,
|
|
54
|
-
default is ``Auth
|
|
68
|
+
default is ``Auth``.
|
|
55
69
|
- ``cookie``: name of cookie for token,
|
|
56
|
-
default is ``auth
|
|
70
|
+
default is ``auth``.
|
|
57
71
|
- ``tparam``: name of parameter for token,
|
|
58
|
-
default is ``AUTH
|
|
72
|
+
default is ``AUTH``.
|
|
73
|
+
- ``ptype``: default parameter type, either ``data`` or ``json``,
|
|
74
|
+
default is ``data``.
|
|
75
|
+
|
|
76
|
+
Note: default values are consistent with `FlaskSimpleAuth <https://pypi.org/project/FlaskSimpleAuth/>`_.
|
|
59
77
|
"""
|
|
60
78
|
|
|
61
79
|
_TOKEN_SCHEMES = {"bearer", "header", "cookie", "tparam"}
|
|
@@ -78,6 +96,7 @@ class Authenticator:
|
|
|
78
96
|
header: str = "Auth",
|
|
79
97
|
cookie: str = "auth",
|
|
80
98
|
tparam: str = "AUTH",
|
|
99
|
+
ptype: str = "data",
|
|
81
100
|
):
|
|
82
101
|
|
|
83
102
|
self._has_pass, self._has_token = False, False
|
|
@@ -97,6 +116,8 @@ class Authenticator:
|
|
|
97
116
|
self._header = header
|
|
98
117
|
self._cookie = cookie
|
|
99
118
|
self._tparam = tparam
|
|
119
|
+
assert ptype in ("json", "data")
|
|
120
|
+
self._ptype = ptype
|
|
100
121
|
|
|
101
122
|
# password and token credentials, cookies
|
|
102
123
|
self._passes: dict[str, str] = {}
|
|
@@ -122,7 +143,7 @@ class Authenticator:
|
|
|
122
143
|
self._set(login, pw, self._passes)
|
|
123
144
|
|
|
124
145
|
def setPasses(self, pws: list[str]):
|
|
125
|
-
"""Associate a list of login:password
|
|
146
|
+
"""Associate a list of *login:password*."""
|
|
126
147
|
for lp in pws:
|
|
127
148
|
login, pw = lp.split(":", 1)
|
|
128
149
|
self.setPass(login, pw)
|
|
@@ -143,7 +164,7 @@ class Authenticator:
|
|
|
143
164
|
self._set(name, val, self._cookies[login])
|
|
144
165
|
|
|
145
166
|
def _param(self, kwargs: dict[str, Any], key: str, val: Any):
|
|
146
|
-
"""Add request parameter to
|
|
167
|
+
"""Add request parameter to ``json`` or ``data``."""
|
|
147
168
|
|
|
148
169
|
if "json" in kwargs:
|
|
149
170
|
assert isinstance(kwargs["json"], dict)
|
|
@@ -152,7 +173,7 @@ class Authenticator:
|
|
|
152
173
|
assert isinstance(kwargs["data"], dict)
|
|
153
174
|
kwargs["data"][key] = val
|
|
154
175
|
else:
|
|
155
|
-
kwargs[
|
|
176
|
+
kwargs[self._ptype] = {key: val}
|
|
156
177
|
|
|
157
178
|
def _try_auth(self, auth: str|None, scheme: str) -> bool:
|
|
158
179
|
"""Whether to try this authentication scheme."""
|
|
@@ -161,10 +182,10 @@ class Authenticator:
|
|
|
161
182
|
def setAuth(self, login: str|None, kwargs: dict[str, Any], cookies: dict[str, str], auth: str|None = None):
|
|
162
183
|
"""Set request authentication.
|
|
163
184
|
|
|
164
|
-
- login
|
|
165
|
-
- kwargs
|
|
166
|
-
- cookies
|
|
167
|
-
- auth
|
|
185
|
+
- ``login``: login target, None means no authentication.
|
|
186
|
+
- ``kwargs``: request parameters.
|
|
187
|
+
- ``cookies``: request cookies.
|
|
188
|
+
- ``auth``: authentication method, default is None.
|
|
168
189
|
|
|
169
190
|
The default behavior is to try allowed schemes: tokens first,
|
|
170
191
|
then password, then fake.
|
|
@@ -234,13 +255,13 @@ class RequestFlaskResponse:
|
|
|
234
255
|
|
|
235
256
|
Available attributes:
|
|
236
257
|
|
|
237
|
-
- ``status_code``: integer status code
|
|
238
|
-
- ``data``: body as bytes
|
|
239
|
-
- ``text``: body as a string
|
|
240
|
-
- ``headers``: dict of headers and their values
|
|
241
|
-
- ``cookies``: dict of cookies
|
|
242
|
-
- ``json``: JSON-converted body, or None
|
|
243
|
-
- ``is_json``: whether body was in JSON
|
|
258
|
+
- ``status_code``: integer status code.
|
|
259
|
+
- ``data``: body as bytes.
|
|
260
|
+
- ``text``: body as a string.
|
|
261
|
+
- ``headers``: dict of headers and their values.
|
|
262
|
+
- ``cookies``: dict of cookies.
|
|
263
|
+
- ``json``: JSON-converted body, or *None*.
|
|
264
|
+
- ``is_json``: whether body was in JSON.
|
|
244
265
|
|
|
245
266
|
Constructor parameter:
|
|
246
267
|
|
|
@@ -268,8 +289,8 @@ class Client:
|
|
|
268
289
|
|
|
269
290
|
Constructor parameters:
|
|
270
291
|
|
|
271
|
-
- ``auth
|
|
272
|
-
- ``default_login
|
|
292
|
+
- ``auth``: authenticator.
|
|
293
|
+
- ``default_login``: if ``login`` is not set.
|
|
273
294
|
"""
|
|
274
295
|
|
|
275
296
|
def __init__(self, auth: Authenticator, default_login: str|None = None):
|
|
@@ -299,16 +320,16 @@ class Client:
|
|
|
299
320
|
|
|
300
321
|
Mandatory parameters:
|
|
301
322
|
|
|
302
|
-
- ``method``: HTTP method ("GET", "POST", "PATCH", "DELETE"…)
|
|
303
|
-
- ``path``: local path under the base URL
|
|
323
|
+
- ``method``: HTTP method ("GET", "POST", "PATCH", "DELETE"…).
|
|
324
|
+
- ``path``: local path under the base URL.
|
|
304
325
|
|
|
305
326
|
Optional parameters:
|
|
306
327
|
|
|
307
|
-
- ``status``: expected HTTP status, *None* to skip status check
|
|
308
|
-
- ``content``: regular expression for response body, *None* to skip content check
|
|
309
|
-
- ``login``: authenticated user, use **explicit** *None* to skip
|
|
310
|
-
- ``auth``: authentication scheme to use instead of default behavior
|
|
311
|
-
- ``**kwargs``: more request parameters (headers, data, json…)
|
|
328
|
+
- ``status``: expected HTTP status, *None* to skip status check.
|
|
329
|
+
- ``content``: regular expression for response body, *None* to skip content check.
|
|
330
|
+
- ``login``: authenticated user, use **explicit** *None* to skip.
|
|
331
|
+
- ``auth``: authentication scheme to use instead of default behavior.
|
|
332
|
+
- ``**kwargs``: more request parameters (headers, data, json…).
|
|
312
333
|
"""
|
|
313
334
|
|
|
314
335
|
if "login" in kwargs:
|
|
@@ -328,43 +349,37 @@ class Client:
|
|
|
328
349
|
# check status
|
|
329
350
|
if status is not None:
|
|
330
351
|
if res.status_code != status: # show error before aborting
|
|
331
|
-
|
|
332
|
-
assert res.status_code == status, f"unexpected status {res.status_code}, expecting {status}"
|
|
352
|
+
_raiseError(f"bad {status} result: {res.status_code} {res.text[:512]}...")
|
|
333
353
|
|
|
334
354
|
# check content
|
|
335
355
|
if content is not None:
|
|
336
356
|
if not re.search(content, res.text, re.DOTALL):
|
|
337
|
-
|
|
338
|
-
assert False, f"expected content {content} not found in {res.text}"
|
|
357
|
+
_raiseError(f"cannot find {content} in {res.text[:512]}...")
|
|
339
358
|
|
|
340
359
|
return res
|
|
341
360
|
|
|
342
|
-
def get(self, path, **kwargs):
|
|
343
|
-
"""HTTP GET request."""
|
|
344
|
-
return self.request("GET", path, **kwargs)
|
|
361
|
+
def get(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
362
|
+
"""HTTP GET request, see ``request`` parameters."""
|
|
363
|
+
return self.request("GET", path, status=status, content=content, **kwargs)
|
|
345
364
|
|
|
346
|
-
def post(self, path, **kwargs):
|
|
347
|
-
"""HTTP POST request."""
|
|
348
|
-
return self.request("POST", path, **kwargs)
|
|
365
|
+
def post(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
366
|
+
"""HTTP POST request, see ``request`` parameters."""
|
|
367
|
+
return self.request("POST", path, status=status, content=content, **kwargs)
|
|
349
368
|
|
|
350
|
-
def put(self, path, **kwargs):
|
|
351
|
-
"""HTTP PUT request."""
|
|
352
|
-
return self.request("PUT", path, **kwargs)
|
|
369
|
+
def put(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
370
|
+
"""HTTP PUT request, see ``request`` parameters."""
|
|
371
|
+
return self.request("PUT", path, status=status, content=content, **kwargs)
|
|
353
372
|
|
|
354
|
-
def patch(self, path, **kwargs):
|
|
355
|
-
"""HTTP PATCH request."""
|
|
356
|
-
return self.request("PATCH", path, **kwargs)
|
|
373
|
+
def patch(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
374
|
+
"""HTTP PATCH request, see ``request`` parameters."""
|
|
375
|
+
return self.request("PATCH", path, status=status, content=content, **kwargs)
|
|
357
376
|
|
|
358
|
-
def delete(self, path, **kwargs):
|
|
359
|
-
"""HTTP DELETE request."""
|
|
360
|
-
return self.request("DELETE", path, **kwargs)
|
|
377
|
+
def delete(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
378
|
+
"""HTTP DELETE request, see ``request`` parameters."""
|
|
379
|
+
return self.request("DELETE", path, status=status, content=content, **kwargs)
|
|
361
380
|
|
|
362
381
|
def check(self, method: str, path: str, status: int, content: str|None = None, **kwargs):
|
|
363
|
-
"""
|
|
364
|
-
|
|
365
|
-
Same as ``request``, but ``status`` is mandatory.
|
|
366
|
-
"""
|
|
367
|
-
|
|
382
|
+
"""Deprecated, use ``request`` or HTTP-method-specific methods."""
|
|
368
383
|
return self.request(method, path, status=status, content=content, **kwargs)
|
|
369
384
|
|
|
370
385
|
|
|
@@ -373,9 +388,9 @@ class RequestClient(Client):
|
|
|
373
388
|
|
|
374
389
|
Constructor parameters:
|
|
375
390
|
|
|
376
|
-
- ``auth
|
|
377
|
-
- ``base_url
|
|
378
|
-
- ``default_login
|
|
391
|
+
- ``auth``: authenticator.
|
|
392
|
+
- ``base_url``: target server.
|
|
393
|
+
- ``default_login``: if ``login`` is not set.
|
|
379
394
|
"""
|
|
380
395
|
|
|
381
396
|
def __init__(self, auth: Authenticator, base_url: str, default_login=None):
|
|
@@ -419,9 +434,9 @@ class FlaskClient(Client):
|
|
|
419
434
|
|
|
420
435
|
Constructor parameters:
|
|
421
436
|
|
|
422
|
-
- ``auth
|
|
423
|
-
- ``client
|
|
424
|
-
- ``default_login
|
|
437
|
+
- ``auth``: authenticator.
|
|
438
|
+
- ``client``: Flask actual ``test_client``.
|
|
439
|
+
- ``default_login``: if ``login`` is not set.
|
|
425
440
|
|
|
426
441
|
Note: this client handles `cookies`.
|
|
427
442
|
"""
|
|
@@ -446,7 +461,7 @@ class FlaskClient(Client):
|
|
|
446
461
|
return self._client.open(method=method, path=path, **kwargs)
|
|
447
462
|
|
|
448
463
|
def _ft_authenticator():
|
|
449
|
-
"""Fixture implementation separated for testing."""
|
|
464
|
+
"""Fixture implementation, separated for testing purposes."""
|
|
450
465
|
|
|
451
466
|
level = os.environ.get("FLASK_TESTER_LOG_LEVEL", "NOTSET")
|
|
452
467
|
log.setLevel(logging.DEBUG if level == "DEBUG" else
|
|
@@ -466,9 +481,11 @@ def _ft_authenticator():
|
|
|
466
481
|
header = os.environ.get("FLASK_TESTER_HEADER", "Auth")
|
|
467
482
|
cookie = os.environ.get("FLASK_TESTER_COOKIE", "auth")
|
|
468
483
|
tparam = os.environ.get("FLASK_TESTER_TPARAM", "AUTH")
|
|
484
|
+
ptype = os.environ.get("FLASK_TESTER_PTYPE", "data")
|
|
469
485
|
|
|
470
486
|
# create authenticator, possibly with initial credentials
|
|
471
|
-
auth = Authenticator(allow, user=user, pwd=pwd, login=login, bearer=bearer,
|
|
487
|
+
auth = Authenticator(allow, user=user, pwd=pwd, login=login, bearer=bearer,
|
|
488
|
+
header=header, cookie=cookie, tparam=tparam, ptype=ptype)
|
|
472
489
|
|
|
473
490
|
if "FLASK_TESTER_AUTH" in os.environ:
|
|
474
491
|
auth.setPasses(os.environ["FLASK_TESTER_AUTH"].split(","))
|
|
@@ -501,6 +518,8 @@ def ft_authenticator():
|
|
|
501
518
|
Default is ``auth``.
|
|
502
519
|
- ``FLASK_TESTER_TPARAM``: parameter for *token* authentication.
|
|
503
520
|
Default is ``AUTH``.
|
|
521
|
+
- ``FLASK_TESTER_PTYPE``: default parameter type, ``data`` or ``json``.
|
|
522
|
+
Default is ``data``.
|
|
504
523
|
- ``FLASK_TESTER_AUTH``: initial comma-separated list of *login:password*.
|
|
505
524
|
Default is not set.
|
|
506
525
|
"""
|
|
@@ -550,7 +569,7 @@ def ft_client(ft_authenticator):
|
|
|
550
569
|
|
|
551
570
|
Target environment variable, one **must** be defined:
|
|
552
571
|
|
|
553
|
-
- ``FLASK_TESTER_URL``: application HTTP base URL
|
|
572
|
+
- ``FLASK_TESTER_URL``: application HTTP base URL, eg ``http://localhost:5000``.
|
|
554
573
|
- ``FLASK_TESTER_APP``: Flask application, eg ``app:create_app``.
|
|
555
574
|
|
|
556
575
|
Other environment variable:
|
FlaskTester-3.5/PKG-INFO
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: FlaskTester
|
|
3
|
+
Version: 3.5
|
|
4
|
+
Summary: Pytest fixtures for Flask internal and external authenticated tests
|
|
5
|
+
Author-email: Fabien Coelho <flask.tester@coelho.net>
|
|
6
|
+
License: CC0
|
|
7
|
+
Project-URL: repository, https://github.com/zx80/flask-tester
|
|
8
|
+
Project-URL: documentation, https://zx80.github.io/flask-tester/
|
|
9
|
+
Project-URL: issues, https://github.com/zx80/flask-tester/issues
|
|
10
|
+
Project-URL: package, https://pypi.org/project/FlaskTester/
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Framework :: Flask
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: requests
|
|
22
|
+
Requires-Dist: flask
|
|
23
|
+
Requires-Dist: pytest
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy; extra == "dev"
|
|
26
|
+
Requires-Dist: pyright; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff; extra == "dev"
|
|
28
|
+
Requires-Dist: coverage; extra == "dev"
|
|
29
|
+
Requires-Dist: pymarkdownlnt!=0.9.5; extra == "dev"
|
|
30
|
+
Requires-Dist: build; extra == "dev"
|
|
31
|
+
Requires-Dist: twine; extra == "dev"
|
|
32
|
+
Requires-Dist: wheel; extra == "dev"
|
|
33
|
+
Requires-Dist: types-flask; extra == "dev"
|
|
34
|
+
Requires-Dist: types-requests; extra == "dev"
|
|
35
|
+
Requires-Dist: FlaskSimpleAuth>=30.0; extra == "dev"
|
|
36
|
+
Requires-Dist: passlib; extra == "dev"
|
|
37
|
+
Provides-Extra: doc
|
|
38
|
+
Requires-Dist: sphinx; extra == "doc"
|
|
39
|
+
Requires-Dist: sphinx_rtd_theme; extra == "doc"
|
|
40
|
+
Requires-Dist: sphinx-autoapi; extra == "doc"
|
|
41
|
+
Requires-Dist: sphinx-lint; extra == "doc"
|
|
42
|
+
Requires-Dist: myst_parser; extra == "doc"
|
|
43
|
+
|
|
44
|
+
# FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
|
|
45
|
+
|
|
46
|
+
This package allows to run authenticated tests against a Flask application,
|
|
47
|
+
either with internal Flask tests (aka `test_client`) or external tests (with
|
|
48
|
+
`requests` which performs actual HTTP requests), including password and token
|
|
49
|
+
authentication and per-user cookies.
|
|
50
|
+
|
|
51
|
+
Only one set of tests is needed, switching from internal to external is
|
|
52
|
+
achieved through environment variables.
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+

|
|
56
|
+

|
|
57
|
+

|
|
58
|
+

|
|
59
|
+

|
|
60
|
+

|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
Install package with `pip install FlaskTester` or equivalent.
|
|
66
|
+
|
|
67
|
+
The following test creates a local fixture with 2 users identified by a
|
|
68
|
+
password, and retrieves tokens for both users using a `/login` route
|
|
69
|
+
provided by the application.
|
|
70
|
+
It then proceeds to run authenticated requests against the `/admin` route.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import pytest
|
|
74
|
+
from FlaskTester import ft_authenticator, ft_client
|
|
75
|
+
import secret
|
|
76
|
+
|
|
77
|
+
@pytest.fixture
|
|
78
|
+
def app(ft_client):
|
|
79
|
+
# add test passwords for Calvin and Hobbes (must be consistent with app!)
|
|
80
|
+
ft_client.setPass("calvin", secret.PASSES["calvin"])
|
|
81
|
+
ft_client.setPass("hobbes", secret.PASSES["hobbes"])
|
|
82
|
+
# get user tokens, assume json result {"token": "<token-value>"}
|
|
83
|
+
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
84
|
+
assert res.is_json
|
|
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"])
|
|
89
|
+
# also set a cookie
|
|
90
|
+
ft_client.setCookie("hobbes", "lang", "fr")
|
|
91
|
+
ft_client.setCookie("calvin", "lang", "en")
|
|
92
|
+
# return working client
|
|
93
|
+
yield ft_client
|
|
94
|
+
|
|
95
|
+
def test_app_admin(app):
|
|
96
|
+
app.get("/admin", login=None, status=401)
|
|
97
|
+
for auth in ["bearer", "basic", "param"]:
|
|
98
|
+
res = app.get("/admin", login="calvin", auth=auth, status=200)
|
|
99
|
+
assert res.json["user"] == "calvin" and res.json["isadmin"]
|
|
100
|
+
res = app.get("/admin", login="hobbes", auth=auth, status=403)
|
|
101
|
+
assert 'not in group "ADMIN"' in res.text
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This can be run against a (local) server:
|
|
105
|
+
|
|
106
|
+
```shell
|
|
107
|
+
export TEST_SEED="some-random-data" # shared test seed
|
|
108
|
+
flask --app app:app run & # start flask app
|
|
109
|
+
pid=$! # keep pid
|
|
110
|
+
export FLASK_TESTER_URL="http://localhost:5000" # set app local url
|
|
111
|
+
pytest test.py # run external tests
|
|
112
|
+
kill $pid # stop app with pid
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Or locally with the Flask internal test infrastructure:
|
|
116
|
+
|
|
117
|
+
```shell
|
|
118
|
+
export FLASK_TESTER_APP="app:app" # set app module
|
|
119
|
+
pytest test.py # run internal tests
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The above test runs with [`tests/app.py`](tests/app.py)
|
|
123
|
+
[Flask](https://flask.palletsprojects.com/)
|
|
124
|
+
REST application back-end with password and token authentication based on
|
|
125
|
+
[FlaskSimpleAuth](https://pypi.org/project/FlaskSimpleAuth/).
|
|
126
|
+
The code uses _25_ lines of Python for implementing
|
|
127
|
+
password (basic and parameters) and token authentications,
|
|
128
|
+
admin group authorization, and routes for
|
|
129
|
+
token generation (2), identity tests (2) and an incredible open cookie-based
|
|
130
|
+
translation service.
|
|
131
|
+
|
|
132
|
+
See the [documentation](https://zx80.github.io/flask-tester/).
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
|
|
137
|
+
|
|
138
|
+
All software has bug, this is software, hence… Beware that you may lose your
|
|
139
|
+
hairs or your friends because of it. If you like it, feel free to send a
|
|
140
|
+
postcard to the author.
|
|
141
|
+
|
|
142
|
+
## Versions
|
|
143
|
+
|
|
144
|
+
Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
|
|
145
|
+
[sources](https://github.com/zx80/flask-tester) are available on GitHub,
|
|
146
|
+
please report any [issues](https://github.com/zx80/flask-tester/issues).
|