no-mistakes(review): Normalize legacy emails to prevent case-based login lockout

This commit is contained in:
root
2026-09-08 12:33:01 +00:00
parent 3ae1062d65
commit f1b68dd1b7
4 changed files with 132 additions and 5 deletions
+70 -1
View File
@@ -14,7 +14,7 @@ from app.core.config import settings
from app.core.database import async_session_factory
from app.core.security import hash_password
from app.models.user import User
from app.services.seed import normalize_legacy_user_roles
from app.services.seed import normalize_legacy_user_emails, normalize_legacy_user_roles
pytestmark = pytest.mark.asyncio
@@ -52,6 +52,12 @@ async def _normalize_roles() -> None:
await session.commit()
async def _normalize_emails() -> None:
async with async_session_factory() as session:
await normalize_legacy_user_emails(session)
await session.commit()
async def _create_ticket(client, token: str, **overrides) -> dict:
payload = {
"unit_id": 2,
@@ -302,6 +308,69 @@ async def test_normalize_does_not_map_ambiguous_admin_alias(client: AsyncClient)
assert user.role == "admin"
# ── P0 email normalization (legacy mixed-case rows) ───────────────────
async def test_legacy_mixed_case_email_migrated_and_authenticates(client: AsyncClient):
"""A legacy row whose email was stored verbatim in mixed case (the old open
register) is lowercased by the startup self-heal and still authenticates."""
await _insert_user("DemoUser@Example.com", "Tech")
await _normalize_emails() # what lifespan does each boot
async with async_session_factory() as session:
from sqlalchemy import select
user = (
await session.execute(select(User).where(User.email == "demouser@example.com"))
).scalar_one()
assert user.email == "demouser@example.com"
for variant in ("demouser@example.com", "DemoUser@Example.com"):
resp = await client.post(
"/api/auth/login", json={"email": variant, "password": "denya123"}
)
assert resp.status_code == 200, resp.text
async def test_login_matches_legacy_mixed_case_email_before_migration(client: AsyncClient):
"""Login compares on the normalized form, so an un-migrated mixed-case row
is still matched by its lowercase login (no hard dependency on the
self-heal having run)."""
await _insert_user("DemoUser@Example.com", "Tech")
resp = await client.post(
"/api/auth/login", json={"email": "demouser@example.com", "password": "denya123"}
)
assert resp.status_code == 200, resp.text
async def test_legacy_email_normalization_is_idempotent(client: AsyncClient):
"""The startup self-heal rewrites once and no-ops on subsequent boots."""
await _insert_user("DemoUser@Example.com", "Tech")
async with async_session_factory() as session:
first = await normalize_legacy_user_emails(session)
await session.commit()
async with async_session_factory() as session:
second = await normalize_legacy_user_emails(session)
await session.commit()
assert first == 1
assert second == 0
async def test_create_user_rejects_case_variant_of_legacy_email(client: AsyncClient):
"""The admin create-user duplicate check compares on the normalized form:
creating a case-variant of a legacy mixed-case row returns 409, not 201."""
await _insert_user("DemoUser@Example.com", "Tech")
token = await _login(client)
resp = await client.post(
"/api/auth/users",
json={
"email": "demouser@example.com",
"password": "password1",
"full_name": "X",
"role": "Tech",
},
headers=_auth(token),
)
assert resp.status_code == 409, resp.text
async def test_admin_only_rbac_gate(client: AsyncClient):
"""Canonical admins pass /api/auth/admin-only; everyone else 403."""
wahab = await _login(client)