fix(whatsapp,roles): self-heal legacy whatsapp_log schema; map underscore role aliases
CT115 (Mumuni relay #747) — two live-instance defects after PR #12:
1. GET /api/whatsapp/mock-log 500'd with a valid admin token:
'no such column: whatsapp_log.message_text'. The model gained
message_text/wa_message_id/ticket_number (and dropped command) in
4afdc36 with no migration, so legacy DBs keep the (command, ...) shape.
ensure_legacy_schema (startup, app/main.py) now adds the three missing
columns idempotently and backfills legacy command bodies into
message_text before dropping the obsolete NOT NULL command column, so
both the mock-log read path and the ORM write path work on healed DBs.
New producer/consumer regression (tests/test_whatsapp_log_legacy_heal.py)
reproduces the exact OperationalError, then asserts 200 + data.
2. ROLE_ALIASES gap: underscore legacy roles (cs_rep, cs_manager,
fm_dispatcher) were not mapped, so normalize_legacy_user_roles could not
converge rows like user 18 (test@denya.com, role 'cs_rep') and the
frontend stranded them on /tickets. Added the underscore aliases; tests
assert normalize_role('cs_rep') == 'CS Rep' and a cs_rep row converges
and authenticates.
This commit is contained in:
@@ -308,6 +308,43 @@ async def test_normalize_does_not_map_ambiguous_admin_alias(client: AsyncClient)
|
||||
assert user.role == "admin"
|
||||
|
||||
|
||||
async def test_underscore_legacy_aliases_normalize_to_canonical():
|
||||
"""Open-register rows can carry underscore role forms ('cs_rep',
|
||||
'cs_manager', 'fm_dispatcher') — the exact gap Mumuni relay #747 found on
|
||||
user 18 (test@denya.com). Each must map onto its canonical role so startup
|
||||
normalization can converge the row instead of stranding it on /tickets."""
|
||||
from app.core.roles import normalize_role
|
||||
|
||||
assert normalize_role("cs_rep") == "CS Rep"
|
||||
assert normalize_role("cs_manager") == "CS Manager"
|
||||
assert normalize_role("fm_dispatcher") == "FM Dispatcher"
|
||||
# Matching is case/whitespace tolerant, like the space-form aliases.
|
||||
assert normalize_role(" CS_REP ") == "CS Rep"
|
||||
assert normalize_role("cs_rep") is not None # known, not fail-closed
|
||||
|
||||
|
||||
async def test_legacy_cs_rep_row_converges_and_authenticates(client: AsyncClient):
|
||||
"""A 'cs_rep' row (user 18 test@denya.com shape) is converged to canonical
|
||||
'CS Rep' by the startup self-heal and can log in again (no fail-closed
|
||||
denial, and the frontend CS nav sees the canonical role)."""
|
||||
await _insert_user("cs-rep-legacy@example.com", "cs_rep")
|
||||
await _normalize_roles() # 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 == "cs-rep-legacy@example.com"))
|
||||
).scalar_one()
|
||||
assert user.role == "CS Rep"
|
||||
|
||||
login = await client.post(
|
||||
"/api/auth/login", json={"email": "cs-rep-legacy@example.com", "password": "denya123"}
|
||||
)
|
||||
assert login.status_code == 200, login.text
|
||||
me = await client.get("/api/auth/me", headers=_auth(login.json()["access_token"]))
|
||||
assert me.json()["role"] == "CS Rep"
|
||||
|
||||
|
||||
# ── 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
|
||||
|
||||
Reference in New Issue
Block a user