test: add invite code validation tests and fix migration rollback

- Add TestInviteCodeSignup integration tests for valid/invalid invite codes
- Fix migration downgrade: avoid dropping trigger dependency
- Add DB CHECK constraint for invite_codes.code format
- Update runtime-route.md with invite_code documentation
- Update runtime-runbook.md with change log
This commit is contained in:
qzl
2026-02-28 10:56:09 +08:00
parent 3d6ae7695f
commit dbd3f68dd4
4 changed files with 103 additions and 3 deletions
@@ -20,7 +20,7 @@ def upgrade() -> None:
""" """
CREATE TABLE invite_codes ( CREATE TABLE invite_codes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(8) NOT NULL UNIQUE, code VARCHAR(8) NOT NULL UNIQUE CHECK (code ~ '^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{8}$'),
owner_id UUID REFERENCES profiles(id) ON DELETE SET NULL, owner_id UUID REFERENCES profiles(id) ON DELETE SET NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled', 'expired')), status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled', 'expired')),
used_count INTEGER NOT NULL DEFAULT 0 CHECK (used_count >= 0), used_count INTEGER NOT NULL DEFAULT 0 CHECK (used_count >= 0),
@@ -153,7 +153,6 @@ def upgrade() -> None:
def downgrade() -> None: def downgrade() -> None:
op.execute("DROP FUNCTION IF EXISTS public.create_profile_for_new_user()")
op.execute( op.execute(
""" """
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user() CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
@@ -792,3 +792,96 @@ def test_password_reset_confirm_weak_password_returns_422() -> None:
assert response.headers["content-type"].startswith("application/problem+json") assert response.headers["content-type"].startswith("application/problem+json")
finally: finally:
app.dependency_overrides = {} app.dependency_overrides = {}
class TestInviteCodeSignup:
def test_signup_with_valid_invite_code_returns_202(self) -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
user=user,
)
app.dependency_overrides[get_auth_service] = _override_auth_service(
FakeAuthService(token_response)
)
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/verifications",
json={
"username": "demo",
"email": "user@example.com",
"password": "secret123",
"invite_code": "A2B3C4D5",
},
)
assert response.status_code == 202
assert response.json() == {"email": "user@example.com"}
finally:
app.dependency_overrides = {}
def test_signup_with_invalid_invite_code_length_returns_422(self) -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
user=user,
)
app.dependency_overrides[get_auth_service] = _override_auth_service(
FakeAuthService(token_response)
)
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/verifications",
json={
"username": "demo",
"email": "user@example.com",
"password": "secret123",
"invite_code": "ABC123",
},
)
assert response.status_code == 422
assert response.headers["content-type"].startswith(
"application/problem+json"
)
finally:
app.dependency_overrides = {}
def test_signup_with_invalid_invite_code_chars_returns_422(self) -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
user=user,
)
app.dependency_overrides[get_auth_service] = _override_auth_service(
FakeAuthService(token_response)
)
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/verifications",
json={
"username": "demo",
"email": "user@example.com",
"password": "secret123",
"invite_code": "ABCD1234",
},
)
assert response.status_code == 422
assert response.headers["content-type"].startswith(
"application/problem+json"
)
finally:
app.dependency_overrides = {}
+8 -1
View File
@@ -20,7 +20,8 @@
"username": "string (3-30 chars)", "username": "string (3-30 chars)",
"email": "string (email)", "email": "string (email)",
"password": "string (min 6 chars)", "password": "string (min 6 chars)",
"redirect_to": "string? (optional)" "redirect_to": "string? (optional)",
"invite_code": "string? (8 chars, 排除易混淆字符 0/1/I/L/O)"
} }
``` ```
@@ -31,6 +32,12 @@
} }
``` ```
**邀请码说明:**
- 可选字段,不填则注册不受影响
- 格式:8 位字母数字组合,排除易混淆字符 (0, 1, I, L, O)
- 注册时传入有效邀请码会建立邀请关系并增加邀请码使用次数
- 无效邀请码(不存在/已禁用/已过期/已达上限)不会阻断注册成功
**Errors:** **Errors:**
- 422: 请求参数无效 - 422: 请求参数无效
- 429: 请求过于频繁 - 429: 请求过于频繁
+1
View File
@@ -245,3 +245,4 @@ docker compose --env-file .env -f infra/docker/docker-compose.yml up -d --force-
| 2026-02-25 | 重构为运维分层手册:Bootstrap Gate、分层验证、故障与回滚流程 | | 2026-02-25 | 重构为运维分层手册:Bootstrap Gate、分层验证、故障与回滚流程 |
| 2026-02-25 | 新增配置漂移故障条目:修复 Auth 邮件模板失效与 signup 超时场景 | | 2026-02-25 | 新增配置漂移故障条目:修复 Auth 邮件模板失效与 signup 超时场景 |
| 2026-02-27 | 用户搜索支持邮箱精确匹配:query 含 @ 符号时走 auth.users → profiles 两步查询 | | 2026-02-27 | 用户搜索支持邮箱精确匹配:query 含 @ 符号时走 auth.users → profiles 两步查询 |
| 2026-02-28 | 邀请码功能:新增 invite_codes 表、profiles.referred_by,注册时可选填邀请码并记录邀请关系 |