fix(supply-api): 修复编译错误和测试问题
- 添加 ErrNotFound 和 ErrConcurrencyConflict 错误定义 - 修复 pgx.NullTime 替换为 *time.Time - 修复 db.go 事务类型 (pgx.Tx vs pgxpool.Tx) - 移除未使用的导入和变量 - 修复 NewSupplyAPI 调用参数 - 修复中间件链路 handler 类型问题 - 修复适配器类型引用 (storage.InMemoryAccountStore 等) - 所有测试通过 Test: go test ./...
This commit is contained in:
@@ -13,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"lijiaoqiao/supply-api/internal/repository"
|
||||
)
|
||||
|
||||
// TokenClaims JWT token claims
|
||||
@@ -27,17 +26,17 @@ type TokenClaims struct {
|
||||
|
||||
// AuthConfig 鉴权中间件配置
|
||||
type AuthConfig struct {
|
||||
SecretKey string
|
||||
Issuer string
|
||||
CacheTTL time.Duration // token状态缓存TTL
|
||||
Enabled bool // 是否启用鉴权
|
||||
SecretKey string
|
||||
Issuer string
|
||||
CacheTTL time.Duration // token状态缓存TTL
|
||||
Enabled bool // 是否启用鉴权
|
||||
}
|
||||
|
||||
// AuthMiddleware 鉴权中间件
|
||||
type AuthMiddleware struct {
|
||||
config AuthConfig
|
||||
tokenCache *TokenCache
|
||||
auditEmitter AuditEmitter
|
||||
config AuthConfig
|
||||
tokenCache *TokenCache
|
||||
auditEmitter AuditEmitter
|
||||
}
|
||||
|
||||
// AuditEmitter 审计事件发射器
|
||||
@@ -63,8 +62,8 @@ func NewAuthMiddleware(config AuthConfig, tokenCache *TokenCache, auditEmitter A
|
||||
config.CacheTTL = 30 * time.Second
|
||||
}
|
||||
return &AuthMiddleware{
|
||||
config: config,
|
||||
tokenCache: tokenCache,
|
||||
config: config,
|
||||
tokenCache: tokenCache,
|
||||
auditEmitter: auditEmitter,
|
||||
}
|
||||
}
|
||||
@@ -274,11 +273,11 @@ func (m *AuthMiddleware) ScopeRoleAuthzMiddleware(requiredScope string) func(htt
|
||||
|
||||
// 路由权限要求
|
||||
routeRoles := map[string]string{
|
||||
"/api/v1/supply/accounts": "owner",
|
||||
"/api/v1/supply/packages": "owner",
|
||||
"/api/v1/supply/settlements": "owner",
|
||||
"/api/v1/supply/billing": "viewer",
|
||||
"/api/v1/supplier/billing": "viewer",
|
||||
"/api/v1/supply/accounts": "owner",
|
||||
"/api/v1/supply/packages": "owner",
|
||||
"/api/v1/supply/settlements": "owner",
|
||||
"/api/v1/supply/billing": "viewer",
|
||||
"/api/v1/supplier/billing": "viewer",
|
||||
}
|
||||
|
||||
for path, requiredRole := range routeRoles {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -16,9 +15,9 @@ func TestTokenVerify(t *testing.T) {
|
||||
issuer := "test-issuer"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
expectError bool
|
||||
name string
|
||||
token string
|
||||
expectError bool
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
@@ -27,21 +26,21 @@ func TestTokenVerify(t *testing.T) {
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "expired token",
|
||||
token: createTestToken(secretKey, issuer, "subject:1", "owner", time.Now().Add(-time.Hour)),
|
||||
expectError: true,
|
||||
name: "expired token",
|
||||
token: createTestToken(secretKey, issuer, "subject:1", "owner", time.Now().Add(-time.Hour)),
|
||||
expectError: true,
|
||||
errorContains: "expired",
|
||||
},
|
||||
{
|
||||
name: "wrong issuer",
|
||||
token: createTestToken(secretKey, "wrong-issuer", "subject:1", "owner", time.Now().Add(time.Hour)),
|
||||
expectError: true,
|
||||
name: "wrong issuer",
|
||||
token: createTestToken(secretKey, "wrong-issuer", "subject:1", "owner", time.Now().Add(time.Hour)),
|
||||
expectError: true,
|
||||
errorContains: "issuer",
|
||||
},
|
||||
{
|
||||
name: "invalid token",
|
||||
token: "invalid.token.string",
|
||||
expectError: true,
|
||||
name: "invalid token",
|
||||
token: "invalid.token.string",
|
||||
expectError: true,
|
||||
errorContains: "",
|
||||
},
|
||||
}
|
||||
@@ -74,38 +73,38 @@ func TestTokenVerify(t *testing.T) {
|
||||
|
||||
func TestQueryKeyRejectMiddleware(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
name string
|
||||
query string
|
||||
expectStatus int
|
||||
}{
|
||||
{
|
||||
name: "no query params",
|
||||
query: "",
|
||||
name: "no query params",
|
||||
query: "",
|
||||
expectStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "normal params",
|
||||
query: "?page=1&size=10",
|
||||
name: "normal params",
|
||||
query: "?page=1&size=10",
|
||||
expectStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "blocked key param",
|
||||
query: "?key=abc123",
|
||||
name: "blocked key param",
|
||||
query: "?key=abc123",
|
||||
expectStatus: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "blocked api_key param",
|
||||
query: "?api_key=secret123",
|
||||
name: "blocked api_key param",
|
||||
query: "?api_key=secret123",
|
||||
expectStatus: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "blocked token param",
|
||||
query: "?token=bearer123",
|
||||
name: "blocked token param",
|
||||
query: "?token=bearer123",
|
||||
expectStatus: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "suspicious long param",
|
||||
query: "?apikey=verylongparamvalueexceeding20chars",
|
||||
name: "suspicious long param",
|
||||
query: "?apikey=verylongparamvalueexceeding20chars",
|
||||
expectStatus: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
@@ -143,28 +142,28 @@ func TestQueryKeyRejectMiddleware(t *testing.T) {
|
||||
|
||||
func TestBearerExtractMiddleware(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
authHeader string
|
||||
name string
|
||||
authHeader string
|
||||
expectStatus int
|
||||
}{
|
||||
{
|
||||
name: "valid bearer",
|
||||
authHeader: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
|
||||
name: "valid bearer",
|
||||
authHeader: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
|
||||
expectStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "missing header",
|
||||
authHeader: "",
|
||||
name: "missing header",
|
||||
authHeader: "",
|
||||
expectStatus: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "wrong prefix",
|
||||
authHeader: "Basic abc123",
|
||||
name: "wrong prefix",
|
||||
authHeader: "Basic abc123",
|
||||
expectStatus: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "empty token",
|
||||
authHeader: "Bearer ",
|
||||
name: "empty token",
|
||||
authHeader: "Bearer ",
|
||||
expectStatus: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
@@ -332,9 +331,9 @@ func createTestToken(secretKey, issuer, subject, role string, expiresAt time.Tim
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
SubjectID: subject,
|
||||
Role: role,
|
||||
Scope: []string{"read", "write"},
|
||||
TenantID: 1,
|
||||
Role: role,
|
||||
Scope: []string{"read", "write"},
|
||||
TenantID: 1,
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
@@ -17,15 +17,15 @@ import (
|
||||
|
||||
// IdempotencyConfig 幂等中间件配置
|
||||
type IdempotencyConfig struct {
|
||||
TTL time.Duration // 幂等有效期,默认24h
|
||||
ProcessingTTL time.Duration // 处理中状态有效期,默认30s
|
||||
Enabled bool // 是否启用幂等
|
||||
TTL time.Duration // 幂等有效期,默认24h
|
||||
ProcessingTTL time.Duration // 处理中状态有效期,默认30s
|
||||
Enabled bool // 是否启用幂等
|
||||
}
|
||||
|
||||
// IdempotencyMiddleware 幂等中间件
|
||||
type IdempotencyMiddleware struct {
|
||||
idempotencyRepo *repository.IdempotencyRepository
|
||||
config IdempotencyConfig
|
||||
config IdempotencyConfig
|
||||
}
|
||||
|
||||
// NewIdempotencyMiddleware 创建幂等中间件
|
||||
@@ -46,8 +46,8 @@ func NewIdempotencyMiddleware(repo *repository.IdempotencyRepository, config Ide
|
||||
type IdempotencyKey struct {
|
||||
TenantID int64
|
||||
OperatorID int64
|
||||
APIPath string
|
||||
Key string
|
||||
APIPath string
|
||||
Key string
|
||||
}
|
||||
|
||||
// ExtractIdempotencyKey 从请求中提取幂等信息
|
||||
@@ -75,8 +75,8 @@ func ExtractIdempotencyKey(r *http.Request, tenantID, operatorID int64) (*Idempo
|
||||
return &IdempotencyKey{
|
||||
TenantID: tenantID,
|
||||
OperatorID: operatorID,
|
||||
APIPath: apiPath,
|
||||
Key: idempotencyKey,
|
||||
APIPath: apiPath,
|
||||
Key: idempotencyKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -157,20 +157,8 @@ func (m *IdempotencyMiddleware) Wrap(handler IdempotentHandler) http.HandlerFunc
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试创建或更新幂等记录
|
||||
requestID := r.Header.Get("X-Request-Id")
|
||||
record := &repository.IdempotencyRecord{
|
||||
TenantID: idempKey.TenantID,
|
||||
OperatorID: idempKey.OperatorID,
|
||||
APIPath: idempKey.APIPath,
|
||||
IdempotencyKey: idempKey.Key,
|
||||
RequestID: requestID,
|
||||
PayloadHash: payloadHash,
|
||||
Status: repository.IdempotencyStatusProcessing,
|
||||
ExpiresAt: time.Now().Add(m.config.TTL),
|
||||
}
|
||||
|
||||
// 使用AcquireLock获取锁
|
||||
requestID := r.Header.Get("X-Request-Id")
|
||||
lockedRecord, err := m.idempotencyRepo.AcquireLock(ctx, idempKey.TenantID, idempKey.OperatorID, idempKey.APIPath, idempKey.Key, m.config.TTL)
|
||||
if err != nil {
|
||||
writeIdempotencyError(w, http.StatusInternalServerError, "IDEMPOTENCY_LOCK_FAILED", err.Error())
|
||||
|
||||
@@ -54,7 +54,7 @@ func (r *MockIdempotencyRepository) AcquireLock(ctx context.Context, tenantID, o
|
||||
record := &repository.IdempotencyRecord{
|
||||
TenantID: tenantID,
|
||||
OperatorID: operatorID,
|
||||
APIPath: apiPath,
|
||||
APIPath: apiPath,
|
||||
IdempotencyKey: idempotencyKey,
|
||||
RequestID: "test-request-id",
|
||||
PayloadHash: "",
|
||||
|
||||
Reference in New Issue
Block a user