FlaskTester 3.3__tar.gz → 3.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.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.1
2
+ Name: FlaskTester
3
+ Version: 3.4
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,7 @@
1
1
  """FlaskTester - Pytest fixtures for Flask authenticated internal and external tests.
2
2
 
3
- Pytest: PYTEST_DONT_REWRITE
3
+ This is needed when testing this module, but may not be desirable when it is
4
+ used depending on the ``assert``: PYTEST_DONT_REWRITE
4
5
  """
5
6
 
6
7
  import os
@@ -25,37 +26,41 @@ class AuthError(FlaskTesterError):
25
26
 
26
27
 
27
28
  class Authenticator:
28
- """Manage authentication for test requests.
29
+ """Manage HTTP request authentication.
29
30
 
30
31
  Supported schemes:
31
32
 
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
33
+ - ``basic``: HTTP Basic Authentication.
34
+ - ``param``: password with HTTP or JSON parameters.
35
+ - ``bearer``: token in ``Authorization`` *bearer* header.
36
+ - ``header``: token in a header.
37
+ - ``cookie``: token in a cookie.
38
+ - ``tparam``: token in a parameter.
39
+ - ``fake``: fake scheme, login directly passed as a parameter.
40
+ - ``none``: no authentication, only cookies.
40
41
 
41
42
  Constructor parameters:
42
43
 
43
44
  - ``allow``: list of allowed schemes.
44
- default is ``["bearer", "basic", "param", "none"]``
45
+ default is ``["bearer", "basic", "param", "none"]``.
45
46
  - ``user``: parameter for user on ``param`` password authentication,
46
- default is ``USER``
47
+ default is ``USER``.
47
48
  - ``pwd``: parameter for password on ``param`` password authentication,
48
- default is ``PASS``
49
+ default is ``PASS``.
49
50
  - ``login``: parameter for user on ``fake`` authentication,
50
- default is ``LOGIN``
51
+ default is ``LOGIN``.
51
52
  - ``bearer``: name of bearer scheme for token,
52
- default is ``Bearer``
53
+ default is ``Bearer``.
53
54
  - ``header``: name of header for token,
54
- default is ``Auth``
55
+ default is ``Auth``.
55
56
  - ``cookie``: name of cookie for token,
56
- default is ``auth``
57
+ default is ``auth``.
57
58
  - ``tparam``: name of parameter for token,
58
- default is ``AUTH``
59
+ default is ``AUTH``.
60
+ - ``ptype``: default parameter type, either ``data`` or ``json``,
61
+ default is ``data``.
62
+
63
+ Note: default values are consistent with `FlaskSimpleAuth <https://pypi.org/project/FlaskSimpleAuth/>`_.
59
64
  """
60
65
 
61
66
  _TOKEN_SCHEMES = {"bearer", "header", "cookie", "tparam"}
@@ -78,6 +83,7 @@ class Authenticator:
78
83
  header: str = "Auth",
79
84
  cookie: str = "auth",
80
85
  tparam: str = "AUTH",
86
+ ptype: str = "data",
81
87
  ):
82
88
 
83
89
  self._has_pass, self._has_token = False, False
@@ -97,6 +103,8 @@ class Authenticator:
97
103
  self._header = header
98
104
  self._cookie = cookie
99
105
  self._tparam = tparam
106
+ assert ptype in ("json", "data")
107
+ self._ptype = ptype
100
108
 
101
109
  # password and token credentials, cookies
102
110
  self._passes: dict[str, str] = {}
@@ -122,7 +130,7 @@ class Authenticator:
122
130
  self._set(login, pw, self._passes)
123
131
 
124
132
  def setPasses(self, pws: list[str]):
125
- """Associate a list of login:password."""
133
+ """Associate a list of *login:password*."""
126
134
  for lp in pws:
127
135
  login, pw = lp.split(":", 1)
128
136
  self.setPass(login, pw)
@@ -143,7 +151,7 @@ class Authenticator:
143
151
  self._set(name, val, self._cookies[login])
144
152
 
145
153
  def _param(self, kwargs: dict[str, Any], key: str, val: Any):
146
- """Add request parameter to "json" or "data"."""
154
+ """Add request parameter to ``json`` or ``data``."""
147
155
 
148
156
  if "json" in kwargs:
149
157
  assert isinstance(kwargs["json"], dict)
@@ -152,7 +160,7 @@ class Authenticator:
152
160
  assert isinstance(kwargs["data"], dict)
153
161
  kwargs["data"][key] = val
154
162
  else:
155
- kwargs["data"] = {key: val}
163
+ kwargs[self._ptype] = {key: val}
156
164
 
157
165
  def _try_auth(self, auth: str|None, scheme: str) -> bool:
158
166
  """Whether to try this authentication scheme."""
@@ -161,10 +169,10 @@ class Authenticator:
161
169
  def setAuth(self, login: str|None, kwargs: dict[str, Any], cookies: dict[str, str], auth: str|None = None):
162
170
  """Set request authentication.
163
171
 
164
- - login: login target, None means no authentication
165
- - kwargs: request parameters
166
- - cookies: request cookies
167
- - auth: authentication method, default is None
172
+ - ``login``: login target, None means no authentication.
173
+ - ``kwargs``: request parameters.
174
+ - ``cookies``: request cookies.
175
+ - ``auth``: authentication method, default is None.
168
176
 
169
177
  The default behavior is to try allowed schemes: tokens first,
170
178
  then password, then fake.
@@ -234,13 +242,13 @@ class RequestFlaskResponse:
234
242
 
235
243
  Available attributes:
236
244
 
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
245
+ - ``status_code``: integer status code.
246
+ - ``data``: body as bytes.
247
+ - ``text``: body as a string.
248
+ - ``headers``: dict of headers and their values.
249
+ - ``cookies``: dict of cookies.
250
+ - ``json``: JSON-converted body, or *None*.
251
+ - ``is_json``: whether body was in JSON.
244
252
 
245
253
  Constructor parameter:
246
254
 
@@ -268,8 +276,8 @@ class Client:
268
276
 
269
277
  Constructor parameters:
270
278
 
271
- - ``auth`` authenticator
272
- - ``default_login`` if ``login`` is not set.
279
+ - ``auth``: authenticator.
280
+ - ``default_login``: if ``login`` is not set.
273
281
  """
274
282
 
275
283
  def __init__(self, auth: Authenticator, default_login: str|None = None):
@@ -299,16 +307,16 @@ class Client:
299
307
 
300
308
  Mandatory parameters:
301
309
 
302
- - ``method``: HTTP method ("GET", "POST", "PATCH", "DELETE"…)
303
- - ``path``: local path under the base URL
310
+ - ``method``: HTTP method ("GET", "POST", "PATCH", "DELETE"…).
311
+ - ``path``: local path under the base URL.
304
312
 
305
313
  Optional parameters:
306
314
 
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…)
315
+ - ``status``: expected HTTP status, *None* to skip status check.
316
+ - ``content``: regular expression for response body, *None* to skip content check.
317
+ - ``login``: authenticated user, use **explicit** *None* to skip.
318
+ - ``auth``: authentication scheme to use instead of default behavior.
319
+ - ``**kwargs``: more request parameters (headers, data, json…).
312
320
  """
313
321
 
314
322
  if "login" in kwargs:
@@ -329,7 +337,7 @@ class Client:
329
337
  if status is not None:
330
338
  if res.status_code != status: # show error before aborting
331
339
  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}"
340
+ assert res.status_code == status, f"unexpected status {res.status_code}, expecting {status}"
333
341
 
334
342
  # check content
335
343
  if content is not None:
@@ -339,32 +347,28 @@ class Client:
339
347
 
340
348
  return res
341
349
 
342
- def get(self, path, **kwargs):
343
- """HTTP GET request."""
344
- return self.request("GET", path, **kwargs)
350
+ def get(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
351
+ """HTTP GET request, see ``request`` parameters."""
352
+ return self.request("GET", path, status=status, content=content, **kwargs)
345
353
 
346
- def post(self, path, **kwargs):
347
- """HTTP POST request."""
348
- return self.request("POST", path, **kwargs)
354
+ def post(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
355
+ """HTTP POST request, see ``request`` parameters."""
356
+ return self.request("POST", path, status=status, content=content, **kwargs)
349
357
 
350
- def put(self, path, **kwargs):
351
- """HTTP PUT request."""
352
- return self.request("PUT", path, **kwargs)
358
+ def put(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
359
+ """HTTP PUT request, see ``request`` parameters."""
360
+ return self.request("PUT", path, status=status, content=content, **kwargs)
353
361
 
354
- def patch(self, path, **kwargs):
355
- """HTTP PATCH request."""
356
- return self.request("PATCH", path, **kwargs)
362
+ def patch(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
363
+ """HTTP PATCH request, see ``request`` parameters."""
364
+ return self.request("PATCH", path, status=status, content=content, **kwargs)
357
365
 
358
- def delete(self, path, **kwargs):
359
- """HTTP DELETE request."""
360
- return self.request("DELETE", path, **kwargs)
366
+ def delete(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
367
+ """HTTP DELETE request, see ``request`` parameters."""
368
+ return self.request("DELETE", path, status=status, content=content, **kwargs)
361
369
 
362
370
  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
-
371
+ """Deprecated, use ``request`` or HTTP-method-specific methods."""
368
372
  return self.request(method, path, status=status, content=content, **kwargs)
369
373
 
370
374
 
@@ -373,9 +377,9 @@ class RequestClient(Client):
373
377
 
374
378
  Constructor parameters:
375
379
 
376
- - ``auth`` authenticator
377
- - ``base_url`` target server
378
- - ``default_login`` if ``login`` is not set.
380
+ - ``auth``: authenticator.
381
+ - ``base_url``: target server.
382
+ - ``default_login``: if ``login`` is not set.
379
383
  """
380
384
 
381
385
  def __init__(self, auth: Authenticator, base_url: str, default_login=None):
@@ -419,9 +423,9 @@ class FlaskClient(Client):
419
423
 
420
424
  Constructor parameters:
421
425
 
422
- - ``auth`` authenticator
423
- - ``client`` Flask actual ``test_client``
424
- - ``default_login`` if ``login`` is not set.
426
+ - ``auth``: authenticator.
427
+ - ``client``: Flask actual ``test_client``.
428
+ - ``default_login``: if ``login`` is not set.
425
429
 
426
430
  Note: this client handles `cookies`.
427
431
  """
@@ -446,7 +450,7 @@ class FlaskClient(Client):
446
450
  return self._client.open(method=method, path=path, **kwargs)
447
451
 
448
452
  def _ft_authenticator():
449
- """Fixture implementation separated for testing."""
453
+ """Fixture implementation, separated for testing purposes."""
450
454
 
451
455
  level = os.environ.get("FLASK_TESTER_LOG_LEVEL", "NOTSET")
452
456
  log.setLevel(logging.DEBUG if level == "DEBUG" else
@@ -466,9 +470,11 @@ def _ft_authenticator():
466
470
  header = os.environ.get("FLASK_TESTER_HEADER", "Auth")
467
471
  cookie = os.environ.get("FLASK_TESTER_COOKIE", "auth")
468
472
  tparam = os.environ.get("FLASK_TESTER_TPARAM", "AUTH")
473
+ ptype = os.environ.get("FLASK_TESTER_PTYPE", "data")
469
474
 
470
475
  # create authenticator, possibly with initial credentials
471
- auth = Authenticator(allow, user=user, pwd=pwd, login=login, bearer=bearer, header=header, cookie=cookie, tparam=tparam)
476
+ auth = Authenticator(allow, user=user, pwd=pwd, login=login, bearer=bearer,
477
+ header=header, cookie=cookie, tparam=tparam, ptype=ptype)
472
478
 
473
479
  if "FLASK_TESTER_AUTH" in os.environ:
474
480
  auth.setPasses(os.environ["FLASK_TESTER_AUTH"].split(","))
@@ -501,6 +507,8 @@ def ft_authenticator():
501
507
  Default is ``auth``.
502
508
  - ``FLASK_TESTER_TPARAM``: parameter for *token* authentication.
503
509
  Default is ``AUTH``.
510
+ - ``FLASK_TESTER_PTYPE``: default parameter type, ``data`` or ``json``.
511
+ Default is ``data``.
504
512
  - ``FLASK_TESTER_AUTH``: initial comma-separated list of *login:password*.
505
513
  Default is not set.
506
514
  """
@@ -550,7 +558,7 @@ def ft_client(ft_authenticator):
550
558
 
551
559
  Target environment variable, one **must** be defined:
552
560
 
553
- - ``FLASK_TESTER_URL``: application HTTP base URL.
561
+ - ``FLASK_TESTER_URL``: application HTTP base URL, eg ``http://localhost:5000``.
554
562
  - ``FLASK_TESTER_APP``: Flask application, eg ``app:create_app``.
555
563
 
556
564
  Other environment variable:
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.1
2
+ Name: FlaskTester
3
+ Version: 3.4
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).