FlaskTester 3.2__tar.gz → 3.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-3.4/FlaskTester.egg-info/PKG-INFO +146 -0
- {FlaskTester-3.2 → FlaskTester-3.4}/FlaskTester.egg-info/requires.txt +1 -1
- {FlaskTester-3.2 → FlaskTester-3.4}/FlaskTester.py +81 -73
- FlaskTester-3.4/PKG-INFO +146 -0
- FlaskTester-3.4/README.md +103 -0
- {FlaskTester-3.2 → FlaskTester-3.4}/pyproject.toml +2 -2
- {FlaskTester-3.2 → FlaskTester-3.4}/tests/test.py +27 -23
- FlaskTester-3.2/FlaskTester.egg-info/PKG-INFO +0 -311
- FlaskTester-3.2/PKG-INFO +0 -311
- FlaskTester-3.2/README.md +0 -268
- {FlaskTester-3.2 → FlaskTester-3.4}/FlaskTester.egg-info/SOURCES.txt +0 -0
- {FlaskTester-3.2 → FlaskTester-3.4}/FlaskTester.egg-info/dependency_links.txt +0 -0
- {FlaskTester-3.2 → FlaskTester-3.4}/FlaskTester.egg-info/top_level.txt +0 -0
- {FlaskTester-3.2 → FlaskTester-3.4}/LICENSE +0 -0
- {FlaskTester-3.2 → FlaskTester-3.4}/MANIFEST.in +0 -0
- {FlaskTester-3.2 → FlaskTester-3.4}/setup.cfg +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: FlaskTester
|
|
3
|
+
Version: 3.4
|
|
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,7 @@
|
|
|
1
1
|
"""FlaskTester - Pytest fixtures for Flask authenticated internal and external tests.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is needed when testing this module, but may not be desirable when it is
|
|
4
|
+
used depending on the ``assert``: PYTEST_DONT_REWRITE
|
|
4
5
|
"""
|
|
5
6
|
|
|
6
7
|
import os
|
|
@@ -25,37 +26,41 @@ class AuthError(FlaskTesterError):
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class Authenticator:
|
|
28
|
-
"""Manage
|
|
29
|
+
"""Manage HTTP request authentication.
|
|
29
30
|
|
|
30
31
|
Supported schemes:
|
|
31
32
|
|
|
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
|
|
33
|
+
- ``basic``: HTTP Basic Authentication.
|
|
34
|
+
- ``param``: password with HTTP or JSON parameters.
|
|
35
|
+
- ``bearer``: token in ``Authorization`` *bearer* header.
|
|
36
|
+
- ``header``: token in a header.
|
|
37
|
+
- ``cookie``: token in a cookie.
|
|
38
|
+
- ``tparam``: token in a parameter.
|
|
39
|
+
- ``fake``: fake scheme, login directly passed as a parameter.
|
|
40
|
+
- ``none``: no authentication, only cookies.
|
|
40
41
|
|
|
41
42
|
Constructor parameters:
|
|
42
43
|
|
|
43
44
|
- ``allow``: list of allowed schemes.
|
|
44
|
-
default is ``["bearer", "basic", "param", "none"]
|
|
45
|
+
default is ``["bearer", "basic", "param", "none"]``.
|
|
45
46
|
- ``user``: parameter for user on ``param`` password authentication,
|
|
46
|
-
default is ``USER
|
|
47
|
+
default is ``USER``.
|
|
47
48
|
- ``pwd``: parameter for password on ``param`` password authentication,
|
|
48
|
-
default is ``PASS
|
|
49
|
+
default is ``PASS``.
|
|
49
50
|
- ``login``: parameter for user on ``fake`` authentication,
|
|
50
|
-
default is ``LOGIN
|
|
51
|
+
default is ``LOGIN``.
|
|
51
52
|
- ``bearer``: name of bearer scheme for token,
|
|
52
|
-
default is ``Bearer
|
|
53
|
+
default is ``Bearer``.
|
|
53
54
|
- ``header``: name of header for token,
|
|
54
|
-
default is ``Auth
|
|
55
|
+
default is ``Auth``.
|
|
55
56
|
- ``cookie``: name of cookie for token,
|
|
56
|
-
default is ``auth
|
|
57
|
+
default is ``auth``.
|
|
57
58
|
- ``tparam``: name of parameter for token,
|
|
58
|
-
default is ``AUTH
|
|
59
|
+
default is ``AUTH``.
|
|
60
|
+
- ``ptype``: default parameter type, either ``data`` or ``json``,
|
|
61
|
+
default is ``data``.
|
|
62
|
+
|
|
63
|
+
Note: default values are consistent with `FlaskSimpleAuth <https://pypi.org/project/FlaskSimpleAuth/>`_.
|
|
59
64
|
"""
|
|
60
65
|
|
|
61
66
|
_TOKEN_SCHEMES = {"bearer", "header", "cookie", "tparam"}
|
|
@@ -78,6 +83,7 @@ class Authenticator:
|
|
|
78
83
|
header: str = "Auth",
|
|
79
84
|
cookie: str = "auth",
|
|
80
85
|
tparam: str = "AUTH",
|
|
86
|
+
ptype: str = "data",
|
|
81
87
|
):
|
|
82
88
|
|
|
83
89
|
self._has_pass, self._has_token = False, False
|
|
@@ -97,6 +103,8 @@ class Authenticator:
|
|
|
97
103
|
self._header = header
|
|
98
104
|
self._cookie = cookie
|
|
99
105
|
self._tparam = tparam
|
|
106
|
+
assert ptype in ("json", "data")
|
|
107
|
+
self._ptype = ptype
|
|
100
108
|
|
|
101
109
|
# password and token credentials, cookies
|
|
102
110
|
self._passes: dict[str, str] = {}
|
|
@@ -122,7 +130,7 @@ class Authenticator:
|
|
|
122
130
|
self._set(login, pw, self._passes)
|
|
123
131
|
|
|
124
132
|
def setPasses(self, pws: list[str]):
|
|
125
|
-
"""Associate a list of login:password
|
|
133
|
+
"""Associate a list of *login:password*."""
|
|
126
134
|
for lp in pws:
|
|
127
135
|
login, pw = lp.split(":", 1)
|
|
128
136
|
self.setPass(login, pw)
|
|
@@ -143,7 +151,7 @@ class Authenticator:
|
|
|
143
151
|
self._set(name, val, self._cookies[login])
|
|
144
152
|
|
|
145
153
|
def _param(self, kwargs: dict[str, Any], key: str, val: Any):
|
|
146
|
-
"""Add request parameter to
|
|
154
|
+
"""Add request parameter to ``json`` or ``data``."""
|
|
147
155
|
|
|
148
156
|
if "json" in kwargs:
|
|
149
157
|
assert isinstance(kwargs["json"], dict)
|
|
@@ -152,7 +160,7 @@ class Authenticator:
|
|
|
152
160
|
assert isinstance(kwargs["data"], dict)
|
|
153
161
|
kwargs["data"][key] = val
|
|
154
162
|
else:
|
|
155
|
-
kwargs[
|
|
163
|
+
kwargs[self._ptype] = {key: val}
|
|
156
164
|
|
|
157
165
|
def _try_auth(self, auth: str|None, scheme: str) -> bool:
|
|
158
166
|
"""Whether to try this authentication scheme."""
|
|
@@ -161,10 +169,10 @@ class Authenticator:
|
|
|
161
169
|
def setAuth(self, login: str|None, kwargs: dict[str, Any], cookies: dict[str, str], auth: str|None = None):
|
|
162
170
|
"""Set request authentication.
|
|
163
171
|
|
|
164
|
-
- login
|
|
165
|
-
- kwargs
|
|
166
|
-
- cookies
|
|
167
|
-
- auth
|
|
172
|
+
- ``login``: login target, None means no authentication.
|
|
173
|
+
- ``kwargs``: request parameters.
|
|
174
|
+
- ``cookies``: request cookies.
|
|
175
|
+
- ``auth``: authentication method, default is None.
|
|
168
176
|
|
|
169
177
|
The default behavior is to try allowed schemes: tokens first,
|
|
170
178
|
then password, then fake.
|
|
@@ -234,13 +242,13 @@ class RequestFlaskResponse:
|
|
|
234
242
|
|
|
235
243
|
Available attributes:
|
|
236
244
|
|
|
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
|
|
245
|
+
- ``status_code``: integer status code.
|
|
246
|
+
- ``data``: body as bytes.
|
|
247
|
+
- ``text``: body as a string.
|
|
248
|
+
- ``headers``: dict of headers and their values.
|
|
249
|
+
- ``cookies``: dict of cookies.
|
|
250
|
+
- ``json``: JSON-converted body, or *None*.
|
|
251
|
+
- ``is_json``: whether body was in JSON.
|
|
244
252
|
|
|
245
253
|
Constructor parameter:
|
|
246
254
|
|
|
@@ -268,8 +276,8 @@ class Client:
|
|
|
268
276
|
|
|
269
277
|
Constructor parameters:
|
|
270
278
|
|
|
271
|
-
- ``auth
|
|
272
|
-
- ``default_login
|
|
279
|
+
- ``auth``: authenticator.
|
|
280
|
+
- ``default_login``: if ``login`` is not set.
|
|
273
281
|
"""
|
|
274
282
|
|
|
275
283
|
def __init__(self, auth: Authenticator, default_login: str|None = None):
|
|
@@ -299,16 +307,16 @@ class Client:
|
|
|
299
307
|
|
|
300
308
|
Mandatory parameters:
|
|
301
309
|
|
|
302
|
-
- ``method``: HTTP method ("GET", "POST", "PATCH", "DELETE"…)
|
|
303
|
-
- ``path``: local path under the base URL
|
|
310
|
+
- ``method``: HTTP method ("GET", "POST", "PATCH", "DELETE"…).
|
|
311
|
+
- ``path``: local path under the base URL.
|
|
304
312
|
|
|
305
313
|
Optional parameters:
|
|
306
314
|
|
|
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…)
|
|
315
|
+
- ``status``: expected HTTP status, *None* to skip status check.
|
|
316
|
+
- ``content``: regular expression for response body, *None* to skip content check.
|
|
317
|
+
- ``login``: authenticated user, use **explicit** *None* to skip.
|
|
318
|
+
- ``auth``: authentication scheme to use instead of default behavior.
|
|
319
|
+
- ``**kwargs``: more request parameters (headers, data, json…).
|
|
312
320
|
"""
|
|
313
321
|
|
|
314
322
|
if "login" in kwargs:
|
|
@@ -329,7 +337,7 @@ class Client:
|
|
|
329
337
|
if status is not None:
|
|
330
338
|
if res.status_code != status: # show error before aborting
|
|
331
339
|
log.error(f"bad {status} result: {res.status_code} {res.text[:512]}")
|
|
332
|
-
|
|
340
|
+
assert res.status_code == status, f"unexpected status {res.status_code}, expecting {status}"
|
|
333
341
|
|
|
334
342
|
# check content
|
|
335
343
|
if content is not None:
|
|
@@ -339,33 +347,29 @@ class Client:
|
|
|
339
347
|
|
|
340
348
|
return res
|
|
341
349
|
|
|
342
|
-
def get(self, path, **kwargs):
|
|
343
|
-
"""HTTP GET request."""
|
|
344
|
-
return self.request("GET", path, **kwargs)
|
|
345
|
-
|
|
346
|
-
def post(self, path, **kwargs):
|
|
347
|
-
"""HTTP POST request."""
|
|
348
|
-
return self.request("POST", path, **kwargs)
|
|
350
|
+
def get(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
351
|
+
"""HTTP GET request, see ``request`` parameters."""
|
|
352
|
+
return self.request("GET", path, status=status, content=content, **kwargs)
|
|
349
353
|
|
|
350
|
-
def
|
|
351
|
-
"""HTTP
|
|
352
|
-
return self.request("
|
|
354
|
+
def post(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
355
|
+
"""HTTP POST request, see ``request`` parameters."""
|
|
356
|
+
return self.request("POST", path, status=status, content=content, **kwargs)
|
|
353
357
|
|
|
354
|
-
def
|
|
355
|
-
"""HTTP
|
|
356
|
-
return self.request("
|
|
358
|
+
def put(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
359
|
+
"""HTTP PUT request, see ``request`` parameters."""
|
|
360
|
+
return self.request("PUT", path, status=status, content=content, **kwargs)
|
|
357
361
|
|
|
358
|
-
def
|
|
359
|
-
"""HTTP
|
|
360
|
-
return self.request("
|
|
362
|
+
def patch(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
363
|
+
"""HTTP PATCH request, see ``request`` parameters."""
|
|
364
|
+
return self.request("PATCH", path, status=status, content=content, **kwargs)
|
|
361
365
|
|
|
362
|
-
def
|
|
363
|
-
"""
|
|
366
|
+
def delete(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
367
|
+
"""HTTP DELETE request, see ``request`` parameters."""
|
|
368
|
+
return self.request("DELETE", path, status=status, content=content, **kwargs)
|
|
364
369
|
|
|
365
|
-
|
|
366
|
-
"""
|
|
367
|
-
|
|
368
|
-
return self.request(method, path, status=status, **kwargs)
|
|
370
|
+
def check(self, method: str, path: str, status: int, content: str|None = None, **kwargs):
|
|
371
|
+
"""Deprecated, use ``request`` or HTTP-method-specific methods."""
|
|
372
|
+
return self.request(method, path, status=status, content=content, **kwargs)
|
|
369
373
|
|
|
370
374
|
|
|
371
375
|
class RequestClient(Client):
|
|
@@ -373,9 +377,9 @@ class RequestClient(Client):
|
|
|
373
377
|
|
|
374
378
|
Constructor parameters:
|
|
375
379
|
|
|
376
|
-
- ``auth
|
|
377
|
-
- ``base_url
|
|
378
|
-
- ``default_login
|
|
380
|
+
- ``auth``: authenticator.
|
|
381
|
+
- ``base_url``: target server.
|
|
382
|
+
- ``default_login``: if ``login`` is not set.
|
|
379
383
|
"""
|
|
380
384
|
|
|
381
385
|
def __init__(self, auth: Authenticator, base_url: str, default_login=None):
|
|
@@ -419,9 +423,9 @@ class FlaskClient(Client):
|
|
|
419
423
|
|
|
420
424
|
Constructor parameters:
|
|
421
425
|
|
|
422
|
-
- ``auth
|
|
423
|
-
- ``client
|
|
424
|
-
- ``default_login
|
|
426
|
+
- ``auth``: authenticator.
|
|
427
|
+
- ``client``: Flask actual ``test_client``.
|
|
428
|
+
- ``default_login``: if ``login`` is not set.
|
|
425
429
|
|
|
426
430
|
Note: this client handles `cookies`.
|
|
427
431
|
"""
|
|
@@ -446,7 +450,7 @@ class FlaskClient(Client):
|
|
|
446
450
|
return self._client.open(method=method, path=path, **kwargs)
|
|
447
451
|
|
|
448
452
|
def _ft_authenticator():
|
|
449
|
-
"""Fixture implementation separated for testing."""
|
|
453
|
+
"""Fixture implementation, separated for testing purposes."""
|
|
450
454
|
|
|
451
455
|
level = os.environ.get("FLASK_TESTER_LOG_LEVEL", "NOTSET")
|
|
452
456
|
log.setLevel(logging.DEBUG if level == "DEBUG" else
|
|
@@ -466,9 +470,11 @@ def _ft_authenticator():
|
|
|
466
470
|
header = os.environ.get("FLASK_TESTER_HEADER", "Auth")
|
|
467
471
|
cookie = os.environ.get("FLASK_TESTER_COOKIE", "auth")
|
|
468
472
|
tparam = os.environ.get("FLASK_TESTER_TPARAM", "AUTH")
|
|
473
|
+
ptype = os.environ.get("FLASK_TESTER_PTYPE", "data")
|
|
469
474
|
|
|
470
475
|
# create authenticator, possibly with initial credentials
|
|
471
|
-
auth = Authenticator(allow, user=user, pwd=pwd, login=login, bearer=bearer,
|
|
476
|
+
auth = Authenticator(allow, user=user, pwd=pwd, login=login, bearer=bearer,
|
|
477
|
+
header=header, cookie=cookie, tparam=tparam, ptype=ptype)
|
|
472
478
|
|
|
473
479
|
if "FLASK_TESTER_AUTH" in os.environ:
|
|
474
480
|
auth.setPasses(os.environ["FLASK_TESTER_AUTH"].split(","))
|
|
@@ -501,6 +507,8 @@ def ft_authenticator():
|
|
|
501
507
|
Default is ``auth``.
|
|
502
508
|
- ``FLASK_TESTER_TPARAM``: parameter for *token* authentication.
|
|
503
509
|
Default is ``AUTH``.
|
|
510
|
+
- ``FLASK_TESTER_PTYPE``: default parameter type, ``data`` or ``json``.
|
|
511
|
+
Default is ``data``.
|
|
504
512
|
- ``FLASK_TESTER_AUTH``: initial comma-separated list of *login:password*.
|
|
505
513
|
Default is not set.
|
|
506
514
|
"""
|
|
@@ -550,7 +558,7 @@ def ft_client(ft_authenticator):
|
|
|
550
558
|
|
|
551
559
|
Target environment variable, one **must** be defined:
|
|
552
560
|
|
|
553
|
-
- ``FLASK_TESTER_URL``: application HTTP base URL
|
|
561
|
+
- ``FLASK_TESTER_URL``: application HTTP base URL, eg ``http://localhost:5000``.
|
|
554
562
|
- ``FLASK_TESTER_APP``: Flask application, eg ``app:create_app``.
|
|
555
563
|
|
|
556
564
|
Other environment variable:
|
FlaskTester-3.4/PKG-INFO
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: FlaskTester
|
|
3
|
+
Version: 3.4
|
|
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).
|