sub2api/backend/internal/service/claude_code_session_id_test.go
win 92433656f5 chore: merge upstream Wei-Shaw/sub2api v0.1.128 — keep fork customizations
Upstream 新功能 (34 commits, ~main..origin/main):
- feat(email): 通知邮件模板服务、模板编辑器、订阅/余额提醒邮件
- feat(notification): NotificationEmailService 注入到 Balance/Payment/Setting
- feat(payment): 支付成功通知邮件
- feat(usage): 用户 API Key 用量页支持按日明细
- feat(openai-gateway): Codex OAuth 浏览器 UA 自动改写规避 Cloudflare 质询
- feat(admin): 邮件模板管理接口
- fix(auth): 停用/删除分组后阻断 API Key
- fix(group): 修正分组账号可用计数口径
- fix(openai): /v1/responses respect force chat completions, images n 参数透传
- test(repository): AES Encryptor 单元测试
- chore: VERSION 0.1.128

冲突解决 (backend/cmd/server/wire_gen.go):
- 引入 upstream 新 wire providers: notificationEmailService,
  ProvidePaymentService(10 args), ProvideAdminSettingHandler(8 args)
- 保留 fork 独有依赖: rpmTokenBucketService (RPM 平滑),
  NewOpsHandler 3 参数版本 (requestEventBus, opsLogBroadcaster)
- ProvideBalanceNotifyService 接受 4 参数 (含 notificationEmailService)

修复 session-id helper 设计 (claude_code_session_id.go):
- 发现: TestGatewayService_AnthropicOAuth_InjectsClaudeCodeSessionHeaderFromMetadata
  在 OAuth + mimicClaudeCode=false 场景失败
- 重新审视设计原则: OAuth 凭证本身就是 Claude Code 客户端,可信任 metadata
  派生 session_id;不应受 mimicClaudeCode 标志阻止
- 修复: metadata 派生只看 tokenType=="oauth";UUID 兜底仍需 oauth && mimic
- 更新测试: OAuthNonMimicDerivesFromMetadata 取代原 IgnoresMetadata

所有 fork 独有功能保留:
- Claude Code 2.1.145 mimicry bundle (上个 commit 引入)
- RPM token bucket smoothing (commit 95814974)
- Windsurf/Antigravity/Omniroute 定制
- claudemask/ 校验包 (upstream 已删除,我们仍在 gateway_service 中使用)

不在范围:
- 不修复 baseline 既存的 2 个测试失败 (TestProxyImportData...,
  TestWindsurfTierAccessService_Snapshot_HappyPath) - 与 merge 无关
2026-05-20 17:50:44 +08:00

203 lines
7.4 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 (
"net/http"
"testing"
"github.com/google/uuid"
)
const (
testValidUUID = "01970000-0000-7000-8000-000000000001"
testValidUUIDAlt = "01970000-0000-7000-8000-000000000002"
)
func newReq(t *testing.T) *http.Request {
t.Helper()
req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
if err != nil {
t.Fatalf("NewRequest: %v", err)
}
return req
}
func TestEnsureClaudeCodeSessionID_NilRequest(t *testing.T) {
// Should not panic.
ensureClaudeCodeSessionID(nil, nil, "oauth", true)
}
func TestEnsureClaudeCodeSessionID_FromMetadataJSON(t *testing.T) {
req := newReq(t)
body := []byte(`{"metadata":{"user_id":"{\"device_id\":\"abc\",\"account_uuid\":\"\",\"session_id\":\"` + testValidUUID + `\"}"}}`)
ensureClaudeCodeSessionID(req, body, "oauth", true)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
if got != testValidUUID {
t.Fatalf("session_id = %q, want %q", got, testValidUUID)
}
}
func TestEnsureClaudeCodeSessionID_FromMetadataLegacy(t *testing.T) {
req := newReq(t)
// legacy format: user_{64hex}_account_{uuid}_session_{uuid}
dev := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
acc := "11111111-2222-3333-4444-555555555555"
uid := "user_" + dev + "_account_" + acc + "_session_" + testValidUUID
body := []byte(`{"metadata":{"user_id":"` + uid + `"}}`)
ensureClaudeCodeSessionID(req, body, "oauth", true)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
if got != testValidUUID {
t.Fatalf("session_id = %q, want %q", got, testValidUUID)
}
}
// 安全测试metadata.user_id 中的 session_id 不是合法 UUID 时,
// 必须 fallback 到 OAuth UUID 兜底,而不是写入恶意值。
func TestEnsureClaudeCodeSessionID_RejectsMalformedMetadataUUID(t *testing.T) {
req := newReq(t)
// session_id 字段是 36 字符但非 UUID 格式(凑数 hex+dash
bogus := "abcdefab-0000-0000-0000-not-a-real-uuid"
body := []byte(`{"metadata":{"user_id":"{\"device_id\":\"abc\",\"account_uuid\":\"\",\"session_id\":\"` + bogus + `\"}"}}`)
ensureClaudeCodeSessionID(req, body, "oauth", true)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
if got == bogus {
t.Fatalf("malformed metadata session_id was written verbatim: %q", got)
}
if got == "" {
t.Fatalf("expected OAuth mimic to fallback to UUID, got empty")
}
if _, err := uuid.Parse(got); err != nil {
t.Fatalf("fallback session_id is not a valid UUID: %q (err=%v)", got, err)
}
}
func TestEnsureClaudeCodeSessionID_PreservesExistingValidHeader(t *testing.T) {
req := newReq(t)
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", testValidUUID)
ensureClaudeCodeSessionID(req, nil, "oauth", true)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
if got != testValidUUID {
t.Fatalf("session_id = %q, want preserved %q", got, testValidUUID)
}
}
func TestEnsureClaudeCodeSessionID_OverwritesInvalidHeader(t *testing.T) {
req := newReq(t)
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", "not-a-uuid")
ensureClaudeCodeSessionID(req, nil, "oauth", true)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
if got == "not-a-uuid" {
t.Fatalf("invalid header was not overwritten: %q", got)
}
if _, err := uuid.Parse(got); err != nil {
t.Fatalf("fallback session_id is not a valid UUID: %q (err=%v)", got, err)
}
}
func TestEnsureClaudeCodeSessionID_OAuthMimicFallback(t *testing.T) {
req := newReq(t)
ensureClaudeCodeSessionID(req, nil, "oauth", true)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
if got == "" {
t.Fatalf("expected OAuth mimic fallback to set X-Claude-Code-Session-Id")
}
if _, err := uuid.Parse(got); err != nil {
t.Fatalf("fallback session_id is not a valid UUID: %q (err=%v)", got, err)
}
}
// API key passthrough 路径必须不被污染:缺失 session-id 时不强制生成。
func TestEnsureClaudeCodeSessionID_DoesNotPolluteAPIKey(t *testing.T) {
req := newReq(t)
ensureClaudeCodeSessionID(req, nil, "api_key", false)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
if got != "" {
t.Fatalf("API key path should NOT auto-generate session-id, got %q", got)
}
}
// API key 路径即使 body 中有合法 metadata.user_id也不应该派生 session-id 头。
// 这是 fresh code review 发现的 C1 修复:保护客户端原始语义。
func TestEnsureClaudeCodeSessionID_APIKeyIgnoresMetadata(t *testing.T) {
req := newReq(t)
// 客户端传入合法的 metadata.user_id但 tokenType=api_key
body := []byte(`{"metadata":{"user_id":"{\"device_id\":\"abc\",\"account_uuid\":\"\",\"session_id\":\"` + testValidUUID + `\"}"}}`)
ensureClaudeCodeSessionID(req, body, "api_key", false)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
if got != "" {
t.Fatalf("API key path must NOT derive session-id from metadata, got %q", got)
}
}
// OAuth 路径即使 mimic=false 也应该从 metadata 派生 header
// OAuth 凭证本身就是 Claude Code 类型账号metadata.user_id 可信任。
// 这与 API key 路径不同API key 是任意第三方调用方)。
func TestEnsureClaudeCodeSessionID_OAuthNonMimicDerivesFromMetadata(t *testing.T) {
req := newReq(t)
body := []byte(`{"metadata":{"user_id":"{\"device_id\":\"abc\",\"account_uuid\":\"\",\"session_id\":\"` + testValidUUID + `\"}"}}`)
ensureClaudeCodeSessionID(req, body, "oauth", false)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
if got != testValidUUID {
t.Fatalf("OAuth must derive session-id from metadata regardless of mimic flag, got %q want %q", got, testValidUUID)
}
}
// API key 路径若客户端传入了非法 UUID header必须删除避免向上游透传。
// 这是 C2 修复UUID 校验承诺要对所有路径生效。
func TestEnsureClaudeCodeSessionID_APIKeyDeletesInvalidHeader(t *testing.T) {
req := newReq(t)
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", "not-a-uuid")
ensureClaudeCodeSessionID(req, nil, "api_key", false)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
if got != "" {
t.Fatalf("API key path must delete invalid header, got %q", got)
}
}
// API key 路径若客户端传入了合法 UUID header规范化保留不删除
func TestEnsureClaudeCodeSessionID_APIKeyPreservesValidHeader(t *testing.T) {
req := newReq(t)
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", testValidUUID)
ensureClaudeCodeSessionID(req, nil, "api_key", false)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
if got != testValidUUID {
t.Fatalf("API key path must preserve valid client header, got %q want %q", got, testValidUUID)
}
}
// OAuth 但非 mimic 场景:保留客户端原始语义,不自动生成。
func TestEnsureClaudeCodeSessionID_OAuthNonMimicDoesNotForce(t *testing.T) {
req := newReq(t)
ensureClaudeCodeSessionID(req, nil, "oauth", false)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
if got != "" {
t.Fatalf("non-mimic OAuth should not auto-generate, got %q", got)
}
}
// 验证不同 UUID 输入会被规范化为 canonical 小写形式。
func TestEnsureClaudeCodeSessionID_CanonicalForm(t *testing.T) {
req := newReq(t)
// 大写 UUID 输入
upper := "01970000-0000-7000-8000-ABCDEF000003"
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", upper)
ensureClaudeCodeSessionID(req, nil, "oauth", true)
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
want := "01970000-0000-7000-8000-abcdef000003"
if got != want {
t.Fatalf("session_id = %q, want canonical %q", got, want)
}
}