Files
CLIProxyAPI/internal/util/header_helpers_test.go
Luis Pater 3bf787fc1d feat(auth): propagate canonical session id for custom header expansion
- Initialize `util.SessionIDResolver` to resolve session IDs from request contexts, metadata, and headers.
- Ensure canonical session metadata is injected into execution options across execution flows.
- Propagate session context in executors to support `$CPA-SESSION-ID` expansion in custom headers.
- Sync cleared or updated session identities back to execution contexts during conductor execution.

Closes: #5690
2026-09-10 01:43:37 +08:00

272 lines
9.8 KiB
Go

package util
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestApplyCustomHeadersFromAttrs_StaticHeaders(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "https://api.example.com", nil)
attrs := map[string]string{
"header:X-Custom-Static": "static-value",
"header:Host": "custom.host.com",
}
ApplyCustomHeadersFromAttrs(req, attrs)
if got := req.Header.Get("X-Custom-Static"); got != "static-value" {
t.Errorf("X-Custom-Static = %q, want %q", got, "static-value")
}
if got := req.Host; got != "custom.host.com" {
t.Errorf("req.Host = %q, want %q", got, "custom.host.com")
}
}
func TestApplyCustomHeadersFromAttrs_MagicVariable(t *testing.T) {
t.Run("present in clientHeaders sets header", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "https://api.example.com", nil)
attrs := map[string]string{
"header:X-Claude-Code-Session-Id": "$ABC",
"header:X-Target-Session": "$X-Claude-Code-Session-Id",
"header:Static-Header": "static-123",
}
clientHeaders := http.Header{
"Abc": []string{"session-abc-456"},
"X-Claude-Code-Session-Id": []string{"claude-code-uuid-789"},
}
ApplyCustomHeadersFromAttrs(req, attrs, clientHeaders)
if got := req.Header.Get("X-Claude-Code-Session-Id"); got != "session-abc-456" {
t.Errorf("X-Claude-Code-Session-Id = %q, want %q", got, "session-abc-456")
}
if got := req.Header.Get("X-Target-Session"); got != "claude-code-uuid-789" {
t.Errorf("X-Target-Session = %q, want %q", got, "claude-code-uuid-789")
}
if got := req.Header.Get("Static-Header"); got != "static-123" {
t.Errorf("Static-Header = %q, want %q", got, "static-123")
}
})
t.Run("absent in clientHeaders does not set header", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "https://api.example.com", nil)
attrs := map[string]string{
"header:X-Claude-Code-Session-Id": "$ABC",
"header:X-Other": "$NONEXISTENT",
"header:Static-Header": "static-123",
}
clientHeaders := http.Header{
"Other-Header": []string{"some-value"},
}
ApplyCustomHeadersFromAttrs(req, attrs, clientHeaders)
if _, exists := req.Header["X-Claude-Code-Session-Id"]; exists {
t.Errorf("expected X-Claude-Code-Session-Id to be omitted when $ABC is absent in clientHeaders, got %q", req.Header.Get("X-Claude-Code-Session-Id"))
}
if _, exists := req.Header["X-Other"]; exists {
t.Errorf("expected X-Other to be omitted when $NONEXISTENT is absent in clientHeaders, got %q", req.Header.Get("X-Other"))
}
if got := req.Header.Get("Static-Header"); got != "static-123" {
t.Errorf("Static-Header = %q, want %q", got, "static-123")
}
})
t.Run("nil clientHeaders does not set variable headers", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "https://api.example.com", nil)
attrs := map[string]string{
"header:X-Claude-Code-Session-Id": "$ABC",
"header:Static-Header": "static-123",
}
ApplyCustomHeadersFromAttrs(req, attrs)
if _, exists := req.Header["X-Claude-Code-Session-Id"]; exists {
t.Errorf("expected X-Claude-Code-Session-Id to be omitted with nil clientHeaders, got %q", req.Header.Get("X-Claude-Code-Session-Id"))
}
if got := req.Header.Get("Static-Header"); got != "static-123" {
t.Errorf("Static-Header = %q, want %q", got, "static-123")
}
})
t.Run("fallback to gin context in request context", func(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
ginCtx, _ := gin.CreateTestContext(w)
ginReq := httptest.NewRequest(http.MethodPost, "/", nil)
ginReq.Header.Set("ABC", "from-gin-ctx-123")
ginCtx.Request = ginReq
req := httptest.NewRequest(http.MethodPost, "https://api.example.com", nil)
req = req.WithContext(ginCtx)
attrs := map[string]string{
"header:X-Claude-Code-Session-Id": "$ABC",
}
ApplyCustomHeadersFromAttrs(req, attrs)
if got := req.Header.Get("X-Claude-Code-Session-Id"); got != "from-gin-ctx-123" {
t.Errorf("X-Claude-Code-Session-Id = %q, want %q", got, "from-gin-ctx-123")
}
})
t.Run("cpa-session-id resolved from context", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "https://api.example.com", nil)
req = req.WithContext(WithSessionID(req.Context(), "claude:session-xyz-123"))
attrs := map[string]string{
"header:X-Session-ID": "$CPA-SESSION-ID",
"header:X-Auth-Token": "Bearer $CPA-SESSION-ID",
"header:X-Lower-Case": "$cpa-session-id",
"header:Static-Header": "static-val",
}
ApplyCustomHeadersFromAttrs(req, attrs)
if got := req.Header.Get("X-Session-ID"); got != "claude:session-xyz-123" {
t.Errorf("X-Session-ID = %q, want %q", got, "claude:session-xyz-123")
}
if got := req.Header.Get("X-Auth-Token"); got != "Bearer claude:session-xyz-123" {
t.Errorf("X-Auth-Token = %q, want %q", got, "Bearer claude:session-xyz-123")
}
if got := req.Header.Get("X-Lower-Case"); got != "claude:session-xyz-123" {
t.Errorf("X-Lower-Case = %q, want %q", got, "claude:session-xyz-123")
}
if got := req.Header.Get("Static-Header"); got != "static-val" {
t.Errorf("Static-Header = %q, want %q", got, "static-val")
}
})
t.Run("cpa-session-id cleared via WithSessionID empty string", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "https://api.example.com", nil)
ctx := WithSessionID(req.Context(), "initial-session-123")
if got := SessionIDFromContext(ctx); got != "initial-session-123" {
t.Fatalf("SessionIDFromContext before clear = %q, want %q", got, "initial-session-123")
}
clearedCtx := WithSessionID(ctx, "")
if got := SessionIDFromContext(clearedCtx); got != "" {
t.Fatalf("SessionIDFromContext after clear = %q, want empty", got)
}
req = req.WithContext(clearedCtx)
attrs := map[string]string{
"header:X-Session-ID": "$CPA-SESSION-ID",
"header:X-Auth-Token": "Bearer $CPA-SESSION-ID",
"header:Static": "ok",
}
ApplyCustomHeadersFromAttrs(req, attrs)
if _, exists := req.Header["X-Session-ID"]; exists {
t.Errorf("expected X-Session-ID to be omitted when cleared, got %q", req.Header.Get("X-Session-ID"))
}
if _, exists := req.Header["X-Auth-Token"]; exists {
t.Errorf("expected X-Auth-Token to be omitted when cleared, got %q", req.Header.Get("X-Auth-Token"))
}
if got := req.Header.Get("Static"); got != "ok" {
t.Errorf("Static = %q, want %q", got, "ok")
}
})
t.Run("cpa-session-id resolved from SessionIDResolver", func(t *testing.T) {
oldResolver := SessionIDResolver
defer func() { SessionIDResolver = oldResolver }()
SessionIDResolver = func(ctx context.Context, clientHeaders http.Header) string {
if clientHeaders != nil && clientHeaders.Get("X-Client-Sess") != "" {
return "resolved:" + clientHeaders.Get("X-Client-Sess")
}
return ""
}
req := httptest.NewRequest(http.MethodPost, "https://api.example.com", nil)
attrs := map[string]string{
"header:X-Forwarded-Session": "$CPA-SESSION-ID",
}
clientHeaders := http.Header{
"X-Client-Sess": []string{"sess-999"},
}
ApplyCustomHeadersFromAttrs(req, attrs, clientHeaders)
if got := req.Header.Get("X-Forwarded-Session"); got != "resolved:sess-999" {
t.Errorf("X-Forwarded-Session = %q, want %q", got, "resolved:sess-999")
}
})
t.Run("cpa-session-id omitted when unresolvable", func(t *testing.T) {
oldResolver := SessionIDResolver
defer func() { SessionIDResolver = oldResolver }()
SessionIDResolver = nil
req := httptest.NewRequest(http.MethodPost, "https://api.example.com", nil)
attrs := map[string]string{
"header:X-Session-ID": "$CPA-SESSION-ID",
"header:X-Auth-Token": "Bearer $CPA-SESSION-ID",
"header:Static": "ok",
}
ApplyCustomHeadersFromAttrs(req, attrs)
if _, exists := req.Header["X-Session-ID"]; exists {
t.Errorf("expected X-Session-ID to be omitted when unresolved, got %q", req.Header.Get("X-Session-ID"))
}
if _, exists := req.Header["X-Auth-Token"]; exists {
t.Errorf("expected X-Auth-Token to be omitted when unresolved, got %q", req.Header.Get("X-Auth-Token"))
}
if got := req.Header.Get("Static"); got != "ok" {
t.Errorf("Static = %q, want %q", got, "ok")
}
})
t.Run("cpa-session-id does not read literal client header CPA-SESSION-ID", func(t *testing.T) {
oldResolver := SessionIDResolver
defer func() { SessionIDResolver = oldResolver }()
SessionIDResolver = nil
req := httptest.NewRequest(http.MethodPost, "https://api.example.com", nil)
attrs := map[string]string{
"header:X-Session-ID": "$CPA-SESSION-ID",
}
clientHeaders := http.Header{
"Cpa-Session-Id": []string{"attacker-spoofed-value"},
}
ApplyCustomHeadersFromAttrs(req, attrs, clientHeaders)
if _, exists := req.Header["X-Session-ID"]; exists {
t.Errorf("expected X-Session-ID not to inherit spoofed client header CPA-SESSION-ID, got %q", req.Header.Get("X-Session-ID"))
}
})
t.Run("cpa-session-id containing magic variable does not infinite loop", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "https://api.example.com", nil)
sessionWithToken := "header:$CPA-SESSION-ID"
req = req.WithContext(WithSessionID(req.Context(), sessionWithToken))
attrs := map[string]string{
"header:X-Session-ID": "$CPA-SESSION-ID",
"header:X-Auth-Token": "Bearer $CPA-SESSION-ID",
"header:X-Auth-Repeat": "Bearer $cpa-session-id-$CPA-SESSION-ID",
}
ApplyCustomHeadersFromAttrs(req, attrs)
if got := req.Header.Get("X-Session-ID"); got != sessionWithToken {
t.Errorf("X-Session-ID = %q, want %q", got, sessionWithToken)
}
wantAuth := "Bearer " + sessionWithToken
if got := req.Header.Get("X-Auth-Token"); got != wantAuth {
t.Errorf("X-Auth-Token = %q, want %q", got, wantAuth)
}
wantRepeat := "Bearer " + sessionWithToken + "-" + sessionWithToken
if got := req.Header.Get("X-Auth-Repeat"); got != wantRepeat {
t.Errorf("X-Auth-Repeat = %q, want %q", got, wantRepeat)
}
})
}