FlaskTester 5.0__tar.gz → 6.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.
- {flasktester-5.0 → flasktester-6.0}/FlaskTester.egg-info/PKG-INFO +5 -4
- {flasktester-5.0 → flasktester-6.0}/FlaskTester.egg-info/SOURCES.txt +1 -3
- {flasktester-5.0 → flasktester-6.0}/FlaskTester.egg-info/requires.txt +1 -0
- {flasktester-5.0 → flasktester-6.0}/FlaskTester.py +40 -26
- flasktester-6.0/MANIFEST.in +1 -0
- {flasktester-5.0 → flasktester-6.0}/PKG-INFO +5 -4
- {flasktester-5.0 → flasktester-6.0}/pyproject.toml +7 -4
- flasktester-5.0/MANIFEST.in +0 -1
- flasktester-5.0/tests/test_app.py +0 -392
- flasktester-5.0/tests/test_app2.py +0 -37
- {flasktester-5.0 → flasktester-6.0}/FlaskTester.egg-info/dependency_links.txt +0 -0
- {flasktester-5.0 → flasktester-6.0}/FlaskTester.egg-info/top_level.txt +0 -0
- {flasktester-5.0 → flasktester-6.0}/LICENSE +0 -0
- {flasktester-5.0 → flasktester-6.0}/README.md +0 -0
- {flasktester-5.0 → flasktester-6.0}/setup.cfg +0 -0
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: FlaskTester
|
|
3
|
-
Version:
|
|
3
|
+
Version: 6.0
|
|
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,6 +25,7 @@ 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"
|
|
28
|
+
Requires-Dist: flake8; extra == "dev"
|
|
29
29
|
Requires-Dist: types-flask; extra == "dev"
|
|
30
30
|
Requires-Dist: types-requests; extra == "dev"
|
|
31
31
|
Requires-Dist: FlaskSimpleAuth>=35.0; extra == "dev"
|
|
@@ -41,6 +41,7 @@ Provides-Extra: pub
|
|
|
41
41
|
Requires-Dist: build; extra == "pub"
|
|
42
42
|
Requires-Dist: twine; extra == "pub"
|
|
43
43
|
Requires-Dist: wheel; extra == "pub"
|
|
44
|
+
Dynamic: license-file
|
|
44
45
|
|
|
45
46
|
# FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
|
|
46
47
|
|
|
@@ -7,7 +7,8 @@ import os
|
|
|
7
7
|
import io
|
|
8
8
|
import re
|
|
9
9
|
# Python >= 3.11: Self
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
|
+
from collections.abc import Callable
|
|
11
12
|
import importlib
|
|
12
13
|
import logging
|
|
13
14
|
import pytest # for explicit fail calls, see _pytestFail
|
|
@@ -90,23 +91,26 @@ class Authenticator:
|
|
|
90
91
|
_AUTH_SCHEMES.update(_TOKEN_SCHEMES)
|
|
91
92
|
_AUTH_SCHEMES.update(_PASS_SCHEMES)
|
|
92
93
|
|
|
94
|
+
# should not be mutable
|
|
95
|
+
_AUTH_DEFAULT_SCHEMES = ["bearer", "basic", "param", "none"]
|
|
96
|
+
|
|
93
97
|
# authenticator login/pass hook
|
|
94
98
|
_AuthHook = Callable[[str, str|None], None]
|
|
95
99
|
|
|
96
100
|
def __init__(self,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
allow: list[str] = _AUTH_DEFAULT_SCHEMES,
|
|
102
|
+
# parameter names for "basic" and "param"
|
|
103
|
+
user: str = "USER",
|
|
104
|
+
pwd: str = "PASS",
|
|
105
|
+
# parameter name for "fake"
|
|
106
|
+
login: str = "LOGIN",
|
|
107
|
+
# 4 token options
|
|
108
|
+
bearer: str = "Bearer",
|
|
109
|
+
header: str = "Auth",
|
|
110
|
+
cookie: str = "auth",
|
|
111
|
+
tparam: str = "AUTH",
|
|
112
|
+
ptype: str = "data",
|
|
113
|
+
):
|
|
110
114
|
|
|
111
115
|
self._has_pass, self._has_token = False, False
|
|
112
116
|
for auth in allow:
|
|
@@ -139,8 +143,7 @@ class Authenticator:
|
|
|
139
143
|
def _set(self, key: str, val: str|None, store: dict[str, str]):
|
|
140
144
|
"""Set a key/value in a directory, with None for delete."""
|
|
141
145
|
if val is None:
|
|
142
|
-
|
|
143
|
-
del store[key]
|
|
146
|
+
store.pop(key, None)
|
|
144
147
|
else:
|
|
145
148
|
assert isinstance(val, str)
|
|
146
149
|
store[key] = val
|
|
@@ -315,6 +318,7 @@ class Client:
|
|
|
315
318
|
AuthHook = Callable[[Any, str, str|None], None] # type: ignore
|
|
316
319
|
|
|
317
320
|
def __init__(self, auth: Authenticator, default_login: str|None = None):
|
|
321
|
+
self._calls = 0
|
|
318
322
|
self._auth = auth
|
|
319
323
|
self._cookies: dict[str, dict[str, str]] = {} # login -> name -> value
|
|
320
324
|
self._default_login = default_login
|
|
@@ -357,6 +361,8 @@ class Client:
|
|
|
357
361
|
:param **kwargs: More request parameters (headers, data, json…).
|
|
358
362
|
"""
|
|
359
363
|
|
|
364
|
+
self._calls += 1
|
|
365
|
+
|
|
360
366
|
if "login" in kwargs:
|
|
361
367
|
login = kwargs["login"]
|
|
362
368
|
del kwargs["login"]
|
|
@@ -387,7 +393,7 @@ class Client:
|
|
|
387
393
|
elif "model_dump" in val.__dir__() and callable(val.model_dump):
|
|
388
394
|
# probably pydantic
|
|
389
395
|
json_param[name] = val.model_dump()
|
|
390
|
-
else:
|
|
396
|
+
else: # pydantic or standard dataclasses?
|
|
391
397
|
json_param[name] = dataclasses.asdict(val)
|
|
392
398
|
|
|
393
399
|
# convert data parameters to simple strings
|
|
@@ -415,16 +421,14 @@ class Client:
|
|
|
415
421
|
res = self._request(method, path, cookies, **kwargs) # type: ignore
|
|
416
422
|
|
|
417
423
|
# check status
|
|
418
|
-
if status is not None:
|
|
419
|
-
if
|
|
420
|
-
|
|
421
|
-
_pytestFail(f"bad {status} result: {res.status_code} {res.text[:512]}...")
|
|
424
|
+
if status is not None and res.status_code != status: # show error before aborting
|
|
425
|
+
# FIXME what if the useful part is at the end?
|
|
426
|
+
_pytestFail(f"bad {status} result: {res.status_code} {res.text[:512]}...")
|
|
422
427
|
|
|
423
428
|
# check content
|
|
424
|
-
if content is not None:
|
|
425
|
-
if
|
|
426
|
-
|
|
427
|
-
_pytestFail(f"cannot find {content} in {res.text[:512]}...")
|
|
429
|
+
if content is not None and not re.search(content, res.text, re.DOTALL):
|
|
430
|
+
# FIXME what if the useful part is at the end?
|
|
431
|
+
_pytestFail(f"cannot find {content} in {res.text[:512]}...")
|
|
428
432
|
|
|
429
433
|
return res
|
|
430
434
|
|
|
@@ -448,6 +452,12 @@ class Client:
|
|
|
448
452
|
"""HTTP DELETE request, see `Client.request`."""
|
|
449
453
|
return self.request("DELETE", path, status=status, content=content, **kwargs)
|
|
450
454
|
|
|
455
|
+
def __str__(self):
|
|
456
|
+
return f"client calls: {self._calls}"
|
|
457
|
+
|
|
458
|
+
def __del__(self):
|
|
459
|
+
log.info(f"ending, {self}")
|
|
460
|
+
|
|
451
461
|
|
|
452
462
|
class RequestClient(Client):
|
|
453
463
|
"""Request-based test provider.
|
|
@@ -526,6 +536,7 @@ class FlaskClient(Client):
|
|
|
526
536
|
|
|
527
537
|
return self._client.open(method=method, path=path, **kwargs)
|
|
528
538
|
|
|
539
|
+
|
|
529
540
|
def _ft_authenticator():
|
|
530
541
|
"""Fixture implementation, separated for testing purposes."""
|
|
531
542
|
|
|
@@ -559,6 +570,7 @@ def _ft_authenticator():
|
|
|
559
570
|
|
|
560
571
|
return auth
|
|
561
572
|
|
|
573
|
+
|
|
562
574
|
@pytest.fixture
|
|
563
575
|
def ft_authenticator():
|
|
564
576
|
"""Pytest Fixture: ft_authenticator.
|
|
@@ -593,6 +605,7 @@ def ft_authenticator():
|
|
|
593
605
|
|
|
594
606
|
yield _ft_authenticator()
|
|
595
607
|
|
|
608
|
+
|
|
596
609
|
def _ft_client(authenticator):
|
|
597
610
|
"""Fixture implementation separated for testing."""
|
|
598
611
|
|
|
@@ -601,7 +614,7 @@ def _ft_client(authenticator):
|
|
|
601
614
|
|
|
602
615
|
test_app = os.environ.get("FLASK_TESTER_APP", "app")
|
|
603
616
|
|
|
604
|
-
if test_app.startswith("http://"
|
|
617
|
+
if test_app.startswith(("http://", "https://")):
|
|
605
618
|
client = RequestClient(authenticator, test_app, default_login)
|
|
606
619
|
else:
|
|
607
620
|
# load app package
|
|
@@ -627,6 +640,7 @@ def _ft_client(authenticator):
|
|
|
627
640
|
|
|
628
641
|
return client
|
|
629
642
|
|
|
643
|
+
|
|
630
644
|
@pytest.fixture
|
|
631
645
|
def ft_client(ft_authenticator):
|
|
632
646
|
"""Pytest Fixture: ft_client.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exclude docs/* tests/* */Makefile
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: FlaskTester
|
|
3
|
-
Version:
|
|
3
|
+
Version: 6.0
|
|
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,6 +25,7 @@ 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"
|
|
28
|
+
Requires-Dist: flake8; extra == "dev"
|
|
29
29
|
Requires-Dist: types-flask; extra == "dev"
|
|
30
30
|
Requires-Dist: types-requests; extra == "dev"
|
|
31
31
|
Requires-Dist: FlaskSimpleAuth>=35.0; extra == "dev"
|
|
@@ -41,6 +41,7 @@ Provides-Extra: pub
|
|
|
41
41
|
Requires-Dist: build; extra == "pub"
|
|
42
42
|
Requires-Dist: twine; extra == "pub"
|
|
43
43
|
Requires-Dist: wheel; extra == "pub"
|
|
44
|
+
Dynamic: license-file
|
|
44
45
|
|
|
45
46
|
# FlaskTester - Pytest fixtures for Flask internal and external authenticated tests
|
|
46
47
|
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "FlaskTester"
|
|
7
|
-
version = "
|
|
7
|
+
version = "6.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"
|
|
11
|
-
license =
|
|
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",
|
|
@@ -34,7 +33,7 @@ package = "https://pypi.org/project/FlaskTester/"
|
|
|
34
33
|
# various dev tools
|
|
35
34
|
dev = [
|
|
36
35
|
# static checks…
|
|
37
|
-
"mypy", "pyright", "ruff", "coverage", "pymarkdownlnt!=0.9.5",
|
|
36
|
+
"mypy", "pyright", "ruff", "coverage", "pymarkdownlnt!=0.9.5", "flake8",
|
|
38
37
|
# tests
|
|
39
38
|
"types-flask", "types-requests", "FlaskSimpleAuth>=35.0", "bcrypt", "pydantic"
|
|
40
39
|
]
|
|
@@ -48,3 +47,7 @@ py-modules = [ "FlaskTester" ]
|
|
|
48
47
|
[tool.black]
|
|
49
48
|
line-length = 100
|
|
50
49
|
target-version = ["py310"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint]
|
|
52
|
+
# ruff rules are what they are, but they are not always right
|
|
53
|
+
ignore = ["BLE001", "B006", "I001", "SIM114", "RUF012", "PIE790"]
|
flasktester-5.0/MANIFEST.in
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
exclude docs/* */Makefile
|
|
@@ -1,392 +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
|
-
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
|
-
|
|
40
|
-
@pytest.fixture
|
|
41
|
-
def app(ft_client):
|
|
42
|
-
# hook when adding login/passwords
|
|
43
|
-
ft_client.setHook(authHook)
|
|
44
|
-
# Calvin authentication
|
|
45
|
-
ft_client.setPass("calvin", secret.PASSES["calvin"])
|
|
46
|
-
ft_client.setCookie("hobbes", "lang", "fr")
|
|
47
|
-
# Hobbes authentication
|
|
48
|
-
ft_client.setPass("hobbes", secret.PASSES["hobbes"])
|
|
49
|
-
ft_client.setCookie("calvin", "lang", "en")
|
|
50
|
-
# return working client
|
|
51
|
-
yield ft_client
|
|
52
|
-
# cleanup
|
|
53
|
-
ft_client.setPass("calvin", None)
|
|
54
|
-
ft_client.setPass("hobbes", None)
|
|
55
|
-
|
|
56
|
-
def test_app_admin(app): # GET /admin
|
|
57
|
-
app.get("/admin", login=None, status=401)
|
|
58
|
-
for auth in ["bearer", "basic", "param"]:
|
|
59
|
-
res = app.get("/admin", login="calvin", auth=auth, status=200)
|
|
60
|
-
assert res.json["user"] == "calvin" and res.json["isadmin"]
|
|
61
|
-
res = app.get("/admin", login="hobbes", auth=auth, status=403)
|
|
62
|
-
assert 'not in group "ADMIN"' in res.text
|
|
63
|
-
|
|
64
|
-
def test_app_who_am_i(app): # GET /who-am-i
|
|
65
|
-
app.get("/who-am-i", login=None, status=401)
|
|
66
|
-
for auth in ["bearer", "basic", "param"]:
|
|
67
|
-
res = app.get("/who-am-i", login="calvin", auth=auth, status=200)
|
|
68
|
-
assert res.json["lang"] == "en" and res.json["isadmin"]
|
|
69
|
-
res = app.get("/who-am-i", login="hobbes", auth=auth, status=200)
|
|
70
|
-
assert res.json["lang"] == "fr" and not res.json["isadmin"]
|
|
71
|
-
|
|
72
|
-
@pytest.fixture
|
|
73
|
-
def api(ft_client):
|
|
74
|
-
# set a default
|
|
75
|
-
ft_client._default_login = "calvin"
|
|
76
|
-
# bad password and token
|
|
77
|
-
ft_client.setPass("moe", None)
|
|
78
|
-
ft_client.setToken("moe", None)
|
|
79
|
-
ft_client.setPass("moe", "bad password")
|
|
80
|
-
ft_client.setToken("moe", "bad token")
|
|
81
|
-
# set language cookies
|
|
82
|
-
ft_client.setCookie("calvin", "lang", "en")
|
|
83
|
-
ft_client.setCookie("hobbes", "lang", "fr")
|
|
84
|
-
# get valid tokens using password authn on /login
|
|
85
|
-
res = ft_client.get("/login", login="calvin", auth="basic", status=200)
|
|
86
|
-
assert res.json["user"] == "calvin"
|
|
87
|
-
ft_client._auth.setToken("calvin", res.json["token"])
|
|
88
|
-
res = ft_client.post("/login", login="hobbes", auth="param", status=201, data={})
|
|
89
|
-
assert res.json["user"] == "hobbes"
|
|
90
|
-
ft_client._auth.setToken("hobbes", res.json["token"])
|
|
91
|
-
res = ft_client.post("/login", login="susie", auth="param", status=201, json={})
|
|
92
|
-
assert res.json["user"] == "susie"
|
|
93
|
-
ft_client._auth.setToken("susie", res.json["token"])
|
|
94
|
-
ft_client.get("/login", login="calvin", auth="none", status=401)
|
|
95
|
-
# check that token auth and cookie is ok
|
|
96
|
-
res = ft_client.get("/who-am-i", login="calvin", status=200, auth="bearer")
|
|
97
|
-
assert res.json["user"] == "calvin" and res.json["lang"] == "en"
|
|
98
|
-
res = ft_client.get("/who-am-i", login="hobbes", status=200, auth="bearer")
|
|
99
|
-
assert res.json["user"] == "hobbes" and res.json["lang"] == "fr"
|
|
100
|
-
res = ft_client.get("/who-am-i", login="susie", status=200, auth="bearer")
|
|
101
|
-
assert res.json["user"] == "susie" and res.json["lang"] is None
|
|
102
|
-
# with defaults
|
|
103
|
-
res = ft_client.get("/who-am-i", auth="basic", status=200)
|
|
104
|
-
assert res.json["user"] == "calvin"
|
|
105
|
-
res = ft_client.get("/who-am-i", auth="param", status=200)
|
|
106
|
-
assert res.json["user"] == "calvin"
|
|
107
|
-
res = ft_client.get("/who-am-i", auth="bearer", status=200)
|
|
108
|
-
assert res.json["user"] == "calvin"
|
|
109
|
-
res = ft_client.get("/who-am-i", status=200)
|
|
110
|
-
assert res.json["user"] == "calvin"
|
|
111
|
-
# ready for testing routes
|
|
112
|
-
yield ft_client
|
|
113
|
-
|
|
114
|
-
def test_admin(api):
|
|
115
|
-
# check authentication schemes
|
|
116
|
-
for auth in (None, "basic", "param", "bearer"):
|
|
117
|
-
api.get("/admin", status=200, login="calvin", auth=auth)
|
|
118
|
-
api.get("/admin", 200, login="susie", auth=auth)
|
|
119
|
-
api.get("/admin", 403, login="hobbes", auth=auth)
|
|
120
|
-
api.get("/admin", 401, login="moe", auth=auth)
|
|
121
|
-
api.get("/admin", 401, login=None, auth=auth)
|
|
122
|
-
# test status error
|
|
123
|
-
try:
|
|
124
|
-
api.get("/admin", status=599, login="calvin", auth=auth)
|
|
125
|
-
pytest.fail("assert on status must fail") # pragma: no cover
|
|
126
|
-
except ft._AssertError as e:
|
|
127
|
-
assert "200" in str(e)
|
|
128
|
-
# test content error
|
|
129
|
-
try:
|
|
130
|
-
api.get("/admin", status=200, login="calvin", auth=auth, content="NOT THERE")
|
|
131
|
-
pytest.fail("assert on content must fail") # pragma: no cover
|
|
132
|
-
except ft._AssertError as e:
|
|
133
|
-
assert "NOT THERE" in str(e)
|
|
134
|
-
|
|
135
|
-
def test_errors(api):
|
|
136
|
-
# these schemes are not allowed
|
|
137
|
-
for scheme in ("header", "cookie", "fake", "tparam"):
|
|
138
|
-
try:
|
|
139
|
-
api.get("/login", login="calvin", auth=scheme)
|
|
140
|
-
pytest.fail("must raise an exception") # pragma: no cover
|
|
141
|
-
except ft.AuthError as e:
|
|
142
|
-
assert "auth is not allowed" in str(e)
|
|
143
|
-
try:
|
|
144
|
-
api.get("/login", login="calvin", auth="foobla")
|
|
145
|
-
pytest.fail("must raise an exception") # pragma: no cover
|
|
146
|
-
except ft.AuthError as e:
|
|
147
|
-
assert "unexpected auth" in str(e)
|
|
148
|
-
|
|
149
|
-
def test_methods(api):
|
|
150
|
-
res = api.get("/who-am-i", login="susie", status=200, cookies={"lang": "it"})
|
|
151
|
-
assert res.json["lang"] == "it"
|
|
152
|
-
api.post("/who-am-i", login="hobbes", status=405)
|
|
153
|
-
api.put("/who-am-i", login="hobbes", status=405)
|
|
154
|
-
api.patch("/who-am-i", login="hobbes", status=405)
|
|
155
|
-
api.delete("/who-am-i", login="hobbes", status=405)
|
|
156
|
-
|
|
157
|
-
def test_hello(api):
|
|
158
|
-
res = api.get("/hello", login="calvin", auth="none", status=200)
|
|
159
|
-
assert res.json["lang"] == "en" and res.json["hello"] == "Hi"
|
|
160
|
-
assert res.headers["FSA-User"] == "None (None)"
|
|
161
|
-
res = api.get("/hello", login="hobbes", auth="none", status=200)
|
|
162
|
-
assert res.json["lang"] == "fr" and res.json["hello"] == "Salut"
|
|
163
|
-
assert res.headers["FSA-User"] == "None (None)"
|
|
164
|
-
res = api.get("/hello", login="susie", auth="none", status=200, cookies={"lang": "it"})
|
|
165
|
-
assert res.json["lang"] == "it" and res.json["hello"] == "Ciao"
|
|
166
|
-
assert res.headers["FSA-User"] == "None (None)"
|
|
167
|
-
res = api.get("/hello", login="moe", auth="none", status=200)
|
|
168
|
-
assert res.json["lang"] == "en" and res.json["hello"] == "Hi"
|
|
169
|
-
assert res.headers["FSA-User"] == "None (None)"
|
|
170
|
-
|
|
171
|
-
def test_authenticator_token():
|
|
172
|
-
# all token carriers
|
|
173
|
-
auth = ft.Authenticator(allow=["bearer", "header", "tparam", "cookie"])
|
|
174
|
-
auth.setToken("calvin", "clv-token")
|
|
175
|
-
auth.setToken("susie", "ss-token")
|
|
176
|
-
auth.setToken("hobbes", "hbs-token")
|
|
177
|
-
auth.setToken("moe", "m-token")
|
|
178
|
-
auth.setCookie("calvin", "what", "clv-cookie")
|
|
179
|
-
kwargs, cookies = {}, {}
|
|
180
|
-
auth.setAuth("calvin", kwargs, cookies, auth="bearer")
|
|
181
|
-
assert kwargs["headers"]["Authorization"] == "Bearer clv-token"
|
|
182
|
-
assert cookies["what"] == "clv-cookie"
|
|
183
|
-
kwargs, cookies = {}, {}
|
|
184
|
-
auth.setAuth("susie", kwargs, cookies, auth="header")
|
|
185
|
-
assert kwargs["headers"]["Auth"] == "ss-token"
|
|
186
|
-
assert not cookies
|
|
187
|
-
kwargs, cookies = {"data": {}}, {}
|
|
188
|
-
auth.setAuth("hobbes", kwargs, cookies, auth="tparam")
|
|
189
|
-
assert kwargs["data"]["AUTH"] == "hbs-token"
|
|
190
|
-
assert not cookies
|
|
191
|
-
kwargs, cookies = {"json": {}}, {}
|
|
192
|
-
auth.setAuth("hobbes", kwargs, cookies, auth="tparam")
|
|
193
|
-
assert kwargs["json"]["AUTH"] == "hbs-token"
|
|
194
|
-
assert not cookies
|
|
195
|
-
kwargs, cookies = {}, {}
|
|
196
|
-
auth.setAuth("moe", kwargs, cookies, auth="cookie")
|
|
197
|
-
assert not kwargs
|
|
198
|
-
assert cookies["auth"] == "m-token"
|
|
199
|
-
# dad does not have a token
|
|
200
|
-
try:
|
|
201
|
-
kwargs, cookies = {}, {}
|
|
202
|
-
auth.setAuth("dad", kwargs, cookies)
|
|
203
|
-
pytest.fail("must raise an error") # pragma: no cover
|
|
204
|
-
except ft.FlaskTesterError:
|
|
205
|
-
assert True, "error raised"
|
|
206
|
-
# rosalyn as a password, but no password carrier is allowed
|
|
207
|
-
try:
|
|
208
|
-
auth.setPass("rosalyn", "rsln-pass")
|
|
209
|
-
pytest.fail("must raise an error") # pragma: no cover
|
|
210
|
-
except ft.AuthError:
|
|
211
|
-
assert True, "error raised"
|
|
212
|
-
# force to trigger later errors
|
|
213
|
-
auth._has_pass = True
|
|
214
|
-
auth.setPass("rosalyn", "rsln-pass")
|
|
215
|
-
try:
|
|
216
|
-
kwargs, cookies = {}, {}
|
|
217
|
-
auth.setAuth("rosalyn", kwargs, cookies)
|
|
218
|
-
pytest.fail("must raise an error") # pragma: no cover
|
|
219
|
-
except ft.FlaskTesterError:
|
|
220
|
-
assert True, "error raised"
|
|
221
|
-
|
|
222
|
-
def test_authenticator_password():
|
|
223
|
-
# all password carriers plus fake
|
|
224
|
-
auth = ft.Authenticator(allow=["basic", "param", "fake"])
|
|
225
|
-
auth.setPass("calvin", "clv-pass")
|
|
226
|
-
auth.setPass("hobbes", "hbs-pass")
|
|
227
|
-
auth.setPass("moe", "m-pass")
|
|
228
|
-
auth.setPass("rosalyn", "rsln-pass")
|
|
229
|
-
auth.setCookie("moe", "hello", "world!")
|
|
230
|
-
kwargs, cookies = {}, {}
|
|
231
|
-
auth.setAuth("calvin", kwargs, cookies, auth="basic")
|
|
232
|
-
assert kwargs["auth"] == ("calvin", "clv-pass")
|
|
233
|
-
assert not cookies
|
|
234
|
-
kwargs, cookies = {"data": {}}, {}
|
|
235
|
-
auth.setAuth("hobbes", kwargs, cookies, auth="param")
|
|
236
|
-
assert kwargs["data"]["USER"] == "hobbes" and kwargs["data"]["PASS"] == "hbs-pass"
|
|
237
|
-
assert not cookies
|
|
238
|
-
kwargs, cookies = {"json": {}}, {}
|
|
239
|
-
auth.setAuth("moe", kwargs, cookies, auth="param")
|
|
240
|
-
assert kwargs["json"]["USER"] == "moe" and kwargs["json"]["PASS"] == "m-pass"
|
|
241
|
-
assert cookies["hello"] == "world!"
|
|
242
|
-
kwargs, cookies = {"data": {}}, {}
|
|
243
|
-
auth.setAuth("rosalyn", kwargs, cookies, auth="fake")
|
|
244
|
-
assert kwargs["data"]["LOGIN"] == "rosalyn"
|
|
245
|
-
assert not cookies
|
|
246
|
-
kwargs, cookies = {"json": {}}, {}
|
|
247
|
-
auth.setAuth("hobbes", kwargs, cookies, auth="fake")
|
|
248
|
-
assert kwargs["json"]["LOGIN"] == "hobbes"
|
|
249
|
-
assert not cookies
|
|
250
|
-
# susie as a token, but no token carrier is allowed
|
|
251
|
-
try:
|
|
252
|
-
auth.setToken("susie", "ss-token")
|
|
253
|
-
pytest.fail("must raise an error") # pragma: no cover
|
|
254
|
-
except ft.FlaskTesterError:
|
|
255
|
-
assert True, "error raised"
|
|
256
|
-
# force to trigger later error
|
|
257
|
-
auth._has_token = True
|
|
258
|
-
auth.setToken("susie", "ss-token")
|
|
259
|
-
try:
|
|
260
|
-
kwargs, cookies = {}, {}
|
|
261
|
-
auth.setAuth("susie", kwargs, cookies)
|
|
262
|
-
pytest.fail("must raise an error") # pragma: no cover
|
|
263
|
-
except ft.FlaskTesterError:
|
|
264
|
-
assert True, "error raised"
|
|
265
|
-
|
|
266
|
-
def test_request_flask_response():
|
|
267
|
-
|
|
268
|
-
class RequestResponse:
|
|
269
|
-
"""Local class for testing RequestFlaskResponse."""
|
|
270
|
-
|
|
271
|
-
def __init__(self, is_json: bool):
|
|
272
|
-
self._is_json = is_json
|
|
273
|
-
self.status_code = 200
|
|
274
|
-
self.content = b"hello world!"
|
|
275
|
-
self.text = "hello world!"
|
|
276
|
-
self.headers = {"Server": "test/0.1"}
|
|
277
|
-
self.cookies = {}
|
|
278
|
-
|
|
279
|
-
def json(self):
|
|
280
|
-
if self._is_json:
|
|
281
|
-
return {"hello": "world!"}
|
|
282
|
-
else:
|
|
283
|
-
raise Exception("not json!")
|
|
284
|
-
|
|
285
|
-
jres = ft.RequestFlaskResponse(RequestResponse(True))
|
|
286
|
-
assert jres.is_json and jres.json is not None
|
|
287
|
-
|
|
288
|
-
xres = ft.RequestFlaskResponse(RequestResponse(False))
|
|
289
|
-
assert not xres.is_json and xres.json is None
|
|
290
|
-
|
|
291
|
-
def test_client():
|
|
292
|
-
# abstract class for coverage
|
|
293
|
-
client = ft.Client(ft.Authenticator())
|
|
294
|
-
try:
|
|
295
|
-
client._request("GET", "/", {})
|
|
296
|
-
pytest.fail("must raise an error") # pragma: no cover
|
|
297
|
-
except NotImplementedError:
|
|
298
|
-
assert True, "expected error raised"
|
|
299
|
-
|
|
300
|
-
def test_request_client():
|
|
301
|
-
# start a tmp server on port 8888 for URL client coverage
|
|
302
|
-
httpd = htsv.HTTPServer(("", 8888), htsv.SimpleHTTPRequestHandler)
|
|
303
|
-
thread = threading.Thread(target = lambda: httpd.serve_forever())
|
|
304
|
-
thread.start()
|
|
305
|
-
try:
|
|
306
|
-
client = ft.RequestClient(ft.Authenticator(), "http://localhost:8888")
|
|
307
|
-
client.get("/", 200)
|
|
308
|
-
client.request("GET", "/", 200)
|
|
309
|
-
hello = io.BytesIO(b"hello world")
|
|
310
|
-
client.post("/", 501, data={"hello": hello})
|
|
311
|
-
hello = io.BytesIO(b"hello world")
|
|
312
|
-
client.post("/", 501, data={"hello": (hello, "hello.txt", "text/plain")})
|
|
313
|
-
client.post("/", 501, data={"hello": "world!"})
|
|
314
|
-
finally:
|
|
315
|
-
httpd.shutdown()
|
|
316
|
-
|
|
317
|
-
def test_client_fixture():
|
|
318
|
-
# ft_client coverage
|
|
319
|
-
auth = ft._ft_authenticator()
|
|
320
|
-
# check and save env
|
|
321
|
-
app = None
|
|
322
|
-
if "FLASK_TESTER_APP" in os.environ:
|
|
323
|
-
app = os.environ["FLASK_TESTER_APP"]
|
|
324
|
-
del os.environ["FLASK_TESTER_APP"]
|
|
325
|
-
# no url nor app, defaults to "app"
|
|
326
|
-
init = ft._ft_client(auth)
|
|
327
|
-
# url
|
|
328
|
-
os.environ["FLASK_TESTER_APP"] = "http://localhost:5000"
|
|
329
|
-
init = ft._ft_client(auth)
|
|
330
|
-
assert isinstance(init, ft.RequestClient)
|
|
331
|
-
del os.environ["FLASK_TESTER_APP"]
|
|
332
|
-
# bad package
|
|
333
|
-
os.environ["FLASK_TESTER_APP"] = "no_such_package"
|
|
334
|
-
try:
|
|
335
|
-
init = ft._ft_client(auth)
|
|
336
|
-
pytest.fail("must fail on missing package") # pragma: no cover
|
|
337
|
-
except ModuleNotFoundError:
|
|
338
|
-
assert True, "expected error raised"
|
|
339
|
-
# bad name
|
|
340
|
-
os.environ["FLASK_TESTER_APP"] = "app:no_such_app"
|
|
341
|
-
try:
|
|
342
|
-
init = ft._ft_client(auth)
|
|
343
|
-
pytest.fail("must fail on missing package") # pragma: no cover
|
|
344
|
-
except ft.FlaskTesterError:
|
|
345
|
-
assert True, "expected error raised"
|
|
346
|
-
del os.environ["FLASK_TESTER_APP"]
|
|
347
|
-
# reset env
|
|
348
|
-
if app:
|
|
349
|
-
os.environ["FLASK_TESTER_APP"] = app
|
|
350
|
-
|
|
351
|
-
def test_classes(api):
|
|
352
|
-
|
|
353
|
-
def thing_eq(ta, tb):
|
|
354
|
-
ta = model.Thing1(**ta) if isinstance(ta, dict) else ta
|
|
355
|
-
tb = model.Thing1(**tb) if isinstance(tb, dict) else tb
|
|
356
|
-
return ta.tid == tb.tid and ta.name == tb.name and ta.owner == tb.owner
|
|
357
|
-
|
|
358
|
-
# Things
|
|
359
|
-
t0 = {"tid": 0, "name": "zero", "owner": "Rosalyn"}
|
|
360
|
-
t1 = model.Thing1(tid=1, name="one", owner="Susie")
|
|
361
|
-
t2 = model.Thing2(tid=2, name="two", owner="Calvin")
|
|
362
|
-
t3 = model.Thing3(tid=3, name="three", owner="Hobbes")
|
|
363
|
-
|
|
364
|
-
n = 0
|
|
365
|
-
# check all combinations
|
|
366
|
-
for path in ["/t0", "/t1", "/t2", "/t3"]:
|
|
367
|
-
for param in [t0, t1, t2, t3]:
|
|
368
|
-
for tclass in [dict, model.Thing1, model.Thing2, model.Thing3]:
|
|
369
|
-
for method in ["GET", "POST"]:
|
|
370
|
-
for mode in ["data", "json"]:
|
|
371
|
-
n +=1
|
|
372
|
-
parameter = {mode: {"t": param}}
|
|
373
|
-
res = api.request(method, path, 200, **parameter)
|
|
374
|
-
assert res.is_json and isinstance(res.json, dict)
|
|
375
|
-
assert thing_eq(tclass(**res.json), param)
|
|
376
|
-
assert n == 256
|
|
377
|
-
|
|
378
|
-
# simple types translation
|
|
379
|
-
assert api.get("/t0", 200, json={"t": None}).json is None
|
|
380
|
-
assert api.get("/t0", 200, data={"t": None}).json is None
|
|
381
|
-
assert api.get("/t0", 200, json={"t": True}).json == True
|
|
382
|
-
assert api.get("/t0", 200, json={"t": False}).json == False
|
|
383
|
-
# non working, bad JsonData conversion
|
|
384
|
-
api.get("/t0", 400, data={"t": True})
|
|
385
|
-
api.get("/t0", 400, data={"t": False})
|
|
386
|
-
assert api.get("/t0", 200, json={"t": 2345}).json == 2345
|
|
387
|
-
assert api.get("/t0", 200, data={"t": 5432}).json == 5432
|
|
388
|
-
assert api.get("/t0", 200, json={"t": 23.45}).json == 23.45
|
|
389
|
-
assert api.get("/t0", 200, data={"t": 54.32}).json == 54.32
|
|
390
|
-
assert api.get("/t0", 200, json={"t": "Susie"}).json == "Susie"
|
|
391
|
-
api.get("/t0", 400, data={"t": "Susie"}) # raw str is not JSON
|
|
392
|
-
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|