FlaskTester 4.2__tar.gz → 5.0__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-4.2 → flasktester-5.0}/FlaskTester.egg-info/PKG-INFO +20 -18
- {flasktester-4.2 → flasktester-5.0}/FlaskTester.egg-info/requires.txt +7 -6
- {flasktester-4.2 → flasktester-5.0}/FlaskTester.py +31 -7
- {flasktester-4.2 → flasktester-5.0}/PKG-INFO +20 -18
- {flasktester-4.2 → flasktester-5.0}/README.md +12 -10
- {flasktester-4.2 → flasktester-5.0}/pyproject.toml +3 -5
- {flasktester-4.2 → flasktester-5.0}/tests/test_app.py +28 -21
- {flasktester-4.2 → flasktester-5.0}/tests/test_app2.py +9 -0
- {flasktester-4.2 → flasktester-5.0}/FlaskTester.egg-info/SOURCES.txt +0 -0
- {flasktester-4.2 → flasktester-5.0}/FlaskTester.egg-info/dependency_links.txt +0 -0
- {flasktester-4.2 → flasktester-5.0}/FlaskTester.egg-info/top_level.txt +0 -0
- {flasktester-4.2 → flasktester-5.0}/LICENSE +0 -0
- {flasktester-4.2 → flasktester-5.0}/MANIFEST.in +0 -0
- {flasktester-4.2 → flasktester-5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: FlaskTester
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0
|
|
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
|
|
@@ -19,7 +19,6 @@ Requires-Python: >=3.10
|
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Requires-Dist: requests
|
|
22
|
-
Requires-Dist: flask
|
|
23
22
|
Requires-Dist: pytest
|
|
24
23
|
Provides-Extra: dev
|
|
25
24
|
Requires-Dist: mypy; extra == "dev"
|
|
@@ -27,13 +26,10 @@ Requires-Dist: pyright; extra == "dev"
|
|
|
27
26
|
Requires-Dist: ruff; extra == "dev"
|
|
28
27
|
Requires-Dist: coverage; extra == "dev"
|
|
29
28
|
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
29
|
Requires-Dist: types-flask; extra == "dev"
|
|
34
30
|
Requires-Dist: types-requests; extra == "dev"
|
|
35
|
-
Requires-Dist: FlaskSimpleAuth>=
|
|
36
|
-
Requires-Dist:
|
|
31
|
+
Requires-Dist: FlaskSimpleAuth>=35.0; extra == "dev"
|
|
32
|
+
Requires-Dist: bcrypt; extra == "dev"
|
|
37
33
|
Requires-Dist: pydantic; extra == "dev"
|
|
38
34
|
Provides-Extra: doc
|
|
39
35
|
Requires-Dist: sphinx; extra == "doc"
|
|
@@ -41,6 +37,10 @@ Requires-Dist: sphinx_rtd_theme; extra == "doc"
|
|
|
41
37
|
Requires-Dist: sphinx-autoapi; extra == "doc"
|
|
42
38
|
Requires-Dist: sphinx-lint; extra == "doc"
|
|
43
39
|
Requires-Dist: myst_parser; extra == "doc"
|
|
40
|
+
Provides-Extra: pub
|
|
41
|
+
Requires-Dist: build; extra == "pub"
|
|
42
|
+
Requires-Dist: twine; extra == "pub"
|
|
43
|
+
Requires-Dist: wheel; extra == "pub"
|
|
44
44
|
|
|
45
45
|
# FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
|
|
46
46
|
|
|
@@ -53,7 +53,7 @@ Only one set of tests is needed, switching from internal to external is
|
|
|
53
53
|
achieved by setting an environment variable.
|
|
54
54
|
|
|
55
55
|

|
|
56
|
-

|
|
57
57
|

|
|
58
58
|

|
|
59
59
|

|
|
@@ -75,18 +75,20 @@ import pytest
|
|
|
75
75
|
from FlaskTester import ft_authenticator, ft_client
|
|
76
76
|
import secret
|
|
77
77
|
|
|
78
|
+
def authHook(api, user: str, pwd: str|None):
|
|
79
|
+
if pwd is not None: # get a token when a login/password is provided
|
|
80
|
+
res = api.get("/login", login=user, auth="basic", status=200)
|
|
81
|
+
api.setToken(user, res.json["token"])
|
|
82
|
+
else: # remove token
|
|
83
|
+
api.setToken(user, None)
|
|
84
|
+
|
|
78
85
|
@pytest.fixture
|
|
79
86
|
def app(ft_client):
|
|
87
|
+
# register authentication hook
|
|
88
|
+
ft_client.setHook(authHook)
|
|
80
89
|
# add test passwords for Calvin and Hobbes (must be consistent with app!)
|
|
81
90
|
ft_client.setPass("calvin", secret.PASSES["calvin"])
|
|
82
91
|
ft_client.setPass("hobbes", secret.PASSES["hobbes"])
|
|
83
|
-
# get user tokens, assume json result {"token": "<token-value>"}
|
|
84
|
-
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
85
|
-
assert res.is_json
|
|
86
|
-
ft_client.setToken("calvin", res.json["token"])
|
|
87
|
-
res = ft_client.post("/login", login="hobbes", auth="param", status=201)
|
|
88
|
-
assert res.is_json
|
|
89
|
-
ft_client.setToken("hobbes", res.json["token"])
|
|
90
92
|
# also set a cookie
|
|
91
93
|
ft_client.setCookie("hobbes", "lang", "fr")
|
|
92
94
|
ft_client.setCookie("calvin", "lang", "en")
|
|
@@ -102,13 +104,13 @@ def test_app_admin(app):
|
|
|
102
104
|
assert 'not in group "ADMIN"' in res.text
|
|
103
105
|
```
|
|
104
106
|
|
|
105
|
-
This can be run against a
|
|
107
|
+
This can be run against a local or remote server:
|
|
106
108
|
|
|
107
109
|
```shell
|
|
108
110
|
export TEST_SEED="some-random-data" # shared test seed
|
|
109
111
|
flask --app app:app run & # start flask app
|
|
110
112
|
pid=$! # keep pid
|
|
111
|
-
export FLASK_TESTER_APP="http://localhost:5000" # set app local
|
|
113
|
+
export FLASK_TESTER_APP="http://localhost:5000" # set app url, local or remote
|
|
112
114
|
pytest test.py # run external tests
|
|
113
115
|
kill $pid # stop app with pid
|
|
114
116
|
```
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
requests
|
|
2
|
-
flask
|
|
3
2
|
pytest
|
|
4
3
|
|
|
5
4
|
[dev]
|
|
@@ -8,13 +7,10 @@ pyright
|
|
|
8
7
|
ruff
|
|
9
8
|
coverage
|
|
10
9
|
pymarkdownlnt!=0.9.5
|
|
11
|
-
build
|
|
12
|
-
twine
|
|
13
|
-
wheel
|
|
14
10
|
types-flask
|
|
15
11
|
types-requests
|
|
16
|
-
FlaskSimpleAuth>=
|
|
17
|
-
|
|
12
|
+
FlaskSimpleAuth>=35.0
|
|
13
|
+
bcrypt
|
|
18
14
|
pydantic
|
|
19
15
|
|
|
20
16
|
[doc]
|
|
@@ -23,3 +19,8 @@ sphinx_rtd_theme
|
|
|
23
19
|
sphinx-autoapi
|
|
24
20
|
sphinx-lint
|
|
25
21
|
myst_parser
|
|
22
|
+
|
|
23
|
+
[pub]
|
|
24
|
+
build
|
|
25
|
+
twine
|
|
26
|
+
wheel
|
|
@@ -6,7 +6,8 @@ PYTEST_DONT_REWRITE: local assertions are really that, not pytest assertions.
|
|
|
6
6
|
import os
|
|
7
7
|
import io
|
|
8
8
|
import re
|
|
9
|
-
|
|
9
|
+
# Python >= 3.11: Self
|
|
10
|
+
from typing import Any, Callable
|
|
10
11
|
import importlib
|
|
11
12
|
import logging
|
|
12
13
|
import pytest # for explicit fail calls, see _pytestFail
|
|
@@ -89,6 +90,9 @@ class Authenticator:
|
|
|
89
90
|
_AUTH_SCHEMES.update(_TOKEN_SCHEMES)
|
|
90
91
|
_AUTH_SCHEMES.update(_PASS_SCHEMES)
|
|
91
92
|
|
|
93
|
+
# authenticator login/pass hook
|
|
94
|
+
_AuthHook = Callable[[str, str|None], None]
|
|
95
|
+
|
|
92
96
|
def __init__(self,
|
|
93
97
|
allow: list[str] = ["bearer", "basic", "param", "none"],
|
|
94
98
|
# parameter names for "basic" and "param"
|
|
@@ -124,6 +128,9 @@ class Authenticator:
|
|
|
124
128
|
assert ptype in ("json", "data")
|
|
125
129
|
self._ptype = ptype
|
|
126
130
|
|
|
131
|
+
# _AuthHook|None, but python cannot stand it:-(
|
|
132
|
+
self._auth_hook: Any = None
|
|
133
|
+
|
|
127
134
|
# password and token credentials, cookies
|
|
128
135
|
self._passes: dict[str, str] = {}
|
|
129
136
|
self._tokens: dict[str, str] = {}
|
|
@@ -138,6 +145,10 @@ class Authenticator:
|
|
|
138
145
|
assert isinstance(val, str)
|
|
139
146
|
store[key] = val
|
|
140
147
|
|
|
148
|
+
def setHook(self, hook: _AuthHook):
|
|
149
|
+
"""Set on password hook."""
|
|
150
|
+
self._auth_hook = hook
|
|
151
|
+
|
|
141
152
|
def setPass(self, login: str, pw: str|None):
|
|
142
153
|
"""Associate a password to a user.
|
|
143
154
|
|
|
@@ -146,6 +157,7 @@ class Authenticator:
|
|
|
146
157
|
if not self._has_pass:
|
|
147
158
|
raise AuthError("cannot set password, no password scheme allowed")
|
|
148
159
|
self._set(login, pw, self._passes)
|
|
160
|
+
_ = self._auth_hook and self._auth_hook(login, pw)
|
|
149
161
|
|
|
150
162
|
def setPasses(self, pws: list[str]):
|
|
151
163
|
"""Associate a list of *login:password*."""
|
|
@@ -298,11 +310,19 @@ class Client:
|
|
|
298
310
|
:param default_login: When ``login`` is not set.
|
|
299
311
|
"""
|
|
300
312
|
|
|
313
|
+
# client login/pass hook (with mypy workaround)
|
|
314
|
+
# Python >= 3.11: Self
|
|
315
|
+
AuthHook = Callable[[Any, str, str|None], None] # type: ignore
|
|
316
|
+
|
|
301
317
|
def __init__(self, auth: Authenticator, default_login: str|None = None):
|
|
302
318
|
self._auth = auth
|
|
303
319
|
self._cookies: dict[str, dict[str, str]] = {} # login -> name -> value
|
|
304
320
|
self._default_login = default_login
|
|
305
321
|
|
|
322
|
+
def setHook(self, hook: AuthHook):
|
|
323
|
+
"""Set on password hook."""
|
|
324
|
+
self._auth.setHook(lambda u, p: hook(self, u, p))
|
|
325
|
+
|
|
306
326
|
def setToken(self, login: str, token: str|None):
|
|
307
327
|
"""Associate a token to a login, *None* to remove."""
|
|
308
328
|
self._auth.setToken(login, token)
|
|
@@ -348,11 +368,13 @@ class Client:
|
|
|
348
368
|
cookies.update(kwargs["cookies"])
|
|
349
369
|
del kwargs["cookies"]
|
|
350
370
|
|
|
351
|
-
#
|
|
352
|
-
|
|
353
|
-
|
|
371
|
+
# this is forbidden by Flask client
|
|
372
|
+
if "json" in kwargs and "data" in kwargs:
|
|
373
|
+
# merge into data to possibly keep uploads
|
|
374
|
+
kwargs["data"].update(kwargs["json"])
|
|
375
|
+
del kwargs["json"]
|
|
354
376
|
|
|
355
|
-
# convert json parameters
|
|
377
|
+
# convert json parameters to json
|
|
356
378
|
if "json" in kwargs:
|
|
357
379
|
json_param = kwargs["json"]
|
|
358
380
|
assert isinstance(json_param, dict)
|
|
@@ -368,7 +390,7 @@ class Client:
|
|
|
368
390
|
else: # pydantic or standard dataclasses?
|
|
369
391
|
json_param[name] = dataclasses.asdict(val)
|
|
370
392
|
|
|
371
|
-
# convert data parameters
|
|
393
|
+
# convert data parameters to simple strings
|
|
372
394
|
if "data" in kwargs:
|
|
373
395
|
data_param = kwargs["data"]
|
|
374
396
|
assert isinstance(data_param, dict)
|
|
@@ -388,6 +410,7 @@ class Client:
|
|
|
388
410
|
else:
|
|
389
411
|
data_param[name] = json.dumps(dataclasses.asdict(val))
|
|
390
412
|
|
|
413
|
+
# now set authentication headers and do the query
|
|
391
414
|
self._auth.setAuth(login, kwargs, cookies, auth=auth)
|
|
392
415
|
res = self._request(method, path, cookies, **kwargs) # type: ignore
|
|
393
416
|
|
|
@@ -530,6 +553,7 @@ def _ft_authenticator():
|
|
|
530
553
|
auth = Authenticator(allow, user=user, pwd=pwd, login=login, bearer=bearer,
|
|
531
554
|
header=header, cookie=cookie, tparam=tparam, ptype=ptype)
|
|
532
555
|
|
|
556
|
+
# possibly load credentials from the environment
|
|
533
557
|
if "FLASK_TESTER_AUTH" in os.environ:
|
|
534
558
|
auth.setPasses(os.environ["FLASK_TESTER_AUTH"].split(","))
|
|
535
559
|
|
|
@@ -599,7 +623,7 @@ def _ft_client(authenticator):
|
|
|
599
623
|
# none found
|
|
600
624
|
if not app:
|
|
601
625
|
raise FlaskTesterError(f"cannot find Flask app in {pkg_name} ({test_app})")
|
|
602
|
-
client = FlaskClient(authenticator, app.test_client(), default_login)
|
|
626
|
+
client = FlaskClient(authenticator, app.test_client(), default_login) # type: ignore
|
|
603
627
|
|
|
604
628
|
return client
|
|
605
629
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: FlaskTester
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0
|
|
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
|
|
@@ -19,7 +19,6 @@ Requires-Python: >=3.10
|
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Requires-Dist: requests
|
|
22
|
-
Requires-Dist: flask
|
|
23
22
|
Requires-Dist: pytest
|
|
24
23
|
Provides-Extra: dev
|
|
25
24
|
Requires-Dist: mypy; extra == "dev"
|
|
@@ -27,13 +26,10 @@ Requires-Dist: pyright; extra == "dev"
|
|
|
27
26
|
Requires-Dist: ruff; extra == "dev"
|
|
28
27
|
Requires-Dist: coverage; extra == "dev"
|
|
29
28
|
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
29
|
Requires-Dist: types-flask; extra == "dev"
|
|
34
30
|
Requires-Dist: types-requests; extra == "dev"
|
|
35
|
-
Requires-Dist: FlaskSimpleAuth>=
|
|
36
|
-
Requires-Dist:
|
|
31
|
+
Requires-Dist: FlaskSimpleAuth>=35.0; extra == "dev"
|
|
32
|
+
Requires-Dist: bcrypt; extra == "dev"
|
|
37
33
|
Requires-Dist: pydantic; extra == "dev"
|
|
38
34
|
Provides-Extra: doc
|
|
39
35
|
Requires-Dist: sphinx; extra == "doc"
|
|
@@ -41,6 +37,10 @@ Requires-Dist: sphinx_rtd_theme; extra == "doc"
|
|
|
41
37
|
Requires-Dist: sphinx-autoapi; extra == "doc"
|
|
42
38
|
Requires-Dist: sphinx-lint; extra == "doc"
|
|
43
39
|
Requires-Dist: myst_parser; extra == "doc"
|
|
40
|
+
Provides-Extra: pub
|
|
41
|
+
Requires-Dist: build; extra == "pub"
|
|
42
|
+
Requires-Dist: twine; extra == "pub"
|
|
43
|
+
Requires-Dist: wheel; extra == "pub"
|
|
44
44
|
|
|
45
45
|
# FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
|
|
46
46
|
|
|
@@ -53,7 +53,7 @@ Only one set of tests is needed, switching from internal to external is
|
|
|
53
53
|
achieved by setting an environment variable.
|
|
54
54
|
|
|
55
55
|

|
|
56
|
-

|
|
57
57
|

|
|
58
58
|

|
|
59
59
|

|
|
@@ -75,18 +75,20 @@ import pytest
|
|
|
75
75
|
from FlaskTester import ft_authenticator, ft_client
|
|
76
76
|
import secret
|
|
77
77
|
|
|
78
|
+
def authHook(api, user: str, pwd: str|None):
|
|
79
|
+
if pwd is not None: # get a token when a login/password is provided
|
|
80
|
+
res = api.get("/login", login=user, auth="basic", status=200)
|
|
81
|
+
api.setToken(user, res.json["token"])
|
|
82
|
+
else: # remove token
|
|
83
|
+
api.setToken(user, None)
|
|
84
|
+
|
|
78
85
|
@pytest.fixture
|
|
79
86
|
def app(ft_client):
|
|
87
|
+
# register authentication hook
|
|
88
|
+
ft_client.setHook(authHook)
|
|
80
89
|
# add test passwords for Calvin and Hobbes (must be consistent with app!)
|
|
81
90
|
ft_client.setPass("calvin", secret.PASSES["calvin"])
|
|
82
91
|
ft_client.setPass("hobbes", secret.PASSES["hobbes"])
|
|
83
|
-
# get user tokens, assume json result {"token": "<token-value>"}
|
|
84
|
-
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
85
|
-
assert res.is_json
|
|
86
|
-
ft_client.setToken("calvin", res.json["token"])
|
|
87
|
-
res = ft_client.post("/login", login="hobbes", auth="param", status=201)
|
|
88
|
-
assert res.is_json
|
|
89
|
-
ft_client.setToken("hobbes", res.json["token"])
|
|
90
92
|
# also set a cookie
|
|
91
93
|
ft_client.setCookie("hobbes", "lang", "fr")
|
|
92
94
|
ft_client.setCookie("calvin", "lang", "en")
|
|
@@ -102,13 +104,13 @@ def test_app_admin(app):
|
|
|
102
104
|
assert 'not in group "ADMIN"' in res.text
|
|
103
105
|
```
|
|
104
106
|
|
|
105
|
-
This can be run against a
|
|
107
|
+
This can be run against a local or remote server:
|
|
106
108
|
|
|
107
109
|
```shell
|
|
108
110
|
export TEST_SEED="some-random-data" # shared test seed
|
|
109
111
|
flask --app app:app run & # start flask app
|
|
110
112
|
pid=$! # keep pid
|
|
111
|
-
export FLASK_TESTER_APP="http://localhost:5000" # set app local
|
|
113
|
+
export FLASK_TESTER_APP="http://localhost:5000" # set app url, local or remote
|
|
112
114
|
pytest test.py # run external tests
|
|
113
115
|
kill $pid # stop app with pid
|
|
114
116
|
```
|
|
@@ -9,7 +9,7 @@ Only one set of tests is needed, switching from internal to external is
|
|
|
9
9
|
achieved by setting an environment variable.
|
|
10
10
|
|
|
11
11
|

|
|
12
|
-

|
|
13
13
|

|
|
14
14
|

|
|
15
15
|

|
|
@@ -31,18 +31,20 @@ import pytest
|
|
|
31
31
|
from FlaskTester import ft_authenticator, ft_client
|
|
32
32
|
import secret
|
|
33
33
|
|
|
34
|
+
def authHook(api, user: str, pwd: str|None):
|
|
35
|
+
if pwd is not None: # get a token when a login/password is provided
|
|
36
|
+
res = api.get("/login", login=user, auth="basic", status=200)
|
|
37
|
+
api.setToken(user, res.json["token"])
|
|
38
|
+
else: # remove token
|
|
39
|
+
api.setToken(user, None)
|
|
40
|
+
|
|
34
41
|
@pytest.fixture
|
|
35
42
|
def app(ft_client):
|
|
43
|
+
# register authentication hook
|
|
44
|
+
ft_client.setHook(authHook)
|
|
36
45
|
# add test passwords for Calvin and Hobbes (must be consistent with app!)
|
|
37
46
|
ft_client.setPass("calvin", secret.PASSES["calvin"])
|
|
38
47
|
ft_client.setPass("hobbes", secret.PASSES["hobbes"])
|
|
39
|
-
# get user tokens, assume json result {"token": "<token-value>"}
|
|
40
|
-
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
41
|
-
assert res.is_json
|
|
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"])
|
|
46
48
|
# also set a cookie
|
|
47
49
|
ft_client.setCookie("hobbes", "lang", "fr")
|
|
48
50
|
ft_client.setCookie("calvin", "lang", "en")
|
|
@@ -58,13 +60,13 @@ def test_app_admin(app):
|
|
|
58
60
|
assert 'not in group "ADMIN"' in res.text
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
This can be run against a
|
|
63
|
+
This can be run against a local or remote server:
|
|
62
64
|
|
|
63
65
|
```shell
|
|
64
66
|
export TEST_SEED="some-random-data" # shared test seed
|
|
65
67
|
flask --app app:app run & # start flask app
|
|
66
68
|
pid=$! # keep pid
|
|
67
|
-
export FLASK_TESTER_APP="http://localhost:5000" # set app local
|
|
69
|
+
export FLASK_TESTER_APP="http://localhost:5000" # set app url, local or remote
|
|
68
70
|
pytest test.py # run external tests
|
|
69
71
|
kill $pid # stop app with pid
|
|
70
72
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "FlaskTester"
|
|
7
|
-
version = "
|
|
7
|
+
version = "5.0"
|
|
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"
|
|
@@ -12,7 +12,6 @@ license = { text = "CC0" }
|
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
13
|
dependencies = [
|
|
14
14
|
"requests",
|
|
15
|
-
"flask",
|
|
16
15
|
"pytest"
|
|
17
16
|
]
|
|
18
17
|
classifiers = [
|
|
@@ -36,13 +35,12 @@ package = "https://pypi.org/project/FlaskTester/"
|
|
|
36
35
|
dev = [
|
|
37
36
|
# static checks…
|
|
38
37
|
"mypy", "pyright", "ruff", "coverage", "pymarkdownlnt!=0.9.5",
|
|
39
|
-
# packaging
|
|
40
|
-
"build", "twine", "wheel",
|
|
41
38
|
# tests
|
|
42
|
-
"types-flask", "types-requests", "FlaskSimpleAuth>=
|
|
39
|
+
"types-flask", "types-requests", "FlaskSimpleAuth>=35.0", "bcrypt", "pydantic"
|
|
43
40
|
]
|
|
44
41
|
# documentation generation
|
|
45
42
|
doc = ["sphinx", "sphinx_rtd_theme", "sphinx-autoapi", "sphinx-lint", "myst_parser"]
|
|
43
|
+
pub = ["build", "twine", "wheel"]
|
|
46
44
|
|
|
47
45
|
[tool.setuptools]
|
|
48
46
|
py-modules = [ "FlaskTester" ]
|
|
@@ -12,7 +12,7 @@ import model
|
|
|
12
12
|
|
|
13
13
|
logging.basicConfig(level=logging.INFO)
|
|
14
14
|
# logging.basicConfig(level=logging.DEBUG)
|
|
15
|
-
|
|
15
|
+
log = logging.getLogger("test")
|
|
16
16
|
|
|
17
17
|
# set authn for ft_authenticator
|
|
18
18
|
os.environ.update(
|
|
@@ -27,23 +27,31 @@ def test_sanity():
|
|
|
27
27
|
# log.debug(f"TEST_SEED={os.environ.get('TEST_SEED')}")
|
|
28
28
|
|
|
29
29
|
# example from README.md
|
|
30
|
+
def authHook(api, user: str, pwd: str|None):
|
|
31
|
+
if pwd is not None: # get token
|
|
32
|
+
try:
|
|
33
|
+
res = api.get("/login", login=user, auth="basic", status=200)
|
|
34
|
+
api.setToken(user, res.json["token"])
|
|
35
|
+
except ft.FlaskTesterError as e: # pragma: no cover
|
|
36
|
+
log.warning(f"error: {e}")
|
|
37
|
+
else: # cleanup
|
|
38
|
+
api.setToken(user, None)
|
|
39
|
+
|
|
30
40
|
@pytest.fixture
|
|
31
41
|
def app(ft_client):
|
|
32
|
-
#
|
|
42
|
+
# hook when adding login/passwords
|
|
43
|
+
ft_client.setHook(authHook)
|
|
44
|
+
# Calvin authentication
|
|
33
45
|
ft_client.setPass("calvin", secret.PASSES["calvin"])
|
|
34
|
-
ft_client.setPass("hobbes", secret.PASSES["hobbes"])
|
|
35
|
-
# get user tokens, assume json result {"token": "<token-value>"}
|
|
36
|
-
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
37
|
-
assert res.is_json
|
|
38
|
-
ft_client.setToken("calvin", res.json["token"])
|
|
39
|
-
res = ft_client.post("/login", login="hobbes", auth="param", status=201)
|
|
40
|
-
assert res.is_json
|
|
41
|
-
ft_client.setToken("hobbes", res.json["token"])
|
|
42
|
-
# also set a cookie
|
|
43
46
|
ft_client.setCookie("hobbes", "lang", "fr")
|
|
47
|
+
# Hobbes authentication
|
|
48
|
+
ft_client.setPass("hobbes", secret.PASSES["hobbes"])
|
|
44
49
|
ft_client.setCookie("calvin", "lang", "en")
|
|
45
50
|
# return working client
|
|
46
51
|
yield ft_client
|
|
52
|
+
# cleanup
|
|
53
|
+
ft_client.setPass("calvin", None)
|
|
54
|
+
ft_client.setPass("hobbes", None)
|
|
47
55
|
|
|
48
56
|
def test_app_admin(app): # GET /admin
|
|
49
57
|
app.get("/admin", login=None, status=401)
|
|
@@ -65,15 +73,15 @@ def test_app_who_am_i(app): # GET /who-am-i
|
|
|
65
73
|
def api(ft_client):
|
|
66
74
|
# set a default
|
|
67
75
|
ft_client._default_login = "calvin"
|
|
68
|
-
# bad password
|
|
76
|
+
# bad password and token
|
|
69
77
|
ft_client.setPass("moe", None)
|
|
70
78
|
ft_client.setToken("moe", None)
|
|
71
79
|
ft_client.setPass("moe", "bad password")
|
|
72
80
|
ft_client.setToken("moe", "bad token")
|
|
73
|
-
# cookies
|
|
81
|
+
# set language cookies
|
|
74
82
|
ft_client.setCookie("calvin", "lang", "en")
|
|
75
83
|
ft_client.setCookie("hobbes", "lang", "fr")
|
|
76
|
-
# get valid tokens using password authn
|
|
84
|
+
# get valid tokens using password authn on /login
|
|
77
85
|
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
78
86
|
assert res.json["user"] == "calvin"
|
|
79
87
|
ft_client._auth.setToken("calvin", res.json["token"])
|
|
@@ -100,7 +108,7 @@ def api(ft_client):
|
|
|
100
108
|
assert res.json["user"] == "calvin"
|
|
101
109
|
res = ft_client.get("/who-am-i", status=200)
|
|
102
110
|
assert res.json["user"] == "calvin"
|
|
103
|
-
#
|
|
111
|
+
# ready for testing routes
|
|
104
112
|
yield ft_client
|
|
105
113
|
|
|
106
114
|
def test_admin(api):
|
|
@@ -343,6 +351,7 @@ def test_client_fixture():
|
|
|
343
351
|
def test_classes(api):
|
|
344
352
|
|
|
345
353
|
def thing_eq(ta, tb):
|
|
354
|
+
ta = model.Thing1(**ta) if isinstance(ta, dict) else ta
|
|
346
355
|
tb = model.Thing1(**tb) if isinstance(tb, dict) else tb
|
|
347
356
|
return ta.tid == tb.tid and ta.name == tb.name and ta.owner == tb.owner
|
|
348
357
|
|
|
@@ -356,17 +365,15 @@ def test_classes(api):
|
|
|
356
365
|
# check all combinations
|
|
357
366
|
for path in ["/t0", "/t1", "/t2", "/t3"]:
|
|
358
367
|
for param in [t0, t1, t2, t3]:
|
|
359
|
-
for tclass in [model.Thing1, model.Thing2, model.Thing3]:
|
|
368
|
+
for tclass in [dict, model.Thing1, model.Thing2, model.Thing3]:
|
|
360
369
|
for method in ["GET", "POST"]:
|
|
361
370
|
for mode in ["data", "json"]:
|
|
362
371
|
n +=1
|
|
363
372
|
parameter = {mode: {"t": param}}
|
|
364
373
|
res = api.request(method, path, 200, **parameter)
|
|
365
|
-
assert res.is_json
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
assert thing_eq(tclass(**json), param)
|
|
369
|
-
assert n == 192
|
|
374
|
+
assert res.is_json and isinstance(res.json, dict)
|
|
375
|
+
assert thing_eq(tclass(**res.json), param)
|
|
376
|
+
assert n == 256
|
|
370
377
|
|
|
371
378
|
# simple types translation
|
|
372
379
|
assert api.get("/t0", 200, json={"t": None}).json is None
|
|
@@ -26,3 +26,12 @@ def test_something(app):
|
|
|
26
26
|
app.get("/open", 200, "Hello", login="calvin", auth="none")
|
|
27
27
|
app.get("/open", 200, "Bonjour", login="hobbes", auth="none")
|
|
28
28
|
app.get("/open", 200, "Guten Tag", login=None)
|
|
29
|
+
|
|
30
|
+
def test_params(app):
|
|
31
|
+
res = app.get("/add", 200, data={"i": 39, "j": 3}, login=None)
|
|
32
|
+
assert res.is_json and res.json["sum"] == 42
|
|
33
|
+
res = app.get("/add", 200, json={"i": 35, "j": 7}, login=None)
|
|
34
|
+
assert res.is_json and res.json["sum"] == 42
|
|
35
|
+
# mixing data/json is okay for FlaskTester
|
|
36
|
+
res = app.get("/add", 200, data={"i": 30}, json={"j": 12}, login=None)
|
|
37
|
+
assert res.is_json and res.json["sum"] == 42
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|