FlaskTester 1.4__tar.gz → 2.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-2.0}/FlaskTester.egg-info/PKG-INFO +13 -3
- {FlaskTester-1.4 → FlaskTester-2.0}/FlaskTester.egg-info/requires.txt +1 -1
- {FlaskTester-1.4 → FlaskTester-2.0}/FlaskTester.py +74 -28
- {FlaskTester-1.4 → FlaskTester-2.0}/PKG-INFO +13 -3
- {FlaskTester-1.4 → FlaskTester-2.0}/README.md +11 -1
- {FlaskTester-1.4 → FlaskTester-2.0}/pyproject.toml +2 -2
- {FlaskTester-1.4 → FlaskTester-2.0}/tests/test.py +55 -33
- {FlaskTester-1.4 → FlaskTester-2.0}/FlaskTester.egg-info/SOURCES.txt +0 -0
- {FlaskTester-1.4 → FlaskTester-2.0}/FlaskTester.egg-info/dependency_links.txt +0 -0
- {FlaskTester-1.4 → FlaskTester-2.0}/FlaskTester.egg-info/top_level.txt +0 -0
- {FlaskTester-1.4 → FlaskTester-2.0}/LICENSE +0 -0
- {FlaskTester-1.4 → FlaskTester-2.0}/MANIFEST.in +0 -0
- {FlaskTester-1.4 → FlaskTester-2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: FlaskTester
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.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.4; extra == "dev"
|
|
36
36
|
Requires-Dist: passlib; extra == "dev"
|
|
37
37
|
Provides-Extra: doc
|
|
38
38
|
Requires-Dist: sphinx; extra == "doc"
|
|
@@ -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
|
|
|
@@ -155,6 +158,7 @@ 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.
|
|
161
|
+
- `setCookie` to add a cookie to a user, set value to _None_ to remove cookie.
|
|
158
162
|
- `setAuth` to add authentication data to a request `kwargs`.
|
|
159
163
|
|
|
160
164
|
- `ft_client` for app testing, which depends on the previous fixture, plus
|
|
@@ -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,12 @@ 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
|
+
### 2.0 on 2024-03-23
|
|
246
|
+
|
|
247
|
+
Add support for cookies.
|
|
248
|
+
Improved documentation and code.
|
|
249
|
+
Improved tests.
|
|
250
|
+
|
|
241
251
|
### 1.4 on 2024-03-19
|
|
242
252
|
|
|
243
253
|
Test expected assert failures.
|
|
@@ -60,6 +60,7 @@ class Authenticator:
|
|
|
60
60
|
_TOKEN_SCHEMES = {"bearer", "header", "cookie", "tparam"}
|
|
61
61
|
_PASS_SCHEMES = {"basic", "param"}
|
|
62
62
|
|
|
63
|
+
# all supported authentication schemes
|
|
63
64
|
_AUTH_SCHEMES = {"fake"}
|
|
64
65
|
_AUTH_SCHEMES.update(_TOKEN_SCHEMES)
|
|
65
66
|
_AUTH_SCHEMES.update(_PASS_SCHEMES)
|
|
@@ -96,21 +97,25 @@ class Authenticator:
|
|
|
96
97
|
self._cookie = cookie
|
|
97
98
|
self._tparam = tparam
|
|
98
99
|
|
|
99
|
-
# password and token credentials
|
|
100
|
+
# password and token credentials, cookies
|
|
100
101
|
self._passes: dict[str, str] = {}
|
|
101
102
|
self._tokens: dict[str, str] = {}
|
|
103
|
+
self._cookies: dict[str, dict[str, str]] = {}
|
|
102
104
|
|
|
103
|
-
def _set(self,
|
|
105
|
+
def _set(self, key: str, val: str|None, store: dict[str, str]):
|
|
104
106
|
"""Set a key/value in a directory, with None for delete."""
|
|
105
107
|
if val is None:
|
|
106
|
-
if
|
|
107
|
-
del store[
|
|
108
|
+
if key in store:
|
|
109
|
+
del store[key]
|
|
108
110
|
else:
|
|
109
111
|
assert isinstance(val, str)
|
|
110
|
-
store[
|
|
112
|
+
store[key] = val
|
|
111
113
|
|
|
112
114
|
def setPass(self, login: str, pw: str|None):
|
|
113
|
-
"""Associate a password to a user.
|
|
115
|
+
"""Associate a password to a user.
|
|
116
|
+
|
|
117
|
+
Set to *None* to remove the password entry.
|
|
118
|
+
"""
|
|
114
119
|
if not self._has_pass:
|
|
115
120
|
raise AuthError("cannot set password, no password scheme allowed")
|
|
116
121
|
self._set(login, pw, self._passes)
|
|
@@ -122,11 +127,20 @@ class Authenticator:
|
|
|
122
127
|
self.setPass(login, pw)
|
|
123
128
|
|
|
124
129
|
def setToken(self, login: str, token: str|None):
|
|
125
|
-
"""Associate a token to a user.
|
|
130
|
+
"""Associate a token to a user.
|
|
131
|
+
|
|
132
|
+
Set to *None* to remove the token entry.
|
|
133
|
+
"""
|
|
126
134
|
if not self._has_token:
|
|
127
135
|
raise AuthError("cannot set token, no token scheme allowed")
|
|
128
136
|
self._set(login, token, self._tokens)
|
|
129
137
|
|
|
138
|
+
def setCookie(self, login: str, name: str, val: str|None = None):
|
|
139
|
+
"""Associate a cookie and its value to a login, *None* to remove."""
|
|
140
|
+
if login not in self._cookies:
|
|
141
|
+
self._cookies[login] = {}
|
|
142
|
+
self._set(name, val, self._cookies[login])
|
|
143
|
+
|
|
130
144
|
def _param(self, kwargs: dict[str, Any], key: str, val: Any):
|
|
131
145
|
"""Add request parameter to "json" or "data"."""
|
|
132
146
|
|
|
@@ -143,12 +157,16 @@ class Authenticator:
|
|
|
143
157
|
"""Whether to try this authentication scheme."""
|
|
144
158
|
return auth in (None, scheme) and scheme in self._allow
|
|
145
159
|
|
|
146
|
-
def setAuth(self, login: str|None, kwargs: dict[str, Any], auth: str|None = None):
|
|
160
|
+
def setAuth(self, login: str|None, kwargs: dict[str, Any], cookies: dict[str, str], auth: str|None = None):
|
|
147
161
|
"""Set request authentication.
|
|
148
162
|
|
|
149
163
|
- login: login target, None means no authentication
|
|
150
164
|
- kwargs: request parameters
|
|
151
|
-
-
|
|
165
|
+
- cookies: request cookies
|
|
166
|
+
- auth: authentication method, default is None
|
|
167
|
+
|
|
168
|
+
The default behavior is to try allowed schemes: tokens first,
|
|
169
|
+
then password, then fake.
|
|
152
170
|
"""
|
|
153
171
|
|
|
154
172
|
log.debug(f"setAuth: login={login} auth={auth} allow={self._allow}")
|
|
@@ -156,29 +174,29 @@ class Authenticator:
|
|
|
156
174
|
if login is None: # not needed
|
|
157
175
|
return
|
|
158
176
|
|
|
177
|
+
cookies.update(self._cookies.get(login, {}))
|
|
178
|
+
|
|
159
179
|
if auth is not None:
|
|
160
180
|
if auth not in self._AUTH_SCHEMES:
|
|
161
181
|
raise AuthError(f"unexpected auth: {auth}")
|
|
162
182
|
if auth not in self._allow:
|
|
163
183
|
raise AuthError(f"auth is not allowed: {auth}")
|
|
164
184
|
|
|
185
|
+
headers = kwargs.get("headers", {})
|
|
186
|
+
|
|
165
187
|
# use token if available and allowed
|
|
166
188
|
if login in self._tokens and auth in (None, "bearer", "header", "cookie", "tparam"):
|
|
167
189
|
|
|
168
190
|
token = self._tokens[login]
|
|
169
191
|
|
|
170
|
-
if "headers" not in kwargs:
|
|
171
|
-
kwargs["headers"] = {}
|
|
172
|
-
|
|
173
192
|
if self._try_auth(auth, "bearer"):
|
|
174
|
-
|
|
193
|
+
headers["Authorization"] = self._bearer + " " + token
|
|
175
194
|
elif self._try_auth(auth, "header"):
|
|
176
|
-
|
|
195
|
+
headers[self._header] = token
|
|
177
196
|
elif self._try_auth(auth, "tparam"):
|
|
178
197
|
self._param(kwargs, self._tparam, token)
|
|
179
198
|
elif self._try_auth(auth, "cookie"):
|
|
180
|
-
|
|
181
|
-
kwargs["headers"]["Cookie"] = self._cookie + "=" + token
|
|
199
|
+
cookies[self._cookie] = token
|
|
182
200
|
else:
|
|
183
201
|
raise AuthError(f"no token carrier: login={login} auth={auth} allow={self._allow}")
|
|
184
202
|
|
|
@@ -200,6 +218,9 @@ class Authenticator:
|
|
|
200
218
|
|
|
201
219
|
raise AuthError(f"no authentication for login={login} auth={auth} allow={self._allow}")
|
|
202
220
|
|
|
221
|
+
if headers: # put headers back if needed
|
|
222
|
+
kwargs["headers"] = headers
|
|
223
|
+
|
|
203
224
|
|
|
204
225
|
class RequestFlaskResponse:
|
|
205
226
|
"""Wrapper to return a Flask-looking response from a request response.
|
|
@@ -238,16 +259,17 @@ class RequestFlaskResponse:
|
|
|
238
259
|
|
|
239
260
|
|
|
240
261
|
class Client:
|
|
241
|
-
"""Common class for flask authenticated testing.
|
|
262
|
+
"""Common (partial) class for flask authenticated testing.
|
|
242
263
|
|
|
243
264
|
Constructor parameters:
|
|
244
265
|
|
|
245
266
|
- ``auth`` authenticator
|
|
246
|
-
- ``default_login`` if
|
|
267
|
+
- ``default_login`` if ``login`` is not set.
|
|
247
268
|
"""
|
|
248
269
|
|
|
249
270
|
def __init__(self, auth: Authenticator, default_login: str|None = None):
|
|
250
271
|
self._auth = auth
|
|
272
|
+
self._cookies: dict[str, dict[str, str]] = {} # login -> name -> value
|
|
251
273
|
self._default_login = default_login
|
|
252
274
|
|
|
253
275
|
def setToken(self, login: str, token: str|None):
|
|
@@ -258,7 +280,11 @@ class Client:
|
|
|
258
280
|
"""Associate a password to a login, *None* to remove."""
|
|
259
281
|
self._auth.setPass(login, password)
|
|
260
282
|
|
|
261
|
-
def
|
|
283
|
+
def setCookie(self, login: str, name: str, val: str|None):
|
|
284
|
+
"""Associate a cookie to a login, *None* name to remove."""
|
|
285
|
+
self._auth.setCookie(login, name, val)
|
|
286
|
+
|
|
287
|
+
def _request(self, method: str, path: str, cookies: dict[str, str], **kwargs):
|
|
262
288
|
"""Run a request and return response."""
|
|
263
289
|
raise NotImplementedError()
|
|
264
290
|
|
|
@@ -274,7 +300,7 @@ class Client:
|
|
|
274
300
|
|
|
275
301
|
- ``status``: expected HTTP status, *None* to skip status check
|
|
276
302
|
- ``login``: authenticated user, use **explicit** *None* to skip
|
|
277
|
-
- ``auth``: authentication scheme to use
|
|
303
|
+
- ``auth``: authentication scheme to use instead of default behavior
|
|
278
304
|
- ``**kwargs``: more request parameters (headers, data, json…)
|
|
279
305
|
"""
|
|
280
306
|
|
|
@@ -284,8 +310,13 @@ class Client:
|
|
|
284
310
|
else: # if unset, use default
|
|
285
311
|
login = self._default_login
|
|
286
312
|
|
|
287
|
-
|
|
288
|
-
|
|
313
|
+
cookies: dict[str, str] = {}
|
|
314
|
+
if "cookies" in kwargs:
|
|
315
|
+
cookies.update(kwargs["cookies"])
|
|
316
|
+
del kwargs["cookies"]
|
|
317
|
+
|
|
318
|
+
self._auth.setAuth(login, kwargs, cookies, auth=auth)
|
|
319
|
+
res = self._request(method, path, cookies, **kwargs) # type: ignore
|
|
289
320
|
|
|
290
321
|
if status is not None:
|
|
291
322
|
if res.status_code != status: # show error before aborting
|
|
@@ -349,7 +380,7 @@ class RequestClient(Client):
|
|
|
349
380
|
|
|
350
381
|
- ``auth`` authenticator
|
|
351
382
|
- ``base_url`` target server
|
|
352
|
-
- ``default_login`` if
|
|
383
|
+
- ``default_login`` if ``login`` is not set.
|
|
353
384
|
"""
|
|
354
385
|
|
|
355
386
|
def __init__(self, auth: Authenticator, base_url: str, default_login=None):
|
|
@@ -359,7 +390,7 @@ class RequestClient(Client):
|
|
|
359
390
|
from requests import Session
|
|
360
391
|
self._requests = Session()
|
|
361
392
|
|
|
362
|
-
def _request(self, method: str, path: str, **kwargs):
|
|
393
|
+
def _request(self, method: str, path: str, cookies: dict[str, str], **kwargs):
|
|
363
394
|
"""Actual request handling."""
|
|
364
395
|
|
|
365
396
|
if "data" in kwargs:
|
|
@@ -383,7 +414,7 @@ class RequestClient(Client):
|
|
|
383
414
|
# sanity
|
|
384
415
|
assert not (files and "json" in kwargs), "cannot mix file upload and json?"
|
|
385
416
|
|
|
386
|
-
res = self._requests.request(method, self._base_url + path, **kwargs)
|
|
417
|
+
res = self._requests.request(method, self._base_url + path, cookies=cookies, **kwargs)
|
|
387
418
|
|
|
388
419
|
return RequestFlaskResponse(res)
|
|
389
420
|
|
|
@@ -394,16 +425,29 @@ class FlaskClient(Client):
|
|
|
394
425
|
Constructor parameters:
|
|
395
426
|
|
|
396
427
|
- ``auth`` authenticator
|
|
397
|
-
- ``client`` Flask ``test_client``
|
|
398
|
-
- ``default_login`` if
|
|
428
|
+
- ``client`` Flask actual ``test_client``
|
|
429
|
+
- ``default_login`` if ``login`` is not set.
|
|
430
|
+
|
|
431
|
+
Note: this client handles `cookies`.
|
|
399
432
|
"""
|
|
400
433
|
|
|
401
434
|
def __init__(self, auth: Authenticator, client, default_login=None):
|
|
402
435
|
super().__init__(auth, default_login)
|
|
403
436
|
self._client = client
|
|
437
|
+
self._cookie_names: set[str] = set() # FIXME all encountered cookies for cleanup :-/
|
|
404
438
|
|
|
405
|
-
def _request(self, method: str, path: str, **kwargs):
|
|
439
|
+
def _request(self, method: str, path: str, cookies: dict[str, str], **kwargs):
|
|
406
440
|
"""Actual request handling."""
|
|
441
|
+
|
|
442
|
+
# hack to cleanup client state :-/
|
|
443
|
+
for cookie in self._cookie_names:
|
|
444
|
+
self._client.delete_cookie(cookie)
|
|
445
|
+
|
|
446
|
+
for cookie, val in cookies.items():
|
|
447
|
+
if cookie not in self._cookie_names:
|
|
448
|
+
self._cookie_names.add(cookie)
|
|
449
|
+
self._client.set_cookie(cookie, val)
|
|
450
|
+
|
|
407
451
|
return self._client.open(method=method, path=path, **kwargs)
|
|
408
452
|
|
|
409
453
|
|
|
@@ -415,6 +459,7 @@ def ft_authenticator():
|
|
|
415
459
|
|
|
416
460
|
- ``FLASK_TESTER_LOG_LEVEL``: package log level in
|
|
417
461
|
``DEBUG INFO WARNING ERROR CRITICAL NOSET``.
|
|
462
|
+
Default is ``NOTSET``.
|
|
418
463
|
- ``FLASK_TESTER_ALLOW``: allowed space-separated authentication schemes, in
|
|
419
464
|
``basic param bearer header cookie tparam fake``.
|
|
420
465
|
Default is ``bearer basic param``.
|
|
@@ -433,6 +478,7 @@ def ft_authenticator():
|
|
|
433
478
|
- ``FLASK_TESTER_TPARAM``: parameter for *token* authentication.
|
|
434
479
|
Default is ``AUTH``.
|
|
435
480
|
- ``FLASK_TESTER_AUTH``: initial comma-separated list of *login:password*.
|
|
481
|
+
Default is not set.
|
|
436
482
|
"""
|
|
437
483
|
|
|
438
484
|
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: 2.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.4; extra == "dev"
|
|
36
36
|
Requires-Dist: passlib; extra == "dev"
|
|
37
37
|
Provides-Extra: doc
|
|
38
38
|
Requires-Dist: sphinx; extra == "doc"
|
|
@@ -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
|
|
|
@@ -155,6 +158,7 @@ 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.
|
|
161
|
+
- `setCookie` to add a cookie to a user, set value to _None_ to remove cookie.
|
|
158
162
|
- `setAuth` to add authentication data to a request `kwargs`.
|
|
159
163
|
|
|
160
164
|
- `ft_client` for app testing, which depends on the previous fixture, plus
|
|
@@ -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,12 @@ 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
|
+
### 2.0 on 2024-03-23
|
|
246
|
+
|
|
247
|
+
Add support for cookies.
|
|
248
|
+
Improved documentation and code.
|
|
249
|
+
Improved tests.
|
|
250
|
+
|
|
241
251
|
### 1.4 on 2024-03-19
|
|
242
252
|
|
|
243
253
|
Test expected assert failures.
|
|
@@ -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
|
|
|
@@ -112,6 +115,7 @@ 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.
|
|
118
|
+
- `setCookie` to add a cookie to a user, set value to _None_ to remove cookie.
|
|
115
119
|
- `setAuth` to add authentication data to a request `kwargs`.
|
|
116
120
|
|
|
117
121
|
- `ft_client` for app testing, which depends on the previous fixture, plus
|
|
@@ -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,12 @@ 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
|
+
### 2.0 on 2024-03-23
|
|
203
|
+
|
|
204
|
+
Add support for cookies.
|
|
205
|
+
Improved documentation and code.
|
|
206
|
+
Improved tests.
|
|
207
|
+
|
|
198
208
|
### 1.4 on 2024-03-19
|
|
199
209
|
|
|
200
210
|
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 = "2.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.4", "passlib"]
|
|
37
37
|
# documentation generation
|
|
38
38
|
doc = ["sphinx", "sphinx_rtd_theme", "sphinx-autoapi", "sphinx-lint", "myst_parser"]
|
|
39
39
|
|
|
@@ -10,6 +10,8 @@ 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(
|
|
@@ -20,6 +22,7 @@ os.environ.update(
|
|
|
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_EN")
|
|
70
|
+
ft_client.setCookie("hobbes", "lang", "fr_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,13 @@ 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
|
-
# check that token auth is ok
|
|
81
|
+
# check that token auth and cookie is ok
|
|
73
82
|
res = ft_client.get("/who-am-i", login="calvin", status=200, auth="bearer")
|
|
74
|
-
assert res.json["user"] == "calvin"
|
|
83
|
+
assert res.json["user"] == "calvin" and res.json["lang"] == "en_EN"
|
|
75
84
|
res = ft_client.get("/who-am-i", login="hobbes", status=200, auth="bearer")
|
|
76
|
-
assert res.json["user"] == "hobbes"
|
|
85
|
+
assert res.json["user"] == "hobbes" and res.json["lang"] == "fr_FR"
|
|
77
86
|
res = ft_client.get("/who-am-i", login="susie", status=200, auth="bearer")
|
|
78
|
-
assert res.json["user"] == "susie"
|
|
87
|
+
assert res.json["user"] == "susie" and res.json["lang"] is None
|
|
79
88
|
# with defaults
|
|
80
89
|
res = ft_client.get("/who-am-i", auth="basic", status=200)
|
|
81
90
|
assert res.json["user"] == "calvin"
|
|
@@ -116,7 +125,8 @@ def test_errors(api):
|
|
|
116
125
|
assert True, "expected error"
|
|
117
126
|
|
|
118
127
|
def test_methods(api):
|
|
119
|
-
api.get("/who-am-i", login="
|
|
128
|
+
res = api.get("/who-am-i", login="susie", status=200, cookies={"lang": "python"})
|
|
129
|
+
assert res.json["lang"] == "python"
|
|
120
130
|
api.post("/who-am-i", login="hobbes", status=405)
|
|
121
131
|
api.put("/who-am-i", login="hobbes", status=405)
|
|
122
132
|
api.patch("/who-am-i", login="hobbes", status=405)
|
|
@@ -129,25 +139,31 @@ def test_authenticator_token():
|
|
|
129
139
|
auth.setToken("susie", "ss-token")
|
|
130
140
|
auth.setToken("hobbes", "hbs-token")
|
|
131
141
|
auth.setToken("moe", "m-token")
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
auth.setCookie("calvin", "what", "clv-cookie")
|
|
143
|
+
kwargs, cookies = {}, {}
|
|
144
|
+
auth.setAuth("calvin", kwargs, cookies, auth="bearer")
|
|
134
145
|
assert kwargs["headers"]["Authorization"] == "Bearer clv-token"
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
assert cookies["what"] == "clv-cookie"
|
|
147
|
+
kwargs, cookies = {}, {}
|
|
148
|
+
auth.setAuth("susie", kwargs, cookies, auth="header")
|
|
137
149
|
assert kwargs["headers"]["Auth"] == "ss-token"
|
|
138
|
-
|
|
139
|
-
|
|
150
|
+
assert not cookies
|
|
151
|
+
kwargs, cookies = {"data": {}}, {}
|
|
152
|
+
auth.setAuth("hobbes", kwargs, cookies, auth="tparam")
|
|
140
153
|
assert kwargs["data"]["AUTH"] == "hbs-token"
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
assert not cookies
|
|
155
|
+
kwargs, cookies = {"json": {}}, {}
|
|
156
|
+
auth.setAuth("hobbes", kwargs, cookies, auth="tparam")
|
|
143
157
|
assert kwargs["json"]["AUTH"] == "hbs-token"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
158
|
+
assert not cookies
|
|
159
|
+
kwargs, cookies = {}, {}
|
|
160
|
+
auth.setAuth("moe", kwargs, cookies, auth="cookie")
|
|
161
|
+
assert not kwargs
|
|
162
|
+
assert cookies["auth"] == "m-token"
|
|
147
163
|
# dad does not have a token
|
|
148
164
|
try:
|
|
149
|
-
kwargs = {}
|
|
150
|
-
auth.setAuth("dad", kwargs)
|
|
165
|
+
kwargs, cookies = {}, {}
|
|
166
|
+
auth.setAuth("dad", kwargs, cookies)
|
|
151
167
|
pytest.fail("must raise an error") # pragma: no cover
|
|
152
168
|
except ft.FlaskTesterError:
|
|
153
169
|
assert True, "error raised"
|
|
@@ -161,8 +177,8 @@ def test_authenticator_token():
|
|
|
161
177
|
auth._has_pass = True
|
|
162
178
|
auth.setPass("rosalyn", "rsln-pass")
|
|
163
179
|
try:
|
|
164
|
-
kwargs={}
|
|
165
|
-
auth.setAuth("rosalyn", kwargs)
|
|
180
|
+
kwargs, cookies = {}, {}
|
|
181
|
+
auth.setAuth("rosalyn", kwargs, cookies)
|
|
166
182
|
pytest.fail("must raise an error") # pragma: no cover
|
|
167
183
|
except ft.FlaskTesterError:
|
|
168
184
|
assert True, "error raised"
|
|
@@ -174,21 +190,27 @@ def test_authenticator_password():
|
|
|
174
190
|
auth.setPass("hobbes", "hbs-pass")
|
|
175
191
|
auth.setPass("moe", "m-pass")
|
|
176
192
|
auth.setPass("rosalyn", "rsln-pass")
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
auth.setCookie("moe", "hello", "world!")
|
|
194
|
+
kwargs, cookies = {}, {}
|
|
195
|
+
auth.setAuth("calvin", kwargs, cookies, auth="basic")
|
|
179
196
|
assert kwargs["auth"] == ("calvin", "clv-pass")
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
assert not cookies
|
|
198
|
+
kwargs, cookies = {"data": {}}, {}
|
|
199
|
+
auth.setAuth("hobbes", kwargs, cookies, auth="param")
|
|
182
200
|
assert kwargs["data"]["USER"] == "hobbes" and kwargs["data"]["PASS"] == "hbs-pass"
|
|
183
|
-
|
|
184
|
-
|
|
201
|
+
assert not cookies
|
|
202
|
+
kwargs, cookies = {"json": {}}, {}
|
|
203
|
+
auth.setAuth("moe", kwargs, cookies, auth="param")
|
|
185
204
|
assert kwargs["json"]["USER"] == "moe" and kwargs["json"]["PASS"] == "m-pass"
|
|
186
|
-
|
|
187
|
-
|
|
205
|
+
assert cookies["hello"] == "world!"
|
|
206
|
+
kwargs, cookies = {"data": {}}, {}
|
|
207
|
+
auth.setAuth("rosalyn", kwargs, cookies, auth="fake")
|
|
188
208
|
assert kwargs["data"]["LOGIN"] == "rosalyn"
|
|
189
|
-
|
|
190
|
-
|
|
209
|
+
assert not cookies
|
|
210
|
+
kwargs, cookies = {"json": {}}, {}
|
|
211
|
+
auth.setAuth("hobbes", kwargs, cookies, auth="fake")
|
|
191
212
|
assert kwargs["json"]["LOGIN"] == "hobbes"
|
|
213
|
+
assert not cookies
|
|
192
214
|
# susie as a token, but no token carrier is allowed
|
|
193
215
|
try:
|
|
194
216
|
auth.setToken("susie", "ss-token")
|
|
@@ -199,8 +221,8 @@ def test_authenticator_password():
|
|
|
199
221
|
auth._has_token = True
|
|
200
222
|
auth.setToken("susie", "ss-token")
|
|
201
223
|
try:
|
|
202
|
-
kwargs={}
|
|
203
|
-
auth.setAuth("susie", kwargs)
|
|
224
|
+
kwargs, cookies = {}, {}
|
|
225
|
+
auth.setAuth("susie", kwargs, cookies)
|
|
204
226
|
pytest.fail("must raise an error") # pragma: no cover
|
|
205
227
|
except ft.FlaskTesterError:
|
|
206
228
|
assert True, "error raised"
|
|
@@ -233,7 +255,7 @@ def test_request_flask_response():
|
|
|
233
255
|
def test_client():
|
|
234
256
|
client = ft.Client(ft.Authenticator())
|
|
235
257
|
try:
|
|
236
|
-
client._request("GET", "/")
|
|
258
|
+
client._request("GET", "/", {})
|
|
237
259
|
pytest.fail("must raise an error") # pragma: no cover
|
|
238
260
|
except NotImplementedError:
|
|
239
261
|
assert True, "expected error raised"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|