fix moderation key handling and key UI
This commit is contained in:
@@ -301,13 +301,14 @@ func TestBuildContentModerationLog_RedactsInputExcerpt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRedactContentModerationSecrets_LongHexAndTokens(t *testing.T) {
|
||||
input := "你哈市多大事cf5bbdc4cd508f3aaf0d2070d529d4a4ac29099f8ecc357f696df28e1df91554 token=abc123456789xyz Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signaturepart"
|
||||
input := "你哈市多大事cf5bbdc4cd508f3aaf0d2070d529d4a4ac29099f8ecc357f696df28e1df91554 token=abc123456789xyz Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signaturepart https://example.com/private/path?token=abc123"
|
||||
|
||||
out := redactContentModerationSecrets(input)
|
||||
|
||||
require.NotContains(t, out, "cf5bbdc4cd508f3aaf0d2070d529d4a4ac29099f8ecc357f696df28e1df91554")
|
||||
require.NotContains(t, out, "abc123456789xyz")
|
||||
require.NotContains(t, out, "eyJhbGciOiJIUzI1NiJ9")
|
||||
require.NotContains(t, out, "https://example.com/private/path")
|
||||
require.Contains(t, out, "[已脱敏]")
|
||||
}
|
||||
|
||||
@@ -320,6 +321,61 @@ func TestContentModerationConfigNormalize_NonHitRetentionMaxThreeDays(t *testing
|
||||
require.Equal(t, 3, cfg.NonHitRetentionDays)
|
||||
}
|
||||
|
||||
func TestContentModerationUpdateConfig_AppendsAndDeletesAPIKeys(t *testing.T) {
|
||||
cfg := defaultContentModerationConfig()
|
||||
cfg.APIKeys = []string{"sk-old-a", "sk-old-b"}
|
||||
rawCfg, err := json.Marshal(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
repo := &contentModerationTestSettingRepo{values: map[string]string{
|
||||
SettingKeyContentModerationConfig: string(rawCfg),
|
||||
}}
|
||||
svc := NewContentModerationService(repo, nil, nil, nil, nil, nil, nil)
|
||||
deleteHashes := []string{moderationAPIKeyHash("sk-old-a")}
|
||||
addKeys := []string{"sk-new-c", "sk-old-b"}
|
||||
|
||||
view, err := svc.UpdateConfig(context.Background(), UpdateContentModerationConfigInput{
|
||||
APIKeys: &addKeys,
|
||||
DeleteAPIKeyHashes: &deleteHashes,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, view.APIKeyCount)
|
||||
require.Equal(t, []string{maskSecretTail("sk-old-b"), maskSecretTail("sk-new-c")}, view.APIKeyMasks)
|
||||
|
||||
var saved ContentModerationConfig
|
||||
require.NoError(t, json.Unmarshal([]byte(repo.values[SettingKeyContentModerationConfig]), &saved))
|
||||
require.Equal(t, []string{"sk-old-b", "sk-new-c"}, saved.apiKeys())
|
||||
}
|
||||
|
||||
func TestContentModerationUpdateConfig_ReplacesAPIKeysWhenRequested(t *testing.T) {
|
||||
cfg := defaultContentModerationConfig()
|
||||
cfg.APIKeys = []string{"sk-old-a", "sk-old-b"}
|
||||
rawCfg, err := json.Marshal(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
repo := &contentModerationTestSettingRepo{values: map[string]string{
|
||||
SettingKeyContentModerationConfig: string(rawCfg),
|
||||
}}
|
||||
svc := NewContentModerationService(repo, nil, nil, nil, nil, nil, nil)
|
||||
deleteHashes := []string{moderationAPIKeyHash("sk-old-a")}
|
||||
replaceKeys := []string{"sk-new-only"}
|
||||
|
||||
view, err := svc.UpdateConfig(context.Background(), UpdateContentModerationConfigInput{
|
||||
APIKeys: &replaceKeys,
|
||||
APIKeysMode: contentModerationAPIKeysModeReplace,
|
||||
DeleteAPIKeyHashes: &deleteHashes,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, view.APIKeyCount)
|
||||
require.Equal(t, []string{maskSecretTail("sk-new-only")}, view.APIKeyMasks)
|
||||
|
||||
var saved ContentModerationConfig
|
||||
require.NoError(t, json.Unmarshal([]byte(repo.values[SettingKeyContentModerationConfig]), &saved))
|
||||
require.Equal(t, []string{"sk-new-only"}, saved.apiKeys())
|
||||
}
|
||||
|
||||
func TestExtractContentModerationInput_AnthropicImageSourceOnlyParticipatesInMemory(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"messages": [
|
||||
@@ -395,6 +451,32 @@ func TestExtractContentModerationInput_OpenAIImagesIncludesPromptAndImages(t *te
|
||||
require.Equal(t, []string{"https://example.com/source.png", "data:image/png;base64,aGVsbG8="}, input.Images)
|
||||
}
|
||||
|
||||
func TestContentModerationInput_NormalizeRandomSamplesOneImageForModerationAPI(t *testing.T) {
|
||||
images := []string{
|
||||
"data:image/png;base64,Zmlyc3Q=",
|
||||
"data:image/png;base64,c2Vjb25k",
|
||||
}
|
||||
input := ContentModerationInput{
|
||||
Text: "check image",
|
||||
Images: append([]string(nil), images...),
|
||||
}
|
||||
input.Normalize()
|
||||
|
||||
require.Len(t, input.Images, 1)
|
||||
require.Contains(t, images, input.Images[0])
|
||||
require.Len(t, input.ModerationInput(), 2)
|
||||
}
|
||||
|
||||
func TestBuildModerationTestInputRejectsMultipleImages(t *testing.T) {
|
||||
_, _, err := buildModerationTestInput("check image", []string{
|
||||
"data:image/png;base64,Zmlyc3Q=",
|
||||
"data:image/png;base64,c2Vjb25k",
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "最多上传 1 张测试图片")
|
||||
}
|
||||
|
||||
func TestExtractContentModerationInput_OpenAIResponsesCodexPayloadUsesLastUserMessage(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"model":"gpt-5.5",
|
||||
@@ -562,6 +644,105 @@ func TestBuildContentModerationTestAuditResult_UsesConfiguredThresholdsOnly(t *t
|
||||
require.Equal(t, 0.98, result.Thresholds["harassment"])
|
||||
}
|
||||
|
||||
func TestContentModerationCallModeration_400DoesNotFreezeAPIKey(t *testing.T) {
|
||||
requestCount := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestCount++
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(`{"error":{"message":"Number of images (5) exceeds maximum of 1","type":"invalid_request_error","param":"input","code":"too_many_images"}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := defaultContentModerationConfig()
|
||||
cfg.BaseURL = server.URL
|
||||
cfg.APIKeys = []string{"sk-test"}
|
||||
cfg.RetryCount = 5
|
||||
svc := NewContentModerationService(nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
_, err := svc.callModeration(context.Background(), cfg, "hello")
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, 1, requestCount)
|
||||
status := svc.apiKeyStatusForHash(0, moderationAPIKeyHash("sk-test"), maskSecretTail("sk-test"), true)
|
||||
require.Equal(t, "error", status.Status)
|
||||
require.Equal(t, http.StatusBadRequest, status.LastHTTPStatus)
|
||||
require.Zero(t, status.FailureCount)
|
||||
require.Nil(t, status.FrozenUntil)
|
||||
}
|
||||
|
||||
func TestContentModerationCallModeration_FreezesByHTTPStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
minFreeze time.Duration
|
||||
maxFreeze time.Duration
|
||||
}{
|
||||
{name: "401 freezes ten minutes", statusCode: http.StatusUnauthorized, minFreeze: 9*time.Minute + 55*time.Second, maxFreeze: 10*time.Minute + time.Second},
|
||||
{name: "403 freezes ten minutes", statusCode: http.StatusForbidden, minFreeze: 9*time.Minute + 55*time.Second, maxFreeze: 10*time.Minute + time.Second},
|
||||
{name: "429 freezes one minute", statusCode: http.StatusTooManyRequests, minFreeze: 55 * time.Second, maxFreeze: time.Minute + time.Second},
|
||||
{name: "529 freezes one minute", statusCode: 529, minFreeze: 55 * time.Second, maxFreeze: time.Minute + time.Second},
|
||||
{name: "500 freezes ten seconds", statusCode: http.StatusInternalServerError, minFreeze: 5 * time.Second, maxFreeze: 11 * time.Second},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(tt.statusCode)
|
||||
_, _ = w.Write([]byte(`{"error":{"message":"upstream error"}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := defaultContentModerationConfig()
|
||||
cfg.BaseURL = server.URL
|
||||
cfg.APIKeys = []string{"sk-test"}
|
||||
cfg.RetryCount = 0
|
||||
svc := NewContentModerationService(nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
_, err := svc.callModeration(context.Background(), cfg, "hello")
|
||||
|
||||
require.Error(t, err)
|
||||
status := svc.apiKeyStatusForHash(0, moderationAPIKeyHash("sk-test"), maskSecretTail("sk-test"), true)
|
||||
require.Equal(t, "frozen", status.Status)
|
||||
require.Equal(t, tt.statusCode, status.LastHTTPStatus)
|
||||
require.Equal(t, 1, status.FailureCount)
|
||||
require.NotNil(t, status.FrozenUntil)
|
||||
remaining := time.Until(*status.FrozenUntil)
|
||||
require.GreaterOrEqual(t, remaining, tt.minFreeze)
|
||||
require.LessOrEqual(t, remaining, tt.maxFreeze)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentModerationTestAPIKeys_400DoesNotFreezeAPIKey(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(`{"error":{"message":"invalid moderation request"}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
svc := NewContentModerationService(
|
||||
&contentModerationTestSettingRepo{values: map[string]string{}},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
result, err := svc.TestAPIKeys(context.Background(), TestContentModerationAPIKeysInput{
|
||||
APIKeys: []string{"sk-test"},
|
||||
BaseURL: server.URL,
|
||||
Prompt: "hello",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Items, 1)
|
||||
require.Equal(t, "error", result.Items[0].Status)
|
||||
require.Equal(t, http.StatusBadRequest, result.Items[0].LastHTTPStatus)
|
||||
require.Zero(t, result.Items[0].FailureCount)
|
||||
require.Nil(t, result.Items[0].FrozenUntil)
|
||||
}
|
||||
|
||||
func TestContentModerationCheck_PreHashUsesRedisHashCache(t *testing.T) {
|
||||
cfg := defaultContentModerationConfig()
|
||||
cfg.Enabled = true
|
||||
|
||||
Reference in New Issue
Block a user