diff --git a/TESTING.md b/TESTING.md index 669ff57..6626945 100644 --- a/TESTING.md +++ b/TESTING.md @@ -26,18 +26,26 @@ go test ./... ## 分阶段计划(长期) -### 阶段 1(已落地/优先级最高) +### 阶段 1(已落地) - provider 归一化:Vertex 默认 `google_location=global` 的回归保护 - SyncService:Redis snapshot 写入与 routing key 生成 - request_id:Gin middleware 透传/生成 -### 阶段 2(扩覆盖) +### 阶段 2(已落地一部分) -- Master/Key:token hash、epoch、scope、状态机分支 -- LogWriter:批处理/刷盘边界(可用 fake clock) +- Master/Key:CreateMaster/ValidateMasterKey/IssueChildKey 的关键分支(含 child key 上限) -### 阶段 3(契约测试) +待补齐: -- 与 DP 的 snapshot schema 契约:用 `testdata` golden 校验字段/格式稳定 +- Master/Key:epoch/禁用等更多状态分支 +- LogWriter:批处理/刷盘边界(可用 fake clock 或缩短 flush interval) +### 阶段 3(已落地一部分:契约测试) + +- 与 DP 的 provider snapshot schema 契约:`internal/service/testdata/provider_snapshot.json` + SyncProvider 输出回归 + +待扩展: + +- model snapshot 契约(能力字段、max_output_tokens 等) +- token/master snapshot 契约(如果后续引入更多字段) diff --git a/internal/service/master_test.go b/internal/service/master_test.go new file mode 100644 index 0000000..5dcdc06 --- /dev/null +++ b/internal/service/master_test.go @@ -0,0 +1,69 @@ +package service + +import ( + "testing" + + "github.com/ez-api/ez-api/internal/model" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func newTestDB(t *testing.T) *gorm.DB { + t.Helper() + db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + if err := db.AutoMigrate(&model.Master{}, &model.Key{}); err != nil { + t.Fatalf("migrate: %v", err) + } + return db +} + +func TestMasterService_CreateAndValidateMasterKey(t *testing.T) { + db := newTestDB(t) + svc := NewMasterService(db) + + m, raw, err := svc.CreateMaster("m1", "default", 2, 10) + if err != nil { + t.Fatalf("CreateMaster: %v", err) + } + if raw == "" { + t.Fatalf("expected raw master key") + } + if m.MasterKeyDigest == "" { + t.Fatalf("expected master key digest to be set") + } + + got, err := svc.ValidateMasterKey(raw) + if err != nil { + t.Fatalf("ValidateMasterKey: %v", err) + } + if got.ID != m.ID { + t.Fatalf("expected master id %d, got %d", m.ID, got.ID) + } +} + +func TestMasterService_IssueChildKey_RespectsLimit(t *testing.T) { + db := newTestDB(t) + svc := NewMasterService(db) + + m, _, err := svc.CreateMaster("m1", "default", 1, 10) + if err != nil { + t.Fatalf("CreateMaster: %v", err) + } + + _, raw1, err := svc.IssueChildKey(m.ID, "default", "chat:write") + if err != nil { + t.Fatalf("IssueChildKey #1: %v", err) + } + if raw1 == "" { + t.Fatalf("expected raw child key") + } + + _, _, err = svc.IssueChildKey(m.ID, "default", "chat:write") + if err == nil { + t.Fatalf("expected child key limit error") + } +} + diff --git a/internal/service/sync_test.go b/internal/service/sync_test.go index ed2b830..7ea5c00 100644 --- a/internal/service/sync_test.go +++ b/internal/service/sync_test.go @@ -2,6 +2,9 @@ package service import ( "encoding/json" + "os" + "path/filepath" + "reflect" "testing" "github.com/alicebob/miniredis/v2" @@ -10,6 +13,16 @@ import ( ) func TestSyncProvider_WritesSnapshotAndRouting(t *testing.T) { + goldenPath := filepath.Join("testdata", "provider_snapshot.json") + goldenRaw, err := os.ReadFile(goldenPath) + if err != nil { + t.Fatalf("read golden %s: %v", goldenPath, err) + } + var golden map[string]any + if err := json.Unmarshal(goldenRaw, &golden); err != nil { + t.Fatalf("parse golden json: %v", err) + } + mr := miniredis.RunT(t) rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()}) @@ -40,8 +53,10 @@ func TestSyncProvider_WritesSnapshotAndRouting(t *testing.T) { if err := json.Unmarshal([]byte(raw), &snap); err != nil { t.Fatalf("invalid snapshot json: %v", err) } - if snap["google_location"] != "global" { - t.Fatalf("expected google_location=global, got %v", snap["google_location"]) + for k, v := range golden { + if !reflect.DeepEqual(snap[k], v) { + t.Fatalf("snapshot mismatch for %q: got=%#v want=%#v", k, snap[k], v) + } } routeKey := "route:group:default:gemini-3-pro-preview" diff --git a/internal/service/testdata/provider_snapshot.json b/internal/service/testdata/provider_snapshot.json new file mode 100644 index 0000000..08b3d0b --- /dev/null +++ b/internal/service/testdata/provider_snapshot.json @@ -0,0 +1,13 @@ +{ + "id": 42, + "name": "p1", + "type": "vertex-express", + "base_url": "", + "api_key": "", + "google_location": "global", + "group": "default", + "models": ["gemini-3-pro-preview"], + "status": "active", + "auto_ban": true +} +