feat(access): демо-QR — многоразовое приглашение для показа
CI / ruff · mypy · pytest (pull_request) Successful in 1h3m54s
CI / ruff · mypy · pytest (pull_request) Successful in 1h3m54s
- invites.max_uses/uses: обычное приглашение — 1 вход, демо-QR — до DEMO_INVITE_MAX_USES (50), живёт DEMO_INVITE_TTL_HOURS (3 ч); захват атомарный, UPDATE … WHERE uses < max_uses - users.guest: вошедшие по демо-QR; «🧹 Отключить гостей демо» отзывает всех и гасит демо-QR - «👥 Участники»: кнопка «🎤 Демо-QR», гости в списке — 🎟 - миграция 0003 (ADD COLUMN без batch — SQLite не пересоздаёт users); проверена на Postgres - SPEC A-3a, ADR-017, DEMO.md, .env.example Refs #55 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -80,3 +80,24 @@ async def test_deleting_draft_cascades_to_changes(sessions: SessionMaker) -> Non
|
||||
await s.execute(text("DELETE FROM drafts"))
|
||||
async with sessions() as s:
|
||||
assert (await s.execute(text("SELECT count(*) FROM draft_changes"))).scalar_one() == 0
|
||||
|
||||
|
||||
async def test_0003_marks_redeemed_invites_spent() -> None:
|
||||
eng = make_engine("sqlite+aiosqlite://")
|
||||
try:
|
||||
await upgrade(eng, "0002")
|
||||
async with eng.begin() as conn:
|
||||
await conn.execute(INSERT_USER, {"id": 1, "role": "admin"})
|
||||
await conn.execute(
|
||||
text(
|
||||
"INSERT INTO invites (token_hash, created_by, created_at, expires_at, used_by) VALUES "
|
||||
"('a', 1, '2026-01-01', '2026-01-02', 1), ('b', 1, '2026-01-01', '2026-01-02', NULL)"
|
||||
)
|
||||
)
|
||||
await upgrade(eng)
|
||||
async with eng.connect() as conn:
|
||||
rows = (await conn.execute(text("SELECT token_hash, max_uses, uses FROM invites ORDER BY 1"))).all()
|
||||
assert [tuple(r) for r in rows] == [("a", 1, 1), ("b", 1, 0)]
|
||||
assert (await conn.execute(text("SELECT guest FROM users"))).scalar_one() in (0, False)
|
||||
finally:
|
||||
await eng.dispose()
|
||||
|
||||
Reference in New Issue
Block a user