FlaskTester 1.2__tar.gz → 1.4__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.2
3
+ Version: 1.4
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
@@ -39,6 +39,7 @@ Requires-Dist: sphinx; extra == "doc"
39
39
  Requires-Dist: sphinx_rtd_theme; extra == "doc"
40
40
  Requires-Dist: sphinx-autoapi; extra == "doc"
41
41
  Requires-Dist: sphinx-lint; extra == "doc"
42
+ Requires-Dist: myst_parser; extra == "doc"
42
43
 
43
44
  # FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
44
45
 
@@ -64,37 +65,48 @@ achieved through environment variables.
64
65
  Install package with `pip install FlaskTester` or equivalent.
65
66
 
66
67
  The following test creates a local fixture with 2 users identified by a
67
- password, and retrieves a token for the first user using a `/token` route
68
+ password, and retrieves tokens for both users using a `/login` route
68
69
  provided by the application.
69
- It then proceeds to run some requests against the `/admin` route.
70
+ It then proceeds to run authenticated requests against the `/admin` route.
70
71
 
71
72
  ```python
72
- import secret
73
73
  import pytest
74
74
  from FlaskTester import ft_authenticator, ft_client
75
+ import secret
75
76
 
76
77
  @pytest.fixture
77
78
  def app(ft_client):
78
79
  # add test passwords for Calvin and Hobbes (must be consistent with app!)
79
80
  ft_client.setPass("calvin", secret.PASSES["calvin"])
80
81
  ft_client.setPass("hobbes", secret.PASSES["hobbes"])
81
- # get Calvin's token, assume json result {"token": "<token-value>"}
82
- res = ft_client.get("/token", login="calvin", auth="basic", status=200)
82
+ # get user tokens, assume json result {"token": "<token-value>"}
83
+ res = ft_client.get("/login", login="calvin", auth="basic", status=200)
83
84
  assert res.is_json
84
85
  ft_client.setToken("calvin", res.json["token"])
86
+ res = ft_client.post("/login", login="hobbes", auth="param", status=201)
87
+ assert res.is_json
88
+ ft_client.setToken("hobbes", res.json["token"])
85
89
  # return working client
86
90
  yield ft_client
87
91
 
88
- def test_app(app):
92
+ def test_app_admin(app):
93
+ # try all authentication schemes for calvin
89
94
  app.get("/admin", login="calvin", auth="bearer", status=200)
90
95
  app.get("/admin", login="calvin", auth="basic", status=200)
96
+ app.get("/admin", login="calvin", auth="param", status=200)
97
+ # try all authentication schemes for hobbes
98
+ res = app.get("/admin", login="hobbes", auth="bearer", status=403)
99
+ assert 'not in group "ADMIN"' in res.text
91
100
  res = app.get("/admin", login="hobbes", auth="basic", status=403)
92
101
  assert 'not in group "ADMIN"' in res.text
102
+ res = app.get("/admin", login="hobbes", auth="param", status=403)
103
+ assert 'not in group "ADMIN"' in res.text
93
104
  ```
94
105
 
95
106
  This can be run against a (local) server:
96
107
 
97
108
  ```shell
109
+ export TEST_SEED="some-random-data" # shared seed
98
110
  flask --app app:app run & # start flask app
99
111
  pid=$! # keep pid
100
112
  export FLASK_TESTER_URL="http://localhost:5000" # set app local url
@@ -213,23 +225,29 @@ The implementation of these fixtures is based on five classes plus exceptions:
213
225
 
214
226
  ## License
215
227
 
216
- This code is public domain.
228
+ This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
229
+
230
+ All software has bug, this is software, hence… Beware that you may lose your
231
+ hairs or your friends because of it. If you like it, feel free to send a
232
+ postcard to the author.
233
+
234
+ ## Versions
217
235
 
218
236
  Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
219
237
  [sources](https://github.com/zx80/flask-tester) are available on GitHub,
220
238
  see also the [documentation](https://zx80.github.io/flask-tester/),
221
239
  please report any [issues](https://github.com/zx80/flask-tester/issues).
222
240
 
223
- ## See Also, or Not
224
-
225
- - [Flask Testing](https://github.com/jarus/flask-testing) an unmaintained
226
- old-style unit test for Flask 1.x, without authentication help.
241
+ ### 1.4 on 2024-03-19
227
242
 
228
- ## TODO
243
+ Test expected assert failures.
244
+ Improved API documentation.
245
+ Keep first found app.
229
246
 
230
- - API documentation generation
247
+ ### 1.3 on 2024-03-16
231
248
 
232
- ## Versions
249
+ Generate API documentation.
250
+ Cleaner code.
233
251
 
234
252
  ### 1.2 on 2024-03-15
235
253
 
@@ -255,3 +273,8 @@ Working coverage tests.
255
273
  ### 0.9 on 2024-03-11
256
274
 
257
275
  Initial revision extracted from a separate project.
276
+
277
+ ## See Also, or Not
278
+
279
+ - [Flask Testing](https://github.com/jarus/flask-testing) an unmaintained
280
+ old-style unit test for Flask 1.x, without authentication help.
@@ -1,5 +1,6 @@
1
1
  FlaskTester.py
2
2
  LICENSE
3
+ MANIFEST.in
3
4
  README.md
4
5
  pyproject.toml
5
6
  FlaskTester.egg-info/PKG-INFO
@@ -21,3 +21,4 @@ sphinx
21
21
  sphinx_rtd_theme
22
22
  sphinx-autoapi
23
23
  sphinx-lint
24
+ myst_parser
@@ -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
@@ -10,10 +15,12 @@ log = logging.getLogger("flask_tester")
10
15
 
11
16
 
12
17
  class FlaskTesterError(BaseException):
18
+ """Base exception for FlaskTester package."""
13
19
  pass
14
20
 
15
21
 
16
22
  class AuthError(FlaskTesterError):
23
+ """Authenticator Exception."""
17
24
  pass
18
25
 
19
26
 
@@ -21,13 +28,33 @@ class Authenticator:
21
28
  """Manage authentication for test requests.
22
29
 
23
30
  Supported schemes:
31
+
24
32
  - ``basic``: HTTP Basic Authentication
25
33
  - ``param``: password with HTTP or JSON parameters
26
- - ``bearer``: token in ``Authorization`` _bearer_ header
34
+ - ``bearer``: token in ``Authorization`` *bearer* header
27
35
  - ``header``: token in a header
28
36
  - ``cookie``: token in a cookie
29
37
  - ``tparam``: token in a parameter
30
38
  - ``fake``: fake scheme, login directly passed as a parameter
39
+
40
+ Constructor parameters:
41
+
42
+ - ``allow``: list of allowed schemes.
43
+ default is ``["bearer", "basic", "param"]``
44
+ - ``user``: parameter for user on ``param`` password authentication,
45
+ default is ``USER``
46
+ - ``pwd``: parameter for password on ``param`` password authentication,
47
+ default is ``PASS``
48
+ - ``login``: parameter for user on ``fake`` authentication,
49
+ default is ``LOGIN``
50
+ - ``bearer``: name of bearer scheme for token,
51
+ default is ``Bearer``
52
+ - ``header``: name of header for token,
53
+ default is ``Auth``
54
+ - ``cookie``: name of cookie for token,
55
+ default is ``auth``
56
+ - ``tparam``: name of parameter for token,
57
+ default is ``AUTH``
31
58
  """
32
59
 
33
60
  _TOKEN_SCHEMES = {"bearer", "header", "cookie", "tparam"}
@@ -50,25 +77,6 @@ class Authenticator:
50
77
  cookie: str = "auth",
51
78
  tparam: str = "AUTH",
52
79
  ):
53
- """Constructor parameters:
54
-
55
- - ``allow``: list of allowed schemes.
56
- default is ``["bearer", "basic", "param"]``
57
- - ``user``: parameter for user on ``param`` password authentication,
58
- default is ``USER``
59
- - ``pwd``: parameter for password on ``param`` password authentication,
60
- default is ``PASS``
61
- - ``login``: parameter for user on ``fake`` authentication,
62
- default is ``LOGIN``
63
- - ``bearer``: name of bearer scheme for token,
64
- default is ``Bearer``
65
- - ``header``: name of header for token,
66
- default is ``Auth``
67
- - ``cookie``: name of cookie for token,
68
- default is ``auth``
69
- - ``tparam``: name of parameter for token,
70
- default is ``AUTH``
71
- """
72
80
 
73
81
  self._has_pass, self._has_token = False, False
74
82
  for auth in allow:
@@ -199,20 +207,21 @@ class RequestFlaskResponse:
199
207
  This only work for simple responses.
200
208
 
201
209
  Available attributes:
202
- - status_code: integer status code
203
- - data: body as bytes
204
- - text: body as a string
205
- - headers: dict of headers and their values
206
- - cookies: dict of cookies
207
- - json: JSON-converted body, or None
208
- - is_json: whether body was in JSON
210
+
211
+ - ``status_code``: integer status code
212
+ - ``data``: body as bytes
213
+ - ``text``: body as a string
214
+ - ``headers``: dict of headers and their values
215
+ - ``cookies``: dict of cookies
216
+ - ``json``: JSON-converted body, or None
217
+ - ``is_json``: whether body was in JSON
218
+
219
+ Constructor parameter:
220
+
221
+ - ``response``: from request.
209
222
  """
210
223
 
211
224
  def __init__(self, response):
212
- """Constructor parameter
213
-
214
- - ``response`` from request.
215
- """
216
225
 
217
226
  self._response = response
218
227
  self.status_code = response.status_code
@@ -229,23 +238,24 @@ class RequestFlaskResponse:
229
238
 
230
239
 
231
240
  class Client:
232
- """Common class for flask authenticated testing."""
241
+ """Common class for flask authenticated testing.
233
242
 
234
- def __init__(self, auth: Authenticator, default_login: str|None = None):
235
- """Constructor parameters.
243
+ Constructor parameters:
236
244
 
237
- - ``auth`` authenticator
238
- - ``default_login`` if not ``login`` is set.
239
- """
245
+ - ``auth`` authenticator
246
+ - ``default_login`` if not ``login`` is set.
247
+ """
248
+
249
+ def __init__(self, auth: Authenticator, default_login: str|None = None):
240
250
  self._auth = auth
241
251
  self._default_login = default_login
242
252
 
243
253
  def setToken(self, login: str, token: str|None):
244
- """Associate a token to a login, None to remove."""
254
+ """Associate a token to a login, *None* to remove."""
245
255
  self._auth.setToken(login, token)
246
256
 
247
257
  def setPass(self, login: str, password: str|None):
248
- """Associate a password to a login, None to remove."""
258
+ """Associate a password to a login, *None* to remove."""
249
259
  self._auth.setPass(login, password)
250
260
 
251
261
  def _request(self, method: str, path: str, **kwargs):
@@ -256,14 +266,16 @@ class Client:
256
266
  """Run a possibly authenticated HTTP request.
257
267
 
258
268
  Mandatory parameters:
259
- - method: HTTP method ("GET", "POST", "PATCH", "DELETE"…)
260
- - path: local path under the base URL
269
+
270
+ - ``method``: HTTP method ("GET", "POST", "PATCH", "DELETE"…)
271
+ - ``path``: local path under the base URL
261
272
 
262
273
  Optional parameters:
263
- - status: expected HTTP status, None to skip status check
264
- - login: authenticated user, use **explicit** None to skip
265
- - auth: authentication schme to use
266
- - **kwargs: more request parameters (headers, data, json…)
274
+
275
+ - ``status``: expected HTTP status, *None* to skip status check
276
+ - ``login``: authenticated user, use **explicit** *None* to skip
277
+ - ``auth``: authentication scheme to use
278
+ - ``**kwargs``: more request parameters (headers, data, json…)
267
279
  """
268
280
 
269
281
  if "login" in kwargs:
@@ -276,9 +288,9 @@ class Client:
276
288
  res = self._request(method, path, **kwargs) # type: ignore
277
289
 
278
290
  if status is not None:
279
- if res.status_code != status: # pragma: no cover # show error before aborting
291
+ if res.status_code != status: # show error before aborting
280
292
  log.error(f"bad {status} result: {res.status_code} {res.text[:512]}")
281
- assert res.status_code == status
293
+ assert res.status_code == status, f"unexpected status {res.status_code}, expecting {status}"
282
294
 
283
295
  return res
284
296
 
@@ -306,14 +318,16 @@ class Client:
306
318
  """Run a query and check the response.
307
319
 
308
320
  Mandatory parameters:
309
- - method: HTTP method ("GET", "POST", "PATCH", "DELETE"…)
310
- - path: local path under the base URL
311
- - status: expected HTTP status
321
+
322
+ - ``method``: HTTP method ("GET", "POST", "PATCH", "DELETE"…)
323
+ - ``path``: local path under the base URL
324
+ - ``status``: expected HTTP status
312
325
 
313
326
  Optional parameters:
314
- - content: regular expression in response body
315
- - login: authenticated user, use explicit None to skip
316
- - **kwargs: more request parameters (headers, data, json…)
327
+
328
+ - ``content``: regular expression in response body
329
+ - ``login``: authenticated user, use explicit *None* to skip
330
+ - ``**kwargs``: more request parameters (headers, data, json…)
317
331
  """
318
332
 
319
333
  # get HTTP response
@@ -321,23 +335,24 @@ class Client:
321
335
 
322
336
  # check contents
323
337
  if content is not None:
324
- if not re.search(content, res.text, re.DOTALL): # pragma: no cover
338
+ if not re.search(content, res.text, re.DOTALL):
325
339
  log.error(f"cannot find {content} in {res.text}")
326
- assert False, "content not found"
340
+ assert False, f"expected content {content} not found in {res.text}"
327
341
 
328
342
  return res
329
343
 
330
344
 
331
345
  class RequestClient(Client):
332
- """Request-based test provider."""
346
+ """Request-based test provider.
333
347
 
334
- def __init__(self, auth: Authenticator, base_url: str, default_login=None):
335
- """Constructor parameters.
348
+ Constructor parameters:
336
349
 
337
- - ``auth`` authenticator
338
- - ``base_url`` target server
339
- - ``default_login`` if not ``login`` is set.
340
- """
350
+ - ``auth`` authenticator
351
+ - ``base_url`` target server
352
+ - ``default_login`` if not ``login`` is set.
353
+ """
354
+
355
+ def __init__(self, auth: Authenticator, base_url: str, default_login=None):
341
356
  super().__init__(auth, default_login)
342
357
  self._base_url = base_url
343
358
  # reuse connections, otherwise it is too slow…
@@ -374,15 +389,16 @@ class RequestClient(Client):
374
389
 
375
390
 
376
391
  class FlaskClient(Client):
377
- """Flask-based test provider."""
392
+ """Flask-based test provider.
378
393
 
379
- def __init__(self, auth: Authenticator, client, default_login=None):
380
- """Constructor parameters.
394
+ Constructor parameters:
381
395
 
382
- - ``auth`` authenticator
383
- - ``client`` Flask ``test_client``
384
- - ``default_login`` if not ``login`` is set.
385
- """
396
+ - ``auth`` authenticator
397
+ - ``client`` Flask ``test_client``
398
+ - ``default_login`` if not ``login`` is set.
399
+ """
400
+
401
+ def __init__(self, auth: Authenticator, client, default_login=None):
386
402
  super().__init__(auth, default_login)
387
403
  self._client = client
388
404
 
@@ -393,7 +409,31 @@ class FlaskClient(Client):
393
409
 
394
410
  @pytest.fixture
395
411
  def ft_authenticator():
396
- """Pytest Fixture: ft_authenticator."""
412
+ """Pytest Fixture: ft_authenticator.
413
+
414
+ Environment variables:
415
+
416
+ - ``FLASK_TESTER_LOG_LEVEL``: package log level in
417
+ ``DEBUG INFO WARNING ERROR CRITICAL NOSET``.
418
+ - ``FLASK_TESTER_ALLOW``: allowed space-separated authentication schemes, in
419
+ ``basic param bearer header cookie tparam fake``.
420
+ Default is ``bearer basic param``.
421
+ - ``FLASK_TESTER_USER``: user login parameter for ``param`` authentication.
422
+ Default is ``USER``.
423
+ - ``FLASK_TESTER_PASS``: user password parameter for ``param`` authentication.
424
+ Default is ``PASS``.
425
+ - ``FLASK_TESTER_LOGIN``: user login parameter for ``fake`` authentication.
426
+ Default is ``LOGIN``.
427
+ - ``FLASK_TESTER_BEARER``: bearer name for *token* authentication.
428
+ Default is ``Bearer``.
429
+ - ``FLASK_TESTER_HEADER``: header name for *token* authentication.
430
+ Default is ``Auth``.
431
+ - ``FLASK_TESTER_COOKIE``: cookie name for *token* authentication.
432
+ Default is ``auth``.
433
+ - ``FLASK_TESTER_TPARAM``: parameter for *token* authentication.
434
+ Default is ``AUTH``.
435
+ - ``FLASK_TESTER_AUTH``: initial comma-separated list of *login:password*.
436
+ """
397
437
 
398
438
  level = os.environ.get("FLASK_TESTER_LOG_LEVEL", "NOTSET")
399
439
  log.setLevel(logging.DEBUG if level == "DEBUG" else
@@ -425,7 +465,18 @@ def ft_authenticator():
425
465
 
426
466
  @pytest.fixture
427
467
  def ft_client(ft_authenticator):
428
- """Pytest Fixture: ft_client."""
468
+ """Pytest Fixture: ft_client.
469
+
470
+ Target environment variable, one **must** be defined:
471
+
472
+ - ``FLASK_TESTER_URL``: application HTTP base URL.
473
+ - ``FLASK_TESTER_APP``: Flask application, eg ``app:create_app``.
474
+
475
+ Other environment variable:
476
+
477
+ - ``FLASK_TESTER_DEFAULT``: Default client login, default is *None* for no
478
+ default.
479
+ """
429
480
 
430
481
  default_login = os.environ.get("FLASK_TESTER_DEFAULT", None)
431
482
  client: Client
@@ -450,6 +501,7 @@ def ft_client(ft_authenticator):
450
501
  app = getattr(pkg, name)
451
502
  if callable(app) and not hasattr(app, "test_client"):
452
503
  app = app()
504
+ break
453
505
  if not app: # pragma: no cover
454
506
  raise FlaskTesterError(f"cannot find Flask app in {pkg_name}")
455
507
  client = FlaskClient(ft_authenticator, app.test_client(), default_login)
@@ -0,0 +1 @@
1
+ exclude docs/* */Makefile
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: FlaskTester
3
- Version: 1.2
3
+ Version: 1.4
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
@@ -39,6 +39,7 @@ Requires-Dist: sphinx; extra == "doc"
39
39
  Requires-Dist: sphinx_rtd_theme; extra == "doc"
40
40
  Requires-Dist: sphinx-autoapi; extra == "doc"
41
41
  Requires-Dist: sphinx-lint; extra == "doc"
42
+ Requires-Dist: myst_parser; extra == "doc"
42
43
 
43
44
  # FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
44
45
 
@@ -64,37 +65,48 @@ achieved through environment variables.
64
65
  Install package with `pip install FlaskTester` or equivalent.
65
66
 
66
67
  The following test creates a local fixture with 2 users identified by a
67
- password, and retrieves a token for the first user using a `/token` route
68
+ password, and retrieves tokens for both users using a `/login` route
68
69
  provided by the application.
69
- It then proceeds to run some requests against the `/admin` route.
70
+ It then proceeds to run authenticated requests against the `/admin` route.
70
71
 
71
72
  ```python
72
- import secret
73
73
  import pytest
74
74
  from FlaskTester import ft_authenticator, ft_client
75
+ import secret
75
76
 
76
77
  @pytest.fixture
77
78
  def app(ft_client):
78
79
  # add test passwords for Calvin and Hobbes (must be consistent with app!)
79
80
  ft_client.setPass("calvin", secret.PASSES["calvin"])
80
81
  ft_client.setPass("hobbes", secret.PASSES["hobbes"])
81
- # get Calvin's token, assume json result {"token": "<token-value>"}
82
- res = ft_client.get("/token", login="calvin", auth="basic", status=200)
82
+ # get user tokens, assume json result {"token": "<token-value>"}
83
+ res = ft_client.get("/login", login="calvin", auth="basic", status=200)
83
84
  assert res.is_json
84
85
  ft_client.setToken("calvin", res.json["token"])
86
+ res = ft_client.post("/login", login="hobbes", auth="param", status=201)
87
+ assert res.is_json
88
+ ft_client.setToken("hobbes", res.json["token"])
85
89
  # return working client
86
90
  yield ft_client
87
91
 
88
- def test_app(app):
92
+ def test_app_admin(app):
93
+ # try all authentication schemes for calvin
89
94
  app.get("/admin", login="calvin", auth="bearer", status=200)
90
95
  app.get("/admin", login="calvin", auth="basic", status=200)
96
+ app.get("/admin", login="calvin", auth="param", status=200)
97
+ # try all authentication schemes for hobbes
98
+ res = app.get("/admin", login="hobbes", auth="bearer", status=403)
99
+ assert 'not in group "ADMIN"' in res.text
91
100
  res = app.get("/admin", login="hobbes", auth="basic", status=403)
92
101
  assert 'not in group "ADMIN"' in res.text
102
+ res = app.get("/admin", login="hobbes", auth="param", status=403)
103
+ assert 'not in group "ADMIN"' in res.text
93
104
  ```
94
105
 
95
106
  This can be run against a (local) server:
96
107
 
97
108
  ```shell
109
+ export TEST_SEED="some-random-data" # shared seed
98
110
  flask --app app:app run & # start flask app
99
111
  pid=$! # keep pid
100
112
  export FLASK_TESTER_URL="http://localhost:5000" # set app local url
@@ -213,23 +225,29 @@ The implementation of these fixtures is based on five classes plus exceptions:
213
225
 
214
226
  ## License
215
227
 
216
- This code is public domain.
228
+ This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
229
+
230
+ All software has bug, this is software, hence… Beware that you may lose your
231
+ hairs or your friends because of it. If you like it, feel free to send a
232
+ postcard to the author.
233
+
234
+ ## Versions
217
235
 
218
236
  Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
219
237
  [sources](https://github.com/zx80/flask-tester) are available on GitHub,
220
238
  see also the [documentation](https://zx80.github.io/flask-tester/),
221
239
  please report any [issues](https://github.com/zx80/flask-tester/issues).
222
240
 
223
- ## See Also, or Not
224
-
225
- - [Flask Testing](https://github.com/jarus/flask-testing) an unmaintained
226
- old-style unit test for Flask 1.x, without authentication help.
241
+ ### 1.4 on 2024-03-19
227
242
 
228
- ## TODO
243
+ Test expected assert failures.
244
+ Improved API documentation.
245
+ Keep first found app.
229
246
 
230
- - API documentation generation
247
+ ### 1.3 on 2024-03-16
231
248
 
232
- ## Versions
249
+ Generate API documentation.
250
+ Cleaner code.
233
251
 
234
252
  ### 1.2 on 2024-03-15
235
253
 
@@ -255,3 +273,8 @@ Working coverage tests.
255
273
  ### 0.9 on 2024-03-11
256
274
 
257
275
  Initial revision extracted from a separate project.
276
+
277
+ ## See Also, or Not
278
+
279
+ - [Flask Testing](https://github.com/jarus/flask-testing) an unmaintained
280
+ old-style unit test for Flask 1.x, without authentication help.
@@ -22,37 +22,48 @@ 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 `/token` route
25
+ password, and retrieves tokens for both users using a `/login` route
26
26
  provided by the application.
27
- It then proceeds to run some requests against the `/admin` route.
27
+ It then proceeds to run authenticated requests against the `/admin` route.
28
28
 
29
29
  ```python
30
- import secret
31
30
  import pytest
32
31
  from FlaskTester import ft_authenticator, ft_client
32
+ import secret
33
33
 
34
34
  @pytest.fixture
35
35
  def app(ft_client):
36
36
  # add test passwords for Calvin and Hobbes (must be consistent with app!)
37
37
  ft_client.setPass("calvin", secret.PASSES["calvin"])
38
38
  ft_client.setPass("hobbes", secret.PASSES["hobbes"])
39
- # get Calvin's token, assume json result {"token": "<token-value>"}
40
- res = ft_client.get("/token", login="calvin", auth="basic", status=200)
39
+ # get user tokens, assume json result {"token": "<token-value>"}
40
+ res = ft_client.get("/login", login="calvin", auth="basic", status=200)
41
41
  assert res.is_json
42
42
  ft_client.setToken("calvin", res.json["token"])
43
+ res = ft_client.post("/login", login="hobbes", auth="param", status=201)
44
+ assert res.is_json
45
+ ft_client.setToken("hobbes", res.json["token"])
43
46
  # return working client
44
47
  yield ft_client
45
48
 
46
- def test_app(app):
49
+ def test_app_admin(app):
50
+ # try all authentication schemes for calvin
47
51
  app.get("/admin", login="calvin", auth="bearer", status=200)
48
52
  app.get("/admin", login="calvin", auth="basic", status=200)
53
+ app.get("/admin", login="calvin", auth="param", status=200)
54
+ # try all authentication schemes for hobbes
55
+ res = app.get("/admin", login="hobbes", auth="bearer", status=403)
56
+ assert 'not in group "ADMIN"' in res.text
49
57
  res = app.get("/admin", login="hobbes", auth="basic", status=403)
50
58
  assert 'not in group "ADMIN"' in res.text
59
+ res = app.get("/admin", login="hobbes", auth="param", status=403)
60
+ assert 'not in group "ADMIN"' in res.text
51
61
  ```
52
62
 
53
63
  This can be run against a (local) server:
54
64
 
55
65
  ```shell
66
+ export TEST_SEED="some-random-data" # shared seed
56
67
  flask --app app:app run & # start flask app
57
68
  pid=$! # keep pid
58
69
  export FLASK_TESTER_URL="http://localhost:5000" # set app local url
@@ -171,23 +182,29 @@ The implementation of these fixtures is based on five classes plus exceptions:
171
182
 
172
183
  ## License
173
184
 
174
- This code is public domain.
185
+ This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
186
+
187
+ All software has bug, this is software, hence… Beware that you may lose your
188
+ hairs or your friends because of it. If you like it, feel free to send a
189
+ postcard to the author.
190
+
191
+ ## Versions
175
192
 
176
193
  Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
177
194
  [sources](https://github.com/zx80/flask-tester) are available on GitHub,
178
195
  see also the [documentation](https://zx80.github.io/flask-tester/),
179
196
  please report any [issues](https://github.com/zx80/flask-tester/issues).
180
197
 
181
- ## See Also, or Not
182
-
183
- - [Flask Testing](https://github.com/jarus/flask-testing) an unmaintained
184
- old-style unit test for Flask 1.x, without authentication help.
198
+ ### 1.4 on 2024-03-19
185
199
 
186
- ## TODO
200
+ Test expected assert failures.
201
+ Improved API documentation.
202
+ Keep first found app.
187
203
 
188
- - API documentation generation
204
+ ### 1.3 on 2024-03-16
189
205
 
190
- ## Versions
206
+ Generate API documentation.
207
+ Cleaner code.
191
208
 
192
209
  ### 1.2 on 2024-03-15
193
210
 
@@ -213,3 +230,8 @@ Working coverage tests.
213
230
  ### 0.9 on 2024-03-11
214
231
 
215
232
  Initial revision extracted from a separate project.
233
+
234
+ ## See Also, or Not
235
+
236
+ - [Flask Testing](https://github.com/jarus/flask-testing) an unmaintained
237
+ old-style unit test for Flask 1.x, without authentication help.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "FlaskTester"
7
- version = "1.2"
7
+ version = "1.4"
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"
@@ -35,7 +35,7 @@ package = "https://pypi.org/project/FlaskTester/"
35
35
  # various dev tools
36
36
  dev = ["mypy", "pyright", "ruff", "coverage", "pymarkdownlnt!=0.9.5", "build", "twine", "wheel", "types-flask", "types-requests", "FlaskSimpleAuth", "passlib"]
37
37
  # documentation generation
38
- doc = ["sphinx", "sphinx_rtd_theme", "sphinx-autoapi", "sphinx-lint"]
38
+ doc = ["sphinx", "sphinx_rtd_theme", "sphinx-autoapi", "sphinx-lint", "myst_parser"]
39
39
 
40
40
  [tool.setuptools]
41
41
  py-modules = [ "FlaskTester" ]
@@ -27,18 +27,28 @@ def app(ft_client):
27
27
  # add test passwords for Calvin and Hobbes (must be consistent with app!)
28
28
  ft_client.setPass("calvin", secret.PASSES["calvin"])
29
29
  ft_client.setPass("hobbes", secret.PASSES["hobbes"])
30
- # get Calvin's token, assume json result {"token": "<token-value>"}
31
- res = ft_client.get("/token", login="calvin", auth="basic", status=200)
30
+ # get user tokens, assume json result {"token": "<token-value>"}
31
+ res = ft_client.get("/login", login="calvin", auth="basic", status=200)
32
32
  assert res.is_json
33
33
  ft_client.setToken("calvin", res.json["token"])
34
+ res = ft_client.post("/login", login="hobbes", auth="param", status=201)
35
+ assert res.is_json
36
+ ft_client.setToken("hobbes", res.json["token"])
34
37
  # return working client
35
38
  yield ft_client
36
39
 
37
- def test_app(app):
40
+ def test_app_admin(app):
41
+ # try all authentication schemes for calvin
38
42
  app.get("/admin", login="calvin", auth="bearer", status=200)
39
43
  app.get("/admin", login="calvin", auth="basic", status=200)
44
+ app.get("/admin", login="calvin", auth="param", status=200)
45
+ # try all authentication schemes for hobbes
46
+ res = app.get("/admin", login="hobbes", auth="bearer", status=403)
47
+ assert 'not in group "ADMIN"' in res.text
40
48
  res = app.get("/admin", login="hobbes", auth="basic", status=403)
41
49
  assert 'not in group "ADMIN"' in res.text
50
+ res = app.get("/admin", login="hobbes", auth="param", status=403)
51
+ assert 'not in group "ADMIN"' in res.text
42
52
 
43
53
  @pytest.fixture
44
54
  def api(ft_client):
@@ -50,13 +60,13 @@ def api(ft_client):
50
60
  ft_client.setPass("moe", "bad password")
51
61
  ft_client.setToken("moe", "bad token")
52
62
  # get valid tokens using password authn
53
- res = ft_client.get("/token", login="calvin", auth="basic", status=200)
63
+ res = ft_client.get("/login", login="calvin", auth="basic", status=200)
54
64
  assert res.json["user"] == "calvin"
55
65
  ft_client._auth.setToken("calvin", res.json["token"])
56
- res = ft_client.post("/token", login="hobbes", auth="param", status=201, data={})
66
+ res = ft_client.post("/login", login="hobbes", auth="param", status=201, data={})
57
67
  assert res.json["user"] == "hobbes"
58
68
  ft_client._auth.setToken("hobbes", res.json["token"])
59
- res = ft_client.post("/token", login="susie", auth="param", status=201, json={})
69
+ res = ft_client.post("/login", login="susie", auth="param", status=201, json={})
60
70
  assert res.json["user"] == "susie"
61
71
  ft_client._auth.setToken("susie", res.json["token"])
62
72
  # check that token auth is ok
@@ -86,12 +96,22 @@ def test_admin(api):
86
96
  api.check("GET", "/admin", 403, login="hobbes", auth=auth)
87
97
  api.check("GET", "/admin", 401, login="moe", auth=auth)
88
98
  api.check("GET", "/admin", 401, login=None, auth=auth)
99
+ try:
100
+ api.check("GET", "/admin", status=599, login="calvin", auth=auth)
101
+ pytest.fail("assert on status must fail") # pragma: no cover
102
+ except AssertionError as e:
103
+ assert "200" in str(e)
104
+ try:
105
+ api.check("GET", "/admin", status=200, login="calvin", auth=auth, content="NOT THERE")
106
+ pytest.fail("assert on content must fail") # pragma: no cover
107
+ except AssertionError as e:
108
+ assert "NOT THERE" in str(e)
89
109
 
90
110
  def test_errors(api):
91
111
  for scheme in ("header", "cookie", "fake", "tparam", "unexpected"):
92
112
  try:
93
- api.get("/token", login="calvin", auth=scheme)
94
- assert False, "must raise an exception" # pragma: no cover
113
+ api.get("/login", login="calvin", auth=scheme)
114
+ pytest.fail("must raise an exception") # pragma: no cover
95
115
  except ft.AuthError as e:
96
116
  assert True, "expected error"
97
117
 
@@ -128,13 +148,13 @@ def test_authenticator_token():
128
148
  try:
129
149
  kwargs = {}
130
150
  auth.setAuth("dad", kwargs)
131
- assert False, "must raise an error" # pragma: no cover
151
+ pytest.fail("must raise an error") # pragma: no cover
132
152
  except ft.FlaskTesterError:
133
153
  assert True, "error raised"
134
154
  # rosalyn as a password, but no password carrier is allowed
135
155
  try:
136
156
  auth.setPass("rosalyn", "rsln-pass")
137
- assert False, "must raise an error" # pragma: no cover
157
+ pytest.fail("must raise an error") # pragma: no cover
138
158
  except ft.AuthError:
139
159
  assert True, "error raised"
140
160
  # force to trigger later errors
@@ -143,7 +163,7 @@ def test_authenticator_token():
143
163
  try:
144
164
  kwargs={}
145
165
  auth.setAuth("rosalyn", kwargs)
146
- assert False, "must raise an error" # pragma: no cover
166
+ pytest.fail("must raise an error") # pragma: no cover
147
167
  except ft.FlaskTesterError:
148
168
  assert True, "error raised"
149
169
 
@@ -172,7 +192,7 @@ def test_authenticator_password():
172
192
  # susie as a token, but no token carrier is allowed
173
193
  try:
174
194
  auth.setToken("susie", "ss-token")
175
- assert False, "must raise an error" # pragma: no cover
195
+ pytest.fail("must raise an error") # pragma: no cover
176
196
  except ft.FlaskTesterError:
177
197
  assert True, "error raised"
178
198
  # force to trigger later error
@@ -181,7 +201,7 @@ def test_authenticator_password():
181
201
  try:
182
202
  kwargs={}
183
203
  auth.setAuth("susie", kwargs)
184
- assert False, "must raise an error" # pragma: no cover
204
+ pytest.fail("must raise an error") # pragma: no cover
185
205
  except ft.FlaskTesterError:
186
206
  assert True, "error raised"
187
207
 
@@ -214,9 +234,9 @@ def test_client():
214
234
  client = ft.Client(ft.Authenticator())
215
235
  try:
216
236
  client._request("GET", "/")
217
- assert False, "must raise an error" # pragma: no cover
237
+ pytest.fail("must raise an error") # pragma: no cover
218
238
  except NotImplementedError:
219
- assert True, "error raised"
239
+ assert True, "expected error raised"
220
240
 
221
241
  def test_request_client():
222
242
  httpd = htsv.HTTPServer(("", 8888), htsv.SimpleHTTPRequestHandler)
File without changes
File without changes