FlaskTester 1.4__tar.gz → 3.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-1.4 → FlaskTester-3.0}/FlaskTester.egg-info/PKG-INFO +20 -6
- {FlaskTester-1.4 → FlaskTester-3.0}/FlaskTester.egg-info/requires.txt +1 -1
- {FlaskTester-1.4 → FlaskTester-3.0}/FlaskTester.py +83 -32
- {FlaskTester-1.4 → FlaskTester-3.0}/PKG-INFO +20 -6
- {FlaskTester-1.4 → FlaskTester-3.0}/README.md +18 -4
- {FlaskTester-1.4 → FlaskTester-3.0}/pyproject.toml +2 -2
- {FlaskTester-1.4 → FlaskTester-3.0}/tests/test.py +72 -34
- {FlaskTester-1.4 → FlaskTester-3.0}/FlaskTester.egg-info/SOURCES.txt +0 -0
- {FlaskTester-1.4 → FlaskTester-3.0}/FlaskTester.egg-info/dependency_links.txt +0 -0
- {FlaskTester-1.4 → FlaskTester-3.0}/FlaskTester.egg-info/top_level.txt +0 -0
- {FlaskTester-1.4 → FlaskTester-3.0}/LICENSE +0 -0
- {FlaskTester-1.4 → FlaskTester-3.0}/MANIFEST.in +0 -0
- {FlaskTester-1.4 → FlaskTester-3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: FlaskTester
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.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
|
|
@@ -32,7 +32,7 @@ Requires-Dist: twine; extra == "dev"
|
|
|
32
32
|
Requires-Dist: wheel; extra == "dev"
|
|
33
33
|
Requires-Dist: types-flask; extra == "dev"
|
|
34
34
|
Requires-Dist: types-requests; extra == "dev"
|
|
35
|
-
Requires-Dist: FlaskSimpleAuth; extra == "dev"
|
|
35
|
+
Requires-Dist: FlaskSimpleAuth>=29.5; extra == "dev"
|
|
36
36
|
Requires-Dist: passlib; extra == "dev"
|
|
37
37
|
Provides-Extra: doc
|
|
38
38
|
Requires-Dist: sphinx; extra == "doc"
|
|
@@ -52,7 +52,7 @@ Only one set of tests is needed, switching from internal to external is
|
|
|
52
52
|
achieved through environment variables.
|
|
53
53
|
|
|
54
54
|

|
|
55
|
-

|
|
56
56
|

|
|
57
57
|

|
|
58
58
|

|
|
@@ -86,6 +86,9 @@ def app(ft_client):
|
|
|
86
86
|
res = ft_client.post("/login", login="hobbes", auth="param", status=201)
|
|
87
87
|
assert res.is_json
|
|
88
88
|
ft_client.setToken("hobbes", res.json["token"])
|
|
89
|
+
# also set a cookie
|
|
90
|
+
ft_client.setCookie("hobbes", "lang", "fr_FR")
|
|
91
|
+
ft_client.setCookie("calvin", "lang", "en_EN")
|
|
89
92
|
# return working client
|
|
90
93
|
yield ft_client
|
|
91
94
|
|
|
@@ -133,7 +136,7 @@ The package provides two fixtures:
|
|
|
133
136
|
- `ft_authenticator` for app authentication, which depends on environment variables:
|
|
134
137
|
|
|
135
138
|
- `FLASK_TESTER_ALLOW` space-separated list of allowed authentication schemes,
|
|
136
|
-
default is `["bearer", "basic", "param"]`.
|
|
139
|
+
default is `["bearer", "basic", "param", "none"]`.
|
|
137
140
|
- `FLASK_TESTER_AUTH` comma-separated list of _login:password_ credentials.
|
|
138
141
|
- `FLASK_TESTER_USER` user login parameter for `param` password authentication,
|
|
139
142
|
default is `USER`.
|
|
@@ -155,7 +158,8 @@ The package provides two fixtures:
|
|
|
155
158
|
The fixture has 3 main methods:
|
|
156
159
|
- `setPass` to associate a password to a user, set to _None_ to remove credential.
|
|
157
160
|
- `setToken` to associate a token to a user, set to _None_ to remove credential.
|
|
158
|
-
- `
|
|
161
|
+
- `setCookie` to add a cookie to a user, set value to _None_ to remove cookie.
|
|
162
|
+
- `setAuth` to add authentication data to a request `kwargs` and `cookies`.
|
|
159
163
|
|
|
160
164
|
- `ft_client` for app testing, which depends on the previous fixture, plus
|
|
161
165
|
environment variables which allow to find the application, at least one must
|
|
@@ -175,7 +179,7 @@ The package provides two fixtures:
|
|
|
175
179
|
The fixture then provides test methods to issue test requests against a Flask application:
|
|
176
180
|
- `request` generic request with `login`, `auth`, `status` end `content` extensions.
|
|
177
181
|
- `get post put patch delete` methods with `login`, `auth` and `status` extensions.
|
|
178
|
-
Moreover, `setPass` and `
|
|
182
|
+
Moreover, `setPass`, `setToken` and `setCookie` are forwarded to the internal authenticator.
|
|
179
183
|
|
|
180
184
|
Authenticator environment variables can be set from the pytest Python test file by
|
|
181
185
|
assigning them through `os.environ`.
|
|
@@ -238,6 +242,16 @@ Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
|
|
|
238
242
|
see also the [documentation](https://zx80.github.io/flask-tester/),
|
|
239
243
|
please report any [issues](https://github.com/zx80/flask-tester/issues).
|
|
240
244
|
|
|
245
|
+
### 3.0 on 2024-03-23
|
|
246
|
+
|
|
247
|
+
Add support for `none` authentication, with only cookies.
|
|
248
|
+
|
|
249
|
+
### 2.0 on 2024-03-23
|
|
250
|
+
|
|
251
|
+
Add support for cookies.
|
|
252
|
+
Improved documentation and code.
|
|
253
|
+
Improved tests.
|
|
254
|
+
|
|
241
255
|
### 1.4 on 2024-03-19
|
|
242
256
|
|
|
243
257
|
Test expected assert failures.
|
|
@@ -36,11 +36,12 @@ class Authenticator:
|
|
|
36
36
|
- ``cookie``: token in a cookie
|
|
37
37
|
- ``tparam``: token in a parameter
|
|
38
38
|
- ``fake``: fake scheme, login directly passed as a parameter
|
|
39
|
+
- ``none``: no authentication, only cookies
|
|
39
40
|
|
|
40
41
|
Constructor parameters:
|
|
41
42
|
|
|
42
43
|
- ``allow``: list of allowed schemes.
|
|
43
|
-
default is ``["bearer", "basic", "param"]``
|
|
44
|
+
default is ``["bearer", "basic", "param", "none"]``
|
|
44
45
|
- ``user``: parameter for user on ``param`` password authentication,
|
|
45
46
|
default is ``USER``
|
|
46
47
|
- ``pwd``: parameter for password on ``param`` password authentication,
|
|
@@ -60,12 +61,13 @@ class Authenticator:
|
|
|
60
61
|
_TOKEN_SCHEMES = {"bearer", "header", "cookie", "tparam"}
|
|
61
62
|
_PASS_SCHEMES = {"basic", "param"}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
# all supported authentication schemes
|
|
65
|
+
_AUTH_SCHEMES = {"fake", "none"}
|
|
64
66
|
_AUTH_SCHEMES.update(_TOKEN_SCHEMES)
|
|
65
67
|
_AUTH_SCHEMES.update(_PASS_SCHEMES)
|
|
66
68
|
|
|
67
69
|
def __init__(self,
|
|
68
|
-
allow: list[str] = ["bearer", "basic", "param"],
|
|
70
|
+
allow: list[str] = ["bearer", "basic", "param", "none"],
|
|
69
71
|
# parameter names for "basic" and "param"
|
|
70
72
|
user: str = "USER",
|
|
71
73
|
pwd: str = "PASS",
|
|
@@ -96,21 +98,25 @@ class Authenticator:
|
|
|
96
98
|
self._cookie = cookie
|
|
97
99
|
self._tparam = tparam
|
|
98
100
|
|
|
99
|
-
# password and token credentials
|
|
101
|
+
# password and token credentials, cookies
|
|
100
102
|
self._passes: dict[str, str] = {}
|
|
101
103
|
self._tokens: dict[str, str] = {}
|
|
104
|
+
self._cookies: dict[str, dict[str, str]] = {}
|
|
102
105
|
|
|
103
|
-
def _set(self,
|
|
106
|
+
def _set(self, key: str, val: str|None, store: dict[str, str]):
|
|
104
107
|
"""Set a key/value in a directory, with None for delete."""
|
|
105
108
|
if val is None:
|
|
106
|
-
if
|
|
107
|
-
del store[
|
|
109
|
+
if key in store:
|
|
110
|
+
del store[key]
|
|
108
111
|
else:
|
|
109
112
|
assert isinstance(val, str)
|
|
110
|
-
store[
|
|
113
|
+
store[key] = val
|
|
111
114
|
|
|
112
115
|
def setPass(self, login: str, pw: str|None):
|
|
113
|
-
"""Associate a password to a user.
|
|
116
|
+
"""Associate a password to a user.
|
|
117
|
+
|
|
118
|
+
Set to *None* to remove the password entry.
|
|
119
|
+
"""
|
|
114
120
|
if not self._has_pass:
|
|
115
121
|
raise AuthError("cannot set password, no password scheme allowed")
|
|
116
122
|
self._set(login, pw, self._passes)
|
|
@@ -122,11 +128,20 @@ class Authenticator:
|
|
|
122
128
|
self.setPass(login, pw)
|
|
123
129
|
|
|
124
130
|
def setToken(self, login: str, token: str|None):
|
|
125
|
-
"""Associate a token to a user.
|
|
131
|
+
"""Associate a token to a user.
|
|
132
|
+
|
|
133
|
+
Set to *None* to remove the token entry.
|
|
134
|
+
"""
|
|
126
135
|
if not self._has_token:
|
|
127
136
|
raise AuthError("cannot set token, no token scheme allowed")
|
|
128
137
|
self._set(login, token, self._tokens)
|
|
129
138
|
|
|
139
|
+
def setCookie(self, login: str, name: str, val: str|None = None):
|
|
140
|
+
"""Associate a cookie and its value to a login, *None* to remove."""
|
|
141
|
+
if login not in self._cookies:
|
|
142
|
+
self._cookies[login] = {}
|
|
143
|
+
self._set(name, val, self._cookies[login])
|
|
144
|
+
|
|
130
145
|
def _param(self, kwargs: dict[str, Any], key: str, val: Any):
|
|
131
146
|
"""Add request parameter to "json" or "data"."""
|
|
132
147
|
|
|
@@ -143,12 +158,16 @@ class Authenticator:
|
|
|
143
158
|
"""Whether to try this authentication scheme."""
|
|
144
159
|
return auth in (None, scheme) and scheme in self._allow
|
|
145
160
|
|
|
146
|
-
def setAuth(self, login: str|None, kwargs: dict[str, Any], auth: str|None = None):
|
|
161
|
+
def setAuth(self, login: str|None, kwargs: dict[str, Any], cookies: dict[str, str], auth: str|None = None):
|
|
147
162
|
"""Set request authentication.
|
|
148
163
|
|
|
149
164
|
- login: login target, None means no authentication
|
|
150
165
|
- kwargs: request parameters
|
|
151
|
-
-
|
|
166
|
+
- cookies: request cookies
|
|
167
|
+
- auth: authentication method, default is None
|
|
168
|
+
|
|
169
|
+
The default behavior is to try allowed schemes: tokens first,
|
|
170
|
+
then password, then fake.
|
|
152
171
|
"""
|
|
153
172
|
|
|
154
173
|
log.debug(f"setAuth: login={login} auth={auth} allow={self._allow}")
|
|
@@ -156,29 +175,29 @@ class Authenticator:
|
|
|
156
175
|
if login is None: # not needed
|
|
157
176
|
return
|
|
158
177
|
|
|
178
|
+
cookies.update(self._cookies.get(login, {}))
|
|
179
|
+
|
|
159
180
|
if auth is not None:
|
|
160
181
|
if auth not in self._AUTH_SCHEMES:
|
|
161
182
|
raise AuthError(f"unexpected auth: {auth}")
|
|
162
183
|
if auth not in self._allow:
|
|
163
184
|
raise AuthError(f"auth is not allowed: {auth}")
|
|
164
185
|
|
|
186
|
+
headers = kwargs.get("headers", {})
|
|
187
|
+
|
|
165
188
|
# use token if available and allowed
|
|
166
189
|
if login in self._tokens and auth in (None, "bearer", "header", "cookie", "tparam"):
|
|
167
190
|
|
|
168
191
|
token = self._tokens[login]
|
|
169
192
|
|
|
170
|
-
if "headers" not in kwargs:
|
|
171
|
-
kwargs["headers"] = {}
|
|
172
|
-
|
|
173
193
|
if self._try_auth(auth, "bearer"):
|
|
174
|
-
|
|
194
|
+
headers["Authorization"] = self._bearer + " " + token
|
|
175
195
|
elif self._try_auth(auth, "header"):
|
|
176
|
-
|
|
196
|
+
headers[self._header] = token
|
|
177
197
|
elif self._try_auth(auth, "tparam"):
|
|
178
198
|
self._param(kwargs, self._tparam, token)
|
|
179
199
|
elif self._try_auth(auth, "cookie"):
|
|
180
|
-
|
|
181
|
-
kwargs["headers"]["Cookie"] = self._cookie + "=" + token
|
|
200
|
+
cookies[self._cookie] = token
|
|
182
201
|
else:
|
|
183
202
|
raise AuthError(f"no token carrier: login={login} auth={auth} allow={self._allow}")
|
|
184
203
|
|
|
@@ -196,10 +215,17 @@ class Authenticator:
|
|
|
196
215
|
|
|
197
216
|
self._param(kwargs, self._login, login)
|
|
198
217
|
|
|
218
|
+
elif self._try_auth(auth, "none"):
|
|
219
|
+
|
|
220
|
+
pass
|
|
221
|
+
|
|
199
222
|
else:
|
|
200
223
|
|
|
201
224
|
raise AuthError(f"no authentication for login={login} auth={auth} allow={self._allow}")
|
|
202
225
|
|
|
226
|
+
if headers: # put headers back if needed
|
|
227
|
+
kwargs["headers"] = headers
|
|
228
|
+
|
|
203
229
|
|
|
204
230
|
class RequestFlaskResponse:
|
|
205
231
|
"""Wrapper to return a Flask-looking response from a request response.
|
|
@@ -238,16 +264,17 @@ class RequestFlaskResponse:
|
|
|
238
264
|
|
|
239
265
|
|
|
240
266
|
class Client:
|
|
241
|
-
"""Common class for flask authenticated testing.
|
|
267
|
+
"""Common (partial) class for flask authenticated testing.
|
|
242
268
|
|
|
243
269
|
Constructor parameters:
|
|
244
270
|
|
|
245
271
|
- ``auth`` authenticator
|
|
246
|
-
- ``default_login`` if
|
|
272
|
+
- ``default_login`` if ``login`` is not set.
|
|
247
273
|
"""
|
|
248
274
|
|
|
249
275
|
def __init__(self, auth: Authenticator, default_login: str|None = None):
|
|
250
276
|
self._auth = auth
|
|
277
|
+
self._cookies: dict[str, dict[str, str]] = {} # login -> name -> value
|
|
251
278
|
self._default_login = default_login
|
|
252
279
|
|
|
253
280
|
def setToken(self, login: str, token: str|None):
|
|
@@ -258,7 +285,11 @@ class Client:
|
|
|
258
285
|
"""Associate a password to a login, *None* to remove."""
|
|
259
286
|
self._auth.setPass(login, password)
|
|
260
287
|
|
|
261
|
-
def
|
|
288
|
+
def setCookie(self, login: str, name: str, val: str|None):
|
|
289
|
+
"""Associate a cookie to a login, *None* name to remove."""
|
|
290
|
+
self._auth.setCookie(login, name, val)
|
|
291
|
+
|
|
292
|
+
def _request(self, method: str, path: str, cookies: dict[str, str], **kwargs):
|
|
262
293
|
"""Run a request and return response."""
|
|
263
294
|
raise NotImplementedError()
|
|
264
295
|
|
|
@@ -274,7 +305,7 @@ class Client:
|
|
|
274
305
|
|
|
275
306
|
- ``status``: expected HTTP status, *None* to skip status check
|
|
276
307
|
- ``login``: authenticated user, use **explicit** *None* to skip
|
|
277
|
-
- ``auth``: authentication scheme to use
|
|
308
|
+
- ``auth``: authentication scheme to use instead of default behavior
|
|
278
309
|
- ``**kwargs``: more request parameters (headers, data, json…)
|
|
279
310
|
"""
|
|
280
311
|
|
|
@@ -284,8 +315,13 @@ class Client:
|
|
|
284
315
|
else: # if unset, use default
|
|
285
316
|
login = self._default_login
|
|
286
317
|
|
|
287
|
-
|
|
288
|
-
|
|
318
|
+
cookies: dict[str, str] = {}
|
|
319
|
+
if "cookies" in kwargs:
|
|
320
|
+
cookies.update(kwargs["cookies"])
|
|
321
|
+
del kwargs["cookies"]
|
|
322
|
+
|
|
323
|
+
self._auth.setAuth(login, kwargs, cookies, auth=auth)
|
|
324
|
+
res = self._request(method, path, cookies, **kwargs) # type: ignore
|
|
289
325
|
|
|
290
326
|
if status is not None:
|
|
291
327
|
if res.status_code != status: # show error before aborting
|
|
@@ -349,7 +385,7 @@ class RequestClient(Client):
|
|
|
349
385
|
|
|
350
386
|
- ``auth`` authenticator
|
|
351
387
|
- ``base_url`` target server
|
|
352
|
-
- ``default_login`` if
|
|
388
|
+
- ``default_login`` if ``login`` is not set.
|
|
353
389
|
"""
|
|
354
390
|
|
|
355
391
|
def __init__(self, auth: Authenticator, base_url: str, default_login=None):
|
|
@@ -359,7 +395,7 @@ class RequestClient(Client):
|
|
|
359
395
|
from requests import Session
|
|
360
396
|
self._requests = Session()
|
|
361
397
|
|
|
362
|
-
def _request(self, method: str, path: str, **kwargs):
|
|
398
|
+
def _request(self, method: str, path: str, cookies: dict[str, str], **kwargs):
|
|
363
399
|
"""Actual request handling."""
|
|
364
400
|
|
|
365
401
|
if "data" in kwargs:
|
|
@@ -383,7 +419,7 @@ class RequestClient(Client):
|
|
|
383
419
|
# sanity
|
|
384
420
|
assert not (files and "json" in kwargs), "cannot mix file upload and json?"
|
|
385
421
|
|
|
386
|
-
res = self._requests.request(method, self._base_url + path, **kwargs)
|
|
422
|
+
res = self._requests.request(method, self._base_url + path, cookies=cookies, **kwargs)
|
|
387
423
|
|
|
388
424
|
return RequestFlaskResponse(res)
|
|
389
425
|
|
|
@@ -394,16 +430,29 @@ class FlaskClient(Client):
|
|
|
394
430
|
Constructor parameters:
|
|
395
431
|
|
|
396
432
|
- ``auth`` authenticator
|
|
397
|
-
- ``client`` Flask ``test_client``
|
|
398
|
-
- ``default_login`` if
|
|
433
|
+
- ``client`` Flask actual ``test_client``
|
|
434
|
+
- ``default_login`` if ``login`` is not set.
|
|
435
|
+
|
|
436
|
+
Note: this client handles `cookies`.
|
|
399
437
|
"""
|
|
400
438
|
|
|
401
439
|
def __init__(self, auth: Authenticator, client, default_login=None):
|
|
402
440
|
super().__init__(auth, default_login)
|
|
403
441
|
self._client = client
|
|
442
|
+
self._cookie_names: set[str] = set() # FIXME all encountered cookies for cleanup :-/
|
|
404
443
|
|
|
405
|
-
def _request(self, method: str, path: str, **kwargs):
|
|
444
|
+
def _request(self, method: str, path: str, cookies: dict[str, str], **kwargs):
|
|
406
445
|
"""Actual request handling."""
|
|
446
|
+
|
|
447
|
+
# hack to cleanup client state :-/
|
|
448
|
+
for cookie in self._cookie_names:
|
|
449
|
+
self._client.delete_cookie(cookie)
|
|
450
|
+
|
|
451
|
+
for cookie, val in cookies.items():
|
|
452
|
+
if cookie not in self._cookie_names:
|
|
453
|
+
self._cookie_names.add(cookie)
|
|
454
|
+
self._client.set_cookie(cookie, val)
|
|
455
|
+
|
|
407
456
|
return self._client.open(method=method, path=path, **kwargs)
|
|
408
457
|
|
|
409
458
|
|
|
@@ -415,8 +464,9 @@ def ft_authenticator():
|
|
|
415
464
|
|
|
416
465
|
- ``FLASK_TESTER_LOG_LEVEL``: package log level in
|
|
417
466
|
``DEBUG INFO WARNING ERROR CRITICAL NOSET``.
|
|
467
|
+
Default is ``NOTSET``.
|
|
418
468
|
- ``FLASK_TESTER_ALLOW``: allowed space-separated authentication schemes, in
|
|
419
|
-
``basic param bearer header cookie tparam fake``.
|
|
469
|
+
``basic param bearer header cookie tparam fake none``.
|
|
420
470
|
Default is ``bearer basic param``.
|
|
421
471
|
- ``FLASK_TESTER_USER``: user login parameter for ``param`` authentication.
|
|
422
472
|
Default is ``USER``.
|
|
@@ -433,6 +483,7 @@ def ft_authenticator():
|
|
|
433
483
|
- ``FLASK_TESTER_TPARAM``: parameter for *token* authentication.
|
|
434
484
|
Default is ``AUTH``.
|
|
435
485
|
- ``FLASK_TESTER_AUTH``: initial comma-separated list of *login:password*.
|
|
486
|
+
Default is not set.
|
|
436
487
|
"""
|
|
437
488
|
|
|
438
489
|
level = os.environ.get("FLASK_TESTER_LOG_LEVEL", "NOTSET")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: FlaskTester
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.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
|
|
@@ -32,7 +32,7 @@ Requires-Dist: twine; extra == "dev"
|
|
|
32
32
|
Requires-Dist: wheel; extra == "dev"
|
|
33
33
|
Requires-Dist: types-flask; extra == "dev"
|
|
34
34
|
Requires-Dist: types-requests; extra == "dev"
|
|
35
|
-
Requires-Dist: FlaskSimpleAuth; extra == "dev"
|
|
35
|
+
Requires-Dist: FlaskSimpleAuth>=29.5; extra == "dev"
|
|
36
36
|
Requires-Dist: passlib; extra == "dev"
|
|
37
37
|
Provides-Extra: doc
|
|
38
38
|
Requires-Dist: sphinx; extra == "doc"
|
|
@@ -52,7 +52,7 @@ Only one set of tests is needed, switching from internal to external is
|
|
|
52
52
|
achieved through environment variables.
|
|
53
53
|
|
|
54
54
|

|
|
55
|
-

|
|
56
56
|

|
|
57
57
|

|
|
58
58
|

|
|
@@ -86,6 +86,9 @@ def app(ft_client):
|
|
|
86
86
|
res = ft_client.post("/login", login="hobbes", auth="param", status=201)
|
|
87
87
|
assert res.is_json
|
|
88
88
|
ft_client.setToken("hobbes", res.json["token"])
|
|
89
|
+
# also set a cookie
|
|
90
|
+
ft_client.setCookie("hobbes", "lang", "fr_FR")
|
|
91
|
+
ft_client.setCookie("calvin", "lang", "en_EN")
|
|
89
92
|
# return working client
|
|
90
93
|
yield ft_client
|
|
91
94
|
|
|
@@ -133,7 +136,7 @@ The package provides two fixtures:
|
|
|
133
136
|
- `ft_authenticator` for app authentication, which depends on environment variables:
|
|
134
137
|
|
|
135
138
|
- `FLASK_TESTER_ALLOW` space-separated list of allowed authentication schemes,
|
|
136
|
-
default is `["bearer", "basic", "param"]`.
|
|
139
|
+
default is `["bearer", "basic", "param", "none"]`.
|
|
137
140
|
- `FLASK_TESTER_AUTH` comma-separated list of _login:password_ credentials.
|
|
138
141
|
- `FLASK_TESTER_USER` user login parameter for `param` password authentication,
|
|
139
142
|
default is `USER`.
|
|
@@ -155,7 +158,8 @@ The package provides two fixtures:
|
|
|
155
158
|
The fixture has 3 main methods:
|
|
156
159
|
- `setPass` to associate a password to a user, set to _None_ to remove credential.
|
|
157
160
|
- `setToken` to associate a token to a user, set to _None_ to remove credential.
|
|
158
|
-
- `
|
|
161
|
+
- `setCookie` to add a cookie to a user, set value to _None_ to remove cookie.
|
|
162
|
+
- `setAuth` to add authentication data to a request `kwargs` and `cookies`.
|
|
159
163
|
|
|
160
164
|
- `ft_client` for app testing, which depends on the previous fixture, plus
|
|
161
165
|
environment variables which allow to find the application, at least one must
|
|
@@ -175,7 +179,7 @@ The package provides two fixtures:
|
|
|
175
179
|
The fixture then provides test methods to issue test requests against a Flask application:
|
|
176
180
|
- `request` generic request with `login`, `auth`, `status` end `content` extensions.
|
|
177
181
|
- `get post put patch delete` methods with `login`, `auth` and `status` extensions.
|
|
178
|
-
Moreover, `setPass` and `
|
|
182
|
+
Moreover, `setPass`, `setToken` and `setCookie` are forwarded to the internal authenticator.
|
|
179
183
|
|
|
180
184
|
Authenticator environment variables can be set from the pytest Python test file by
|
|
181
185
|
assigning them through `os.environ`.
|
|
@@ -238,6 +242,16 @@ Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
|
|
|
238
242
|
see also the [documentation](https://zx80.github.io/flask-tester/),
|
|
239
243
|
please report any [issues](https://github.com/zx80/flask-tester/issues).
|
|
240
244
|
|
|
245
|
+
### 3.0 on 2024-03-23
|
|
246
|
+
|
|
247
|
+
Add support for `none` authentication, with only cookies.
|
|
248
|
+
|
|
249
|
+
### 2.0 on 2024-03-23
|
|
250
|
+
|
|
251
|
+
Add support for cookies.
|
|
252
|
+
Improved documentation and code.
|
|
253
|
+
Improved tests.
|
|
254
|
+
|
|
241
255
|
### 1.4 on 2024-03-19
|
|
242
256
|
|
|
243
257
|
Test expected assert failures.
|
|
@@ -9,7 +9,7 @@ Only one set of tests is needed, switching from internal to external is
|
|
|
9
9
|
achieved through environment variables.
|
|
10
10
|
|
|
11
11
|

|
|
12
|
-

|
|
13
13
|

|
|
14
14
|

|
|
15
15
|

|
|
@@ -43,6 +43,9 @@ def app(ft_client):
|
|
|
43
43
|
res = ft_client.post("/login", login="hobbes", auth="param", status=201)
|
|
44
44
|
assert res.is_json
|
|
45
45
|
ft_client.setToken("hobbes", res.json["token"])
|
|
46
|
+
# also set a cookie
|
|
47
|
+
ft_client.setCookie("hobbes", "lang", "fr_FR")
|
|
48
|
+
ft_client.setCookie("calvin", "lang", "en_EN")
|
|
46
49
|
# return working client
|
|
47
50
|
yield ft_client
|
|
48
51
|
|
|
@@ -90,7 +93,7 @@ The package provides two fixtures:
|
|
|
90
93
|
- `ft_authenticator` for app authentication, which depends on environment variables:
|
|
91
94
|
|
|
92
95
|
- `FLASK_TESTER_ALLOW` space-separated list of allowed authentication schemes,
|
|
93
|
-
default is `["bearer", "basic", "param"]`.
|
|
96
|
+
default is `["bearer", "basic", "param", "none"]`.
|
|
94
97
|
- `FLASK_TESTER_AUTH` comma-separated list of _login:password_ credentials.
|
|
95
98
|
- `FLASK_TESTER_USER` user login parameter for `param` password authentication,
|
|
96
99
|
default is `USER`.
|
|
@@ -112,7 +115,8 @@ The package provides two fixtures:
|
|
|
112
115
|
The fixture has 3 main methods:
|
|
113
116
|
- `setPass` to associate a password to a user, set to _None_ to remove credential.
|
|
114
117
|
- `setToken` to associate a token to a user, set to _None_ to remove credential.
|
|
115
|
-
- `
|
|
118
|
+
- `setCookie` to add a cookie to a user, set value to _None_ to remove cookie.
|
|
119
|
+
- `setAuth` to add authentication data to a request `kwargs` and `cookies`.
|
|
116
120
|
|
|
117
121
|
- `ft_client` for app testing, which depends on the previous fixture, plus
|
|
118
122
|
environment variables which allow to find the application, at least one must
|
|
@@ -132,7 +136,7 @@ The package provides two fixtures:
|
|
|
132
136
|
The fixture then provides test methods to issue test requests against a Flask application:
|
|
133
137
|
- `request` generic request with `login`, `auth`, `status` end `content` extensions.
|
|
134
138
|
- `get post put patch delete` methods with `login`, `auth` and `status` extensions.
|
|
135
|
-
Moreover, `setPass` and `
|
|
139
|
+
Moreover, `setPass`, `setToken` and `setCookie` are forwarded to the internal authenticator.
|
|
136
140
|
|
|
137
141
|
Authenticator environment variables can be set from the pytest Python test file by
|
|
138
142
|
assigning them through `os.environ`.
|
|
@@ -195,6 +199,16 @@ Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
|
|
|
195
199
|
see also the [documentation](https://zx80.github.io/flask-tester/),
|
|
196
200
|
please report any [issues](https://github.com/zx80/flask-tester/issues).
|
|
197
201
|
|
|
202
|
+
### 3.0 on 2024-03-23
|
|
203
|
+
|
|
204
|
+
Add support for `none` authentication, with only cookies.
|
|
205
|
+
|
|
206
|
+
### 2.0 on 2024-03-23
|
|
207
|
+
|
|
208
|
+
Add support for cookies.
|
|
209
|
+
Improved documentation and code.
|
|
210
|
+
Improved tests.
|
|
211
|
+
|
|
198
212
|
### 1.4 on 2024-03-19
|
|
199
213
|
|
|
200
214
|
Test expected assert failures.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "FlaskTester"
|
|
7
|
-
version = "
|
|
7
|
+
version = "3.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"
|
|
@@ -33,7 +33,7 @@ package = "https://pypi.org/project/FlaskTester/"
|
|
|
33
33
|
|
|
34
34
|
[project.optional-dependencies]
|
|
35
35
|
# various dev tools
|
|
36
|
-
dev = ["mypy", "pyright", "ruff", "coverage", "pymarkdownlnt!=0.9.5", "build", "twine", "wheel", "types-flask", "types-requests", "FlaskSimpleAuth", "passlib"]
|
|
36
|
+
dev = ["mypy", "pyright", "ruff", "coverage", "pymarkdownlnt!=0.9.5", "build", "twine", "wheel", "types-flask", "types-requests", "FlaskSimpleAuth>=29.5", "passlib"]
|
|
37
37
|
# documentation generation
|
|
38
38
|
doc = ["sphinx", "sphinx_rtd_theme", "sphinx-autoapi", "sphinx-lint", "myst_parser"]
|
|
39
39
|
|
|
@@ -10,16 +10,19 @@ import io
|
|
|
10
10
|
import logging
|
|
11
11
|
|
|
12
12
|
logging.basicConfig(level=logging.INFO)
|
|
13
|
+
# logging.basicConfig(level=logging.DEBUG)
|
|
14
|
+
# log = logging.getLogger("test")
|
|
13
15
|
|
|
14
16
|
# set authn for ft_authenticator
|
|
15
17
|
os.environ.update(
|
|
16
|
-
FLASK_TESTER_ALLOW="bearer basic param",
|
|
18
|
+
FLASK_TESTER_ALLOW="bearer basic param none",
|
|
17
19
|
FLASK_TESTER_AUTH=",".join(f"{l}:{p}" for l, p in secret.PASSES.items()),
|
|
18
20
|
)
|
|
19
21
|
|
|
20
22
|
def test_sanity():
|
|
21
23
|
# must provide url or package of Flask application to test
|
|
22
24
|
assert "FLASK_TESTER_URL" in os.environ or "FLASK_TESTER_APP" in os.environ
|
|
25
|
+
# log.debug(f"TEST_SEED={os.environ.get('TEST_SEED')}")
|
|
23
26
|
|
|
24
27
|
# example from README.md
|
|
25
28
|
@pytest.fixture
|
|
@@ -34,6 +37,9 @@ def app(ft_client):
|
|
|
34
37
|
res = ft_client.post("/login", login="hobbes", auth="param", status=201)
|
|
35
38
|
assert res.is_json
|
|
36
39
|
ft_client.setToken("hobbes", res.json["token"])
|
|
40
|
+
# also set a cookie
|
|
41
|
+
ft_client.setCookie("hobbes", "lang", "fr_FR")
|
|
42
|
+
ft_client.setCookie("calvin", "lang", "en_EN")
|
|
37
43
|
# return working client
|
|
38
44
|
yield ft_client
|
|
39
45
|
|
|
@@ -59,6 +65,9 @@ def api(ft_client):
|
|
|
59
65
|
ft_client.setToken("moe", None)
|
|
60
66
|
ft_client.setPass("moe", "bad password")
|
|
61
67
|
ft_client.setToken("moe", "bad token")
|
|
68
|
+
# cookies
|
|
69
|
+
ft_client.setCookie("calvin", "lang", "en")
|
|
70
|
+
ft_client.setCookie("hobbes", "lang", "fr")
|
|
62
71
|
# get valid tokens using password authn
|
|
63
72
|
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
64
73
|
assert res.json["user"] == "calvin"
|
|
@@ -69,13 +78,14 @@ def api(ft_client):
|
|
|
69
78
|
res = ft_client.post("/login", login="susie", auth="param", status=201, json={})
|
|
70
79
|
assert res.json["user"] == "susie"
|
|
71
80
|
ft_client._auth.setToken("susie", res.json["token"])
|
|
72
|
-
|
|
81
|
+
ft_client.get("/login", login="calvin", auth="none", status=401)
|
|
82
|
+
# check that token auth and cookie is ok
|
|
73
83
|
res = ft_client.get("/who-am-i", login="calvin", status=200, auth="bearer")
|
|
74
|
-
assert res.json["user"] == "calvin"
|
|
84
|
+
assert res.json["user"] == "calvin" and res.json["lang"] == "en"
|
|
75
85
|
res = ft_client.get("/who-am-i", login="hobbes", status=200, auth="bearer")
|
|
76
|
-
assert res.json["user"] == "hobbes"
|
|
86
|
+
assert res.json["user"] == "hobbes" and res.json["lang"] == "fr"
|
|
77
87
|
res = ft_client.get("/who-am-i", login="susie", status=200, auth="bearer")
|
|
78
|
-
assert res.json["user"] == "susie"
|
|
88
|
+
assert res.json["user"] == "susie" and res.json["lang"] is None
|
|
79
89
|
# with defaults
|
|
80
90
|
res = ft_client.get("/who-am-i", auth="basic", status=200)
|
|
81
91
|
assert res.json["user"] == "calvin"
|
|
@@ -108,6 +118,7 @@ def test_admin(api):
|
|
|
108
118
|
assert "NOT THERE" in str(e)
|
|
109
119
|
|
|
110
120
|
def test_errors(api):
|
|
121
|
+
# these schemes are not allowed
|
|
111
122
|
for scheme in ("header", "cookie", "fake", "tparam", "unexpected"):
|
|
112
123
|
try:
|
|
113
124
|
api.get("/login", login="calvin", auth=scheme)
|
|
@@ -116,12 +127,27 @@ def test_errors(api):
|
|
|
116
127
|
assert True, "expected error"
|
|
117
128
|
|
|
118
129
|
def test_methods(api):
|
|
119
|
-
api.get("/who-am-i", login="
|
|
130
|
+
res = api.get("/who-am-i", login="susie", status=200, cookies={"lang": "it"})
|
|
131
|
+
assert res.json["lang"] == "it"
|
|
120
132
|
api.post("/who-am-i", login="hobbes", status=405)
|
|
121
133
|
api.put("/who-am-i", login="hobbes", status=405)
|
|
122
134
|
api.patch("/who-am-i", login="hobbes", status=405)
|
|
123
135
|
api.delete("/who-am-i", login="hobbes", status=405)
|
|
124
136
|
|
|
137
|
+
def test_hello(api):
|
|
138
|
+
res = api.get("/hello", login="calvin", auth="none", status=200)
|
|
139
|
+
assert res.json["hello"] == "Hi"
|
|
140
|
+
assert res.headers["FSA-User"] == "None (None)"
|
|
141
|
+
res = api.get("/hello", login="hobbes", auth="none", status=200)
|
|
142
|
+
assert res.json["hello"] == "Salut"
|
|
143
|
+
assert res.headers["FSA-User"] == "None (None)"
|
|
144
|
+
res = api.get("/hello", login="susie", auth="none", status=200, cookies={"lang": "it"})
|
|
145
|
+
assert res.json["hello"] == "Ciao"
|
|
146
|
+
assert res.headers["FSA-User"] == "None (None)"
|
|
147
|
+
res = api.get("/hello", login="moe", auth="none", status=200)
|
|
148
|
+
assert res.json["hello"] == "Hi"
|
|
149
|
+
assert res.headers["FSA-User"] == "None (None)"
|
|
150
|
+
|
|
125
151
|
def test_authenticator_token():
|
|
126
152
|
# all token carriers
|
|
127
153
|
auth = ft.Authenticator(allow=["bearer", "header", "tparam", "cookie"])
|
|
@@ -129,25 +155,31 @@ def test_authenticator_token():
|
|
|
129
155
|
auth.setToken("susie", "ss-token")
|
|
130
156
|
auth.setToken("hobbes", "hbs-token")
|
|
131
157
|
auth.setToken("moe", "m-token")
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
auth.setCookie("calvin", "what", "clv-cookie")
|
|
159
|
+
kwargs, cookies = {}, {}
|
|
160
|
+
auth.setAuth("calvin", kwargs, cookies, auth="bearer")
|
|
134
161
|
assert kwargs["headers"]["Authorization"] == "Bearer clv-token"
|
|
135
|
-
|
|
136
|
-
|
|
162
|
+
assert cookies["what"] == "clv-cookie"
|
|
163
|
+
kwargs, cookies = {}, {}
|
|
164
|
+
auth.setAuth("susie", kwargs, cookies, auth="header")
|
|
137
165
|
assert kwargs["headers"]["Auth"] == "ss-token"
|
|
138
|
-
|
|
139
|
-
|
|
166
|
+
assert not cookies
|
|
167
|
+
kwargs, cookies = {"data": {}}, {}
|
|
168
|
+
auth.setAuth("hobbes", kwargs, cookies, auth="tparam")
|
|
140
169
|
assert kwargs["data"]["AUTH"] == "hbs-token"
|
|
141
|
-
|
|
142
|
-
|
|
170
|
+
assert not cookies
|
|
171
|
+
kwargs, cookies = {"json": {}}, {}
|
|
172
|
+
auth.setAuth("hobbes", kwargs, cookies, auth="tparam")
|
|
143
173
|
assert kwargs["json"]["AUTH"] == "hbs-token"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
174
|
+
assert not cookies
|
|
175
|
+
kwargs, cookies = {}, {}
|
|
176
|
+
auth.setAuth("moe", kwargs, cookies, auth="cookie")
|
|
177
|
+
assert not kwargs
|
|
178
|
+
assert cookies["auth"] == "m-token"
|
|
147
179
|
# dad does not have a token
|
|
148
180
|
try:
|
|
149
|
-
kwargs = {}
|
|
150
|
-
auth.setAuth("dad", kwargs)
|
|
181
|
+
kwargs, cookies = {}, {}
|
|
182
|
+
auth.setAuth("dad", kwargs, cookies)
|
|
151
183
|
pytest.fail("must raise an error") # pragma: no cover
|
|
152
184
|
except ft.FlaskTesterError:
|
|
153
185
|
assert True, "error raised"
|
|
@@ -161,8 +193,8 @@ def test_authenticator_token():
|
|
|
161
193
|
auth._has_pass = True
|
|
162
194
|
auth.setPass("rosalyn", "rsln-pass")
|
|
163
195
|
try:
|
|
164
|
-
kwargs={}
|
|
165
|
-
auth.setAuth("rosalyn", kwargs)
|
|
196
|
+
kwargs, cookies = {}, {}
|
|
197
|
+
auth.setAuth("rosalyn", kwargs, cookies)
|
|
166
198
|
pytest.fail("must raise an error") # pragma: no cover
|
|
167
199
|
except ft.FlaskTesterError:
|
|
168
200
|
assert True, "error raised"
|
|
@@ -174,21 +206,27 @@ def test_authenticator_password():
|
|
|
174
206
|
auth.setPass("hobbes", "hbs-pass")
|
|
175
207
|
auth.setPass("moe", "m-pass")
|
|
176
208
|
auth.setPass("rosalyn", "rsln-pass")
|
|
177
|
-
|
|
178
|
-
|
|
209
|
+
auth.setCookie("moe", "hello", "world!")
|
|
210
|
+
kwargs, cookies = {}, {}
|
|
211
|
+
auth.setAuth("calvin", kwargs, cookies, auth="basic")
|
|
179
212
|
assert kwargs["auth"] == ("calvin", "clv-pass")
|
|
180
|
-
|
|
181
|
-
|
|
213
|
+
assert not cookies
|
|
214
|
+
kwargs, cookies = {"data": {}}, {}
|
|
215
|
+
auth.setAuth("hobbes", kwargs, cookies, auth="param")
|
|
182
216
|
assert kwargs["data"]["USER"] == "hobbes" and kwargs["data"]["PASS"] == "hbs-pass"
|
|
183
|
-
|
|
184
|
-
|
|
217
|
+
assert not cookies
|
|
218
|
+
kwargs, cookies = {"json": {}}, {}
|
|
219
|
+
auth.setAuth("moe", kwargs, cookies, auth="param")
|
|
185
220
|
assert kwargs["json"]["USER"] == "moe" and kwargs["json"]["PASS"] == "m-pass"
|
|
186
|
-
|
|
187
|
-
|
|
221
|
+
assert cookies["hello"] == "world!"
|
|
222
|
+
kwargs, cookies = {"data": {}}, {}
|
|
223
|
+
auth.setAuth("rosalyn", kwargs, cookies, auth="fake")
|
|
188
224
|
assert kwargs["data"]["LOGIN"] == "rosalyn"
|
|
189
|
-
|
|
190
|
-
|
|
225
|
+
assert not cookies
|
|
226
|
+
kwargs, cookies = {"json": {}}, {}
|
|
227
|
+
auth.setAuth("hobbes", kwargs, cookies, auth="fake")
|
|
191
228
|
assert kwargs["json"]["LOGIN"] == "hobbes"
|
|
229
|
+
assert not cookies
|
|
192
230
|
# susie as a token, but no token carrier is allowed
|
|
193
231
|
try:
|
|
194
232
|
auth.setToken("susie", "ss-token")
|
|
@@ -199,8 +237,8 @@ def test_authenticator_password():
|
|
|
199
237
|
auth._has_token = True
|
|
200
238
|
auth.setToken("susie", "ss-token")
|
|
201
239
|
try:
|
|
202
|
-
kwargs={}
|
|
203
|
-
auth.setAuth("susie", kwargs)
|
|
240
|
+
kwargs, cookies = {}, {}
|
|
241
|
+
auth.setAuth("susie", kwargs, cookies)
|
|
204
242
|
pytest.fail("must raise an error") # pragma: no cover
|
|
205
243
|
except ft.FlaskTesterError:
|
|
206
244
|
assert True, "error raised"
|
|
@@ -233,7 +271,7 @@ def test_request_flask_response():
|
|
|
233
271
|
def test_client():
|
|
234
272
|
client = ft.Client(ft.Authenticator())
|
|
235
273
|
try:
|
|
236
|
-
client._request("GET", "/")
|
|
274
|
+
client._request("GET", "/", {})
|
|
237
275
|
pytest.fail("must raise an error") # pragma: no cover
|
|
238
276
|
except NotImplementedError:
|
|
239
277
|
assert True, "expected error raised"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|