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:
@@ -20,7 +20,7 @@ def upgrade() -> None:
|
||||
"""
|
||||
CREATE TABLE invite_codes (
|
||||
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,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled', 'expired')),
|
||||
used_count INTEGER NOT NULL DEFAULT 0 CHECK (used_count >= 0),
|
||||
@@ -153,7 +153,6 @@ def upgrade() -> None:
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP FUNCTION IF EXISTS public.create_profile_for_new_user()")
|
||||
op.execute(
|
||||
"""
|
||||
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")
|
||||
finally:
|
||||
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 = {}
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"username": "string (3-30 chars)",
|
||||
"email": "string (email)",
|
||||
"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:**
|
||||
- 422: 请求参数无效
|
||||
- 429: 请求过于频繁
|
||||
|
||||
@@ -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 | 新增配置漂移故障条目:修复 Auth 邮件模板失效与 signup 超时场景 |
|
||||
| 2026-02-27 | 用户搜索支持邮箱精确匹配:query 含 @ 符号时走 auth.users → profiles 两步查询 |
|
||||
| 2026-02-28 | 邀请码功能:新增 invite_codes 表、profiles.referred_by,注册时可选填邀请码并记录邀请关系 |
|
||||
|
||||
Reference in New Issue
Block a user