FlaskTester 1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: FlaskTester
3
- Version: 1.3
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"
@@ -65,7 +65,7 @@ achieved through environment variables.
65
65
  Install package with `pip install FlaskTester` or equivalent.
66
66
 
67
67
  The following test creates a local fixture with 2 users identified by a
68
- password, and retrieves a token for the first user using a `/login` route
68
+ password, and retrieves tokens for both users using a `/login` route
69
69
  provided by the application.
70
70
  It then proceeds to run authenticated requests against the `/admin` route.
71
71
 
@@ -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 `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`.
@@ -225,14 +229,30 @@ The implementation of these fixtures is based on five classes plus exceptions:
225
229
 
226
230
  ## License
227
231
 
228
- This code is public domain.
232
+ This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
233
+
234
+ All software has bug, this is software, hence… Beware that you may lose your
235
+ hairs or your friends because of it. If you like it, feel free to send a
236
+ postcard to the author.
237
+
238
+ ## Versions
229
239
 
230
240
  Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
231
241
  [sources](https://github.com/zx80/flask-tester) are available on GitHub,
232
242
  see also the [documentation](https://zx80.github.io/flask-tester/),
233
243
  please report any [issues](https://github.com/zx80/flask-tester/issues).
234
244
 
235
- ## Versions
245
+ ### 2.0 on 2024-03-23
246
+
247
+ Add support for cookies.
248
+ Improved documentation and code.
249
+ Improved tests.
250
+
251
+ ### 1.4 on 2024-03-19
252
+
253
+ Test expected assert failures.
254
+ Improved API documentation.
255
+ Keep first found app.
236
256
 
237
257
  ### 1.3 on 2024-03-16
238
258
 
@@ -13,7 +13,7 @@ twine
13
13
  wheel
14
14
  types-flask
15
15
  types-requests
16
- FlaskSimpleAuth
16
+ FlaskSimpleAuth>=29.4
17
17
  passlib
18
18
 
19
19
  [doc]
@@ -1,3 +1,8 @@
1
+ """FlaskTester - Pytest fixtures for Flask authenticated internal and external tests.
2
+
3
+ Pytest: PYTEST_DONT_REWRITE
4
+ """
5
+
1
6
  import os
2
7
  import io
3
8
  import re
@@ -55,6 +60,7 @@ class Authenticator:
55
60
  _TOKEN_SCHEMES = {"bearer", "header", "cookie", "tparam"}
56
61
  _PASS_SCHEMES = {"basic", "param"}
57
62
 
63
+ # all supported authentication schemes
58
64
  _AUTH_SCHEMES = {"fake"}
59
65
  _AUTH_SCHEMES.update(_TOKEN_SCHEMES)
60
66
  _AUTH_SCHEMES.update(_PASS_SCHEMES)
@@ -91,21 +97,25 @@ class Authenticator:
91
97
  self._cookie = cookie
92
98
  self._tparam = tparam
93
99
 
94
- # password and token credentials
100
+ # password and token credentials, cookies
95
101
  self._passes: dict[str, str] = {}
96
102
  self._tokens: dict[str, str] = {}
103
+ self._cookies: dict[str, dict[str, str]] = {}
97
104
 
98
- def _set(self, login: str, val: str|None, store: dict[str, str]):
105
+ def _set(self, key: str, val: str|None, store: dict[str, str]):
99
106
  """Set a key/value in a directory, with None for delete."""
100
107
  if val is None:
101
- if login in store:
102
- del store[login]
108
+ if key in store:
109
+ del store[key]
103
110
  else:
104
111
  assert isinstance(val, str)
105
- store[login] = val
112
+ store[key] = val
106
113
 
107
114
  def setPass(self, login: str, pw: str|None):
108
- """Associate a password to a user."""
115
+ """Associate a password to a user.
116
+
117
+ Set to *None* to remove the password entry.
118
+ """
109
119
  if not self._has_pass:
110
120
  raise AuthError("cannot set password, no password scheme allowed")
111
121
  self._set(login, pw, self._passes)
@@ -117,11 +127,20 @@ class Authenticator:
117
127
  self.setPass(login, pw)
118
128
 
119
129
  def setToken(self, login: str, token: str|None):
120
- """Associate a token to a user."""
130
+ """Associate a token to a user.
131
+
132
+ Set to *None* to remove the token entry.
133
+ """
121
134
  if not self._has_token:
122
135
  raise AuthError("cannot set token, no token scheme allowed")
123
136
  self._set(login, token, self._tokens)
124
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
+
125
144
  def _param(self, kwargs: dict[str, Any], key: str, val: Any):
126
145
  """Add request parameter to "json" or "data"."""
127
146
 
@@ -138,12 +157,16 @@ class Authenticator:
138
157
  """Whether to try this authentication scheme."""
139
158
  return auth in (None, scheme) and scheme in self._allow
140
159
 
141
- 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):
142
161
  """Set request authentication.
143
162
 
144
163
  - login: login target, None means no authentication
145
164
  - kwargs: request parameters
146
- - auth: authentication method, default None is to try allowed schemes, tokens first.
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.
147
170
  """
148
171
 
149
172
  log.debug(f"setAuth: login={login} auth={auth} allow={self._allow}")
@@ -151,29 +174,29 @@ class Authenticator:
151
174
  if login is None: # not needed
152
175
  return
153
176
 
177
+ cookies.update(self._cookies.get(login, {}))
178
+
154
179
  if auth is not None:
155
180
  if auth not in self._AUTH_SCHEMES:
156
181
  raise AuthError(f"unexpected auth: {auth}")
157
182
  if auth not in self._allow:
158
183
  raise AuthError(f"auth is not allowed: {auth}")
159
184
 
185
+ headers = kwargs.get("headers", {})
186
+
160
187
  # use token if available and allowed
161
188
  if login in self._tokens and auth in (None, "bearer", "header", "cookie", "tparam"):
162
189
 
163
190
  token = self._tokens[login]
164
191
 
165
- if "headers" not in kwargs:
166
- kwargs["headers"] = {}
167
-
168
192
  if self._try_auth(auth, "bearer"):
169
- kwargs["headers"]["Authorization"] = self._bearer + " " + token
193
+ headers["Authorization"] = self._bearer + " " + token
170
194
  elif self._try_auth(auth, "header"):
171
- kwargs["headers"][self._header] = token
195
+ headers[self._header] = token
172
196
  elif self._try_auth(auth, "tparam"):
173
197
  self._param(kwargs, self._tparam, token)
174
198
  elif self._try_auth(auth, "cookie"):
175
- # FIXME cookie?
176
- kwargs["headers"]["Cookie"] = self._cookie + "=" + token
199
+ cookies[self._cookie] = token
177
200
  else:
178
201
  raise AuthError(f"no token carrier: login={login} auth={auth} allow={self._allow}")
179
202
 
@@ -195,6 +218,9 @@ class Authenticator:
195
218
 
196
219
  raise AuthError(f"no authentication for login={login} auth={auth} allow={self._allow}")
197
220
 
221
+ if headers: # put headers back if needed
222
+ kwargs["headers"] = headers
223
+
198
224
 
199
225
  class RequestFlaskResponse:
200
226
  """Wrapper to return a Flask-looking response from a request response.
@@ -233,16 +259,17 @@ class RequestFlaskResponse:
233
259
 
234
260
 
235
261
  class Client:
236
- """Common class for flask authenticated testing.
262
+ """Common (partial) class for flask authenticated testing.
237
263
 
238
264
  Constructor parameters:
239
265
 
240
266
  - ``auth`` authenticator
241
- - ``default_login`` if not ``login`` is set.
267
+ - ``default_login`` if ``login`` is not set.
242
268
  """
243
269
 
244
270
  def __init__(self, auth: Authenticator, default_login: str|None = None):
245
271
  self._auth = auth
272
+ self._cookies: dict[str, dict[str, str]] = {} # login -> name -> value
246
273
  self._default_login = default_login
247
274
 
248
275
  def setToken(self, login: str, token: str|None):
@@ -253,7 +280,11 @@ class Client:
253
280
  """Associate a password to a login, *None* to remove."""
254
281
  self._auth.setPass(login, password)
255
282
 
256
- def _request(self, method: str, path: str, **kwargs):
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):
257
288
  """Run a request and return response."""
258
289
  raise NotImplementedError()
259
290
 
@@ -269,7 +300,7 @@ class Client:
269
300
 
270
301
  - ``status``: expected HTTP status, *None* to skip status check
271
302
  - ``login``: authenticated user, use **explicit** *None* to skip
272
- - ``auth``: authentication scheme to use
303
+ - ``auth``: authentication scheme to use instead of default behavior
273
304
  - ``**kwargs``: more request parameters (headers, data, json…)
274
305
  """
275
306
 
@@ -279,13 +310,18 @@ class Client:
279
310
  else: # if unset, use default
280
311
  login = self._default_login
281
312
 
282
- self._auth.setAuth(login, kwargs, auth=auth)
283
- res = self._request(method, path, **kwargs) # type: ignore
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
284
320
 
285
321
  if status is not None:
286
- if res.status_code != status: # pragma: no cover # show error before aborting
322
+ if res.status_code != status: # show error before aborting
287
323
  log.error(f"bad {status} result: {res.status_code} {res.text[:512]}")
288
- assert res.status_code == status
324
+ assert res.status_code == status, f"unexpected status {res.status_code}, expecting {status}"
289
325
 
290
326
  return res
291
327
 
@@ -330,9 +366,9 @@ class Client:
330
366
 
331
367
  # check contents
332
368
  if content is not None:
333
- if not re.search(content, res.text, re.DOTALL): # pragma: no cover
369
+ if not re.search(content, res.text, re.DOTALL):
334
370
  log.error(f"cannot find {content} in {res.text}")
335
- assert False, "content not found"
371
+ assert False, f"expected content {content} not found in {res.text}"
336
372
 
337
373
  return res
338
374
 
@@ -344,7 +380,7 @@ class RequestClient(Client):
344
380
 
345
381
  - ``auth`` authenticator
346
382
  - ``base_url`` target server
347
- - ``default_login`` if not ``login`` is set.
383
+ - ``default_login`` if ``login`` is not set.
348
384
  """
349
385
 
350
386
  def __init__(self, auth: Authenticator, base_url: str, default_login=None):
@@ -354,7 +390,7 @@ class RequestClient(Client):
354
390
  from requests import Session
355
391
  self._requests = Session()
356
392
 
357
- def _request(self, method: str, path: str, **kwargs):
393
+ def _request(self, method: str, path: str, cookies: dict[str, str], **kwargs):
358
394
  """Actual request handling."""
359
395
 
360
396
  if "data" in kwargs:
@@ -378,7 +414,7 @@ class RequestClient(Client):
378
414
  # sanity
379
415
  assert not (files and "json" in kwargs), "cannot mix file upload and json?"
380
416
 
381
- res = self._requests.request(method, self._base_url + path, **kwargs)
417
+ res = self._requests.request(method, self._base_url + path, cookies=cookies, **kwargs)
382
418
 
383
419
  return RequestFlaskResponse(res)
384
420
 
@@ -389,22 +425,61 @@ class FlaskClient(Client):
389
425
  Constructor parameters:
390
426
 
391
427
  - ``auth`` authenticator
392
- - ``client`` Flask ``test_client``
393
- - ``default_login`` if not ``login`` is set.
428
+ - ``client`` Flask actual ``test_client``
429
+ - ``default_login`` if ``login`` is not set.
430
+
431
+ Note: this client handles `cookies`.
394
432
  """
395
433
 
396
434
  def __init__(self, auth: Authenticator, client, default_login=None):
397
435
  super().__init__(auth, default_login)
398
436
  self._client = client
437
+ self._cookie_names: set[str] = set() # FIXME all encountered cookies for cleanup :-/
399
438
 
400
- def _request(self, method: str, path: str, **kwargs):
439
+ def _request(self, method: str, path: str, cookies: dict[str, str], **kwargs):
401
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
+
402
451
  return self._client.open(method=method, path=path, **kwargs)
403
452
 
404
453
 
405
454
  @pytest.fixture
406
455
  def ft_authenticator():
407
- """Pytest Fixture: ft_authenticator."""
456
+ """Pytest Fixture: ft_authenticator.
457
+
458
+ Environment variables:
459
+
460
+ - ``FLASK_TESTER_LOG_LEVEL``: package log level in
461
+ ``DEBUG INFO WARNING ERROR CRITICAL NOSET``.
462
+ Default is ``NOTSET``.
463
+ - ``FLASK_TESTER_ALLOW``: allowed space-separated authentication schemes, in
464
+ ``basic param bearer header cookie tparam fake``.
465
+ Default is ``bearer basic param``.
466
+ - ``FLASK_TESTER_USER``: user login parameter for ``param`` authentication.
467
+ Default is ``USER``.
468
+ - ``FLASK_TESTER_PASS``: user password parameter for ``param`` authentication.
469
+ Default is ``PASS``.
470
+ - ``FLASK_TESTER_LOGIN``: user login parameter for ``fake`` authentication.
471
+ Default is ``LOGIN``.
472
+ - ``FLASK_TESTER_BEARER``: bearer name for *token* authentication.
473
+ Default is ``Bearer``.
474
+ - ``FLASK_TESTER_HEADER``: header name for *token* authentication.
475
+ Default is ``Auth``.
476
+ - ``FLASK_TESTER_COOKIE``: cookie name for *token* authentication.
477
+ Default is ``auth``.
478
+ - ``FLASK_TESTER_TPARAM``: parameter for *token* authentication.
479
+ Default is ``AUTH``.
480
+ - ``FLASK_TESTER_AUTH``: initial comma-separated list of *login:password*.
481
+ Default is not set.
482
+ """
408
483
 
409
484
  level = os.environ.get("FLASK_TESTER_LOG_LEVEL", "NOTSET")
410
485
  log.setLevel(logging.DEBUG if level == "DEBUG" else
@@ -436,7 +511,18 @@ def ft_authenticator():
436
511
 
437
512
  @pytest.fixture
438
513
  def ft_client(ft_authenticator):
439
- """Pytest Fixture: ft_client."""
514
+ """Pytest Fixture: ft_client.
515
+
516
+ Target environment variable, one **must** be defined:
517
+
518
+ - ``FLASK_TESTER_URL``: application HTTP base URL.
519
+ - ``FLASK_TESTER_APP``: Flask application, eg ``app:create_app``.
520
+
521
+ Other environment variable:
522
+
523
+ - ``FLASK_TESTER_DEFAULT``: Default client login, default is *None* for no
524
+ default.
525
+ """
440
526
 
441
527
  default_login = os.environ.get("FLASK_TESTER_DEFAULT", None)
442
528
  client: Client
@@ -461,6 +547,7 @@ def ft_client(ft_authenticator):
461
547
  app = getattr(pkg, name)
462
548
  if callable(app) and not hasattr(app, "test_client"):
463
549
  app = app()
550
+ break
464
551
  if not app: # pragma: no cover
465
552
  raise FlaskTesterError(f"cannot find Flask app in {pkg_name}")
466
553
  client = FlaskClient(ft_authenticator, app.test_client(), default_login)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: FlaskTester
3
- Version: 1.3
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"
@@ -65,7 +65,7 @@ achieved through environment variables.
65
65
  Install package with `pip install FlaskTester` or equivalent.
66
66
 
67
67
  The following test creates a local fixture with 2 users identified by a
68
- password, and retrieves a token for the first user using a `/login` route
68
+ password, and retrieves tokens for both users using a `/login` route
69
69
  provided by the application.
70
70
  It then proceeds to run authenticated requests against the `/admin` route.
71
71
 
@@ -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 `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`.
@@ -225,14 +229,30 @@ The implementation of these fixtures is based on five classes plus exceptions:
225
229
 
226
230
  ## License
227
231
 
228
- This code is public domain.
232
+ This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
233
+
234
+ All software has bug, this is software, hence… Beware that you may lose your
235
+ hairs or your friends because of it. If you like it, feel free to send a
236
+ postcard to the author.
237
+
238
+ ## Versions
229
239
 
230
240
  Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
231
241
  [sources](https://github.com/zx80/flask-tester) are available on GitHub,
232
242
  see also the [documentation](https://zx80.github.io/flask-tester/),
233
243
  please report any [issues](https://github.com/zx80/flask-tester/issues).
234
244
 
235
- ## Versions
245
+ ### 2.0 on 2024-03-23
246
+
247
+ Add support for cookies.
248
+ Improved documentation and code.
249
+ Improved tests.
250
+
251
+ ### 1.4 on 2024-03-19
252
+
253
+ Test expected assert failures.
254
+ Improved API documentation.
255
+ Keep first found app.
236
256
 
237
257
  ### 1.3 on 2024-03-16
238
258
 
@@ -22,7 +22,7 @@ achieved through environment variables.
22
22
  Install package with `pip install FlaskTester` or equivalent.
23
23
 
24
24
  The following test creates a local fixture with 2 users identified by a
25
- password, and retrieves a token for the first user using a `/login` route
25
+ password, and retrieves tokens for both users using a `/login` route
26
26
  provided by the application.
27
27
  It then proceeds to run authenticated requests against the `/admin` route.
28
28
 
@@ -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 `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`.
@@ -182,14 +186,30 @@ The implementation of these fixtures is based on five classes plus exceptions:
182
186
 
183
187
  ## License
184
188
 
185
- This code is public domain.
189
+ This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
190
+
191
+ All software has bug, this is software, hence… Beware that you may lose your
192
+ hairs or your friends because of it. If you like it, feel free to send a
193
+ postcard to the author.
194
+
195
+ ## Versions
186
196
 
187
197
  Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
188
198
  [sources](https://github.com/zx80/flask-tester) are available on GitHub,
189
199
  see also the [documentation](https://zx80.github.io/flask-tester/),
190
200
  please report any [issues](https://github.com/zx80/flask-tester/issues).
191
201
 
192
- ## Versions
202
+ ### 2.0 on 2024-03-23
203
+
204
+ Add support for cookies.
205
+ Improved documentation and code.
206
+ Improved tests.
207
+
208
+ ### 1.4 on 2024-03-19
209
+
210
+ Test expected assert failures.
211
+ Improved API documentation.
212
+ Keep first found app.
193
213
 
194
214
  ### 1.3 on 2024-03-16
195
215
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "FlaskTester"
7
- version = "1.3"
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"
@@ -96,17 +105,28 @@ def test_admin(api):
96
105
  api.check("GET", "/admin", 403, login="hobbes", auth=auth)
97
106
  api.check("GET", "/admin", 401, login="moe", auth=auth)
98
107
  api.check("GET", "/admin", 401, login=None, auth=auth)
108
+ try:
109
+ api.check("GET", "/admin", status=599, login="calvin", auth=auth)
110
+ pytest.fail("assert on status must fail") # pragma: no cover
111
+ except AssertionError as e:
112
+ assert "200" in str(e)
113
+ try:
114
+ api.check("GET", "/admin", status=200, login="calvin", auth=auth, content="NOT THERE")
115
+ pytest.fail("assert on content must fail") # pragma: no cover
116
+ except AssertionError as e:
117
+ assert "NOT THERE" in str(e)
99
118
 
100
119
  def test_errors(api):
101
120
  for scheme in ("header", "cookie", "fake", "tparam", "unexpected"):
102
121
  try:
103
122
  api.get("/login", login="calvin", auth=scheme)
104
- assert False, "must raise an exception" # pragma: no cover
123
+ pytest.fail("must raise an exception") # pragma: no cover
105
124
  except ft.AuthError as e:
106
125
  assert True, "expected error"
107
126
 
108
127
  def test_methods(api):
109
- api.get("/who-am-i", login="hobbes", status=200)
128
+ res = api.get("/who-am-i", login="susie", status=200, cookies={"lang": "python"})
129
+ assert res.json["lang"] == "python"
110
130
  api.post("/who-am-i", login="hobbes", status=405)
111
131
  api.put("/who-am-i", login="hobbes", status=405)
112
132
  api.patch("/who-am-i", login="hobbes", status=405)
@@ -119,41 +139,47 @@ def test_authenticator_token():
119
139
  auth.setToken("susie", "ss-token")
120
140
  auth.setToken("hobbes", "hbs-token")
121
141
  auth.setToken("moe", "m-token")
122
- kwargs = {}
123
- auth.setAuth("calvin", kwargs, auth="bearer")
142
+ auth.setCookie("calvin", "what", "clv-cookie")
143
+ kwargs, cookies = {}, {}
144
+ auth.setAuth("calvin", kwargs, cookies, auth="bearer")
124
145
  assert kwargs["headers"]["Authorization"] == "Bearer clv-token"
125
- kwargs = {}
126
- auth.setAuth("susie", kwargs, auth="header")
146
+ assert cookies["what"] == "clv-cookie"
147
+ kwargs, cookies = {}, {}
148
+ auth.setAuth("susie", kwargs, cookies, auth="header")
127
149
  assert kwargs["headers"]["Auth"] == "ss-token"
128
- kwargs = {"data": {}}
129
- auth.setAuth("hobbes", kwargs, auth="tparam")
150
+ assert not cookies
151
+ kwargs, cookies = {"data": {}}, {}
152
+ auth.setAuth("hobbes", kwargs, cookies, auth="tparam")
130
153
  assert kwargs["data"]["AUTH"] == "hbs-token"
131
- kwargs = {"json": {}}
132
- auth.setAuth("hobbes", kwargs, auth="tparam")
154
+ assert not cookies
155
+ kwargs, cookies = {"json": {}}, {}
156
+ auth.setAuth("hobbes", kwargs, cookies, auth="tparam")
133
157
  assert kwargs["json"]["AUTH"] == "hbs-token"
134
- kwargs = {}
135
- auth.setAuth("moe", kwargs, auth="cookie")
136
- assert kwargs["headers"]["Cookie"] == "auth=m-token"
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"
137
163
  # dad does not have a token
138
164
  try:
139
- kwargs = {}
140
- auth.setAuth("dad", kwargs)
141
- assert False, "must raise an error" # pragma: no cover
165
+ kwargs, cookies = {}, {}
166
+ auth.setAuth("dad", kwargs, cookies)
167
+ pytest.fail("must raise an error") # pragma: no cover
142
168
  except ft.FlaskTesterError:
143
169
  assert True, "error raised"
144
170
  # rosalyn as a password, but no password carrier is allowed
145
171
  try:
146
172
  auth.setPass("rosalyn", "rsln-pass")
147
- assert False, "must raise an error" # pragma: no cover
173
+ pytest.fail("must raise an error") # pragma: no cover
148
174
  except ft.AuthError:
149
175
  assert True, "error raised"
150
176
  # force to trigger later errors
151
177
  auth._has_pass = True
152
178
  auth.setPass("rosalyn", "rsln-pass")
153
179
  try:
154
- kwargs={}
155
- auth.setAuth("rosalyn", kwargs)
156
- assert False, "must raise an error" # pragma: no cover
180
+ kwargs, cookies = {}, {}
181
+ auth.setAuth("rosalyn", kwargs, cookies)
182
+ pytest.fail("must raise an error") # pragma: no cover
157
183
  except ft.FlaskTesterError:
158
184
  assert True, "error raised"
159
185
 
@@ -164,34 +190,40 @@ def test_authenticator_password():
164
190
  auth.setPass("hobbes", "hbs-pass")
165
191
  auth.setPass("moe", "m-pass")
166
192
  auth.setPass("rosalyn", "rsln-pass")
167
- kwargs = {}
168
- auth.setAuth("calvin", kwargs, auth="basic")
193
+ auth.setCookie("moe", "hello", "world!")
194
+ kwargs, cookies = {}, {}
195
+ auth.setAuth("calvin", kwargs, cookies, auth="basic")
169
196
  assert kwargs["auth"] == ("calvin", "clv-pass")
170
- kwargs = {"data": {}}
171
- auth.setAuth("hobbes", kwargs, auth="param")
197
+ assert not cookies
198
+ kwargs, cookies = {"data": {}}, {}
199
+ auth.setAuth("hobbes", kwargs, cookies, auth="param")
172
200
  assert kwargs["data"]["USER"] == "hobbes" and kwargs["data"]["PASS"] == "hbs-pass"
173
- kwargs = {"json": {}}
174
- auth.setAuth("moe", kwargs, auth="param")
201
+ assert not cookies
202
+ kwargs, cookies = {"json": {}}, {}
203
+ auth.setAuth("moe", kwargs, cookies, auth="param")
175
204
  assert kwargs["json"]["USER"] == "moe" and kwargs["json"]["PASS"] == "m-pass"
176
- kwargs = {"data": {}}
177
- auth.setAuth("rosalyn", kwargs, auth="fake")
205
+ assert cookies["hello"] == "world!"
206
+ kwargs, cookies = {"data": {}}, {}
207
+ auth.setAuth("rosalyn", kwargs, cookies, auth="fake")
178
208
  assert kwargs["data"]["LOGIN"] == "rosalyn"
179
- kwargs = {"json": {}}
180
- auth.setAuth("hobbes", kwargs, auth="fake")
209
+ assert not cookies
210
+ kwargs, cookies = {"json": {}}, {}
211
+ auth.setAuth("hobbes", kwargs, cookies, auth="fake")
181
212
  assert kwargs["json"]["LOGIN"] == "hobbes"
213
+ assert not cookies
182
214
  # susie as a token, but no token carrier is allowed
183
215
  try:
184
216
  auth.setToken("susie", "ss-token")
185
- assert False, "must raise an error" # pragma: no cover
217
+ pytest.fail("must raise an error") # pragma: no cover
186
218
  except ft.FlaskTesterError:
187
219
  assert True, "error raised"
188
220
  # force to trigger later error
189
221
  auth._has_token = True
190
222
  auth.setToken("susie", "ss-token")
191
223
  try:
192
- kwargs={}
193
- auth.setAuth("susie", kwargs)
194
- assert False, "must raise an error" # pragma: no cover
224
+ kwargs, cookies = {}, {}
225
+ auth.setAuth("susie", kwargs, cookies)
226
+ pytest.fail("must raise an error") # pragma: no cover
195
227
  except ft.FlaskTesterError:
196
228
  assert True, "error raised"
197
229
 
@@ -223,10 +255,10 @@ def test_request_flask_response():
223
255
  def test_client():
224
256
  client = ft.Client(ft.Authenticator())
225
257
  try:
226
- client._request("GET", "/")
227
- assert False, "must raise an error" # pragma: no cover
258
+ client._request("GET", "/", {})
259
+ pytest.fail("must raise an error") # pragma: no cover
228
260
  except NotImplementedError:
229
- assert True, "error raised"
261
+ assert True, "expected error raised"
230
262
 
231
263
  def test_request_client():
232
264
  httpd = htsv.HTTPServer(("", 8888), htsv.SimpleHTTPRequestHandler)
File without changes
File without changes
File without changes