FlaskTester 3.6__tar.gz → 4.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.
- {FlaskTester-3.6 → flasktester-4.1}/FlaskTester.egg-info/PKG-INFO +4 -3
- {FlaskTester-3.6 → flasktester-4.1}/FlaskTester.egg-info/requires.txt +1 -0
- {FlaskTester-3.6 → flasktester-4.1}/FlaskTester.py +130 -88
- {FlaskTester-3.6 → flasktester-4.1}/PKG-INFO +4 -3
- {FlaskTester-3.6 → flasktester-4.1}/README.md +2 -2
- {FlaskTester-3.6 → flasktester-4.1}/pyproject.toml +9 -2
- {FlaskTester-3.6 → flasktester-4.1}/tests/test_app.py +37 -10
- flasktester-4.1/tests/test_app2.py +28 -0
- FlaskTester-3.6/tests/test_app2.py +0 -28
- {FlaskTester-3.6 → flasktester-4.1}/FlaskTester.egg-info/SOURCES.txt +0 -0
- {FlaskTester-3.6 → flasktester-4.1}/FlaskTester.egg-info/dependency_links.txt +0 -0
- {FlaskTester-3.6 → flasktester-4.1}/FlaskTester.egg-info/top_level.txt +0 -0
- {FlaskTester-3.6 → flasktester-4.1}/LICENSE +0 -0
- {FlaskTester-3.6 → flasktester-4.1}/MANIFEST.in +0 -0
- {FlaskTester-3.6 → flasktester-4.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: FlaskTester
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.1
|
|
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
|
|
@@ -34,6 +34,7 @@ Requires-Dist: types-flask; extra == "dev"
|
|
|
34
34
|
Requires-Dist: types-requests; extra == "dev"
|
|
35
35
|
Requires-Dist: FlaskSimpleAuth>=30.0; extra == "dev"
|
|
36
36
|
Requires-Dist: passlib; extra == "dev"
|
|
37
|
+
Requires-Dist: pydantic; extra == "dev"
|
|
37
38
|
Provides-Extra: doc
|
|
38
39
|
Requires-Dist: sphinx; extra == "doc"
|
|
39
40
|
Requires-Dist: sphinx_rtd_theme; extra == "doc"
|
|
@@ -49,7 +50,7 @@ either with internal Flask tests (aka `test_client`) or external tests (with
|
|
|
49
50
|
authentication and per-user cookies.
|
|
50
51
|
|
|
51
52
|
Only one set of tests is needed, switching from internal to external is
|
|
52
|
-
achieved
|
|
53
|
+
achieved by setting an environment variable.
|
|
53
54
|
|
|
54
55
|

|
|
55
56
|

|
|
@@ -123,7 +124,7 @@ The above test runs with [`tests/app.py`](tests/app.py)
|
|
|
123
124
|
[Flask](https://flask.palletsprojects.com/)
|
|
124
125
|
REST application back-end with password and token authentication based on
|
|
125
126
|
[FlaskSimpleAuth](https://pypi.org/project/FlaskSimpleAuth/).
|
|
126
|
-
The code uses
|
|
127
|
+
The code uses _23_ lines of Python for implementing
|
|
127
128
|
password (basic and parameters) and token authentications,
|
|
128
129
|
admin group authorization, and routes for
|
|
129
130
|
token generation (2), identity tests (2) and an incredible open cookie-based
|
|
@@ -9,7 +9,9 @@ import re
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
import importlib
|
|
11
11
|
import logging
|
|
12
|
-
import pytest
|
|
12
|
+
import pytest # for explicit fail calls, see _pytestFail
|
|
13
|
+
import json
|
|
14
|
+
import dataclasses
|
|
13
15
|
|
|
14
16
|
log = logging.getLogger("flask_tester")
|
|
15
17
|
|
|
@@ -29,12 +31,15 @@ class _AssertError(FlaskTesterError):
|
|
|
29
31
|
pass
|
|
30
32
|
|
|
31
33
|
|
|
32
|
-
def
|
|
33
|
-
"""Undocumented switch for FlaskTester own tests.
|
|
34
|
+
def _pytestFail(msg: str):
|
|
35
|
+
"""Undocumented switch for FlaskTester own tests.
|
|
36
|
+
|
|
37
|
+
A pytest test cannot check an expected pytest failure without failing.
|
|
38
|
+
"""
|
|
34
39
|
log.error(msg)
|
|
35
40
|
if "FLASK_TESTER_TESTING" in os.environ:
|
|
36
41
|
raise _AssertError(msg)
|
|
37
|
-
else: # pragma: no cover
|
|
42
|
+
else: # pragma: no cover
|
|
38
43
|
pytest.fail(msg)
|
|
39
44
|
|
|
40
45
|
|
|
@@ -54,24 +59,24 @@ class Authenticator:
|
|
|
54
59
|
|
|
55
60
|
Constructor parameters:
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
:param allow: List of allowed schemes.
|
|
63
|
+
defaults to ``["bearer", "basic", "param", "none"]``.
|
|
64
|
+
:param user: Parameter for user on ``param`` password authentication,
|
|
65
|
+
defaults to ``"USER"``.
|
|
66
|
+
:param pwd: Parameter for password on ``param`` password authentication,
|
|
67
|
+
defaults to ``"PASS"``.
|
|
68
|
+
:param login: Parameter for user on ``fake`` authentication,
|
|
69
|
+
defaults to ``"LOGIN"``.
|
|
70
|
+
:param bearer: Name of bearer scheme for token,
|
|
71
|
+
defaults to ``"Bearer"``.
|
|
72
|
+
:param header: Name of header for token,
|
|
73
|
+
defaults to ``"Auth"``.
|
|
74
|
+
:param cookie: Name of cookie for token,
|
|
75
|
+
defaults to ``"auth"``.
|
|
76
|
+
:param tparam: Name of parameter for token,
|
|
77
|
+
defaults to ``"AUTH"``.
|
|
78
|
+
:param ptype: Default parameter type, either *data* or *json*,
|
|
79
|
+
defaults to ``"data"``.
|
|
75
80
|
|
|
76
81
|
Note: default values are consistent with `FlaskSimpleAuth <https://pypi.org/project/FlaskSimpleAuth/>`_.
|
|
77
82
|
"""
|
|
@@ -182,12 +187,12 @@ class Authenticator:
|
|
|
182
187
|
def setAuth(self, login: str|None, kwargs: dict[str, Any], cookies: dict[str, str], auth: str|None = None):
|
|
183
188
|
"""Set request authentication.
|
|
184
189
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
:param login: Login target, *None* means no authentication.
|
|
191
|
+
:param kwargs: Request parameters to modify.
|
|
192
|
+
:param cookies: Request cookies to modify.
|
|
193
|
+
:param auth: Authentication method, default is *None*.
|
|
189
194
|
|
|
190
|
-
The default behavior is to try allowed schemes:
|
|
195
|
+
The default behavior is to try allowed schemes: token first,
|
|
191
196
|
then password, then fake.
|
|
192
197
|
"""
|
|
193
198
|
|
|
@@ -251,9 +256,9 @@ class Authenticator:
|
|
|
251
256
|
class RequestFlaskResponse:
|
|
252
257
|
"""Wrapper to return a Flask-looking response from a request response.
|
|
253
258
|
|
|
254
|
-
This only
|
|
259
|
+
This only works for simple responses.
|
|
255
260
|
|
|
256
|
-
Available attributes:
|
|
261
|
+
Available public attributes:
|
|
257
262
|
|
|
258
263
|
- ``status_code``: integer status code.
|
|
259
264
|
- ``data``: body as bytes.
|
|
@@ -265,7 +270,7 @@ class RequestFlaskResponse:
|
|
|
265
270
|
|
|
266
271
|
Constructor parameter:
|
|
267
272
|
|
|
268
|
-
|
|
273
|
+
:param response: Response from request.
|
|
269
274
|
"""
|
|
270
275
|
|
|
271
276
|
def __init__(self, response):
|
|
@@ -289,8 +294,8 @@ class Client:
|
|
|
289
294
|
|
|
290
295
|
Constructor parameters:
|
|
291
296
|
|
|
292
|
-
|
|
293
|
-
|
|
297
|
+
:param auth: Authenticator.
|
|
298
|
+
:param default_login: When ``login`` is not set.
|
|
294
299
|
"""
|
|
295
300
|
|
|
296
301
|
def __init__(self, auth: Authenticator, default_login: str|None = None):
|
|
@@ -320,16 +325,16 @@ class Client:
|
|
|
320
325
|
|
|
321
326
|
Mandatory parameters:
|
|
322
327
|
|
|
323
|
-
|
|
324
|
-
|
|
328
|
+
:param method: HTTP method ("GET", "POST", "PATCH", "DELETE"…).
|
|
329
|
+
:param path: Local path under the base URL.
|
|
325
330
|
|
|
326
331
|
Optional parameters:
|
|
327
332
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
+
:param status: Expected HTTP status, *None* to skip status check.
|
|
334
|
+
:param content: Regular expression for response body, *None* to skip content check.
|
|
335
|
+
:param login: Authenticated user, use **explicit** *None* to skip default.
|
|
336
|
+
:param auth: Authentication scheme to use instead of default behavior.
|
|
337
|
+
:param **kwargs: More request parameters (headers, data, json…).
|
|
333
338
|
"""
|
|
334
339
|
|
|
335
340
|
if "login" in kwargs:
|
|
@@ -343,54 +348,89 @@ class Client:
|
|
|
343
348
|
cookies.update(kwargs["cookies"])
|
|
344
349
|
del kwargs["cookies"]
|
|
345
350
|
|
|
351
|
+
# FIXME allow or forbid?
|
|
352
|
+
# if "json" in kwargs and "data" in kwargs:
|
|
353
|
+
# log.warning("mix of json and data parameters in request")
|
|
354
|
+
|
|
355
|
+
# convert json parameters
|
|
356
|
+
if "json" in kwargs:
|
|
357
|
+
json_param = kwargs["json"]
|
|
358
|
+
assert isinstance(json_param, dict)
|
|
359
|
+
for name in list(json_param.keys()):
|
|
360
|
+
val = json_param[name]
|
|
361
|
+
if val is None:
|
|
362
|
+
pass
|
|
363
|
+
elif isinstance(val, (bool, int, float, str, tuple, list, dict)):
|
|
364
|
+
pass
|
|
365
|
+
elif "model_dump" in val.__dir__() and callable(val.model_dump):
|
|
366
|
+
# probably pydantic
|
|
367
|
+
json_param[name] = val.model_dump()
|
|
368
|
+
else: # pydantic or standard dataclasses?
|
|
369
|
+
json_param[name] = dataclasses.asdict(val)
|
|
370
|
+
|
|
371
|
+
# convert data parameters
|
|
372
|
+
if "data" in kwargs:
|
|
373
|
+
data_param = kwargs["data"]
|
|
374
|
+
assert isinstance(data_param, dict)
|
|
375
|
+
for name in list(data_param.keys()):
|
|
376
|
+
val = data_param[name]
|
|
377
|
+
if val is None:
|
|
378
|
+
data_param[name] = "null"
|
|
379
|
+
elif isinstance(val, (io.IOBase, tuple)):
|
|
380
|
+
pass # file parameters?
|
|
381
|
+
elif isinstance(val, (bool, int, float, str, list, dict)):
|
|
382
|
+
data_param[name] = json.dumps(val)
|
|
383
|
+
elif "model_dump_json" in val.__dir__() and callable(val.model_dump_json):
|
|
384
|
+
data_param[name] = val.model_dump_json()
|
|
385
|
+
else:
|
|
386
|
+
data_param[name] = json.dumps(dataclasses.asdict(val))
|
|
387
|
+
|
|
346
388
|
self._auth.setAuth(login, kwargs, cookies, auth=auth)
|
|
347
389
|
res = self._request(method, path, cookies, **kwargs) # type: ignore
|
|
348
390
|
|
|
349
391
|
# check status
|
|
350
392
|
if status is not None:
|
|
351
393
|
if res.status_code != status: # show error before aborting
|
|
352
|
-
|
|
394
|
+
# FIXME what if the useful part is at the end?
|
|
395
|
+
_pytestFail(f"bad {status} result: {res.status_code} {res.text[:512]}...")
|
|
353
396
|
|
|
354
397
|
# check content
|
|
355
398
|
if content is not None:
|
|
356
399
|
if not re.search(content, res.text, re.DOTALL):
|
|
357
|
-
|
|
400
|
+
# FIXME what if the useful part is at the end?
|
|
401
|
+
_pytestFail(f"cannot find {content} in {res.text[:512]}...")
|
|
358
402
|
|
|
359
403
|
return res
|
|
360
404
|
|
|
361
405
|
def get(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
362
|
-
"""HTTP GET request, see
|
|
406
|
+
"""HTTP GET request, see `Client.request`."""
|
|
363
407
|
return self.request("GET", path, status=status, content=content, **kwargs)
|
|
364
408
|
|
|
365
409
|
def post(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
366
|
-
"""HTTP POST request, see
|
|
410
|
+
"""HTTP POST request, see `Client.request`."""
|
|
367
411
|
return self.request("POST", path, status=status, content=content, **kwargs)
|
|
368
412
|
|
|
369
413
|
def put(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
370
|
-
"""HTTP PUT request, see
|
|
414
|
+
"""HTTP PUT request, see `Client.request`."""
|
|
371
415
|
return self.request("PUT", path, status=status, content=content, **kwargs)
|
|
372
416
|
|
|
373
417
|
def patch(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
374
|
-
"""HTTP PATCH request, see
|
|
418
|
+
"""HTTP PATCH request, see `Client.request`."""
|
|
375
419
|
return self.request("PATCH", path, status=status, content=content, **kwargs)
|
|
376
420
|
|
|
377
421
|
def delete(self, path: str, status: int|None = None, content: str|None = None, **kwargs):
|
|
378
|
-
"""HTTP DELETE request, see
|
|
422
|
+
"""HTTP DELETE request, see `Client.request`."""
|
|
379
423
|
return self.request("DELETE", path, status=status, content=content, **kwargs)
|
|
380
424
|
|
|
381
|
-
def check(self, method: str, path: str, status: int, content: str|None = None, **kwargs):
|
|
382
|
-
"""Deprecated, use ``request`` or HTTP-method-specific methods."""
|
|
383
|
-
return self.request(method, path, status=status, content=content, **kwargs)
|
|
384
|
-
|
|
385
425
|
|
|
386
426
|
class RequestClient(Client):
|
|
387
427
|
"""Request-based test provider.
|
|
388
428
|
|
|
389
429
|
Constructor parameters:
|
|
390
430
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
431
|
+
:param auth: Authenticator.
|
|
432
|
+
:param base_url: Target server.
|
|
433
|
+
:param default_login: When ``login`` is not set.
|
|
394
434
|
"""
|
|
395
435
|
|
|
396
436
|
def __init__(self, auth: Authenticator, base_url: str, default_login=None):
|
|
@@ -434,9 +474,9 @@ class FlaskClient(Client):
|
|
|
434
474
|
|
|
435
475
|
Constructor parameters:
|
|
436
476
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
477
|
+
:param auth: Authenticator.
|
|
478
|
+
:param client: Flask actual ``test_client``.
|
|
479
|
+
:param default_login: When ``login`` is not set.
|
|
440
480
|
|
|
441
481
|
Note: this client handles `cookies`.
|
|
442
482
|
"""
|
|
@@ -499,29 +539,29 @@ def ft_authenticator():
|
|
|
499
539
|
Environment variables:
|
|
500
540
|
|
|
501
541
|
- ``FLASK_TESTER_LOG_LEVEL``: package log level in
|
|
502
|
-
``DEBUG INFO WARNING ERROR CRITICAL NOSET
|
|
503
|
-
|
|
542
|
+
``DEBUG INFO WARNING ERROR CRITICAL NOSET``,
|
|
543
|
+
defaults to ``NOTSET``.
|
|
504
544
|
- ``FLASK_TESTER_ALLOW``: allowed space-separated authentication schemes, in
|
|
505
|
-
``basic param bearer header cookie tparam fake none
|
|
506
|
-
|
|
507
|
-
- ``FLASK_TESTER_USER``: user login parameter for ``param`` authentication
|
|
508
|
-
|
|
509
|
-
- ``FLASK_TESTER_PASS``: user password parameter for ``param`` authentication
|
|
510
|
-
|
|
511
|
-
- ``FLASK_TESTER_LOGIN``: user login parameter for ``fake`` authentication
|
|
512
|
-
|
|
513
|
-
- ``FLASK_TESTER_BEARER``: bearer name for *token* authentication
|
|
514
|
-
|
|
515
|
-
- ``FLASK_TESTER_HEADER``: header name for *token* authentication
|
|
516
|
-
|
|
517
|
-
- ``FLASK_TESTER_COOKIE``: cookie name for *token* authentication
|
|
518
|
-
|
|
519
|
-
- ``FLASK_TESTER_TPARAM``: parameter for *token* authentication
|
|
520
|
-
|
|
521
|
-
- ``FLASK_TESTER_PTYPE``: default parameter type, ``data`` or ``json
|
|
522
|
-
|
|
523
|
-
- ``FLASK_TESTER_AUTH``: initial comma-separated list of *login:password
|
|
524
|
-
|
|
545
|
+
``basic param bearer header cookie tparam fake none``,
|
|
546
|
+
defaults to ``bearer basic param none``.
|
|
547
|
+
- ``FLASK_TESTER_USER``: user login parameter for ``param`` authentication,
|
|
548
|
+
defaults to ``USER``.
|
|
549
|
+
- ``FLASK_TESTER_PASS``: user password parameter for ``param`` authentication,
|
|
550
|
+
defaults to ``PASS``.
|
|
551
|
+
- ``FLASK_TESTER_LOGIN``: user login parameter for ``fake`` authentication,
|
|
552
|
+
defaults to ``LOGIN``.
|
|
553
|
+
- ``FLASK_TESTER_BEARER``: bearer name for *token* authentication,
|
|
554
|
+
defaults to ``Bearer``.
|
|
555
|
+
- ``FLASK_TESTER_HEADER``: header name for *token* authentication,
|
|
556
|
+
defaults to ``Auth``.
|
|
557
|
+
- ``FLASK_TESTER_COOKIE``: cookie name for *token* authentication,
|
|
558
|
+
defaults to ``auth``.
|
|
559
|
+
- ``FLASK_TESTER_TPARAM``: parameter for *token* authentication,
|
|
560
|
+
defaults to ``AUTH``.
|
|
561
|
+
- ``FLASK_TESTER_PTYPE``: default parameter type, ``data`` or ``json``,
|
|
562
|
+
defaults to ``data``.
|
|
563
|
+
- ``FLASK_TESTER_AUTH``: initial comma-separated list of *login:password*,
|
|
564
|
+
defaults to not set.
|
|
525
565
|
"""
|
|
526
566
|
|
|
527
567
|
yield _ft_authenticator()
|
|
@@ -532,28 +572,30 @@ def _ft_client(authenticator):
|
|
|
532
572
|
default_login = os.environ.get("FLASK_TESTER_DEFAULT", None)
|
|
533
573
|
client: Client
|
|
534
574
|
|
|
535
|
-
|
|
536
|
-
app_def = os.environ.get("FLASK_TESTER_APP", app_def)
|
|
575
|
+
test_app = os.environ.get("FLASK_TESTER_APP", "app")
|
|
537
576
|
|
|
538
|
-
if
|
|
539
|
-
client = RequestClient(authenticator,
|
|
577
|
+
if test_app.startswith("http://") or test_app.startswith("https://"):
|
|
578
|
+
client = RequestClient(authenticator, test_app, default_login)
|
|
540
579
|
else:
|
|
541
580
|
# load app package
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if ":" in pkg_name: # override defaults
|
|
545
|
-
pkg_name, app_name = pkg_name.split(":", 1)
|
|
581
|
+
if ":" in test_app: # override defaults
|
|
582
|
+
pkg_name, app_name = test_app.split(":", 1)
|
|
546
583
|
app_names = [app_name]
|
|
584
|
+
else:
|
|
585
|
+
pkg_name = test_app
|
|
586
|
+
app_names = ["app", "application", "create_app", "make_app"]
|
|
547
587
|
pkg = importlib.import_module(pkg_name)
|
|
548
588
|
# find app in package
|
|
589
|
+
app = None
|
|
549
590
|
for name in app_names:
|
|
550
591
|
if hasattr(pkg, name):
|
|
551
592
|
app = getattr(pkg, name)
|
|
552
593
|
if callable(app) and not hasattr(app, "test_client"):
|
|
553
594
|
app = app()
|
|
554
595
|
break
|
|
596
|
+
# none found
|
|
555
597
|
if not app:
|
|
556
|
-
raise FlaskTesterError(f"cannot find Flask app in {pkg_name}")
|
|
598
|
+
raise FlaskTesterError(f"cannot find Flask app in {pkg_name} ({test_app})")
|
|
557
599
|
client = FlaskClient(authenticator, app.test_client(), default_login)
|
|
558
600
|
|
|
559
601
|
return client
|
|
@@ -562,7 +604,7 @@ def _ft_client(authenticator):
|
|
|
562
604
|
def ft_client(ft_authenticator):
|
|
563
605
|
"""Pytest Fixture: ft_client.
|
|
564
606
|
|
|
565
|
-
|
|
607
|
+
Mandatory target environment variable:
|
|
566
608
|
|
|
567
609
|
- ``FLASK_TESTER_APP``: find the Flask application, eg
|
|
568
610
|
``app:create_app`` for an internal test, or
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: FlaskTester
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.1
|
|
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
|
|
@@ -34,6 +34,7 @@ Requires-Dist: types-flask; extra == "dev"
|
|
|
34
34
|
Requires-Dist: types-requests; extra == "dev"
|
|
35
35
|
Requires-Dist: FlaskSimpleAuth>=30.0; extra == "dev"
|
|
36
36
|
Requires-Dist: passlib; extra == "dev"
|
|
37
|
+
Requires-Dist: pydantic; extra == "dev"
|
|
37
38
|
Provides-Extra: doc
|
|
38
39
|
Requires-Dist: sphinx; extra == "doc"
|
|
39
40
|
Requires-Dist: sphinx_rtd_theme; extra == "doc"
|
|
@@ -49,7 +50,7 @@ either with internal Flask tests (aka `test_client`) or external tests (with
|
|
|
49
50
|
authentication and per-user cookies.
|
|
50
51
|
|
|
51
52
|
Only one set of tests is needed, switching from internal to external is
|
|
52
|
-
achieved
|
|
53
|
+
achieved by setting an environment variable.
|
|
53
54
|
|
|
54
55
|

|
|
55
56
|

|
|
@@ -123,7 +124,7 @@ The above test runs with [`tests/app.py`](tests/app.py)
|
|
|
123
124
|
[Flask](https://flask.palletsprojects.com/)
|
|
124
125
|
REST application back-end with password and token authentication based on
|
|
125
126
|
[FlaskSimpleAuth](https://pypi.org/project/FlaskSimpleAuth/).
|
|
126
|
-
The code uses
|
|
127
|
+
The code uses _23_ lines of Python for implementing
|
|
127
128
|
password (basic and parameters) and token authentications,
|
|
128
129
|
admin group authorization, and routes for
|
|
129
130
|
token generation (2), identity tests (2) and an incredible open cookie-based
|
|
@@ -6,7 +6,7 @@ either with internal Flask tests (aka `test_client`) or external tests (with
|
|
|
6
6
|
authentication and per-user cookies.
|
|
7
7
|
|
|
8
8
|
Only one set of tests is needed, switching from internal to external is
|
|
9
|
-
achieved
|
|
9
|
+
achieved by setting an environment variable.
|
|
10
10
|
|
|
11
11
|

|
|
12
12
|

|
|
@@ -80,7 +80,7 @@ The above test runs with [`tests/app.py`](tests/app.py)
|
|
|
80
80
|
[Flask](https://flask.palletsprojects.com/)
|
|
81
81
|
REST application back-end with password and token authentication based on
|
|
82
82
|
[FlaskSimpleAuth](https://pypi.org/project/FlaskSimpleAuth/).
|
|
83
|
-
The code uses
|
|
83
|
+
The code uses _23_ lines of Python for implementing
|
|
84
84
|
password (basic and parameters) and token authentications,
|
|
85
85
|
admin group authorization, and routes for
|
|
86
86
|
token generation (2), identity tests (2) and an incredible open cookie-based
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "FlaskTester"
|
|
7
|
-
version = "
|
|
7
|
+
version = "4.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"
|
|
@@ -33,7 +33,14 @@ package = "https://pypi.org/project/FlaskTester/"
|
|
|
33
33
|
|
|
34
34
|
[project.optional-dependencies]
|
|
35
35
|
# various dev tools
|
|
36
|
-
dev = [
|
|
36
|
+
dev = [
|
|
37
|
+
# static checks…
|
|
38
|
+
"mypy", "pyright", "ruff", "coverage", "pymarkdownlnt!=0.9.5",
|
|
39
|
+
# packaging
|
|
40
|
+
"build", "twine", "wheel",
|
|
41
|
+
# tests
|
|
42
|
+
"types-flask", "types-requests", "FlaskSimpleAuth>=30.0", "passlib", "pydantic"
|
|
43
|
+
]
|
|
37
44
|
# documentation generation
|
|
38
45
|
doc = ["sphinx", "sphinx_rtd_theme", "sphinx-autoapi", "sphinx-lint", "myst_parser"]
|
|
39
46
|
|
|
@@ -8,6 +8,7 @@ import http.server as htsv
|
|
|
8
8
|
import threading
|
|
9
9
|
import io
|
|
10
10
|
import logging
|
|
11
|
+
import model
|
|
11
12
|
|
|
12
13
|
logging.basicConfig(level=logging.INFO)
|
|
13
14
|
# logging.basicConfig(level=logging.DEBUG)
|
|
@@ -22,7 +23,7 @@ os.environ.update(
|
|
|
22
23
|
|
|
23
24
|
def test_sanity():
|
|
24
25
|
# must provide url or package of Flask application to test
|
|
25
|
-
assert "
|
|
26
|
+
assert "FLASK_TESTER_APP" in os.environ
|
|
26
27
|
# log.debug(f"TEST_SEED={os.environ.get('TEST_SEED')}")
|
|
27
28
|
|
|
28
29
|
# example from README.md
|
|
@@ -296,7 +297,7 @@ def test_request_client():
|
|
|
296
297
|
try:
|
|
297
298
|
client = ft.RequestClient(ft.Authenticator(), "http://localhost:8888")
|
|
298
299
|
client.get("/", 200)
|
|
299
|
-
client.
|
|
300
|
+
client.request("GET", "/", 200)
|
|
300
301
|
hello = io.BytesIO(b"hello world")
|
|
301
302
|
client.post("/", 501, data={"hello": hello})
|
|
302
303
|
hello = io.BytesIO(b"hello world")
|
|
@@ -309,10 +310,6 @@ def test_client_fixture():
|
|
|
309
310
|
# ft_client coverage
|
|
310
311
|
auth = ft._ft_authenticator()
|
|
311
312
|
# check and save env
|
|
312
|
-
url = None
|
|
313
|
-
if "FLASK_TESTER_URL" in os.environ: # pragma: no cover
|
|
314
|
-
url = os.environ["FLASK_TESTER_URL"]
|
|
315
|
-
del os.environ["FLASK_TESTER_URL"]
|
|
316
313
|
app = None
|
|
317
314
|
if "FLASK_TESTER_APP" in os.environ:
|
|
318
315
|
app = os.environ["FLASK_TESTER_APP"]
|
|
@@ -320,10 +317,10 @@ def test_client_fixture():
|
|
|
320
317
|
# no url nor app, defaults to "app"
|
|
321
318
|
init = ft._ft_client(auth)
|
|
322
319
|
# url
|
|
323
|
-
os.environ["
|
|
320
|
+
os.environ["FLASK_TESTER_APP"] = "http://localhost:5000"
|
|
324
321
|
init = ft._ft_client(auth)
|
|
325
322
|
assert isinstance(init, ft.RequestClient)
|
|
326
|
-
del os.environ["
|
|
323
|
+
del os.environ["FLASK_TESTER_APP"]
|
|
327
324
|
# bad package
|
|
328
325
|
os.environ["FLASK_TESTER_APP"] = "no_such_package"
|
|
329
326
|
try:
|
|
@@ -340,7 +337,37 @@ def test_client_fixture():
|
|
|
340
337
|
assert True, "expected error raised"
|
|
341
338
|
del os.environ["FLASK_TESTER_APP"]
|
|
342
339
|
# reset env
|
|
343
|
-
if url: # pragma: no cover
|
|
344
|
-
os.environ["FLASK_TESTER_URL"] = url
|
|
345
340
|
if app:
|
|
346
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
|
+
# null 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
|
|
@@ -0,0 +1,28 @@
|
|
|
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)
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import pytest
|
|
3
|
-
from FlaskTester import ft_client, ft_authenticator
|
|
4
|
-
|
|
5
|
-
os.environ.update(FLASK_TESTER_ALLOW="basic param none")
|
|
6
|
-
|
|
7
|
-
@pytest.fixture
|
|
8
|
-
def app(ft_client):
|
|
9
|
-
ft_client.setPass("calvin", "clv-pw")
|
|
10
|
-
ft_client.setCookie("calvin", "lang", "en")
|
|
11
|
-
ft_client.setPass("hobbes", "hbs-pw")
|
|
12
|
-
ft_client.setCookie("hobbes", "lang", "fr")
|
|
13
|
-
yield ft_client
|
|
14
|
-
|
|
15
|
-
def test_something(app):
|
|
16
|
-
# requires an authentication
|
|
17
|
-
app.get("/authenticated", 401, login=None)
|
|
18
|
-
res = app.get("/authenticated", 200, login="calvin")
|
|
19
|
-
assert "Hello" in res.text
|
|
20
|
-
app.get("/authenticated", 200, "Bonjour", login="hobbes")
|
|
21
|
-
# only allowed to calvin
|
|
22
|
-
app.get("/only-calvin", 401, login=None)
|
|
23
|
-
app.get("/only-calvin", 200, login="calvin")
|
|
24
|
-
app.get("/only-calvin", 403, login="hobbes")
|
|
25
|
-
# no authentication required, but depends on lang
|
|
26
|
-
res = app.get("/no-auth", 200, login="calvin", auth="none")
|
|
27
|
-
assert "Hello" in res.text
|
|
28
|
-
app.get("/no-auth", 200, "Bonjour", login="hobbes", auth="none")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|