package httpapi_test import ( "net/http" "strings" "testing" ) // C12(后半):CHECK 类非法值必须走 HTTP 就落成 400,而不是一路落到 SQLite 的 // CHECK 上变成 500 + 原始约束原文。事件枚举、组别性别、通道 scope/basis、纪录 level、 // 用户 role、成绩 status 都已有用例;人工调整的 category(point_adjustments.category // 的 CHECK)没有——它的写路由 POST /api/adjustments 到本轮为止一条用例都没有。 func TestAdjustmentCategoryEnumValidation(t *testing.T) { env := newTestEnv(t) ids := env.seed(t) client := recorderClient(t, env) // 人工调整默认关闭,先打开:否则还没走到 category 就被「未启用」拦下,用例假绿。 env.exec(t, `UPDATE settings SET value = 'true' WHERE key = 'manual_adjustment.enabled'`) for _, bad := range []string{"bogus", "PENALTY", "罚款", " "} { status, resp := env.do(client, http.MethodPost, "/api/adjustments", map[string]any{ "teamId": ids.teamA, "delta": 3, "category": bad, "reason": "测试", }, true) if status != http.StatusBadRequest || resp.Error == nil || resp.Error.Code != "VALIDATION_FAILED" { t.Fatalf("category=%q 应 400 VALIDATION_FAILED,得到 %d %+v", bad, status, resp.Error) } if !strings.Contains(resp.Error.Message, "category") { t.Fatalf("错误文案应带字段名 category,得到 %q", resp.Error.Message) } // 撞 CHECK 的 500 会带出英文约束原文;这里必须一点都看不到。 for _, leak := range []string{"CHECK", "constraint", "SQLITE", "sqlite"} { if strings.Contains(resp.Error.Message, leak) || strings.Contains(string(resp.Error.Detail), leak) { t.Fatalf("错误里泄露了数据库约束原文 %q:%+v", leak, resp.Error) } } } // 被拒的请求一条都不许落库。 if n := countRows(t, env, `SELECT COUNT(*) FROM point_adjustments`); n != 0 { t.Fatalf("非法 category 的请求不该落库,得到 %d 行", n) } // 对照:三种合法类别都能落库(CHECK 允许 penalty / bonus / correction)。 for _, ok := range []string{"penalty", "bonus", "correction"} { status, after := env.do(client, http.MethodPost, "/api/adjustments", map[string]any{ "teamId": ids.teamA, "delta": -1, "category": ok, "reason": "测试", "requestId": "adj-" + ok, }, true) if status != http.StatusOK || !after.OK { t.Fatalf("category=%q 应 200,得到 %d %+v", ok, status, after.Error) } } if n := countRows(t, env, `SELECT COUNT(*) FROM point_adjustments`); n != 3 { t.Fatalf("三个合法类别应各落一条,得到 %d 行", n) } }