onesvm-browser-server/server/internal/gateway/admin_test.go
chii eb972dfa93 feat: 落地 browser-server 控制面并打通 mgr1 海外订阅
单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 15:05:12 +08:00

188 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
}
}