Files
sub2api/backend/internal/service/ops_service_batch_test.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

151 lines
4.9 KiB
Go

package service
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestOpsServiceRecordErrorBatch_SanitizesAndBatches(t *testing.T) {
t.Parallel()
var captured []*OpsInsertErrorLogInput
repo := &opsRepoMock{
BatchInsertErrorLogsFn: func(ctx context.Context, inputs []*OpsInsertErrorLogInput) (int64, error) {
captured = append(captured, inputs...)
return int64(len(inputs)), nil
},
}
svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
msg := " upstream failed: https://example.com?access_token=secret-value "
detail := `{"authorization":"Bearer secret-token"}`
entries := []*OpsInsertErrorLogInput{
{
ErrorBody: `{"error":"bad","access_token":"secret"}`,
UpstreamStatusCode: intPtr(-10),
UpstreamErrorMessage: strPtr(msg),
UpstreamErrorDetail: strPtr(detail),
UpstreamErrors: []*OpsUpstreamErrorEvent{
{
AccountID: -2,
UpstreamStatusCode: 429,
Message: " token leaked ",
Detail: `{"refresh_token":"secret"}`,
},
},
},
{
ErrorPhase: "upstream",
ErrorType: "upstream_error",
CreatedAt: time.Now().UTC(),
},
}
require.NoError(t, svc.RecordErrorBatch(context.Background(), entries))
require.Len(t, captured, 2)
first := captured[0]
require.Equal(t, "internal", first.ErrorPhase)
require.Equal(t, "api_error", first.ErrorType)
require.Nil(t, first.UpstreamStatusCode)
require.NotNil(t, first.UpstreamErrorMessage)
require.NotContains(t, *first.UpstreamErrorMessage, "secret-value")
require.Contains(t, *first.UpstreamErrorMessage, "access_token=***")
require.NotNil(t, first.UpstreamErrorDetail)
require.NotContains(t, *first.UpstreamErrorDetail, "secret-token")
require.NotContains(t, first.ErrorBody, "secret")
require.Nil(t, first.UpstreamErrors)
require.NotNil(t, first.UpstreamErrorsJSON)
require.NotContains(t, *first.UpstreamErrorsJSON, "secret")
require.Contains(t, *first.UpstreamErrorsJSON, "[REDACTED]")
second := captured[1]
require.Equal(t, "upstream", second.ErrorPhase)
require.Equal(t, "upstream_error", second.ErrorType)
require.False(t, second.CreatedAt.IsZero())
}
func TestOpsServiceRecordErrorBatch_DoesNotFallbackToSingleInsertsWhenBatchFails(t *testing.T) {
t.Parallel()
var (
batchCalls int
singleCalls int
)
repo := &opsRepoMock{
BatchInsertErrorLogsFn: func(ctx context.Context, inputs []*OpsInsertErrorLogInput) (int64, error) {
batchCalls++
return 0, errors.New("batch failed")
},
InsertErrorLogFn: func(ctx context.Context, input *OpsInsertErrorLogInput) (int64, error) {
singleCalls++
return int64(singleCalls), nil
},
}
svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
err := svc.RecordErrorBatch(context.Background(), []*OpsInsertErrorLogInput{
{ErrorMessage: "first"},
{ErrorMessage: "second"},
})
require.Error(t, err)
require.Equal(t, 1, batchCalls)
require.Zero(t, singleCalls)
}
func TestOpsServiceRecordErrorPersistsExplicitAccountAuthStatusZero(t *testing.T) {
t.Parallel()
var captured *OpsInsertErrorLogInput
repo := &opsRepoMock{
InsertErrorLogFn: func(_ context.Context, input *OpsInsertErrorLogInput) (int64, error) {
captured = input
return 1, nil
},
}
svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
staleStatus := 403
staleMessage := "stale inference message"
staleDetail := "stale inference detail"
err := svc.RecordError(context.Background(), &OpsInsertErrorLogInput{
ErrorPhase: "upstream",
ErrorType: "upstream_error",
ErrorOwner: "provider",
ErrorSource: "upstream_http",
UpstreamStatusCode: &staleStatus,
UpstreamErrorMessage: &staleMessage,
UpstreamErrorDetail: &staleDetail,
UpstreamErrors: []*OpsUpstreamErrorEvent{
{Stage: string(GatewayFailureStageInference), UpstreamStatusCode: 403, Message: staleMessage, Detail: staleDetail},
{
Stage: string(GatewayFailureStageAccountAuth), Scope: string(GatewayFailureScopeAccount),
Reason: string(GrokCredentialReasonRevoked), Message: "Grok OAuth credentials require account action",
},
},
})
require.NoError(t, err)
require.NotNil(t, captured)
require.Equal(t, "account_auth", captured.ErrorPhase)
require.Equal(t, "provider", captured.ErrorOwner)
require.Equal(t, "gateway", captured.ErrorSource)
require.NotNil(t, captured.UpstreamStatusCode)
require.Zero(t, *captured.UpstreamStatusCode)
require.NotNil(t, captured.UpstreamErrorMessage)
require.Equal(t, "Grok OAuth credentials require account action", *captured.UpstreamErrorMessage)
require.Nil(t, captured.UpstreamErrorDetail)
require.Nil(t, captured.UpstreamErrors)
require.NotNil(t, captured.UpstreamErrorsJSON)
require.Contains(t, *captured.UpstreamErrorsJSON, `"upstream_status_code":403`)
require.Contains(t, *captured.UpstreamErrorsJSON, `"stage":"account_auth"`)
}
func strPtr(v string) *string {
return &v
}