单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。 Co-authored-by: Cursor <cursoragent@cursor.com>
188 lines
6.3 KiB
Go
188 lines
6.3 KiB
Go
// 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
|
||
}
|
||
}
|