mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
136 lines
5.7 KiB
Go
136 lines
5.7 KiB
Go
package handler
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||
|
|
"github.com/gin-gonic/gin"
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
)
|
||
|
|
|
||
|
|
// recordGoogleFormatOpsError 让 handle 以 Google 错误格式回写错误,返回 ops 中间件记录的错误日志。
|
||
|
|
func recordGoogleFormatOpsError(t *testing.T, handle gin.HandlerFunc) *service.OpsInsertErrorLogInput {
|
||
|
|
t.Helper()
|
||
|
|
setupOpsErrorLogTestQueue(t, 1)
|
||
|
|
gin.SetMode(gin.TestMode)
|
||
|
|
ops := service.NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||
|
|
router := gin.New()
|
||
|
|
router.Use(OpsErrorLoggerMiddleware(ops))
|
||
|
|
router.POST("/v1beta/cachedContents", handle)
|
||
|
|
|
||
|
|
router.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1beta/cachedContents", nil))
|
||
|
|
|
||
|
|
require.Equal(t, int64(1), OpsErrorLogQueueLength())
|
||
|
|
return (<-opsErrorLogQueue).entry
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOpsErrorLoggerMiddleware_GoogleClientErrorsClassifiedAsClient(t *testing.T) {
|
||
|
|
for _, tc := range []struct {
|
||
|
|
status int
|
||
|
|
errType string
|
||
|
|
phase string
|
||
|
|
}{
|
||
|
|
{status: http.StatusBadRequest, errType: "invalid_request_error", phase: "request"},
|
||
|
|
{status: http.StatusRequestEntityTooLarge, errType: "invalid_request_error", phase: "request"},
|
||
|
|
{status: http.StatusUnauthorized, errType: "authentication_error", phase: "auth"},
|
||
|
|
{status: http.StatusForbidden, errType: "permission_error", phase: "request"},
|
||
|
|
{status: http.StatusNotFound, errType: "not_found_error", phase: "request"},
|
||
|
|
} {
|
||
|
|
t.Run(http.StatusText(tc.status), func(t *testing.T) {
|
||
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
||
|
|
googleError(c, tc.status, "client request problem")
|
||
|
|
})
|
||
|
|
require.Equal(t, tc.status, entry.StatusCode)
|
||
|
|
require.Equal(t, tc.errType, entry.ErrorType)
|
||
|
|
require.Equal(t, tc.phase, entry.ErrorPhase)
|
||
|
|
require.Equal(t, "client", entry.ErrorOwner)
|
||
|
|
require.Equal(t, "client_request", entry.ErrorSource)
|
||
|
|
require.Equal(t, "P3", entry.Severity)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOpsErrorLoggerMiddleware_GooglePlatformErrorStaysPlatform(t *testing.T) {
|
||
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
||
|
|
googlePlatformError(c, http.StatusBadRequest, "Explicit context caching is not available for model m: storage pricing is not configured")
|
||
|
|
})
|
||
|
|
require.Equal(t, "api_error", entry.ErrorType)
|
||
|
|
require.Equal(t, "internal", entry.ErrorPhase)
|
||
|
|
require.Equal(t, "platform", entry.ErrorOwner)
|
||
|
|
require.Equal(t, "gateway", entry.ErrorSource)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOpsErrorLoggerMiddleware_GoogleServerErrorsNotTypedByStatus(t *testing.T) {
|
||
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
||
|
|
googleError(c, http.StatusInternalServerError, "Failed to persist cached content")
|
||
|
|
})
|
||
|
|
require.Equal(t, "api_error", entry.ErrorType)
|
||
|
|
require.Equal(t, "platform", entry.ErrorOwner)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOpsErrorLoggerMiddleware_GoogleBillingErrorMatchesClaudeFormat(t *testing.T) {
|
||
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
||
|
|
status, code, message, _ := billingErrorDetails(service.ErrInsufficientBalance)
|
||
|
|
googleErrorWithType(c, status, code, "", message)
|
||
|
|
})
|
||
|
|
require.Equal(t, "billing_error", entry.ErrorType)
|
||
|
|
require.Equal(t, "request", entry.ErrorPhase)
|
||
|
|
require.Equal(t, "client", entry.ErrorOwner)
|
||
|
|
require.True(t, entry.IsBusinessLimited)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOpsErrorLoggerMiddleware_GoogleConcurrencyErrorClassifiedAsRequest(t *testing.T) {
|
||
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
||
|
|
googleConcurrencyError(c, &ConcurrencyError{SlotType: "user"}, "user")
|
||
|
|
})
|
||
|
|
require.Equal(t, http.StatusTooManyRequests, entry.StatusCode)
|
||
|
|
require.Equal(t, "rate_limit_error", entry.ErrorType)
|
||
|
|
require.Equal(t, "request", entry.ErrorPhase)
|
||
|
|
require.Equal(t, "client", entry.ErrorOwner)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOpsErrorLoggerMiddleware_GoogleErrorAfterUpstreamFailureKeepsUpstreamAttribution(t *testing.T) {
|
||
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
||
|
|
service.SetOpsUpstreamError(c, http.StatusBadRequest, "upstream rejected", "")
|
||
|
|
googleError(c, http.StatusBadRequest, "upstream rejected")
|
||
|
|
})
|
||
|
|
require.Equal(t, "upstream", entry.ErrorPhase)
|
||
|
|
require.Equal(t, "provider", entry.ErrorOwner)
|
||
|
|
require.Equal(t, "upstream_http", entry.ErrorSource)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOpsErrorLoggerMiddleware_RequestScopedUpstreamErrorClassifiedAsClient(t *testing.T) {
|
||
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
||
|
|
service.SetOpsUpstreamError(c, http.StatusBadRequest, "Invalid resource state for cache content 8079767076522164224.", "")
|
||
|
|
c.Set(service.OpsUpstreamErrorsKey, []*service.OpsUpstreamErrorEvent{{
|
||
|
|
Platform: service.PlatformGemini, AccountID: 173, UpstreamStatusCode: http.StatusBadRequest, Kind: "http_error",
|
||
|
|
Message: "Invalid resource state for cache content 8079767076522164224.",
|
||
|
|
}})
|
||
|
|
service.MarkOpsRequestScopedError(c, "permission_error")
|
||
|
|
c.Data(http.StatusForbidden, "application/json", []byte(service.GeminiCachedContentNotFoundResponse))
|
||
|
|
})
|
||
|
|
require.Equal(t, http.StatusForbidden, entry.StatusCode)
|
||
|
|
require.Equal(t, "permission_error", entry.ErrorType)
|
||
|
|
require.Equal(t, "request", entry.ErrorPhase)
|
||
|
|
require.Equal(t, "client", entry.ErrorOwner)
|
||
|
|
require.Equal(t, "client_request", entry.ErrorSource)
|
||
|
|
require.False(t, entry.IsBusinessLimited)
|
||
|
|
require.NotNil(t, entry.UpstreamStatusCode, "上游原始错误保留为明细")
|
||
|
|
require.Equal(t, http.StatusBadRequest, *entry.UpstreamStatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSetOpsLocalErrorTypeClearsRequestScopedMark(t *testing.T) {
|
||
|
|
gin.SetMode(gin.TestMode)
|
||
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||
|
|
service.MarkOpsRequestScopedError(c, "permission_error")
|
||
|
|
require.True(t, service.IsOpsRequestScopedError(c))
|
||
|
|
|
||
|
|
service.SetOpsLocalErrorType(c, "", "")
|
||
|
|
require.False(t, service.IsOpsRequestScopedError(c))
|
||
|
|
errType, code := service.OpsLocalErrorType(c)
|
||
|
|
require.Empty(t, errType)
|
||
|
|
require.Empty(t, code)
|
||
|
|
}
|