Files
sub2api/backend/internal/service/audit_log_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

201 lines
6.3 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 service
import (
"encoding/json"
"strings"
"testing"
)
func TestMaskAuditCredential(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
{"empty", "", ""},
{"short", "abc", "****"},
{"boundary_14", "12345678901234", "****"},
{"long", "sk-ant-api03-abcdefghijklmnop1234", "sk-ant****1234"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := MaskAuditCredential(tc.in)
if got != tc.want {
t.Fatalf("MaskAuditCredential(%q) = %q, want %q", tc.in, got, tc.want)
}
// 掩码结果绝不能包含原始凭证的中间部分。
if len(tc.in) > 14 && strings.Contains(got, tc.in) {
t.Fatalf("masked value leaks full credential: %q", got)
}
})
}
}
func TestRedactAuditBody_JSONRedactsSecrets(t *testing.T) {
raw := []byte(`{
"name": "acc1",
"base_url": "https://evil.example.com",
"credentials": {"api_key": "sk-secret-123", "base_url": "https://evil.example.com"},
"new_password": "hunter2",
"totp_code": "123456",
"nested": [{"access_token": "tok_abc"}]
}`)
out := RedactAuditBody(raw, "application/json")
var parsed map[string]any
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
t.Fatalf("output is not valid JSON: %v\n%s", err, out)
}
// 敏感字段被擦除。
for _, secret := range []string{"sk-secret-123", "hunter2", "123456", "tok_abc"} {
if strings.Contains(out, secret) {
t.Fatalf("redacted body still contains secret %q: %s", secret, out)
}
}
// 非敏感字段(base_url、name)保留以便追责。
if !strings.Contains(out, "evil.example.com") {
t.Fatalf("base_url should be preserved for accountability: %s", out)
}
if !strings.Contains(out, "acc1") {
t.Fatalf("name should be preserved: %s", out)
}
}
// 裸键 "session"Ollama Cloud 会话保存的请求体字段)值整体就是浏览器 Cookie 明文,
// 必须命中键级脱敏;session_id 等运行态标识不受影响,保留以便追责。
func TestRedactAuditBody_BareSessionKeyRedacted(t *testing.T) {
raw := []byte(`{"session": "wos-session=cookie-canary", "session_id": "sid-visible"}`)
out := RedactAuditBody(raw, "application/json")
if strings.Contains(out, "cookie-canary") {
t.Fatalf("redacted body still contains the session cookie: %s", out)
}
if !strings.Contains(out, "sid-visible") {
t.Fatalf("session_id should be preserved for accountability: %s", out)
}
}
// TestRedactAuditBody_AuthoritativeTablesSynced 覆盖曾经漏网的凭证字段:
// 账号 credentials 敏感子键、支付渠道无分隔符密钥、字符串值内嵌凭证的 proxy_key / custom_key
// 以及 camelCase 等命名变体(归一化比对)。
func TestRedactAuditBody_AuthoritativeTablesSynced(t *testing.T) {
raw := []byte(`{
"credentials": {
"session_key": "sk-session-aaa",
"service_account_json": "{\"private_key\":\"pem-body-bbb\"}",
"service_account": "sa-blob-ccc"
},
"proxy_key": "socks5|1.2.3.4|1080|proxyuser|proxypass-ddd",
"custom_key": "sk-custom-eee",
"config": {
"pkey": "easypay-merchant-fff",
"privateKey": "alipay-pem-ggg",
"apiv3key": "wxpay-v3-hhh",
"SecretKey": "stripe-sk-iii",
"webhookSecret": "whsec-jjj"
},
"provider_key": "stripe",
"name": "instance-1"
}`)
out := RedactAuditBody(raw, "application/json")
for _, secret := range []string{
"sk-session-aaa", "pem-body-bbb", "sa-blob-ccc",
"proxypass-ddd", "sk-custom-eee",
"easypay-merchant-fff", "alipay-pem-ggg", "wxpay-v3-hhh",
"stripe-sk-iii", "whsec-jjj",
} {
if strings.Contains(out, secret) {
t.Fatalf("redacted body still contains secret %q: %s", secret, out)
}
}
// provider_key 是渠道标识而非密钥,必须保留以便追责。
if !strings.Contains(out, `"provider_key":"stripe"`) {
t.Fatalf("provider_key should be preserved for accountability: %s", out)
}
if !strings.Contains(out, "instance-1") {
t.Fatalf("name should be preserved: %s", out)
}
}
// SensitiveCredentialKeys 中的每个键都必须被审计脱敏判定命中(防两表漂移的守卫)。
func TestAuditSensitiveKeys_CoverCredentialTable(t *testing.T) {
for _, k := range SensitiveCredentialKeys {
if !isAuditSensitiveBodyKey(k) {
t.Fatalf("credential key %q is not covered by audit redaction", k)
}
}
for provider, fields := range providerSensitiveConfigFields {
for k := range fields {
if !isAuditSensitiveBodyKey(k) {
t.Fatalf("payment provider %q sensitive field %q is not covered by audit redaction", provider, k)
}
}
}
}
func TestRedactAuditBody_NonJSONOmitted(t *testing.T) {
out := RedactAuditBody([]byte("username=admin&password=secret"), "application/x-www-form-urlencoded")
if strings.Contains(out, "secret") {
t.Fatalf("non-json body must not leak content: %s", out)
}
if !strings.Contains(out, "omitted") {
t.Fatalf("expected omission marker, got: %s", out)
}
}
func TestRedactAuditBody_Empty(t *testing.T) {
if got := RedactAuditBody(nil, "application/json"); got != "" {
t.Fatalf("expected empty for nil body, got %q", got)
}
}
func TestSessionBindingHash(t *testing.T) {
a := &SessionBinding{IP: "1.2.3.4", UserAgent: "Mozilla/5.0"}
b := &SessionBinding{IP: "1.2.3.4", UserAgent: "Mozilla/5.0"}
if a.Hash() != b.Hash() {
t.Fatalf("identical bindings must hash equal")
}
if a.Hash() == "" {
t.Fatalf("non-empty binding must produce non-empty hash")
}
// IP 变化 → 哈希变化。
c := &SessionBinding{IP: "5.6.7.8", UserAgent: "Mozilla/5.0"}
if a.Hash() == c.Hash() {
t.Fatalf("changing IP must change hash")
}
// UA 变化 → 哈希变化。
d := &SessionBinding{IP: "1.2.3.4", UserAgent: "curl/8.0"}
if a.Hash() == d.Hash() {
t.Fatalf("changing UA must change hash")
}
// 空指纹 → 空哈希(旧 token 兼容)。
empty := &SessionBinding{}
if empty.Hash() != "" {
t.Fatalf("empty binding must hash to empty string")
}
var nilBinding *SessionBinding
if nilBinding.Hash() != "" {
t.Fatalf("nil binding must hash to empty string")
}
}
func TestParseAuditLogRetentionDays(t *testing.T) {
cases := map[string]int{
"": defaultAuditLogRetentionDays,
"abc": defaultAuditLogRetentionDays,
"90": 90,
"0": 0,
"-1": 0,
" 30 ": 30,
}
for in, want := range cases {
if got := parseAuditLogRetentionDays(in); got != want {
t.Fatalf("parseAuditLogRetentionDays(%q) = %d, want %d", in, got, want)
}
}
}