FlaskTester 4.3__tar.gz → 5.1__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,16 +1,15 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: FlaskTester
3
- Version: 4.3
3
+ Version: 5.1
4
4
  Summary: Pytest fixtures for Flask internal and external authenticated tests
5
5
  Author-email: Fabien Coelho <flask.tester@coelho.net>
6
- License: CC0
6
+ License-Expression: CC0-1.0
7
7
  Project-URL: repository, https://github.com/zx80/flask-tester
8
8
  Project-URL: documentation, https://zx80.github.io/flask-tester/
9
9
  Project-URL: issues, https://github.com/zx80/flask-tester/issues
10
10
  Project-URL: package, https://pypi.org/project/FlaskTester/
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Intended Audience :: Developers
13
- Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
14
13
  Classifier: Programming Language :: Python
15
14
  Classifier: Environment :: Web Environment
16
15
  Classifier: Framework :: Flask
@@ -26,13 +25,9 @@ Requires-Dist: pyright; extra == "dev"
26
25
  Requires-Dist: ruff; extra == "dev"
27
26
  Requires-Dist: coverage; extra == "dev"
28
27
  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
28
  Requires-Dist: types-flask; extra == "dev"
33
29
  Requires-Dist: types-requests; extra == "dev"
34
- Requires-Dist: FlaskSimpleAuth>=30.0; extra == "dev"
35
- Requires-Dist: passlib; extra == "dev"
30
+ Requires-Dist: FlaskSimpleAuth>=35.0; extra == "dev"
36
31
  Requires-Dist: bcrypt; extra == "dev"
37
32
  Requires-Dist: pydantic; extra == "dev"
38
33
  Provides-Extra: doc
@@ -41,6 +36,11 @@ Requires-Dist: sphinx_rtd_theme; extra == "doc"
41
36
  Requires-Dist: sphinx-autoapi; extra == "doc"
42
37
  Requires-Dist: sphinx-lint; extra == "doc"
43
38
  Requires-Dist: myst_parser; extra == "doc"
39
+ Provides-Extra: pub
40
+ Requires-Dist: build; extra == "pub"
41
+ Requires-Dist: twine; extra == "pub"
42
+ Requires-Dist: wheel; extra == "pub"
43
+ Dynamic: license-file
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,6 +7,4 @@ FlaskTester.egg-info/PKG-INFO
7
7
  FlaskTester.egg-info/SOURCES.txt
8
8
  FlaskTester.egg-info/dependency_links.txt
9
9
  FlaskTester.egg-info/requires.txt
10
- FlaskTester.egg-info/top_level.txt
11
- tests/test_app.py
12
- tests/test_app2.py
10
+ FlaskTester.egg-info/top_level.txt
@@ -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
 
@@ -0,0 +1 @@
1
+ exclude docs/* tests/* */Makefile
@@ -1,16 +1,15 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: FlaskTester
3
- Version: 4.3
3
+ Version: 5.1
4
4
  Summary: Pytest fixtures for Flask internal and external authenticated tests
5
5
  Author-email: Fabien Coelho <flask.tester@coelho.net>
6
- License: CC0
6
+ License-Expression: CC0-1.0
7
7
  Project-URL: repository, https://github.com/zx80/flask-tester
8
8
  Project-URL: documentation, https://zx80.github.io/flask-tester/
9
9
  Project-URL: issues, https://github.com/zx80/flask-tester/issues
10
10
  Project-URL: package, https://pypi.org/project/FlaskTester/
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Intended Audience :: Developers
13
- Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
14
13
  Classifier: Programming Language :: Python
15
14
  Classifier: Environment :: Web Environment
16
15
  Classifier: Framework :: Flask
@@ -26,13 +25,9 @@ Requires-Dist: pyright; extra == "dev"
26
25
  Requires-Dist: ruff; extra == "dev"
27
26
  Requires-Dist: coverage; extra == "dev"
28
27
  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
28
  Requires-Dist: types-flask; extra == "dev"
33
29
  Requires-Dist: types-requests; extra == "dev"
34
- Requires-Dist: FlaskSimpleAuth>=30.0; extra == "dev"
35
- Requires-Dist: passlib; extra == "dev"
30
+ Requires-Dist: FlaskSimpleAuth>=35.0; extra == "dev"
36
31
  Requires-Dist: bcrypt; extra == "dev"
37
32
  Requires-Dist: pydantic; extra == "dev"
38
33
  Provides-Extra: doc
@@ -41,6 +36,11 @@ Requires-Dist: sphinx_rtd_theme; extra == "doc"
41
36
  Requires-Dist: sphinx-autoapi; extra == "doc"
42
37
  Requires-Dist: sphinx-lint; extra == "doc"
43
38
  Requires-Dist: myst_parser; extra == "doc"
39
+ Provides-Extra: pub
40
+ Requires-Dist: build; extra == "pub"
41
+ Requires-Dist: twine; extra == "pub"
42
+ Requires-Dist: wheel; extra == "pub"
43
+ Dynamic: license-file
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,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "FlaskTester"
7
- version = "4.3"
7
+ version = "5.1"
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"
11
- license = { text = "CC0" }
11
+ license = "CC0-1.0"
12
12
  requires-python = ">=3.10"
13
13
  dependencies = [
14
14
  "requests",
@@ -17,7 +17,6 @@ dependencies = [
17
17
  classifiers = [
18
18
  "Development Status :: 5 - Production/Stable",
19
19
  "Intended Audience :: Developers",
20
- "License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
21
20
  "Programming Language :: Python",
22
21
  "Environment :: Web Environment",
23
22
  "Framework :: Flask",
@@ -35,13 +34,12 @@ package = "https://pypi.org/project/FlaskTester/"
35
34
  dev = [
36
35
  # static checks…
37
36
  "mypy", "pyright", "ruff", "coverage", "pymarkdownlnt!=0.9.5",
38
- # packaging
39
- "build", "twine", "wheel",
40
37
  # tests
41
- "types-flask", "types-requests", "FlaskSimpleAuth>=30.0", "passlib", "bcrypt", "pydantic"
38
+ "types-flask", "types-requests", "FlaskSimpleAuth>=35.0", "bcrypt", "pydantic"
42
39
  ]
43
40
  # documentation generation
44
41
  doc = ["sphinx", "sphinx_rtd_theme", "sphinx-autoapi", "sphinx-lint", "myst_parser"]
42
+ pub = ["build", "twine", "wheel"]
45
43
 
46
44
  [tool.setuptools]
47
45
  py-modules = [ "FlaskTester" ]
@@ -1 +0,0 @@
1
- exclude docs/* */Makefile
@@ -1,385 +0,0 @@
1
- import os
2
- import pytest
3
- import FlaskSimpleAuth as fsa
4
- import FlaskTester as ft
5
- from FlaskTester import ft_client, ft_authenticator
6
- import secret
7
- import http.server as htsv
8
- import threading
9
- import io
10
- import logging
11
- import model
12
-
13
- logging.basicConfig(level=logging.INFO)
14
- # logging.basicConfig(level=logging.DEBUG)
15
- # log = logging.getLogger("test")
16
-
17
- # set authn for ft_authenticator
18
- os.environ.update(
19
- FLASK_TESTER_TESTING="*",
20
- FLASK_TESTER_ALLOW="bearer basic param none",
21
- FLASK_TESTER_AUTH=",".join(f"{l}:{p}" for l, p in secret.PASSES.items()),
22
- )
23
-
24
- def test_sanity():
25
- # must provide url or package of Flask application to test
26
- assert "FLASK_TESTER_APP" in os.environ
27
- # log.debug(f"TEST_SEED={os.environ.get('TEST_SEED')}")
28
-
29
- # example from README.md
30
- @pytest.fixture
31
- def app(ft_client):
32
- # add test passwords for Calvin and Hobbes (must be consistent with app!)
33
- 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
- ft_client.setCookie("hobbes", "lang", "fr")
44
- ft_client.setCookie("calvin", "lang", "en")
45
- # return working client
46
- yield ft_client
47
-
48
- def test_app_admin(app): # GET /admin
49
- app.get("/admin", login=None, status=401)
50
- for auth in ["bearer", "basic", "param"]:
51
- res = app.get("/admin", login="calvin", auth=auth, status=200)
52
- assert res.json["user"] == "calvin" and res.json["isadmin"]
53
- res = app.get("/admin", login="hobbes", auth=auth, status=403)
54
- assert 'not in group "ADMIN"' in res.text
55
-
56
- def test_app_who_am_i(app): # GET /who-am-i
57
- app.get("/who-am-i", login=None, status=401)
58
- for auth in ["bearer", "basic", "param"]:
59
- res = app.get("/who-am-i", login="calvin", auth=auth, status=200)
60
- assert res.json["lang"] == "en" and res.json["isadmin"]
61
- res = app.get("/who-am-i", login="hobbes", auth=auth, status=200)
62
- assert res.json["lang"] == "fr" and not res.json["isadmin"]
63
-
64
- @pytest.fixture
65
- def api(ft_client):
66
- # set a default
67
- ft_client._default_login = "calvin"
68
- # bad password and token
69
- ft_client.setPass("moe", None)
70
- ft_client.setToken("moe", None)
71
- ft_client.setPass("moe", "bad password")
72
- ft_client.setToken("moe", "bad token")
73
- # set language cookies
74
- ft_client.setCookie("calvin", "lang", "en")
75
- ft_client.setCookie("hobbes", "lang", "fr")
76
- # get valid tokens using password authn on /login
77
- res = ft_client.get("/login", login="calvin", auth="basic", status=200)
78
- assert res.json["user"] == "calvin"
79
- ft_client._auth.setToken("calvin", res.json["token"])
80
- res = ft_client.post("/login", login="hobbes", auth="param", status=201, data={})
81
- assert res.json["user"] == "hobbes"
82
- ft_client._auth.setToken("hobbes", res.json["token"])
83
- res = ft_client.post("/login", login="susie", auth="param", status=201, json={})
84
- assert res.json["user"] == "susie"
85
- ft_client._auth.setToken("susie", res.json["token"])
86
- ft_client.get("/login", login="calvin", auth="none", status=401)
87
- # check that token auth and cookie is ok
88
- res = ft_client.get("/who-am-i", login="calvin", status=200, auth="bearer")
89
- assert res.json["user"] == "calvin" and res.json["lang"] == "en"
90
- res = ft_client.get("/who-am-i", login="hobbes", status=200, auth="bearer")
91
- assert res.json["user"] == "hobbes" and res.json["lang"] == "fr"
92
- res = ft_client.get("/who-am-i", login="susie", status=200, auth="bearer")
93
- assert res.json["user"] == "susie" and res.json["lang"] is None
94
- # with defaults
95
- res = ft_client.get("/who-am-i", auth="basic", status=200)
96
- assert res.json["user"] == "calvin"
97
- res = ft_client.get("/who-am-i", auth="param", status=200)
98
- assert res.json["user"] == "calvin"
99
- res = ft_client.get("/who-am-i", auth="bearer", status=200)
100
- assert res.json["user"] == "calvin"
101
- res = ft_client.get("/who-am-i", status=200)
102
- assert res.json["user"] == "calvin"
103
- # ready for testing routes
104
- yield ft_client
105
-
106
- def test_admin(api):
107
- # check authentication schemes
108
- for auth in (None, "basic", "param", "bearer"):
109
- api.get("/admin", status=200, login="calvin", auth=auth)
110
- api.get("/admin", 200, login="susie", auth=auth)
111
- api.get("/admin", 403, login="hobbes", auth=auth)
112
- api.get("/admin", 401, login="moe", auth=auth)
113
- api.get("/admin", 401, login=None, auth=auth)
114
- # test status error
115
- try:
116
- api.get("/admin", status=599, login="calvin", auth=auth)
117
- pytest.fail("assert on status must fail") # pragma: no cover
118
- except ft._AssertError as e:
119
- assert "200" in str(e)
120
- # test content error
121
- try:
122
- api.get("/admin", status=200, login="calvin", auth=auth, content="NOT THERE")
123
- pytest.fail("assert on content must fail") # pragma: no cover
124
- except ft._AssertError as e:
125
- assert "NOT THERE" in str(e)
126
-
127
- def test_errors(api):
128
- # these schemes are not allowed
129
- for scheme in ("header", "cookie", "fake", "tparam"):
130
- try:
131
- api.get("/login", login="calvin", auth=scheme)
132
- pytest.fail("must raise an exception") # pragma: no cover
133
- except ft.AuthError as e:
134
- assert "auth is not allowed" in str(e)
135
- try:
136
- api.get("/login", login="calvin", auth="foobla")
137
- pytest.fail("must raise an exception") # pragma: no cover
138
- except ft.AuthError as e:
139
- assert "unexpected auth" in str(e)
140
-
141
- def test_methods(api):
142
- res = api.get("/who-am-i", login="susie", status=200, cookies={"lang": "it"})
143
- assert res.json["lang"] == "it"
144
- api.post("/who-am-i", login="hobbes", status=405)
145
- api.put("/who-am-i", login="hobbes", status=405)
146
- api.patch("/who-am-i", login="hobbes", status=405)
147
- api.delete("/who-am-i", login="hobbes", status=405)
148
-
149
- def test_hello(api):
150
- res = api.get("/hello", login="calvin", auth="none", status=200)
151
- assert res.json["lang"] == "en" and res.json["hello"] == "Hi"
152
- assert res.headers["FSA-User"] == "None (None)"
153
- res = api.get("/hello", login="hobbes", auth="none", status=200)
154
- assert res.json["lang"] == "fr" and res.json["hello"] == "Salut"
155
- assert res.headers["FSA-User"] == "None (None)"
156
- res = api.get("/hello", login="susie", auth="none", status=200, cookies={"lang": "it"})
157
- assert res.json["lang"] == "it" and res.json["hello"] == "Ciao"
158
- assert res.headers["FSA-User"] == "None (None)"
159
- res = api.get("/hello", login="moe", auth="none", status=200)
160
- assert res.json["lang"] == "en" and res.json["hello"] == "Hi"
161
- assert res.headers["FSA-User"] == "None (None)"
162
-
163
- def test_authenticator_token():
164
- # all token carriers
165
- auth = ft.Authenticator(allow=["bearer", "header", "tparam", "cookie"])
166
- auth.setToken("calvin", "clv-token")
167
- auth.setToken("susie", "ss-token")
168
- auth.setToken("hobbes", "hbs-token")
169
- auth.setToken("moe", "m-token")
170
- auth.setCookie("calvin", "what", "clv-cookie")
171
- kwargs, cookies = {}, {}
172
- auth.setAuth("calvin", kwargs, cookies, auth="bearer")
173
- assert kwargs["headers"]["Authorization"] == "Bearer clv-token"
174
- assert cookies["what"] == "clv-cookie"
175
- kwargs, cookies = {}, {}
176
- auth.setAuth("susie", kwargs, cookies, auth="header")
177
- assert kwargs["headers"]["Auth"] == "ss-token"
178
- assert not cookies
179
- kwargs, cookies = {"data": {}}, {}
180
- auth.setAuth("hobbes", kwargs, cookies, auth="tparam")
181
- assert kwargs["data"]["AUTH"] == "hbs-token"
182
- assert not cookies
183
- kwargs, cookies = {"json": {}}, {}
184
- auth.setAuth("hobbes", kwargs, cookies, auth="tparam")
185
- assert kwargs["json"]["AUTH"] == "hbs-token"
186
- assert not cookies
187
- kwargs, cookies = {}, {}
188
- auth.setAuth("moe", kwargs, cookies, auth="cookie")
189
- assert not kwargs
190
- assert cookies["auth"] == "m-token"
191
- # dad does not have a token
192
- try:
193
- kwargs, cookies = {}, {}
194
- auth.setAuth("dad", kwargs, cookies)
195
- pytest.fail("must raise an error") # pragma: no cover
196
- except ft.FlaskTesterError:
197
- assert True, "error raised"
198
- # rosalyn as a password, but no password carrier is allowed
199
- try:
200
- auth.setPass("rosalyn", "rsln-pass")
201
- pytest.fail("must raise an error") # pragma: no cover
202
- except ft.AuthError:
203
- assert True, "error raised"
204
- # force to trigger later errors
205
- auth._has_pass = True
206
- auth.setPass("rosalyn", "rsln-pass")
207
- try:
208
- kwargs, cookies = {}, {}
209
- auth.setAuth("rosalyn", kwargs, cookies)
210
- pytest.fail("must raise an error") # pragma: no cover
211
- except ft.FlaskTesterError:
212
- assert True, "error raised"
213
-
214
- def test_authenticator_password():
215
- # all password carriers plus fake
216
- auth = ft.Authenticator(allow=["basic", "param", "fake"])
217
- auth.setPass("calvin", "clv-pass")
218
- auth.setPass("hobbes", "hbs-pass")
219
- auth.setPass("moe", "m-pass")
220
- auth.setPass("rosalyn", "rsln-pass")
221
- auth.setCookie("moe", "hello", "world!")
222
- kwargs, cookies = {}, {}
223
- auth.setAuth("calvin", kwargs, cookies, auth="basic")
224
- assert kwargs["auth"] == ("calvin", "clv-pass")
225
- assert not cookies
226
- kwargs, cookies = {"data": {}}, {}
227
- auth.setAuth("hobbes", kwargs, cookies, auth="param")
228
- assert kwargs["data"]["USER"] == "hobbes" and kwargs["data"]["PASS"] == "hbs-pass"
229
- assert not cookies
230
- kwargs, cookies = {"json": {}}, {}
231
- auth.setAuth("moe", kwargs, cookies, auth="param")
232
- assert kwargs["json"]["USER"] == "moe" and kwargs["json"]["PASS"] == "m-pass"
233
- assert cookies["hello"] == "world!"
234
- kwargs, cookies = {"data": {}}, {}
235
- auth.setAuth("rosalyn", kwargs, cookies, auth="fake")
236
- assert kwargs["data"]["LOGIN"] == "rosalyn"
237
- assert not cookies
238
- kwargs, cookies = {"json": {}}, {}
239
- auth.setAuth("hobbes", kwargs, cookies, auth="fake")
240
- assert kwargs["json"]["LOGIN"] == "hobbes"
241
- assert not cookies
242
- # susie as a token, but no token carrier is allowed
243
- try:
244
- auth.setToken("susie", "ss-token")
245
- pytest.fail("must raise an error") # pragma: no cover
246
- except ft.FlaskTesterError:
247
- assert True, "error raised"
248
- # force to trigger later error
249
- auth._has_token = True
250
- auth.setToken("susie", "ss-token")
251
- try:
252
- kwargs, cookies = {}, {}
253
- auth.setAuth("susie", kwargs, cookies)
254
- pytest.fail("must raise an error") # pragma: no cover
255
- except ft.FlaskTesterError:
256
- assert True, "error raised"
257
-
258
- def test_request_flask_response():
259
-
260
- class RequestResponse:
261
- """Local class for testing RequestFlaskResponse."""
262
-
263
- def __init__(self, is_json: bool):
264
- self._is_json = is_json
265
- self.status_code = 200
266
- self.content = b"hello world!"
267
- self.text = "hello world!"
268
- self.headers = {"Server": "test/0.1"}
269
- self.cookies = {}
270
-
271
- def json(self):
272
- if self._is_json:
273
- return {"hello": "world!"}
274
- else:
275
- raise Exception("not json!")
276
-
277
- jres = ft.RequestFlaskResponse(RequestResponse(True))
278
- assert jres.is_json and jres.json is not None
279
-
280
- xres = ft.RequestFlaskResponse(RequestResponse(False))
281
- assert not xres.is_json and xres.json is None
282
-
283
- def test_client():
284
- # abstract class for coverage
285
- client = ft.Client(ft.Authenticator())
286
- try:
287
- client._request("GET", "/", {})
288
- pytest.fail("must raise an error") # pragma: no cover
289
- except NotImplementedError:
290
- assert True, "expected error raised"
291
-
292
- def test_request_client():
293
- # start a tmp server on port 8888 for URL client coverage
294
- httpd = htsv.HTTPServer(("", 8888), htsv.SimpleHTTPRequestHandler)
295
- thread = threading.Thread(target = lambda: httpd.serve_forever())
296
- thread.start()
297
- try:
298
- client = ft.RequestClient(ft.Authenticator(), "http://localhost:8888")
299
- client.get("/", 200)
300
- client.request("GET", "/", 200)
301
- hello = io.BytesIO(b"hello world")
302
- client.post("/", 501, data={"hello": hello})
303
- hello = io.BytesIO(b"hello world")
304
- client.post("/", 501, data={"hello": (hello, "hello.txt", "text/plain")})
305
- client.post("/", 501, data={"hello": "world!"})
306
- finally:
307
- httpd.shutdown()
308
-
309
- def test_client_fixture():
310
- # ft_client coverage
311
- auth = ft._ft_authenticator()
312
- # check and save env
313
- app = None
314
- if "FLASK_TESTER_APP" in os.environ:
315
- app = os.environ["FLASK_TESTER_APP"]
316
- del os.environ["FLASK_TESTER_APP"]
317
- # no url nor app, defaults to "app"
318
- init = ft._ft_client(auth)
319
- # url
320
- os.environ["FLASK_TESTER_APP"] = "http://localhost:5000"
321
- init = ft._ft_client(auth)
322
- assert isinstance(init, ft.RequestClient)
323
- del os.environ["FLASK_TESTER_APP"]
324
- # bad package
325
- os.environ["FLASK_TESTER_APP"] = "no_such_package"
326
- try:
327
- init = ft._ft_client(auth)
328
- pytest.fail("must fail on missing package") # pragma: no cover
329
- except ModuleNotFoundError:
330
- assert True, "expected error raised"
331
- # bad name
332
- os.environ["FLASK_TESTER_APP"] = "app:no_such_app"
333
- try:
334
- init = ft._ft_client(auth)
335
- pytest.fail("must fail on missing package") # pragma: no cover
336
- except ft.FlaskTesterError:
337
- assert True, "expected error raised"
338
- del os.environ["FLASK_TESTER_APP"]
339
- # reset env
340
- if app:
341
- os.environ["FLASK_TESTER_APP"] = app
342
-
343
- def test_classes(api):
344
-
345
- def thing_eq(ta, tb):
346
- tb = model.Thing1(**tb) if isinstance(tb, dict) else tb
347
- return ta.tid == tb.tid and ta.name == tb.name and ta.owner == tb.owner
348
-
349
- # Things
350
- t0 = {"tid": 0, "name": "zero", "owner": "Rosalyn"}
351
- t1 = model.Thing1(tid=1, name="one", owner="Susie")
352
- t2 = model.Thing2(tid=2, name="two", owner="Calvin")
353
- t3 = model.Thing3(tid=3, name="three", owner="Hobbes")
354
-
355
- n = 0
356
- # check all combinations
357
- for path in ["/t0", "/t1", "/t2", "/t3"]:
358
- for param in [t0, t1, t2, t3]:
359
- for tclass in [model.Thing1, model.Thing2, model.Thing3]:
360
- for method in ["GET", "POST"]:
361
- for mode in ["data", "json"]:
362
- n +=1
363
- parameter = {mode: {"t": param}}
364
- 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
370
-
371
- # simple types translation
372
- assert api.get("/t0", 200, json={"t": None}).json is None
373
- assert api.get("/t0", 200, data={"t": None}).json is None
374
- assert api.get("/t0", 200, json={"t": True}).json == True
375
- assert api.get("/t0", 200, json={"t": False}).json == False
376
- # non working, bad JsonData conversion
377
- api.get("/t0", 400, data={"t": True})
378
- api.get("/t0", 400, data={"t": False})
379
- assert api.get("/t0", 200, json={"t": 2345}).json == 2345
380
- assert api.get("/t0", 200, data={"t": 5432}).json == 5432
381
- assert api.get("/t0", 200, json={"t": 23.45}).json == 23.45
382
- assert api.get("/t0", 200, data={"t": 54.32}).json == 54.32
383
- assert api.get("/t0", 200, json={"t": "Susie"}).json == "Susie"
384
- api.get("/t0", 400, data={"t": "Susie"}) # raw str is not JSON
385
- assert api.get("/t0", 200, data={"t": "\"Susie\""}).json == "Susie"
@@ -1,37 +0,0 @@
1
- import os
2
- import pytest
3
- from FlaskTester import ft_client, ft_authenticator
4
- import secret
5
-
6
- os.environ.update(FLASK_TESTER_ALLOW="basic param none")
7
-
8
- @pytest.fixture
9
- def app(ft_client):
10
- ft_client.setPass("calvin", secret.PASSES["calvin"])
11
- ft_client.setCookie("calvin", "lang", "en")
12
- ft_client.setPass("hobbes", secret.PASSES["hobbes"])
13
- ft_client.setCookie("hobbes", "lang", "fr")
14
- yield ft_client
15
-
16
- def test_something(app):
17
- # requires an authentication
18
- app.get("/authenticated", 401, login=None)
19
- app.get("/authenticated", 200, "Hello", login="calvin")
20
- app.get("/authenticated", 200, "Bonjour", login="hobbes")
21
- # only allowed to calvin
22
- app.get("/only-admin", 401, login=None)
23
- app.get("/only-admin", 200, "administrateur", login="calvin")
24
- app.get("/only-admin", 403, "not in group", login="hobbes")
25
- # no authentication required, but depends on lang
26
- app.get("/open", 200, "Hello", login="calvin", auth="none")
27
- app.get("/open", 200, "Bonjour", login="hobbes", auth="none")
28
- app.get("/open", 200, "Guten Tag", login=None)
29
-
30
- def test_params(app):
31
- res = app.get("/add", 200, data={"i": 39, "j": 3}, login=None)
32
- assert res.is_json and res.json["sum"] == 42
33
- res = app.get("/add", 200, json={"i": 35, "j": 7}, login=None)
34
- assert res.is_json and res.json["sum"] == 42
35
- # mixing data/json is okay for FlaskTester
36
- res = app.get("/add", 200, data={"i": 30}, json={"j": 12}, login=None)
37
- assert res.is_json and res.json["sum"] == 42
File without changes
File without changes