// admin_test.go:admin 面签发→使用→吊销→401 全链路 + 列表安全(不泄 hash)。 package gateway import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) // adminReq admin 请求构造。 func adminReq(e *testEnv, method, path, body string) *httptest.ResponseRecorder { var rd *strRd if body != "" { rd = strReader(body) } req := httptest.NewRequest(method, path, rd) req.Header.Set("X-Service-Token", e.srv.deps.AdminToken) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() e.srv.Handler().ServeHTTP(rec, req) return rec } // TestAdminIssueUseRevoke 签发(明文仅一次)→ 消费成功 → 吊销 → 401。 func TestAdminIssueUseRevoke(t *testing.T) { e := newTestEnv(t) e.sched.autoComplete = true e.sched.doneEnv = func(rid string) json.RawMessage { b, _ := json.Marshal(searchOKEnvelope(rid)) return b } // 签发 rec := adminReq(e, http.MethodPost, "/admin/keys", `{"consumer_name":"vlepontas","scopes":["search","read"],"rpm":30,"daily":50,"monthly":500,"concurrent":1}`) if rec.Code != http.StatusOK && rec.Code != http.StatusCreated { t.Fatalf("签发应 2xx: %d %s", rec.Code, rec.Body.String()) } var issued struct { OK bool `json:"ok"` KeyID int64 `json:"key_id"` ConsumerID int64 `json:"consumer_id"` Key string `json:"key"` Prefix string `json:"prefix"` Note string `json:"note"` } if err := json.Unmarshal(rec.Body.Bytes(), &issued); err != nil { t.Fatal(err) } if !issued.OK || issued.KeyID == 0 || !strings.HasPrefix(issued.Key, "bs_") { t.Fatalf("签发响应不符: %s", rec.Body.String()) } if issued.Prefix != issued.Key[:10] { t.Fatalf("prefix 不符: %s vs %s", issued.Prefix, issued.Key[:10]) } // ITER-3 DECL-1:月配额口径声明必须在场(monthly=500 已传但仍声明仅日窗生效)。 if !strings.Contains(issued.Note, "monthly_quota 首版未生效") { t.Fatalf("签发响应缺月配额口径 note: %s", rec.Body.String()) } // 新 key 消费成功 recU := e.postV1As(t, issued.Key, "/v1/search", `{"query":"admin测试","region":"domestic"}`) if recU.Code != http.StatusOK { t.Fatalf("新 key 消费应 200: %d %s", recU.Code, recU.Body.String()) } // 列表:给 prefix/status,不给 hash/salt recL := adminReq(e, http.MethodGet, "/admin/keys", "") if recL.Code != http.StatusOK { t.Fatalf("列表应 200: %d", recL.Code) } body := recL.Body.String() if strings.Contains(body, `"hash"`) || strings.Contains(body, `"salt"`) { t.Fatal("admin 列表泄漏 hash/salt") } if !strings.Contains(body, issued.Prefix) { t.Fatal("列表应含新 key prefix") } // 吊销即时 recR := adminReq(e, http.MethodDelete, "/admin/keys/"+itoa64(issued.KeyID), "") if recR.Code != http.StatusOK { t.Fatalf("吊销应 200: %d %s", recR.Code, recR.Body.String()) } recU2 := e.postV1As(t, issued.Key, "/v1/search", `{"query":"admin测试2","region":"domestic"}`) if recU2.Code != http.StatusUnauthorized { t.Fatalf("吊销后应 401: %d %s", recU2.Code, recU2.Body.String()) } } // TestAdminAuth admin token 校验:错 token 401;consumer key 不通 admin 面。 func TestAdminAuth(t *testing.T) { e := newTestEnv(t) // 无 token req := httptest.NewRequest(http.MethodPost, "/admin/keys", strReader(`{"consumer_name":"x"}`)) rec := httptest.NewRecorder() e.srv.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Fatalf("无 token 应 401: %d", rec.Code) } // 错 token req2 := httptest.NewRequest(http.MethodGet, "/admin/keys", nil) req2.Header.Set("X-Service-Token", "wrong-admin-token") rec2 := httptest.NewRecorder() e.srv.Handler().ServeHTTP(rec2, req2) if rec2.Code != http.StatusUnauthorized { t.Fatalf("错 token 应 401: %d", rec2.Code) } // consumer key 不通 admin 面 req3 := httptest.NewRequest(http.MethodGet, "/admin/keys", nil) req3.Header.Set("X-Service-Token", e.plainKey) rec3 := httptest.NewRecorder() e.srv.Handler().ServeHTTP(rec3, req3) if rec3.Code != http.StatusUnauthorized { t.Fatalf("consumer key 不应通 admin 面: %d", rec3.Code) } // 正 token 通过 rec4 := adminReq(e, http.MethodGet, "/admin/keys", "") if rec4.Code != http.StatusOK { t.Fatalf("正 token 应 200: %d", rec4.Code) } } // TestAdminIssueValidation 签发参数校验。 func TestAdminIssueValidation(t *testing.T) { e := newTestEnv(t) // 缺 consumer_name rec := adminReq(e, http.MethodPost, "/admin/keys", `{"scopes":["search"]}`) if rec.Code != http.StatusBadRequest { t.Fatalf("缺名应 400: %d %s", rec.Code, rec.Body.String()) } // 非法 expires_at rec2 := adminReq(e, http.MethodPost, "/admin/keys", `{"consumer_name":"c","expires_at":"yesterday"}`) if rec2.Code != http.StatusBadRequest { t.Fatalf("非法时间应 400: %d", rec2.Code) } // 默认值回填(rpm/daily/monthly/concurrent 缺省给设计值) rec3 := adminReq(e, http.MethodPost, "/admin/keys", `{"consumer_name":"def-c"}`) if rec3.Code != http.StatusOK && rec3.Code != http.StatusCreated { t.Fatalf("最小参数应 2xx: %d %s", rec3.Code, rec3.Body.String()) } var issued struct { KeyID int64 `json:"key_id"` } _ = json.Unmarshal(rec3.Body.Bytes(), &issued) k, err := e.db.KeyByID(issued.KeyID) if err != nil { t.Fatal(err) } if k.RPM != 60 || k.DailyQuota != 1000 || k.MonthlyQuota != 20000 || k.ConcurrentSessions != 2 { t.Fatalf("默认值不符: %+v", k) } } // TestReadyzHealthy scheduler 可达时 readyz 200。 func TestReadyzHealthy(t *testing.T) { e := newTestEnv(t) req := httptest.NewRequest(http.MethodGet, "/readyz", nil) rec := httptest.NewRecorder() e.srv.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("健康 readyz 应 200: %d %s", rec.Code, rec.Body.String()) } // healthz 恒 200 req2 := httptest.NewRequest(http.MethodGet, "/healthz", nil) rec2 := httptest.NewRecorder() e.srv.Handler().ServeHTTP(rec2, req2) if rec2.Code != http.StatusOK { t.Fatalf("healthz 应 200: %d", rec2.Code) } } // TestRequestIDUnique 请求 ID 单调唯一性(并发 500 无重复)。 func TestRequestIDUnique(t *testing.T) { seen := make(map[string]bool, 2000) for i := 0; i < 2000; i++ { id := newRequestID() if len(id) != 26 { t.Fatalf("ID 长度 %d ≠ 26", len(id)) } if seen[id] { t.Fatalf("ID 重复: %s", id) } seen[id] = true } }