FlaskTester 3.3__tar.gz → 3.5__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.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.1
2
+ Name: FlaskTester
3
+ Version: 3.5
4
+ Summary: Pytest fixtures for Flask internal and external authenticated tests
5
+ Author-email: Fabien Coelho <flask.tester@coelho.net>
6
+ License: CC0
7
+ Project-URL: repository, https://github.com/zx80/flask-tester
8
+ Project-URL: documentation, https://zx80.github.io/flask-tester/
9
+ Project-URL: issues, https://github.com/zx80/flask-tester/issues
10
+ Project-URL: package, https://pypi.org/project/FlaskTester/
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Environment :: Web Environment
16
+ Classifier: Framework :: Flask
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: requests
22
+ Requires-Dist: flask
23
+ Requires-Dist: pytest
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy; extra == "dev"
26
+ Requires-Dist: pyright; extra == "dev"
27
+ Requires-Dist: ruff; extra == "dev"
28
+ Requires-Dist: coverage; extra == "dev"
29
+ Requires-Dist: pymarkdownlnt!=0.9.5; extra == "dev"
30
+ Requires-Dist: build; extra == "dev"
31
+ Requires-Dist: twine; extra == "dev"
32
+ Requires-Dist: wheel; extra == "dev"
33
+ Requires-Dist: types-flask; extra == "dev"
34
+ Requires-Dist: types-requests; extra == "dev"
35
+ Requires-Dist: FlaskSimpleAuth>=30.0; extra == "dev"
36
+ Requires-Dist: passlib; extra == "dev"
37
+ Provides-Extra: doc
38
+ Requires-Dist: sphinx; extra == "doc"
39
+ Requires-Dist: sphinx_rtd_theme; extra == "doc"
40
+ Requires-Dist: sphinx-autoapi; extra == "doc"
41
+ Requires-Dist: sphinx-lint; extra == "doc"
42
+ Requires-Dist: myst_parser; extra == "doc"
43
+
44
+ # FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
45
+
46
+ This package allows to run authenticated tests against a Flask application,
47
+ either with internal Flask tests (aka `test_client`) or external tests (with
48
+ `requests` which performs actual HTTP requests), including password and token
49
+ authentication and per-user cookies.
50
+
51
+ Only one set of tests is needed, switching from internal to external is
52
+ achieved through environment variables.
53
+
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-13%20✓-success)
56
+ ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
57
+ ![Issues](https://img.shields.io/github/issues/zx80/flask-tester?style=flat)
58
+ ![Python](https://img.shields.io/badge/python-3-informational)
59
+ ![Version](https://img.shields.io/pypi/v/FlaskTester)
60
+ ![Badges](https://img.shields.io/badge/badges-8-informational)
61
+ ![License](https://img.shields.io/pypi/l/flasktester?style=flat)
62
+
63
+ ## Usage
64
+
65
+ Install package with `pip install FlaskTester` or equivalent.
66
+
67
+ The following test creates a local fixture with 2 users identified by a
68
+ password, and retrieves tokens for both users using a `/login` route
69
+ provided by the application.
70
+ It then proceeds to run authenticated requests against the `/admin` route.
71
+
72
+ ```python
73
+ import pytest
74
+ from FlaskTester import ft_authenticator, ft_client
75
+ import secret
76
+
77
+ @pytest.fixture
78
+ def app(ft_client):
79
+ # add test passwords for Calvin and Hobbes (must be consistent with app!)
80
+ ft_client.setPass("calvin", secret.PASSES["calvin"])
81
+ ft_client.setPass("hobbes", secret.PASSES["hobbes"])
82
+ # get user tokens, assume json result {"token": "<token-value>"}
83
+ res = ft_client.get("/login", login="calvin", auth="basic", status=200)
84
+ assert res.is_json
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"])
89
+ # also set a cookie
90
+ ft_client.setCookie("hobbes", "lang", "fr")
91
+ ft_client.setCookie("calvin", "lang", "en")
92
+ # return working client
93
+ yield ft_client
94
+
95
+ def test_app_admin(app):
96
+ app.get("/admin", login=None, status=401)
97
+ for auth in ["bearer", "basic", "param"]:
98
+ res = app.get("/admin", login="calvin", auth=auth, status=200)
99
+ assert res.json["user"] == "calvin" and res.json["isadmin"]
100
+ res = app.get("/admin", login="hobbes", auth=auth, status=403)
101
+ assert 'not in group "ADMIN"' in res.text
102
+ ```
103
+
104
+ This can be run against a (local) server:
105
+
106
+ ```shell
107
+ export TEST_SEED="some-random-data" # shared test seed
108
+ flask --app app:app run & # start flask app
109
+ pid=$! # keep pid
110
+ export FLASK_TESTER_URL="http://localhost:5000" # set app local url
111
+ pytest test.py # run external tests
112
+ kill $pid # stop app with pid
113
+ ```
114
+
115
+ Or locally with the Flask internal test infrastructure:
116
+
117
+ ```shell
118
+ export FLASK_TESTER_APP="app:app" # set app module
119
+ pytest test.py # run internal tests
120
+ ```
121
+
122
+ The above test runs with [`tests/app.py`](tests/app.py)
123
+ [Flask](https://flask.palletsprojects.com/)
124
+ REST application back-end with password and token authentication based on
125
+ [FlaskSimpleAuth](https://pypi.org/project/FlaskSimpleAuth/).
126
+ The code uses _25_ lines of Python for implementing
127
+ password (basic and parameters) and token authentications,
128
+ admin group authorization, and routes for
129
+ token generation (2), identity tests (2) and an incredible open cookie-based
130
+ translation service.
131
+
132
+ See the [documentation](https://zx80.github.io/flask-tester/).
133
+
134
+ ## License
135
+
136
+ This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
137
+
138
+ All software has bug, this is software, hence… Beware that you may lose your
139
+ hairs or your friends because of it. If you like it, feel free to send a
140
+ postcard to the author.
141
+
142
+ ## Versions
143
+
144
+ Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
145
+ [sources](https://github.com/zx80/flask-tester) are available on GitHub,
146
+ please report any [issues](https://github.com/zx80/flask-tester/issues).
@@ -13,7 +13,7 @@ twine
13
13
  wheel
14
14
  types-flask
15
15
  types-requests
16
- FlaskSimpleAuth>=29.5
16
+ FlaskSimpleAuth>=30.0
17
17
  passlib
18
18
 
19
19
  [doc]
@@ -1,6 +1,6 @@
1
1
  """FlaskTester - Pytest fixtures for Flask authenticated internal and external tests.
2
2
 
3
- Pytest: PYTEST_DONT_REWRITE
3
+ PYTEST_DONT_REWRITE: local assertions are really that, not pytest assertions.
4
4
  """
5
5
 
6
6
  import os
@@ -24,38 +24,56 @@ class AuthError(FlaskTesterError):
24
24
  pass
25
25
 
26
26
 
27
+ class _AssertError(FlaskTesterError):
28
+ """User assertion Exception, only for internal testing."""
29
+ pass
30
+
31
+
32
+ def _raiseError(msg: str):
33
+ """Undocumented switch for FlaskTester own tests."""
34
+ log.error(msg)
35
+ if "FLASK_TESTER_TESTING" in os.environ:
36
+ raise _AssertError(msg)
37
+ else: # pragma: no cover # cannot cover an expected pytest failure!
38
+ pytest.fail(msg)
39
+
40
+
27
41
  class Authenticator:
28
- """Manage authentication for test requests.
42
+ """Manage HTTP request authentication.
29
43
 
30
44
  Supported schemes:
31
45
 
32
- - ``basic``: HTTP Basic Authentication
33
- - ``param``: password with HTTP or JSON parameters
34
- - ``bearer``: token in ``Authorization`` *bearer* header
35
- - ``header``: token in a header
36
- - ``cookie``: token in a cookie
37
- - ``tparam``: token in a parameter
38
- - ``fake``: fake scheme, login directly passed as a parameter
39
- - ``none``: no authentication, only cookies
46
+ - ``basic``: HTTP Basic Authentication.
47
+ - ``param``: password with HTTP or JSON parameters.
48
+ - ``bearer``: token in ``Authorization`` *bearer* header.
49
+ - ``header``: token in a header.
50
+ - ``cookie``: token in a cookie.
51
+ - ``tparam``: token in a parameter.
52
+ - ``fake``: fake scheme, login directly passed as a parameter.
53
+ - ``none``: no authentication, only cookies.
40
54
 
41
55
  Constructor parameters:
42
56
 
43
57
  - ``allow``: list of allowed schemes.
44
- default is ``["bearer", "basic", "param", "none"]``
58
+ default is ``["bearer", "basic", "param", "none"]``.
45
59
  - ``user``: parameter for user on ``param`` password authentication,
46
- default is ``USER``
60
+ default is ``USER``.
47
61
  - ``pwd``: parameter for password on ``param`` password authentication,
48
- default is ``PASS``
62
+ default is ``PASS``.
49
63
  - ``login``: parameter for user on ``fake`` authentication,
50
- default is ``LOGIN``
64
+ default is ``LOGIN``.
51
65
  - ``bearer``: name of bearer scheme for token,
52
- default is ``Bearer``
66
+ default is ``Bearer``.
53
67
  - ``header``: name of header for token,
54
- default is ``Auth``
68
+ default is ``Auth``.
55
69
  - ``cookie``: name of cookie for token,
56
- default is ``auth``
70
+ default is ``auth``.
57
71
  - ``tparam``: name of parameter for token,
58
- default is ``AUTH``
72
+ default is ``AUTH``.
73
+ - ``ptype``: default parameter type, either ``data`` or ``json``,
74
+ default is ``data``.
75
+
76
+ Note: default values are consistent with `FlaskSimpleAuth <https://pypi.org/project/FlaskSimpleAuth/>`_.
59
77
  """
60
78
 
61
79
  _TOKEN_SCHEMES = {"bearer", "header", "cookie", "tparam"}
@@ -78,6 +96,7 @@ class Authenticator:
78
96
  header: str = "Auth",
79
97
  cookie: str = "auth",
80
98
  tparam: str = "AUTH",
99
+ ptype: str = "data",
81
100
  ):
82
101
 
83
102
  self._has_pass, self._has_token = False, False
@@ -97,6 +116,8 @@ class Authenticator:
97
116
  self._header = header
98
117
  self._cookie = cookie
99
118
  self._tparam = tparam
119
+ assert ptype in ("json", "data")
120
+ self._ptype = ptype
100
121
 
101
122
  # password and token credentials, cookies
102
123
  self._passes: dict[str, str] = {}
@@ -122,7 +143,7 @@ class Authenticator:
122
143
  self._set(login, pw, self._passes)
123
144
 
124
145
  def setPasses(self, pws: list[str]):
125
- """Associate a list of login:password."""
146
+ """Associate a list of *login:password*."""
126
147
  for lp in pws:
127
148
  login, pw = lp.split(":", 1)
128
149
  self.setPass(login, pw)
@@ -143,7 +164,7 @@ class Authenticator:
143
164
  self._set(name, val, self._cookies[login])
144
165
 
145
166
  def _param(self, kwargs: dict[str, Any], key: str, val: Any):
146
- """Add request parameter to "json" or "data"."""
167
+ """Add request parameter to ``json`` or ``data``."""
147
168
 
148
169
  if "json" in kwargs:
149
170
  assert isinstance(kwargs["json"], dict)
@@ -152,7 +173,7 @@ class Authenticator:
152
173
  assert isinstance(kwargs["data"], dict)
153
174
  kwargs["data"][key] = val
154
175
  else:
155
- kwargs["data"] = {key: val}
176
+ kwargs[self._ptype] = {key: val}
156
177
 
157
178
  def _try_auth(self, auth: str|None, scheme: str) -> bool:
158
179
  """Whether to try this authentication scheme."""
@@ -161,10 +182,10 @@ class Authenticator:
161
182
  def setAuth(self, login: str|None, kwargs: dict[str, Any], cookies: dict[str, str], auth: str|None = None):
162
183
  """Set request authentication.
163
184
 
164
- - login: login target, None means no authentication
165
- - kwargs: request parameters
166
- - cookies: request cookies
167
- - auth: authentication method, default is None
185
+ - ``login``: login target, None means no authentication.
186
+ - ``kwargs``: request parameters.
187
+ - ``cookies``: request cookies.
188
+ - ``auth``: authentication method, default is None.
168
189
 
169
190
  The default behavior is to try allowed schemes: tokens first,
170
191
  then password, then fake.
@@ -234,13 +255,13 @@ class RequestFlaskResponse:
234
255
 
235
256
  Available attributes:
236
257
 
237
- - ``status_code``: integer status code
238
- - ``data``: body as bytes
239
- - ``text``: body as a string
240
- - ``headers``: dict of headers and their values
241
- - ``cookies``: dict of cookies
242
- - ``json``: JSON-converted body, or None
243
- - ``is_json``: whether body was in JSON
258
+ - ``status_code``: integer status code.
259
+ - ``data``: body as bytes.
260
+ - ``text``: body as a string.
261
+ - ``headers``: dict of headers and their values.
262
+ - ``cookies``: dict of cookies.
263
+ - ``json``: JSON-converted body, or *None*.
264
+ - ``is_json``: whether body was in JSON.
244
265
 
245
266
  Constructor parameter:
246
267
 
@@ -268,8 +289,8 @@ class Client:
268
289
 
269
290
  Constructor parameters:
270
291
 
271
- - ``auth`` authenticator
272
- - ``default_login`` if ``login`` is not set.
292
+ - ``auth``: authenticator.
293
+ - ``default_login``: if ``login`` is not set.
273
294
  """
274
295
 
275
296
  def __init__(self, auth: Authenticator, default_login: str|None = None):
@@ -299,16 +320,16 @@ class Client:
299
320
 
300
321
  Mandatory parameters:
301
322
 
302
- - ``method``: HTTP method ("GET", "POST", "PATCH", "DELETE"…)
303
- - ``path``: local path under the base URL
323
+ - ``method``: HTTP method ("GET", "POST", "PATCH", "DELETE"…).
324
+ - ``path``: local path under the base URL.
304
325
 
305
326
  Optional parameters:
306
327
 
307
- - ``status``: expected HTTP status, *None* to skip status check
308
- - ``content``: regular expression for response body, *None* to skip content check
309
- - ``login``: authenticated user, use **explicit** *None* to skip
310
- - ``auth``: authentication scheme to use instead of default behavior
311
- - ``**kwargs``: more request parameters (headers, data, json…)
328
+ - ``status``: expected HTTP status, *None* to skip status check.
329
+ - ``content``: regular expression for response body, *None* to skip content check.
330
+ - ``login``: authenticated user, use **explicit** *None* to skip.
331
+ - ``auth``: authentication scheme to use instead of default behavior.
332
+ - ``**kwargs``: more request parameters (headers, data, json…).
312
333
  """
313
334
 
314
335
  if "login" in kwargs:
@@ -328,43 +349,37 @@ class Client:
328
349
  # check status
329
350
  if status is not None:
330
351
  if res.status_code != status: # show error before aborting
331
- log.error(f"bad {status} result: {res.status_code} {res.text[:512]}")
332
- assert res.status_code == status, f"unexpected status {res.status_code}, expecting {status}"
352
+ _raiseError(f"bad {status} result: {res.status_code} {res.text[:512]}...")
333
353
 
334
354
  # check content
335
355
  if content is not None:
336
356
  if not re.search(content, res.text, re.DOTALL):
337
- log.error(f"cannot find {content} in {res.text}")
338
- assert False, f"expected content {content} not found in {res.text}"
357
+ _raiseError(f"cannot find {content} in {res.text[:512]}...")
339
358
 
340
359
  return res
341
360
 
342
- def get(self, path, **kwargs):
343
- """HTTP GET request."""
344
- return self.request("GET", path, **kwargs)
361
+ def get(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
362
+ """HTTP GET request, see ``request`` parameters."""
363
+ return self.request("GET", path, status=status, content=content, **kwargs)
345
364
 
346
- def post(self, path, **kwargs):
347
- """HTTP POST request."""
348
- return self.request("POST", path, **kwargs)
365
+ def post(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
366
+ """HTTP POST request, see ``request`` parameters."""
367
+ return self.request("POST", path, status=status, content=content, **kwargs)
349
368
 
350
- def put(self, path, **kwargs):
351
- """HTTP PUT request."""
352
- return self.request("PUT", path, **kwargs)
369
+ def put(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
370
+ """HTTP PUT request, see ``request`` parameters."""
371
+ return self.request("PUT", path, status=status, content=content, **kwargs)
353
372
 
354
- def patch(self, path, **kwargs):
355
- """HTTP PATCH request."""
356
- return self.request("PATCH", path, **kwargs)
373
+ def patch(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
374
+ """HTTP PATCH request, see ``request`` parameters."""
375
+ return self.request("PATCH", path, status=status, content=content, **kwargs)
357
376
 
358
- def delete(self, path, **kwargs):
359
- """HTTP DELETE request."""
360
- return self.request("DELETE", path, **kwargs)
377
+ def delete(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
378
+ """HTTP DELETE request, see ``request`` parameters."""
379
+ return self.request("DELETE", path, status=status, content=content, **kwargs)
361
380
 
362
381
  def check(self, method: str, path: str, status: int, content: str|None = None, **kwargs):
363
- """Run a query and check the response status.
364
-
365
- Same as ``request``, but ``status`` is mandatory.
366
- """
367
-
382
+ """Deprecated, use ``request`` or HTTP-method-specific methods."""
368
383
  return self.request(method, path, status=status, content=content, **kwargs)
369
384
 
370
385
 
@@ -373,9 +388,9 @@ class RequestClient(Client):
373
388
 
374
389
  Constructor parameters:
375
390
 
376
- - ``auth`` authenticator
377
- - ``base_url`` target server
378
- - ``default_login`` if ``login`` is not set.
391
+ - ``auth``: authenticator.
392
+ - ``base_url``: target server.
393
+ - ``default_login``: if ``login`` is not set.
379
394
  """
380
395
 
381
396
  def __init__(self, auth: Authenticator, base_url: str, default_login=None):
@@ -419,9 +434,9 @@ class FlaskClient(Client):
419
434
 
420
435
  Constructor parameters:
421
436
 
422
- - ``auth`` authenticator
423
- - ``client`` Flask actual ``test_client``
424
- - ``default_login`` if ``login`` is not set.
437
+ - ``auth``: authenticator.
438
+ - ``client``: Flask actual ``test_client``.
439
+ - ``default_login``: if ``login`` is not set.
425
440
 
426
441
  Note: this client handles `cookies`.
427
442
  """
@@ -446,7 +461,7 @@ class FlaskClient(Client):
446
461
  return self._client.open(method=method, path=path, **kwargs)
447
462
 
448
463
  def _ft_authenticator():
449
- """Fixture implementation separated for testing."""
464
+ """Fixture implementation, separated for testing purposes."""
450
465
 
451
466
  level = os.environ.get("FLASK_TESTER_LOG_LEVEL", "NOTSET")
452
467
  log.setLevel(logging.DEBUG if level == "DEBUG" else
@@ -466,9 +481,11 @@ def _ft_authenticator():
466
481
  header = os.environ.get("FLASK_TESTER_HEADER", "Auth")
467
482
  cookie = os.environ.get("FLASK_TESTER_COOKIE", "auth")
468
483
  tparam = os.environ.get("FLASK_TESTER_TPARAM", "AUTH")
484
+ ptype = os.environ.get("FLASK_TESTER_PTYPE", "data")
469
485
 
470
486
  # create authenticator, possibly with initial credentials
471
- auth = Authenticator(allow, user=user, pwd=pwd, login=login, bearer=bearer, header=header, cookie=cookie, tparam=tparam)
487
+ auth = Authenticator(allow, user=user, pwd=pwd, login=login, bearer=bearer,
488
+ header=header, cookie=cookie, tparam=tparam, ptype=ptype)
472
489
 
473
490
  if "FLASK_TESTER_AUTH" in os.environ:
474
491
  auth.setPasses(os.environ["FLASK_TESTER_AUTH"].split(","))
@@ -501,6 +518,8 @@ def ft_authenticator():
501
518
  Default is ``auth``.
502
519
  - ``FLASK_TESTER_TPARAM``: parameter for *token* authentication.
503
520
  Default is ``AUTH``.
521
+ - ``FLASK_TESTER_PTYPE``: default parameter type, ``data`` or ``json``.
522
+ Default is ``data``.
504
523
  - ``FLASK_TESTER_AUTH``: initial comma-separated list of *login:password*.
505
524
  Default is not set.
506
525
  """
@@ -550,7 +569,7 @@ def ft_client(ft_authenticator):
550
569
 
551
570
  Target environment variable, one **must** be defined:
552
571
 
553
- - ``FLASK_TESTER_URL``: application HTTP base URL.
572
+ - ``FLASK_TESTER_URL``: application HTTP base URL, eg ``http://localhost:5000``.
554
573
  - ``FLASK_TESTER_APP``: Flask application, eg ``app:create_app``.
555
574
 
556
575
  Other environment variable:
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.1
2
+ Name: FlaskTester
3
+ Version: 3.5
4
+ Summary: Pytest fixtures for Flask internal and external authenticated tests
5
+ Author-email: Fabien Coelho <flask.tester@coelho.net>
6
+ License: CC0
7
+ Project-URL: repository, https://github.com/zx80/flask-tester
8
+ Project-URL: documentation, https://zx80.github.io/flask-tester/
9
+ Project-URL: issues, https://github.com/zx80/flask-tester/issues
10
+ Project-URL: package, https://pypi.org/project/FlaskTester/
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Environment :: Web Environment
16
+ Classifier: Framework :: Flask
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: requests
22
+ Requires-Dist: flask
23
+ Requires-Dist: pytest
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy; extra == "dev"
26
+ Requires-Dist: pyright; extra == "dev"
27
+ Requires-Dist: ruff; extra == "dev"
28
+ Requires-Dist: coverage; extra == "dev"
29
+ Requires-Dist: pymarkdownlnt!=0.9.5; extra == "dev"
30
+ Requires-Dist: build; extra == "dev"
31
+ Requires-Dist: twine; extra == "dev"
32
+ Requires-Dist: wheel; extra == "dev"
33
+ Requires-Dist: types-flask; extra == "dev"
34
+ Requires-Dist: types-requests; extra == "dev"
35
+ Requires-Dist: FlaskSimpleAuth>=30.0; extra == "dev"
36
+ Requires-Dist: passlib; extra == "dev"
37
+ Provides-Extra: doc
38
+ Requires-Dist: sphinx; extra == "doc"
39
+ Requires-Dist: sphinx_rtd_theme; extra == "doc"
40
+ Requires-Dist: sphinx-autoapi; extra == "doc"
41
+ Requires-Dist: sphinx-lint; extra == "doc"
42
+ Requires-Dist: myst_parser; extra == "doc"
43
+
44
+ # FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
45
+
46
+ This package allows to run authenticated tests against a Flask application,
47
+ either with internal Flask tests (aka `test_client`) or external tests (with
48
+ `requests` which performs actual HTTP requests), including password and token
49
+ authentication and per-user cookies.
50
+
51
+ Only one set of tests is needed, switching from internal to external is
52
+ achieved through environment variables.
53
+
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-13%20✓-success)
56
+ ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
57
+ ![Issues](https://img.shields.io/github/issues/zx80/flask-tester?style=flat)
58
+ ![Python](https://img.shields.io/badge/python-3-informational)
59
+ ![Version](https://img.shields.io/pypi/v/FlaskTester)
60
+ ![Badges](https://img.shields.io/badge/badges-8-informational)
61
+ ![License](https://img.shields.io/pypi/l/flasktester?style=flat)
62
+
63
+ ## Usage
64
+
65
+ Install package with `pip install FlaskTester` or equivalent.
66
+
67
+ The following test creates a local fixture with 2 users identified by a
68
+ password, and retrieves tokens for both users using a `/login` route
69
+ provided by the application.
70
+ It then proceeds to run authenticated requests against the `/admin` route.
71
+
72
+ ```python
73
+ import pytest
74
+ from FlaskTester import ft_authenticator, ft_client
75
+ import secret
76
+
77
+ @pytest.fixture
78
+ def app(ft_client):
79
+ # add test passwords for Calvin and Hobbes (must be consistent with app!)
80
+ ft_client.setPass("calvin", secret.PASSES["calvin"])
81
+ ft_client.setPass("hobbes", secret.PASSES["hobbes"])
82
+ # get user tokens, assume json result {"token": "<token-value>"}
83
+ res = ft_client.get("/login", login="calvin", auth="basic", status=200)
84
+ assert res.is_json
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"])
89
+ # also set a cookie
90
+ ft_client.setCookie("hobbes", "lang", "fr")
91
+ ft_client.setCookie("calvin", "lang", "en")
92
+ # return working client
93
+ yield ft_client
94
+
95
+ def test_app_admin(app):
96
+ app.get("/admin", login=None, status=401)
97
+ for auth in ["bearer", "basic", "param"]:
98
+ res = app.get("/admin", login="calvin", auth=auth, status=200)
99
+ assert res.json["user"] == "calvin" and res.json["isadmin"]
100
+ res = app.get("/admin", login="hobbes", auth=auth, status=403)
101
+ assert 'not in group "ADMIN"' in res.text
102
+ ```
103
+
104
+ This can be run against a (local) server:
105
+
106
+ ```shell
107
+ export TEST_SEED="some-random-data" # shared test seed
108
+ flask --app app:app run & # start flask app
109
+ pid=$! # keep pid
110
+ export FLASK_TESTER_URL="http://localhost:5000" # set app local url
111
+ pytest test.py # run external tests
112
+ kill $pid # stop app with pid
113
+ ```
114
+
115
+ Or locally with the Flask internal test infrastructure:
116
+
117
+ ```shell
118
+ export FLASK_TESTER_APP="app:app" # set app module
119
+ pytest test.py # run internal tests
120
+ ```
121
+
122
+ The above test runs with [`tests/app.py`](tests/app.py)
123
+ [Flask](https://flask.palletsprojects.com/)
124
+ REST application back-end with password and token authentication based on
125
+ [FlaskSimpleAuth](https://pypi.org/project/FlaskSimpleAuth/).
126
+ The code uses _25_ lines of Python for implementing
127
+ password (basic and parameters) and token authentications,
128
+ admin group authorization, and routes for
129
+ token generation (2), identity tests (2) and an incredible open cookie-based
130
+ translation service.
131
+
132
+ See the [documentation](https://zx80.github.io/flask-tester/).
133
+
134
+ ## License
135
+
136
+ This code is [Public Domain](https://creativecommons.org/publicdomain/zero/1.0/).
137
+
138
+ All software has bug, this is software, hence… Beware that you may lose your
139
+ hairs or your friends because of it. If you like it, feel free to send a
140
+ postcard to the author.
141
+
142
+ ## Versions
143
+
144
+ Packages are distributed from [PyPI](https://pypi.org/project/FlaskTester/),
145
+ [sources](https://github.com/zx80/flask-tester) are available on GitHub,
146
+ please report any [issues](https://github.com/zx80/flask-tester/issues).