diff --git a/TESTING.md b/TESTING.md index 6626945..650d560 100644 --- a/TESTING.md +++ b/TESTING.md @@ -44,6 +44,7 @@ go test ./... ### 阶段 3(已落地一部分:契约测试) - 与 DP 的 provider snapshot schema 契约:`internal/service/testdata/provider_snapshot.json` + SyncProvider 输出回归 + - golden 来自 `github.com/ez-api/foundation/contract`(go:embed),避免 DP/CP 两边复制文件 待扩展: diff --git a/internal/api/handler.go b/internal/api/handler.go index ddae8ca..a146e6c 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -8,6 +8,7 @@ import ( "github.com/ez-api/ez-api/internal/dto" "github.com/ez-api/ez-api/internal/model" "github.com/ez-api/ez-api/internal/service" + groupx "github.com/ez-api/foundation/group" "github.com/ez-api/foundation/provider" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -157,7 +158,7 @@ func (h *Handler) UpdateProvider(c *gin.Context) { update["models"] = strings.Join(req.Models, ",") } if strings.TrimSpace(req.Group) != "" { - update["group"] = normalizeGroup(req.Group) + update["group"] = groupx.Normalize(req.Group) } if req.AutoBan != nil { update["auto_ban"] = *req.AutoBan @@ -354,10 +355,3 @@ func (h *Handler) IngestLog(c *gin.Context) { h.logger.Write(rec) c.JSON(http.StatusAccepted, gin.H{"status": "queued"}) } - -func normalizeGroup(group string) string { - if strings.TrimSpace(group) == "" { - return "default" - } - return group -} diff --git a/internal/middleware/request_id.go b/internal/middleware/request_id.go index f305711..573bb96 100644 --- a/internal/middleware/request_id.go +++ b/internal/middleware/request_id.go @@ -1,35 +1,20 @@ package middleware import ( - "crypto/rand" - "encoding/hex" - "strings" - "time" - + "github.com/ez-api/foundation/requestid" "github.com/gin-gonic/gin" ) // RequestID ensures every request has an X-Request-ID and echoes it back to the client. func RequestID() gin.HandlerFunc { return func(c *gin.Context) { - id := strings.TrimSpace(c.GetHeader("X-Request-ID")) + id := requestid.Extract(c.GetHeader) if id == "" { - id = strings.TrimSpace(c.GetHeader("X-Request-Id")) + id = requestid.New() } - if id == "" { - id = newRequestID() - } - c.Request.Header.Set("X-Request-ID", id) - c.Writer.Header().Set("X-Request-ID", id) + c.Request.Header.Set(requestid.HeaderName, id) + c.Writer.Header().Set(requestid.HeaderName, id) c.Set("request_id", id) c.Next() } } - -func newRequestID() string { - var b [16]byte - if _, err := rand.Read(b[:]); err != nil { - return hex.EncodeToString([]byte(time.Now().Format(time.RFC3339Nano))) - } - return hex.EncodeToString(b[:]) -} diff --git a/internal/service/master.go b/internal/service/master.go index 3a6fb02..269f721 100644 --- a/internal/service/master.go +++ b/internal/service/master.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/ez-api/ez-api/internal/model" - "github.com/ez-api/ez-api/internal/util" + "github.com/ez-api/foundation/tokenhash" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) @@ -32,7 +32,7 @@ func (s *MasterService) CreateMaster(name, group string, maxChildKeys, globalQPS return nil, "", fmt.Errorf("failed to hash master key: %w", err) } - masterKeyDigest := util.HashToken(rawMasterKey) + masterKeyDigest := tokenhash.HashToken(rawMasterKey) master := &model.Master{ Name: name, @@ -53,7 +53,7 @@ func (s *MasterService) CreateMaster(name, group string, maxChildKeys, globalQPS } func (s *MasterService) ValidateMasterKey(masterKey string) (*model.Master, error) { - digest := util.HashToken(masterKey) + digest := tokenhash.HashToken(masterKey) var master model.Master if err := s.db.Where("master_key_digest = ?", digest).First(&master).Error; err != nil { @@ -108,7 +108,7 @@ func (s *MasterService) IssueChildKey(masterID uint, group string, scopes string return nil, "", fmt.Errorf("failed to generate child key: %w", err) } - tokenHash := util.HashToken(rawChildKey) + tokenHash := tokenhash.HashToken(rawChildKey) hashedChildKey, err := bcrypt.GenerateFromPassword([]byte(rawChildKey), bcrypt.DefaultCost) if err != nil { diff --git a/internal/service/sync.go b/internal/service/sync.go index c2c9b7f..4bc4c3f 100644 --- a/internal/service/sync.go +++ b/internal/service/sync.go @@ -7,8 +7,9 @@ import ( "time" "github.com/ez-api/ez-api/internal/model" - "github.com/ez-api/ez-api/internal/util" + groupx "github.com/ez-api/foundation/group" "github.com/ez-api/foundation/jsoncodec" + "github.com/ez-api/foundation/tokenhash" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -26,7 +27,7 @@ func (s *SyncService) SyncKey(key *model.Key) error { ctx := context.Background() tokenHash := key.TokenHash if strings.TrimSpace(tokenHash) == "" { - tokenHash = util.HashToken(key.KeySecret) // backward compatibility + tokenHash = tokenhash.HashToken(key.KeySecret) // backward compatibility } if strings.TrimSpace(tokenHash) == "" { return fmt.Errorf("token hash missing for key %d", key.ID) @@ -62,7 +63,7 @@ func (s *SyncService) SyncMaster(master *model.Master) error { // SyncProvider writes a single provider into Redis hash storage and updates routing tables. func (s *SyncService) SyncProvider(provider *model.Provider) error { ctx := context.Background() - group := normalizeGroup(provider.Group) + group := groupx.Normalize(provider.Group) models := strings.Split(provider.Models, ",") snap := providerSnapshot{ @@ -198,7 +199,7 @@ func (s *SyncService) SyncAll(db *gorm.DB) error { // Ideally, we should scan "route:group:*" and del, but let's just rebuild. for _, p := range providers { - group := normalizeGroup(p.Group) + group := groupx.Normalize(p.Group) models := strings.Split(p.Models, ",") snap := providerSnapshot{ @@ -244,7 +245,7 @@ func (s *SyncService) SyncAll(db *gorm.DB) error { for _, k := range keys { tokenHash := strings.TrimSpace(k.TokenHash) if tokenHash == "" { - tokenHash = util.HashToken(k.KeySecret) // fallback for legacy rows + tokenHash = tokenhash.HashToken(k.KeySecret) // fallback for legacy rows } if tokenHash == "" { return fmt.Errorf("token hash missing for key %d", k.ID) @@ -302,13 +303,6 @@ func (s *SyncService) hsetJSON(ctx context.Context, key, field string, val inter return nil } -func normalizeGroup(group string) string { - if strings.TrimSpace(group) == "" { - return "default" - } - return group -} - func normalizeStatus(status string) string { st := strings.ToLower(strings.TrimSpace(status)) if st == "" { diff --git a/internal/service/sync_test.go b/internal/service/sync_test.go index 7ea5c00..06b5e3b 100644 --- a/internal/service/sync_test.go +++ b/internal/service/sync_test.go @@ -2,22 +2,17 @@ package service import ( "encoding/json" - "os" - "path/filepath" "reflect" "testing" "github.com/alicebob/miniredis/v2" "github.com/ez-api/ez-api/internal/model" + "github.com/ez-api/foundation/contract" "github.com/redis/go-redis/v9" ) 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) - } + goldenRaw := contract.ProviderSnapshotJSON() var golden map[string]any if err := json.Unmarshal(goldenRaw, &golden); err != nil { t.Fatalf("parse golden json: %v", err) diff --git a/internal/service/testdata/provider_snapshot.json b/internal/service/testdata/provider_snapshot.json deleted file mode 100644 index 08b3d0b..0000000 --- a/internal/service/testdata/provider_snapshot.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "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 -} - diff --git a/internal/service/token.go b/internal/service/token.go index 1dcf1d4..5389a20 100644 --- a/internal/service/token.go +++ b/internal/service/token.go @@ -6,7 +6,7 @@ import ( "fmt" "strconv" - "github.com/ez-api/ez-api/internal/util" + "github.com/ez-api/foundation/tokenhash" "github.com/redis/go-redis/v9" ) @@ -28,7 +28,7 @@ type TokenInfo struct { // ValidateToken checks a child key against Redis for validity. // This is designed to be called by the data plane (balancer). func (s *TokenService) ValidateToken(ctx context.Context, token string) (*TokenInfo, error) { - tokenHash := util.HashToken(token) + tokenHash := tokenhash.HashToken(token) tokenKey := fmt.Sprintf("auth:token:%s", tokenHash) // 1. Get token metadata from Redis diff --git a/internal/util/hash.go b/internal/util/hash.go deleted file mode 100644 index 2189d91..0000000 --- a/internal/util/hash.go +++ /dev/null @@ -1,12 +0,0 @@ -package util - -import ( - "crypto/sha256" - "encoding/hex" -) - -func HashToken(token string) string { - hasher := sha256.New() - hasher.Write([]byte(token)) - return hex.EncodeToString(hasher.Sum(nil)) -}