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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: FlaskTester
3
- Version: 1.4
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
  ![Status](https://github.com/zx80/flask-tester/actions/workflows/package.yml/badge.svg?branch=main&style=flat)
55
- ![Tests](https://img.shields.io/badge/tests-10%20✓-success)
55
+ ![Tests](https://img.shields.io/badge/tests-11%20✓-success)
56
56
  ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
57
57
  ![Issues](https://img.shields.io/github/issues/zx80/flask-tester?style=flat)
58
58
  ![Python](https://img.shields.io/badge/python-3-informational)
@@ -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
- - `setAuth` to add authentication data to a request `kwargs`.
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 `setToken` are forwarded to the internal authenticator.
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.
@@ -13,7 +13,7 @@ twine
13
13
  wheel
14
14
  types-flask
15
15
  types-requests
16
- FlaskSimpleAuth
16
+ FlaskSimpleAuth>=29.5
17
17
  passlib
18
18
 
19
19
  [doc]
@@ -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
- _AUTH_SCHEMES = {"fake"}
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, login: str, val: str|None, store: dict[str, str]):
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 login in store:
107
- del store[login]
109
+ if key in store:
110
+ del store[key]
108
111
  else:
109
112
  assert isinstance(val, str)
110
- store[login] = val
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
- - auth: authentication method, default None is to try allowed schemes, tokens first.
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
- kwargs["headers"]["Authorization"] = self._bearer + " " + token
194
+ headers["Authorization"] = self._bearer + " " + token
175
195
  elif self._try_auth(auth, "header"):
176
- kwargs["headers"][self._header] = token
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
- # FIXME cookie?
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 not ``login`` is set.
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 _request(self, method: str, path: str, **kwargs):
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
- self._auth.setAuth(login, kwargs, auth=auth)
288
- res = self._request(method, path, **kwargs) # type: ignore
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 not ``login`` is set.
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 not ``login`` is set.
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: 1.4
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
  ![Status](https://github.com/zx80/flask-tester/actions/workflows/package.yml/badge.svg?branch=main&style=flat)
55
- ![Tests](https://img.shields.io/badge/tests-10%20✓-success)
55
+ ![Tests](https://img.shields.io/badge/tests-11%20✓-success)
56
56
  ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
57
57
  ![Issues](https://img.shields.io/github/issues/zx80/flask-tester?style=flat)
58
58
  ![Python](https://img.shields.io/badge/python-3-informational)
@@ -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
- - `setAuth` to add authentication data to a request `kwargs`.
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 `setToken` are forwarded to the internal authenticator.
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
  ![Status](https://github.com/zx80/flask-tester/actions/workflows/package.yml/badge.svg?branch=main&style=flat)
12
- ![Tests](https://img.shields.io/badge/tests-10%20✓-success)
12
+ ![Tests](https://img.shields.io/badge/tests-11%20✓-success)
13
13
  ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
14
14
  ![Issues](https://img.shields.io/github/issues/zx80/flask-tester?style=flat)
15
15
  ![Python](https://img.shields.io/badge/python-3-informational)
@@ -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
- - `setAuth` to add authentication data to a request `kwargs`.
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 `setToken` are forwarded to the internal authenticator.
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 = "1.4"
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
- # check that token auth is ok
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="hobbes", status=200)
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
- kwargs = {}
133
- auth.setAuth("calvin", kwargs, auth="bearer")
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
- kwargs = {}
136
- auth.setAuth("susie", kwargs, auth="header")
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
- kwargs = {"data": {}}
139
- auth.setAuth("hobbes", kwargs, auth="tparam")
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
- kwargs = {"json": {}}
142
- auth.setAuth("hobbes", kwargs, auth="tparam")
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
- kwargs = {}
145
- auth.setAuth("moe", kwargs, auth="cookie")
146
- assert kwargs["headers"]["Cookie"] == "auth=m-token"
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
- kwargs = {}
178
- auth.setAuth("calvin", kwargs, auth="basic")
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
- kwargs = {"data": {}}
181
- auth.setAuth("hobbes", kwargs, auth="param")
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
- kwargs = {"json": {}}
184
- auth.setAuth("moe", kwargs, auth="param")
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
- kwargs = {"data": {}}
187
- auth.setAuth("rosalyn", kwargs, auth="fake")
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
- kwargs = {"json": {}}
190
- auth.setAuth("hobbes", kwargs, auth="fake")
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