sub2api/backend/internal/repository/ops_error_where_test.go
DaydreamCoding cfb195c7b2 feat(usage): 记录并展示失败请求(用户端+管理端)
- 记录失败请求并在用户端/管理端展示;分类下拉改用统一 Select 组件
- 模型过滤改后端 ILIKE 模糊匹配;新增「Key 名称」列(含已删除标记)与按 Key 过滤;时间列移至末列
- 用户可见「已删除 key 失败请求」:OpsErrorLogFilter 加 MatchDeletedKeyOwner,用户侧归属
  放宽为 (user_id OR deleted_key_owner_user_id),让 key 原所有者能看到删除 key 后继续请求
  导致的认证失败记录(他人仍 NotFound,不泄露存在性)
- 迁移 148:ops_error_logs 用户+时间索引

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:00:57 +08:00

96 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository
import (
"strings"
"testing"
"github.com/Wei-Shaw/sub2api/internal/service"
)
func TestBuildOpsErrorLogsWhere_UserScopedFilters(t *testing.T) {
uid := int64(42)
kid := int64(7)
filter := &service.OpsErrorLogFilter{
UserID: &uid,
APIKeyID: &kid,
Model: "claude-sonnet-4-5",
ExcludeCountTokens: true,
ErrorPhasesAny: []string{"auth"},
ErrorTypesAny: []string{"rate_limit_error"},
View: "all",
}
where, args := buildOpsErrorLogsWhere(filter)
for _, want := range []string{
"e.user_id = $",
"e.api_key_id = $",
"COALESCE(e.requested_model, e.model, '') = $",
"COALESCE(e.is_count_tokens, false) = false",
"e.error_phase = ANY($",
"e.error_type = ANY($",
} {
if !strings.Contains(where, want) {
t.Fatalf("where missing %q\nfull: %s", want, where)
}
}
if len(args) != 5 {
t.Fatalf("expected 5 args, got %d", len(args))
}
}
func TestBuildOpsErrorLogsWhere_ModelFuzzy(t *testing.T) {
// 默认ModelFuzzy=false保持精确匹配
exact := &service.OpsErrorLogFilter{Model: "claude"}
whereExact, _ := buildOpsErrorLogsWhere(exact)
if !strings.Contains(whereExact, "COALESCE(e.requested_model, e.model, '') = $") {
t.Fatalf("default should be exact match, got: %s", whereExact)
}
// ModelFuzzy=true → ILIKE
fuzzy := &service.OpsErrorLogFilter{Model: "claude", ModelFuzzy: true}
whereFuzzy, args := buildOpsErrorLogsWhere(fuzzy)
if !strings.Contains(whereFuzzy, "COALESCE(e.requested_model, e.model, '') ILIKE $") {
t.Fatalf("ModelFuzzy should use ILIKE, got: %s", whereFuzzy)
}
if len(args) != 1 || args[0] != "%claude%" {
t.Fatalf("expected arg \"%%claude%%\", got %v", args)
}
// 通配符转义:输入含 % 应被转义为字面量
esc := &service.OpsErrorLogFilter{Model: "50%off", ModelFuzzy: true}
_, escArgs := buildOpsErrorLogsWhere(esc)
if len(escArgs) != 1 || escArgs[0] != `%50\%off%` {
t.Fatalf("expected escaped arg, got %v", escArgs)
}
esc2 := &service.OpsErrorLogFilter{Model: "gpt_4o", ModelFuzzy: true}
_, escArgs2 := buildOpsErrorLogsWhere(esc2)
if len(escArgs2) != 1 || escArgs2[0] != `%gpt\_4o%` {
t.Fatalf("underscore should be escaped, got %v", escArgs2)
}
}
func TestBuildOpsErrorLogsWhere_MatchDeletedKeyOwner(t *testing.T) {
uid := int64(42)
// 开关开启 → 归属放宽为 OR(user_id 或 deleted_key_owner_user_id),且共用同一占位符
on := &service.OpsErrorLogFilter{UserID: &uid, MatchDeletedKeyOwner: true}
whereOn, argsOn := buildOpsErrorLogsWhere(on)
if !strings.Contains(whereOn, "(e.user_id = $1 OR e.deleted_key_owner_user_id = $1)") {
t.Fatalf("MatchDeletedKeyOwner=true should widen to OR, got: %s", whereOn)
}
if len(argsOn) != 1 || argsOn[0] != uid {
t.Fatalf("expected single reused arg %d, got %v", uid, argsOn)
}
// 开关关闭(默认)→ 仅精确 user_id,绝不出现 deleted_key_owner_user_id(admin 回归)
off := &service.OpsErrorLogFilter{UserID: &uid}
whereOff, _ := buildOpsErrorLogsWhere(off)
if !strings.Contains(whereOff, "e.user_id = $1") {
t.Fatalf("default should match user_id exactly, got: %s", whereOff)
}
if strings.Contains(whereOff, "deleted_key_owner_user_id") {
t.Fatalf("default must NOT include deleted_key_owner_user_id, got: %s", whereOff)
}
}