FlaskTester 4.3__tar.gz → 5.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
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: FlaskTester
3
- Version: 4.3
3
+ Version: 5.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
@@ -26,13 +26,9 @@ Requires-Dist: pyright; extra == "dev"
26
26
  Requires-Dist: ruff; extra == "dev"
27
27
  Requires-Dist: coverage; extra == "dev"
28
28
  Requires-Dist: pymarkdownlnt!=0.9.5; extra == "dev"
29
- Requires-Dist: build; extra == "dev"
30
- Requires-Dist: twine; extra == "dev"
31
- Requires-Dist: wheel; extra == "dev"
32
29
  Requires-Dist: types-flask; extra == "dev"
33
30
  Requires-Dist: types-requests; extra == "dev"
34
- Requires-Dist: FlaskSimpleAuth>=30.0; extra == "dev"
35
- Requires-Dist: passlib; extra == "dev"
31
+ Requires-Dist: FlaskSimpleAuth>=35.0; extra == "dev"
36
32
  Requires-Dist: bcrypt; extra == "dev"
37
33
  Requires-Dist: pydantic; extra == "dev"
38
34
  Provides-Extra: doc
@@ -41,6 +37,10 @@ Requires-Dist: sphinx_rtd_theme; extra == "doc"
41
37
  Requires-Dist: sphinx-autoapi; extra == "doc"
42
38
  Requires-Dist: sphinx-lint; extra == "doc"
43
39
  Requires-Dist: myst_parser; extra == "doc"
40
+ Provides-Extra: pub
41
+ Requires-Dist: build; extra == "pub"
42
+ Requires-Dist: twine; extra == "pub"
43
+ Requires-Dist: wheel; extra == "pub"
44
44
 
45
45
  # FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
46
46
 
@@ -53,7 +53,7 @@ Only one set of tests is needed, switching from internal to external is
53
53
  achieved by setting an environment variable.
54
54
 
55
55
  ![Status](https://github.com/zx80/flask-tester/actions/workflows/package.yml/badge.svg?branch=main&style=flat)
56
- ![Tests](https://img.shields.io/badge/tests-14%20✓-success)
56
+ ![Tests](https://img.shields.io/badge/tests-16%20✓-success)
57
57
  ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
58
58
  ![Issues](https://img.shields.io/github/issues/zx80/flask-tester?style=flat)
59
59
  ![Python](https://img.shields.io/badge/python-3-informational)
@@ -75,18 +75,20 @@ import pytest
75
75
  from FlaskTester import ft_authenticator, ft_client
76
76
  import secret
77
77
 
78
+ def authHook(api, user: str, pwd: str|None):
79
+ if pwd is not None: # get a token when a login/password is provided
80
+ res = api.get("/login", login=user, auth="basic", status=200)
81
+ api.setToken(user, res.json["token"])
82
+ else: # remove token
83
+ api.setToken(user, None)
84
+
78
85
  @pytest.fixture
79
86
  def app(ft_client):
87
+ # register authentication hook
88
+ ft_client.setHook(authHook)
80
89
  # add test passwords for Calvin and Hobbes (must be consistent with app!)
81
90
  ft_client.setPass("calvin", secret.PASSES["calvin"])
82
91
  ft_client.setPass("hobbes", secret.PASSES["hobbes"])
83
- # get user tokens, assume json result {"token": "<token-value>"}
84
- res = ft_client.get("/login", login="calvin", auth="basic", status=200)
85
- assert res.is_json
86
- ft_client.setToken("calvin", res.json["token"])
87
- res = ft_client.post("/login", login="hobbes", auth="param", status=201)
88
- assert res.is_json
89
- ft_client.setToken("hobbes", res.json["token"])
90
92
  # also set a cookie
91
93
  ft_client.setCookie("hobbes", "lang", "fr")
92
94
  ft_client.setCookie("calvin", "lang", "en")
@@ -102,13 +104,13 @@ def test_app_admin(app):
102
104
  assert 'not in group "ADMIN"' in res.text
103
105
  ```
104
106
 
105
- This can be run against a (local) server:
107
+ This can be run against a local or remote server:
106
108
 
107
109
  ```shell
108
110
  export TEST_SEED="some-random-data" # shared test seed
109
111
  flask --app app:app run & # start flask app
110
112
  pid=$! # keep pid
111
- export FLASK_TESTER_APP="http://localhost:5000" # set app local url
113
+ export FLASK_TESTER_APP="http://localhost:5000" # set app url, local or remote
112
114
  pytest test.py # run external tests
113
115
  kill $pid # stop app with pid
114
116
  ```
@@ -7,13 +7,9 @@ pyright
7
7
  ruff
8
8
  coverage
9
9
  pymarkdownlnt!=0.9.5
10
- build
11
- twine
12
- wheel
13
10
  types-flask
14
11
  types-requests
15
- FlaskSimpleAuth>=30.0
16
- passlib
12
+ FlaskSimpleAuth>=35.0
17
13
  bcrypt
18
14
  pydantic
19
15
 
@@ -23,3 +19,8 @@ sphinx_rtd_theme
23
19
  sphinx-autoapi
24
20
  sphinx-lint
25
21
  myst_parser
22
+
23
+ [pub]
24
+ build
25
+ twine
26
+ wheel
@@ -6,7 +6,8 @@ PYTEST_DONT_REWRITE: local assertions are really that, not pytest assertions.
6
6
  import os
7
7
  import io
8
8
  import re
9
- from typing import Any
9
+ # Python >= 3.11: Self
10
+ from typing import Any, Callable
10
11
  import importlib
11
12
  import logging
12
13
  import pytest # for explicit fail calls, see _pytestFail
@@ -89,6 +90,9 @@ class Authenticator:
89
90
  _AUTH_SCHEMES.update(_TOKEN_SCHEMES)
90
91
  _AUTH_SCHEMES.update(_PASS_SCHEMES)
91
92
 
93
+ # authenticator login/pass hook
94
+ _AuthHook = Callable[[str, str|None], None]
95
+
92
96
  def __init__(self,
93
97
  allow: list[str] = ["bearer", "basic", "param", "none"],
94
98
  # parameter names for "basic" and "param"
@@ -124,6 +128,9 @@ class Authenticator:
124
128
  assert ptype in ("json", "data")
125
129
  self._ptype = ptype
126
130
 
131
+ # _AuthHook|None, but python cannot stand it:-(
132
+ self._auth_hook: Any = None
133
+
127
134
  # password and token credentials, cookies
128
135
  self._passes: dict[str, str] = {}
129
136
  self._tokens: dict[str, str] = {}
@@ -138,6 +145,10 @@ class Authenticator:
138
145
  assert isinstance(val, str)
139
146
  store[key] = val
140
147
 
148
+ def setHook(self, hook: _AuthHook):
149
+ """Set on password hook."""
150
+ self._auth_hook = hook
151
+
141
152
  def setPass(self, login: str, pw: str|None):
142
153
  """Associate a password to a user.
143
154
 
@@ -146,6 +157,7 @@ class Authenticator:
146
157
  if not self._has_pass:
147
158
  raise AuthError("cannot set password, no password scheme allowed")
148
159
  self._set(login, pw, self._passes)
160
+ _ = self._auth_hook and self._auth_hook(login, pw)
149
161
 
150
162
  def setPasses(self, pws: list[str]):
151
163
  """Associate a list of *login:password*."""
@@ -298,11 +310,19 @@ class Client:
298
310
  :param default_login: When ``login`` is not set.
299
311
  """
300
312
 
313
+ # client login/pass hook (with mypy workaround)
314
+ # Python >= 3.11: Self
315
+ AuthHook = Callable[[Any, str, str|None], None] # type: ignore
316
+
301
317
  def __init__(self, auth: Authenticator, default_login: str|None = None):
302
318
  self._auth = auth
303
319
  self._cookies: dict[str, dict[str, str]] = {} # login -> name -> value
304
320
  self._default_login = default_login
305
321
 
322
+ def setHook(self, hook: AuthHook):
323
+ """Set on password hook."""
324
+ self._auth.setHook(lambda u, p: hook(self, u, p))
325
+
306
326
  def setToken(self, login: str, token: str|None):
307
327
  """Associate a token to a login, *None* to remove."""
308
328
  self._auth.setToken(login, token)
@@ -603,7 +623,7 @@ def _ft_client(authenticator):
603
623
  # none found
604
624
  if not app:
605
625
  raise FlaskTesterError(f"cannot find Flask app in {pkg_name} ({test_app})")
606
- client = FlaskClient(authenticator, app.test_client(), default_login)
626
+ client = FlaskClient(authenticator, app.test_client(), default_login) # type: ignore
607
627
 
608
628
  return client
609
629
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: FlaskTester
3
- Version: 4.3
3
+ Version: 5.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
@@ -26,13 +26,9 @@ Requires-Dist: pyright; extra == "dev"
26
26
  Requires-Dist: ruff; extra == "dev"
27
27
  Requires-Dist: coverage; extra == "dev"
28
28
  Requires-Dist: pymarkdownlnt!=0.9.5; extra == "dev"
29
- Requires-Dist: build; extra == "dev"
30
- Requires-Dist: twine; extra == "dev"
31
- Requires-Dist: wheel; extra == "dev"
32
29
  Requires-Dist: types-flask; extra == "dev"
33
30
  Requires-Dist: types-requests; extra == "dev"
34
- Requires-Dist: FlaskSimpleAuth>=30.0; extra == "dev"
35
- Requires-Dist: passlib; extra == "dev"
31
+ Requires-Dist: FlaskSimpleAuth>=35.0; extra == "dev"
36
32
  Requires-Dist: bcrypt; extra == "dev"
37
33
  Requires-Dist: pydantic; extra == "dev"
38
34
  Provides-Extra: doc
@@ -41,6 +37,10 @@ Requires-Dist: sphinx_rtd_theme; extra == "doc"
41
37
  Requires-Dist: sphinx-autoapi; extra == "doc"
42
38
  Requires-Dist: sphinx-lint; extra == "doc"
43
39
  Requires-Dist: myst_parser; extra == "doc"
40
+ Provides-Extra: pub
41
+ Requires-Dist: build; extra == "pub"
42
+ Requires-Dist: twine; extra == "pub"
43
+ Requires-Dist: wheel; extra == "pub"
44
44
 
45
45
  # FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
46
46
 
@@ -53,7 +53,7 @@ Only one set of tests is needed, switching from internal to external is
53
53
  achieved by setting an environment variable.
54
54
 
55
55
  ![Status](https://github.com/zx80/flask-tester/actions/workflows/package.yml/badge.svg?branch=main&style=flat)
56
- ![Tests](https://img.shields.io/badge/tests-14%20✓-success)
56
+ ![Tests](https://img.shields.io/badge/tests-16%20✓-success)
57
57
  ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
58
58
  ![Issues](https://img.shields.io/github/issues/zx80/flask-tester?style=flat)
59
59
  ![Python](https://img.shields.io/badge/python-3-informational)
@@ -75,18 +75,20 @@ import pytest
75
75
  from FlaskTester import ft_authenticator, ft_client
76
76
  import secret
77
77
 
78
+ def authHook(api, user: str, pwd: str|None):
79
+ if pwd is not None: # get a token when a login/password is provided
80
+ res = api.get("/login", login=user, auth="basic", status=200)
81
+ api.setToken(user, res.json["token"])
82
+ else: # remove token
83
+ api.setToken(user, None)
84
+
78
85
  @pytest.fixture
79
86
  def app(ft_client):
87
+ # register authentication hook
88
+ ft_client.setHook(authHook)
80
89
  # add test passwords for Calvin and Hobbes (must be consistent with app!)
81
90
  ft_client.setPass("calvin", secret.PASSES["calvin"])
82
91
  ft_client.setPass("hobbes", secret.PASSES["hobbes"])
83
- # get user tokens, assume json result {"token": "<token-value>"}
84
- res = ft_client.get("/login", login="calvin", auth="basic", status=200)
85
- assert res.is_json
86
- ft_client.setToken("calvin", res.json["token"])
87
- res = ft_client.post("/login", login="hobbes", auth="param", status=201)
88
- assert res.is_json
89
- ft_client.setToken("hobbes", res.json["token"])
90
92
  # also set a cookie
91
93
  ft_client.setCookie("hobbes", "lang", "fr")
92
94
  ft_client.setCookie("calvin", "lang", "en")
@@ -102,13 +104,13 @@ def test_app_admin(app):
102
104
  assert 'not in group "ADMIN"' in res.text
103
105
  ```
104
106
 
105
- This can be run against a (local) server:
107
+ This can be run against a local or remote server:
106
108
 
107
109
  ```shell
108
110
  export TEST_SEED="some-random-data" # shared test seed
109
111
  flask --app app:app run & # start flask app
110
112
  pid=$! # keep pid
111
- export FLASK_TESTER_APP="http://localhost:5000" # set app local url
113
+ export FLASK_TESTER_APP="http://localhost:5000" # set app url, local or remote
112
114
  pytest test.py # run external tests
113
115
  kill $pid # stop app with pid
114
116
  ```
@@ -9,7 +9,7 @@ Only one set of tests is needed, switching from internal to external is
9
9
  achieved by setting an environment variable.
10
10
 
11
11
  ![Status](https://github.com/zx80/flask-tester/actions/workflows/package.yml/badge.svg?branch=main&style=flat)
12
- ![Tests](https://img.shields.io/badge/tests-14%20✓-success)
12
+ ![Tests](https://img.shields.io/badge/tests-16%20✓-success)
13
13
  ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
14
14
  ![Issues](https://img.shields.io/github/issues/zx80/flask-tester?style=flat)
15
15
  ![Python](https://img.shields.io/badge/python-3-informational)
@@ -31,18 +31,20 @@ import pytest
31
31
  from FlaskTester import ft_authenticator, ft_client
32
32
  import secret
33
33
 
34
+ def authHook(api, user: str, pwd: str|None):
35
+ if pwd is not None: # get a token when a login/password is provided
36
+ res = api.get("/login", login=user, auth="basic", status=200)
37
+ api.setToken(user, res.json["token"])
38
+ else: # remove token
39
+ api.setToken(user, None)
40
+
34
41
  @pytest.fixture
35
42
  def app(ft_client):
43
+ # register authentication hook
44
+ ft_client.setHook(authHook)
36
45
  # add test passwords for Calvin and Hobbes (must be consistent with app!)
37
46
  ft_client.setPass("calvin", secret.PASSES["calvin"])
38
47
  ft_client.setPass("hobbes", secret.PASSES["hobbes"])
39
- # get user tokens, assume json result {"token": "<token-value>"}
40
- res = ft_client.get("/login", login="calvin", auth="basic", status=200)
41
- assert res.is_json
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"])
46
48
  # also set a cookie
47
49
  ft_client.setCookie("hobbes", "lang", "fr")
48
50
  ft_client.setCookie("calvin", "lang", "en")
@@ -58,13 +60,13 @@ def test_app_admin(app):
58
60
  assert 'not in group "ADMIN"' in res.text
59
61
  ```
60
62
 
61
- This can be run against a (local) server:
63
+ This can be run against a local or remote server:
62
64
 
63
65
  ```shell
64
66
  export TEST_SEED="some-random-data" # shared test seed
65
67
  flask --app app:app run & # start flask app
66
68
  pid=$! # keep pid
67
- export FLASK_TESTER_APP="http://localhost:5000" # set app local url
69
+ export FLASK_TESTER_APP="http://localhost:5000" # set app url, local or remote
68
70
  pytest test.py # run external tests
69
71
  kill $pid # stop app with pid
70
72
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "FlaskTester"
7
- version = "4.3"
7
+ version = "5.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"
@@ -35,13 +35,12 @@ package = "https://pypi.org/project/FlaskTester/"
35
35
  dev = [
36
36
  # static checks…
37
37
  "mypy", "pyright", "ruff", "coverage", "pymarkdownlnt!=0.9.5",
38
- # packaging
39
- "build", "twine", "wheel",
40
38
  # tests
41
- "types-flask", "types-requests", "FlaskSimpleAuth>=30.0", "passlib", "bcrypt", "pydantic"
39
+ "types-flask", "types-requests", "FlaskSimpleAuth>=35.0", "bcrypt", "pydantic"
42
40
  ]
43
41
  # documentation generation
44
42
  doc = ["sphinx", "sphinx_rtd_theme", "sphinx-autoapi", "sphinx-lint", "myst_parser"]
43
+ pub = ["build", "twine", "wheel"]
45
44
 
46
45
  [tool.setuptools]
47
46
  py-modules = [ "FlaskTester" ]
@@ -12,7 +12,7 @@ import model
12
12
 
13
13
  logging.basicConfig(level=logging.INFO)
14
14
  # logging.basicConfig(level=logging.DEBUG)
15
- # log = logging.getLogger("test")
15
+ log = logging.getLogger("test")
16
16
 
17
17
  # set authn for ft_authenticator
18
18
  os.environ.update(
@@ -27,23 +27,31 @@ def test_sanity():
27
27
  # log.debug(f"TEST_SEED={os.environ.get('TEST_SEED')}")
28
28
 
29
29
  # example from README.md
30
+ def authHook(api, user: str, pwd: str|None):
31
+ if pwd is not None: # get token
32
+ try:
33
+ res = api.get("/login", login=user, auth="basic", status=200)
34
+ api.setToken(user, res.json["token"])
35
+ except ft.FlaskTesterError as e: # pragma: no cover
36
+ log.warning(f"error: {e}")
37
+ else: # cleanup
38
+ api.setToken(user, None)
39
+
30
40
  @pytest.fixture
31
41
  def app(ft_client):
32
- # add test passwords for Calvin and Hobbes (must be consistent with app!)
42
+ # hook when adding login/passwords
43
+ ft_client.setHook(authHook)
44
+ # Calvin authentication
33
45
  ft_client.setPass("calvin", secret.PASSES["calvin"])
34
- ft_client.setPass("hobbes", secret.PASSES["hobbes"])
35
- # get user tokens, assume json result {"token": "<token-value>"}
36
- res = ft_client.get("/login", login="calvin", auth="basic", status=200)
37
- assert res.is_json
38
- ft_client.setToken("calvin", res.json["token"])
39
- res = ft_client.post("/login", login="hobbes", auth="param", status=201)
40
- assert res.is_json
41
- ft_client.setToken("hobbes", res.json["token"])
42
- # also set a cookie
43
46
  ft_client.setCookie("hobbes", "lang", "fr")
47
+ # Hobbes authentication
48
+ ft_client.setPass("hobbes", secret.PASSES["hobbes"])
44
49
  ft_client.setCookie("calvin", "lang", "en")
45
50
  # return working client
46
51
  yield ft_client
52
+ # cleanup
53
+ ft_client.setPass("calvin", None)
54
+ ft_client.setPass("hobbes", None)
47
55
 
48
56
  def test_app_admin(app): # GET /admin
49
57
  app.get("/admin", login=None, status=401)
@@ -343,6 +351,7 @@ def test_client_fixture():
343
351
  def test_classes(api):
344
352
 
345
353
  def thing_eq(ta, tb):
354
+ ta = model.Thing1(**ta) if isinstance(ta, dict) else ta
346
355
  tb = model.Thing1(**tb) if isinstance(tb, dict) else tb
347
356
  return ta.tid == tb.tid and ta.name == tb.name and ta.owner == tb.owner
348
357
 
@@ -356,17 +365,15 @@ def test_classes(api):
356
365
  # check all combinations
357
366
  for path in ["/t0", "/t1", "/t2", "/t3"]:
358
367
  for param in [t0, t1, t2, t3]:
359
- for tclass in [model.Thing1, model.Thing2, model.Thing3]:
368
+ for tclass in [dict, model.Thing1, model.Thing2, model.Thing3]:
360
369
  for method in ["GET", "POST"]:
361
370
  for mode in ["data", "json"]:
362
371
  n +=1
363
372
  parameter = {mode: {"t": param}}
364
373
  res = api.request(method, path, 200, **parameter)
365
- assert res.is_json
366
- json = res.json
367
- assert isinstance(json, dict)
368
- assert thing_eq(tclass(**json), param)
369
- assert n == 192
374
+ assert res.is_json and isinstance(res.json, dict)
375
+ assert thing_eq(tclass(**res.json), param)
376
+ assert n == 256
370
377
 
371
378
  # simple types translation
372
379
  assert api.get("/t0", 200, json={"t": None}).json is None
File without changes
File without changes
File without changes
File without changes