feat(access): add guarded local password lifecycle and recovery
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from alembic import command
|
||||
from sqlalchemy import create_engine, inspect, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_access.backend.db.models import Account
|
||||
from govoplan_access.backend.security.passwords import hash_password
|
||||
from govoplan_core.db.migrations import alembic_config
|
||||
|
||||
|
||||
class PasswordRecoveryMigrationTests(unittest.TestCase):
|
||||
def test_release_and_development_upgrade_preserve_existing_credentials(self):
|
||||
for track in ("release", "dev"):
|
||||
with (
|
||||
self.subTest(track=track),
|
||||
tempfile.TemporaryDirectory(
|
||||
prefix="govoplan-password-migration-"
|
||||
) as directory,
|
||||
):
|
||||
url = f"sqlite:///{Path(directory) / 'isolated-upgrade.db'}"
|
||||
config = alembic_config(
|
||||
database_url=url, enabled_modules=("access",), migration_track=track
|
||||
)
|
||||
command.upgrade(config, "4f2a9c8e7b6d")
|
||||
command.upgrade(config, "d8f1b4e7a0c3")
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with Session(engine) as session:
|
||||
session.add(
|
||||
Account(
|
||||
id="existing",
|
||||
email="existing@example.test",
|
||||
normalized_email="existing@example.test",
|
||||
password_hash=hash_password("Existing-password"),
|
||||
password_reset_required=True,
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
with engine.connect() as connection:
|
||||
before = list(
|
||||
connection.execute(
|
||||
text("SELECT * FROM access_accounts")
|
||||
).mappings()
|
||||
)
|
||||
tables = set(inspect(connection).get_table_names())
|
||||
command.upgrade(config, "e9a2c5f8b1d4")
|
||||
command.upgrade(config, "e9a2c5f8b1d4")
|
||||
with engine.connect() as connection:
|
||||
inspector = inspect(connection)
|
||||
self.assertEqual(
|
||||
tables | {"access_password_recoveries"},
|
||||
set(inspector.get_table_names()),
|
||||
)
|
||||
self.assertEqual(
|
||||
before,
|
||||
list(
|
||||
connection.execute(
|
||||
text("SELECT * FROM access_accounts")
|
||||
).mappings()
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
{
|
||||
"id",
|
||||
"account_id",
|
||||
"issuer_account_id",
|
||||
"issuer_membership_id",
|
||||
"code_hash",
|
||||
"expires_at",
|
||||
"consumed_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
},
|
||||
{
|
||||
column["name"]
|
||||
for column in inspector.get_columns(
|
||||
"access_password_recoveries"
|
||||
)
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
3,
|
||||
len(
|
||||
inspector.get_foreign_keys("access_password_recoveries")
|
||||
),
|
||||
)
|
||||
self.assertIn(
|
||||
["code_hash"],
|
||||
[
|
||||
constraint["column_names"]
|
||||
for constraint in inspector.get_unique_constraints(
|
||||
"access_password_recoveries"
|
||||
)
|
||||
],
|
||||
)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user