From a6536cf798f3109ad78a86bb8cf82b09b681bdd8 Mon Sep 17 00:00:00 2001 From: Chris Wagner Date: Sun, 29 Dec 2024 22:06:04 -0800 Subject: [PATCH] Add 3.13 support. Most of the changes were due to 3.13 sqlite3 now being strict about not closing db connections - which we didn't do in our tests. Took the opportunity to simplify/remove some fixtures that were used only once or not at all. --- .github/workflows/tests.yml | 1 + .pre-commit-config.yaml | 4 +- CHANGES.rst | 13 ++ examples/fsqlalchemy1/tests/test_api.py | 5 + pyproject-too.toml | 1 + pyproject.toml | 1 + pytest.ini | 4 +- requirements/docs.txt | 1 + tests/conftest.py | 211 ++++++++++-------------- tests/test_basic.py | 14 +- tests/test_changeable.py | 51 +++--- tests/test_datastore.py | 12 +- tests/test_recoverable.py | 4 +- tests/test_two_factor.py | 31 ++-- tests/test_unified_signin.py | 3 + tox.ini | 4 +- 16 files changed, 182 insertions(+), 178 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f1690fa2..778810ad 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,7 @@ jobs: - {python: '3.11', tox: 'py311-low'} - {python: '3.12', tox: 'py312-release' } - {python: '3.12', tox: 'py312-low' } + - {python: '3.13', tox: 'py313-release' } - {python: 'pypy-3.9', tox: 'pypy39-release'} - {python: 'pypy-3.9', tox: 'pypy39-low'} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0fcd1245..50ccf192 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: check-merge-conflict - id: fix-byte-order-marker - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 + rev: v3.19.1 hooks: - id: pyupgrade args: [--py39-plus] @@ -31,7 +31,7 @@ repos: - flake8-bugbear - flake8-implicit-str-concat - repo: https://github.com/Riverside-Healthcare/djLint - rev: v1.36.1 + rev: v1.36.4 hooks: - id: djlint-jinja files: "\\.html" diff --git a/CHANGES.rst b/CHANGES.rst index e175ed71..c2a7a95b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,10 +6,23 @@ Here you can see the full list of changes between each Flask-Security release. Version 5.6.0 ------------- +Released TBD + Features & Improvements +++++++++++++++++++++++ - (:issue:`1038`) Add support for 'secret_key' rotation - (:issue:`980`) Add support for username recovery in simple login flows +- (:pr:`xx`) Add support for Python 3.13 + +Notes ++++++ +Python 3.13 removed ``crypt``, which passlib attempts to import and use as +part of its safe_crypt() method (fallback is to return None). +However - that method only appears to be called in a few crypt handlers and +for bcrypt - only for the built-in bcrypt - not if the bcrypt package is installed. +passlib is not maintained - a new fork (10/1/2024) (https://pypi.org/project/libpass/) +seems promising and has been tested with python 3.13. If that fork matures we will +change the dependencies appropriately. Version 5.5.2 ------------- diff --git a/examples/fsqlalchemy1/tests/test_api.py b/examples/fsqlalchemy1/tests/test_api.py index 3a967901..b9c0f40c 100644 --- a/examples/fsqlalchemy1/tests/test_api.py +++ b/examples/fsqlalchemy1/tests/test_api.py @@ -26,6 +26,8 @@ def test_monitor_404(myapp): headers={myapp.config["SECURITY_TOKEN_AUTHENTICATION_HEADER"]: "token"}, ) assert resp.status_code == 403 + with myapp.app_context(): + ds.db.engine.dispose() def test_blog_write(myapp): @@ -50,3 +52,6 @@ def test_blog_write(myapp): ) assert resp.status_code == 200 assert b"Yes, unittest@me.com can update blog" == resp.data + + with myapp.app_context(): + ds.db.engine.dispose() diff --git a/pyproject-too.toml b/pyproject-too.toml index e92715e1..6d4af63b 100644 --- a/pyproject-too.toml +++ b/pyproject-too.toml @@ -27,6 +27,7 @@ classifiers=[ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Development Status :: 5 - Production/Stable", diff --git a/pyproject.toml b/pyproject.toml index afe251e3..b12bc11b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ classifiers=[ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Development Status :: 5 - Production/Stable", diff --git a/pytest.ini b/pytest.ini index d38cc6f4..30ba23d3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -27,6 +27,8 @@ filterwarnings = ignore::DeprecationWarning:pkg_resources:0 ignore::DeprecationWarning:dateutil:0 ignore:.*passwordless feature.*:DeprecationWarning:flask_security:0 - ignore::DeprecationWarning:passlib:0 + ignore:.*pkg_resources.*:DeprecationWarning:passlib:0 + ignore:.*__version__.*:DeprecationWarning:passlib:0 + ignore::DeprecationWarning:pony:0 ignore:.*'sms' was enabled in SECURITY_US_ENABLED_METHODS;.*:UserWarning:flask_security:0 ignore:.*'get_token_status' is deprecated.*:DeprecationWarning:flask_security:0 diff --git a/requirements/docs.txt b/requirements/docs.txt index b3f0d791..75a9fd56 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -6,3 +6,4 @@ Flask-Login Flask-SQLAlchemy sqlalchemy sqlalchemy-utils +setuptools diff --git a/tests/conftest.py b/tests/conftest.py index 70131acb..b6618169 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,8 @@ from __future__ import annotations +import sqlite3 +import gc import os import tempfile import time @@ -63,9 +65,6 @@ except ImportError: NO_BABEL = True -if t.TYPE_CHECKING: # pragma: no cover - from flask.testing import FlaskClient - class FastHash(PasswordHash): """Our own 'hasher'. For testing @@ -95,14 +94,19 @@ def using(cls, relaxed=False, **settings): return type("fasthash2", (cls,), {}) -class SecurityFixture(Flask): - security: Security - mail: Mail +# python 3.13 is strict about not closing sqlite3 db connections. +def find_sqlite_connections(): + connections = [] + for obj in gc.get_objects(): + if isinstance(obj, sqlite3.Connection): + connections.append(obj) + return connections @pytest.fixture() -def app(request: pytest.FixtureRequest) -> SecurityFixture: - app = SecurityFixture(__name__) +def app(request): + # assert not find_sqlite_connections() # hopefully find tests that don't clean up + app = Flask(__name__) app.response_class = Response app.debug = True app.config["SECRET_KEY"] = "secret" @@ -335,15 +339,20 @@ def revert_forms(): del app.security.forms[form_name].cls.username request.addfinalizer(revert_forms) - return app + yield app + # help find tests that don't clean up - note that pony leaves a connection so + # we can't use this in 'production'... + # assert not find_sqlite_connections() @pytest.fixture() -def mongoengine_datastore(request, app, tmpdir, realmongodburl): - return mongoengine_setup(request, app, tmpdir, realmongodburl) +def mongoengine_datastore(app, tmpdir, realmongodburl): + ds, td = mongoengine_setup(app, tmpdir, realmongodburl) + yield ds + td() -def mongoengine_setup(request, app, tmpdir, realmongodburl): +def mongoengine_setup(app, tmpdir, realmongodburl): # To run against a realdb: mongod --dbpath import pymongo import mongomock @@ -442,17 +451,17 @@ def tear_down(): db.drop_database(db_name) disconnect_all() - request.addfinalizer(tear_down) - - return MongoEngineUserDatastore(db, User, Role, WebAuthn) + return MongoEngineUserDatastore(db, User, Role, WebAuthn), tear_down @pytest.fixture() -def sqlalchemy_datastore(request, app, tmpdir, realdburl): - return sqlalchemy_setup(request, app, tmpdir, realdburl) +def sqlalchemy_datastore(app, tmpdir, realdburl): + ds, td = sqlalchemy_setup(app, tmpdir, realdburl) + yield ds + td() -def sqlalchemy_setup(request, app, tmpdir, realdburl): +def sqlalchemy_setup(app, tmpdir, realdburl): pytest.importorskip("flask_sqlalchemy") from flask_sqlalchemy import SQLAlchemy from sqlalchemy import Column, Integer @@ -497,22 +506,24 @@ def augment_auth_token(self, tdata): db.create_all() def tear_down(): - if realdburl: - with app.app_context(): + with app.app_context(): + if realdburl: db.drop_all() _teardown_realdb(db_info) + engine = db.engine + engine.dispose() - request.addfinalizer(tear_down) - - return SQLAlchemyUserDatastore(db, User, Role, WebAuthn) + return SQLAlchemyUserDatastore(db, User, Role, WebAuthn), tear_down @pytest.fixture() -def fsqlalite_datastore(request, app, tmpdir, realdburl): - return fsqlalite_setup(request, app, tmpdir, realdburl) +def fsqlalite_datastore(app, tmpdir, realdburl): + ds, td = fsqlalite_setup(app, tmpdir, realdburl) + yield ds + td() -def fsqlalite_setup(request, app, tmpdir, realdburl): +def fsqlalite_setup(app, tmpdir, realdburl): pytest.importorskip("flask_sqlalchemy_lite") from flask_sqlalchemy_lite import SQLAlchemy from sqlalchemy.orm import DeclarativeBase, mapped_column @@ -557,21 +568,24 @@ def get_security_payload(self) -> dict[str, t.Any]: def tear_down(): with app.app_context(): Model.metadata.drop_all(db.engine) + engine = db.engine + engine.dispose() if realdburl: _teardown_realdb(db_info) - request.addfinalizer(tear_down) - return FSQLALiteUserDatastore(db, User, Role, WebAuthn) + return FSQLALiteUserDatastore(db, User, Role, WebAuthn), tear_down @pytest.fixture() -def sqlalchemy_session_datastore(request, app, tmpdir, realdburl): +def sqlalchemy_session_datastore(app, tmpdir, realdburl): if sys.version_info < (3, 10): pytest.skip("requires python3.10 or higher") - return sqlalchemy_session_setup(request, app, tmpdir, realdburl) + ds, td = sqlalchemy_session_setup(app, tmpdir, realdburl) + yield ds + td() -def sqlalchemy_session_setup(request, app, tmpdir, realdburl, **engine_kwargs): +def sqlalchemy_session_setup(app, tmpdir, realdburl, **engine_kwargs): """ Note that we test having a different user id column name here. """ @@ -651,20 +665,21 @@ def get_security_payload(self): def tear_down(): with app.app_context(): Base.metadata.drop_all(bind=engine) + engine.dispose() if realdburl: _teardown_realdb(db_info) - request.addfinalizer(tear_down) - - return SQLAlchemySessionUserDatastore(db_session, User, Role, WebAuthn) + return SQLAlchemySessionUserDatastore(db_session, User, Role, WebAuthn), tear_down @pytest.fixture() -def peewee_datastore(request, app, tmpdir, realdburl): - return peewee_setup(request, app, tmpdir, realdburl) +def peewee_datastore(app, tmpdir, realdburl): + ds, td = peewee_setup(app, tmpdir, realdburl) + yield ds + td() -def peewee_setup(request, app, tmpdir, realdburl): +def peewee_setup(app, tmpdir, realdburl): pytest.importorskip("peewee") from peewee import ( TextField, @@ -790,17 +805,17 @@ def tear_down(): os.close(f) os.remove(path) - request.addfinalizer(tear_down) - - return PeeweeUserDatastore(db, User, Role, UserRoles, WebAuthn) + return PeeweeUserDatastore(db, User, Role, UserRoles, WebAuthn), tear_down @pytest.fixture() -def pony_datastore(request, app, tmpdir, realdburl): - return pony_setup(request, app, tmpdir, realdburl) +def pony_datastore(app, tmpdir, realdburl): + ds, td = pony_setup(app, tmpdir, realdburl) + yield ds + td() -def pony_setup(request, app, tmpdir, realdburl): +def pony_setup(app, tmpdir, realdburl): pytest.importorskip("pony") from pony.orm import Database, Optional, Required, Set from pony.orm.core import SetInstance @@ -856,85 +871,25 @@ def has_role(self, name): db.generate_mapping(create_tables=True) def tear_down(): + db.disconnect() if realdburl: _teardown_realdb(db_info) - request.addfinalizer(tear_down) - - return PonyUserDatastore(db, User, Role) - - -@pytest.fixture() -def sqlalchemy_app( - app: SecurityFixture, sqlalchemy_datastore: SQLAlchemyUserDatastore -) -> t.Callable[[], SecurityFixture]: - def create() -> SecurityFixture: - security = Security(app, datastore=sqlalchemy_datastore) - app.security = security - return app - - return create - - -@pytest.fixture() -def fsqlalite_app( - app: SecurityFixture, fsqlalite_datastore: FSQLALiteUserDatastore -) -> t.Callable[[], SecurityFixture]: - def create() -> SecurityFixture: - app.security = Security(app, datastore=fsqlalite_datastore) - return app - - return create - - -@pytest.fixture() -def sqlalchemy_session_app(app, sqlalchemy_session_datastore): - def create(): - app.security = Security(app, datastore=sqlalchemy_session_datastore) - return app - - return create - - -@pytest.fixture() -def peewee_app(app, peewee_datastore): - def create(): - app.security = Security(app, datastore=peewee_datastore) - return app - - return create - - -@pytest.fixture() -def mongoengine_app(app, mongoengine_datastore): - def create(): - app.security = Security(app, datastore=mongoengine_datastore) - return app - - return create - - -@pytest.fixture() -def pony_app(app, pony_datastore): - def create(): - app.security = Security(app, datastore=pony_datastore) - return app - - return create + return PonyUserDatastore(db, User, Role), tear_down @pytest.fixture() -def client(request: pytest.FixtureRequest, sqlalchemy_app: t.Callable) -> FlaskClient: - app = sqlalchemy_app() +def client(request, app, sqlalchemy_datastore): + app.security = Security(app, datastore=sqlalchemy_datastore) populate_data(app) return app.test_client() @pytest.fixture() -def client_nc(request, sqlalchemy_app): +def client_nc(request, app, sqlalchemy_datastore): # useful for testing token auth. # No Cookies for You! - app = sqlalchemy_app() + app.security = Security(app, datastore=sqlalchemy_datastore) populate_data(app) return app.test_client(use_cookies=False) @@ -950,32 +905,33 @@ def client_nc(request, sqlalchemy_app): ) def clients(request, app, tmpdir, realdburl, realmongodburl): if request.param == "cl-fsqlalchemy": - ds = sqlalchemy_setup(request, app, tmpdir, realdburl) + ds, td = sqlalchemy_setup(app, tmpdir, realdburl) elif request.param == "cl-sqla-session": if sys.version_info < (3, 10): pytest.skip("requires python3.10 or higher") - ds = sqlalchemy_session_setup(request, app, tmpdir, realdburl) + ds, td = sqlalchemy_session_setup(app, tmpdir, realdburl) elif request.param == "cl-mongo": - ds = mongoengine_setup(request, app, tmpdir, realmongodburl) + ds, td = mongoengine_setup(app, tmpdir, realmongodburl) elif request.param == "cl-peewee": - ds = peewee_setup(request, app, tmpdir, realdburl) + ds, td = peewee_setup(app, tmpdir, realdburl) elif request.param == "cl-pony": # Not working yet. - ds = pony_setup(request, app, tmpdir, realdburl) + ds, td = pony_setup(app, tmpdir, realdburl) elif request.param == "cl-fsqlalite": - ds = fsqlalite_setup(request, app, tmpdir, realdburl) + ds, td = fsqlalite_setup(app, tmpdir, realdburl) app.security = Security(app, datastore=ds) populate_data(app) if request.param == "cl-peewee": # peewee is insistent on a single connection? ds.db.close_db(None) - return app.test_client() + yield app.test_client() + td() @pytest.fixture() -def in_app_context(request, sqlalchemy_app): - app = sqlalchemy_app() +def in_app_context(request, app, sqlalchemy_datastore): + app.security = Security(app, datastore=sqlalchemy_datastore) with app.app_context(): yield app @@ -1009,20 +965,23 @@ def fn(key, **kwargs): ) def datastore(request, app, tmpdir, realdburl, realmongodburl): if request.param == "sqlalchemy": - rv = sqlalchemy_setup(request, app, tmpdir, realdburl) + ds, td = sqlalchemy_setup(app, tmpdir, realdburl) elif request.param == "sqlalchemy-session": if sys.version_info < (3, 10): pytest.skip("requires python3.10 or higher") - rv = sqlalchemy_session_setup(request, app, tmpdir, realdburl) + ds, td = sqlalchemy_session_setup(app, tmpdir, realdburl) elif request.param == "mongoengine": - rv = mongoengine_setup(request, app, tmpdir, realmongodburl) + ds, td = mongoengine_setup(app, tmpdir, realmongodburl) elif request.param == "peewee": - rv = peewee_setup(request, app, tmpdir, realdburl) + ds, td = peewee_setup(app, tmpdir, realdburl) elif request.param == "pony": - rv = pony_setup(request, app, tmpdir, realdburl) + if sys.version_info > (3, 12): + pytest.skip("requires python3.12 or lower") + ds, td = pony_setup(app, tmpdir, realdburl) elif request.param == "fsqlalite": - rv = fsqlalite_setup(request, app, tmpdir, realdburl) - return rv + ds, td = fsqlalite_setup(app, tmpdir, realdburl) + yield ds + td() @pytest.fixture() @@ -1095,7 +1054,7 @@ def realmongodburl(request): def _setup_realdb(realdburl): """ Called when we want to run unit tests against a real DB. - This is useful since different DB drivers are pickier about queries etc + This is useful since different DB drivers are pickier about queries etc. (such as pyscopg2 and postgres) """ from sqlalchemy import create_engine diff --git a/tests/test_basic.py b/tests/test_basic.py index 09dfa8e4..073b3d67 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1173,6 +1173,9 @@ class User(db.Model, fsqla.FsUserMixin): token = response.json["response"]["user"]["authentication_token"] verify_token(client_nc, token) + with app.app_context(): + db.engine.dispose() + def test_null_token_uniquifier(app): pytest.importorskip("sqlalchemy") @@ -1214,11 +1217,14 @@ class User(db.Model, fsqla.FsUserMixin): ds.put(user) ds.commit() - client_nc = app.test_client(use_cookies=False) + client_nc = app.test_client(use_cookies=False) - response = json_authenticate(client_nc) - token = response.json["response"]["user"]["authentication_token"] - verify_token(client_nc, token) + response = json_authenticate(client_nc) + token = response.json["response"]["user"]["authentication_token"] + verify_token(client_nc, token) + + with app.app_context(): + db.engine.dispose() def test_token_query(app, client_nc): diff --git a/tests/test_changeable.py b/tests/test_changeable.py index f10205e3..bebd993b 100644 --- a/tests/test_changeable.py +++ b/tests/test_changeable.py @@ -291,31 +291,34 @@ class User(db.Model, fsqla.FsUserMixin): ) ds.commit() - client = app.test_client() - - # standard login with auth token - response = json_authenticate(client) - token = response.json["response"]["user"]["authentication_token"] - headers = {"Authentication-Token": token} - # make sure can access restricted page - response = client.get("/token", headers=headers) - assert b"Token Authentication" in response.data - - # change password - response = client.post( - "/change", - data={ - "password": "password", - "new_password": "new strong password", - "new_password_confirm": "new strong password", - }, - follow_redirects=True, - ) - assert response.status_code == 200 + client = app.test_client() + + # standard login with auth token + response = json_authenticate(client) + token = response.json["response"]["user"]["authentication_token"] + headers = {"Authentication-Token": token} + # make sure can access restricted page + response = client.get("/token", headers=headers) + assert b"Token Authentication" in response.data + + # change password + response = client.post( + "/change", + data={ + "password": "password", + "new_password": "new strong password", + "new_password_confirm": "new strong password", + }, + follow_redirects=True, + ) + assert response.status_code == 200 + + # authtoken should still be valid + response = client.get("/token", headers=headers) + assert response.status_code == 200 - # authtoken should still be valid - response = client.get("/token", headers=headers) - assert response.status_code == 200 + with app.app_context(): + db.engine.dispose() @pytest.mark.app_settings(babel_default_locale="fr_FR") diff --git a/tests/test_datastore.py b/tests/test_datastore.py index 29b28e57..79610814 100644 --- a/tests/test_datastore.py +++ b/tests/test_datastore.py @@ -251,7 +251,7 @@ def test_create_user_with_roles_and_permissions(app, datastore): ds = datastore if not hasattr(ds.role_model, "permissions"): return - init_app_with_options(app, datastore) + init_app_with_options(app, ds) with app.app_context(): role = ds.create_role(name="test1", permissions={"read"}) @@ -260,9 +260,9 @@ def test_create_user_with_roles_and_permissions(app, datastore): user = ds.create_user( email="dude@lp.com", username="dude", password="password", roles=[role] ) - datastore.commit() + ds.commit() - user = datastore.find_user(email="dude@lp.com") + user = ds.find_user(email="dude@lp.com") assert user.has_role("test1") is True assert user.has_permission("read") is True assert user.has_permission("write") is False @@ -652,6 +652,8 @@ class User(db.Model, fsqla.FsUserMixin): t5 = ds.find_role("test5") assert {"read"} == t5.get_permissions() + with app.app_context(): + db.engine.dispose() def test_permissions_41(request, app, realdburl): @@ -715,6 +717,8 @@ class User(db.Model, fsqla.FsUserMixin): with app.app_context(): r1 = ds.find_role("r1") assert r1.get_permissions() == {"read", "write"} + with app.app_context(): + db.engine.dispose() def test_fsqlalite_table_name(app): @@ -762,4 +766,6 @@ class User(Model, sqla.FsUserMixin): ds.commit() user = ds.find_user(email="me@lp.com") assert user + with app.app_context(): Model.metadata.drop_all(db.engine) + db.engine.dispose() diff --git a/tests/test_recoverable.py b/tests/test_recoverable.py index 2f912198..6f0f7906 100644 --- a/tests/test_recoverable.py +++ b/tests/test_recoverable.py @@ -293,8 +293,8 @@ def test_recover_invalidates_session(app, client): assert response.location == "/login?next=/profile" -def test_login_form_description(sqlalchemy_app): - app = sqlalchemy_app() +def test_login_form_description(app, sqlalchemy_datastore): + app.security = Security(app, datastore=sqlalchemy_datastore) with app.test_request_context("/login"): login_form = LoginForm() expected = 'Forgot password?' diff --git a/tests/test_two_factor.py b/tests/test_two_factor.py index 3984e18a..1daaa562 100644 --- a/tests/test_two_factor.py +++ b/tests/test_two_factor.py @@ -1375,20 +1375,20 @@ def tf_send_security_token(self, method, **kwargs): ) ds.commit() - data = dict(email="trp@lp.com", password="password") - response = client.post("/login", data=data, follow_redirects=True) - assert b"Please enter your authentication code" in response.data - rescue_data = dict(help_setup="email") - response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) - assert b"That didnt work out as we planned" in response.data - - # Test JSON - headers = {"Accept": "application/json", "Content-Type": "application/json"} - response = client.post("/tf-rescue", json=rescue_data, headers=headers) - assert response.status_code == 500 - assert ( - response.json["response"]["field_errors"]["help_setup"][0] == "Failed Again" - ) + data = dict(email="trp@lp.com", password="password") + response = client.post("/login", data=data, follow_redirects=True) + assert b"Please enter your authentication code" in response.data + rescue_data = dict(help_setup="email") + response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) + assert b"That didnt work out as we planned" in response.data + + # Test JSON + headers = {"Accept": "application/json", "Content-Type": "application/json"} + response = client.post("/tf-rescue", json=rescue_data, headers=headers) + assert response.status_code == 500 + assert response.json["response"]["field_errors"]["help_setup"][0] == "Failed Again" + with app.app_context(): + db.engine.dispose() def test_propagate_next(app, client): @@ -1557,6 +1557,9 @@ class User(db.Model, UserMixin): response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed your two-factor method" in response.data + with app.app_context(): + db.engine.dispose() + @pytest.mark.settings(two_factor_post_setup_view="/post_setup_view") def test_post_setup_redirect(app, client): diff --git a/tests/test_unified_signin.py b/tests/test_unified_signin.py index c39f3d02..15b09f88 100644 --- a/tests/test_unified_signin.py +++ b/tests/test_unified_signin.py @@ -1699,6 +1699,9 @@ def us_send_security_token(self, method, **kwargs): response = client.post("/us-signin/send-code", data=data, follow_redirects=True) assert b"Code has been sent" in response.data + with app.app_context(): + db.engine.dispose() # sqlite wants everything cleaned up + @pytest.mark.settings(us_enabled_methods=["password"]) def test_only_passwd(app, client, get_message): diff --git a/tox.ini b/tox.ini index 4e946efd..e2f5a7d4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] basepython = python3.11 envlist = - py{39,310,311,312,py39}-{low,release} + py{39,310,311,312,313,py39}-{low,release} mypy async nowebauthn @@ -23,7 +23,7 @@ commands = tox -e compile_catalog pytest -W ignore --basetemp={envtmpdir} {posargs:tests} -[testenv:py{39,310,311,312}-release] +[testenv:py{39,310,311,312,313}-release] deps = -r requirements/tests.txt commands =