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
156 lines
5.2 KiB
Go
156 lines
5.2 KiB
Go
//go:build unit
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newSessionHeaderContext(t *testing.T, headers map[string]string) *gin.Context {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
c.Request = req
|
|
return c
|
|
}
|
|
|
|
func TestSanitizeSessionID(t *testing.T) {
|
|
longRunes := strings.Repeat("a", maxPersistedSessionIDLength+50)
|
|
multibyte := strings.Repeat("好", maxPersistedSessionIDLength+10)
|
|
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"empty", "", ""},
|
|
{"whitespace only", " \t ", ""},
|
|
{"trims surrounding whitespace", " sess-123 ", "sess-123"},
|
|
{"plain value", "conv_abc-123.XYZ", "conv_abc-123.XYZ"},
|
|
{"uuid", "550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000"},
|
|
{"reject CR", "sess\r123", ""},
|
|
{"reject LF", "sess\n123", ""},
|
|
{"reject CRLF injection", "sess-1\r\nSet-Cookie: x=y", ""},
|
|
{"reject tab inside", "sess\t123", ""},
|
|
{"reject NUL", "sess\x00123", ""},
|
|
{"reject DEL", "sess\x7f123", ""},
|
|
{"reject invalid UTF-8", string([]byte{'s', 'e', 's', 's', '-', 0xff}), ""},
|
|
{"accepts value at column bound", strings.Repeat("b", maxPersistedSessionIDLength), strings.Repeat("b", maxPersistedSessionIDLength)},
|
|
{"rejects overlong value", longRunes, ""},
|
|
{"rejects overlong multibyte value", multibyte, ""},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := sanitizeSessionID(tc.in)
|
|
require.Equal(t, tc.want, got, "sanitizeSessionID(%q)", tc.in)
|
|
// Sanitized output must never exceed the DB column bound (rune-counted).
|
|
require.LessOrEqual(t, len([]rune(got)), maxPersistedSessionIDLength)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractClientSessionID_NilContext(t *testing.T) {
|
|
require.Equal(t, "", ExtractClientSessionID(nil))
|
|
}
|
|
|
|
func TestExtractClientSessionID_NilRequest(t *testing.T) {
|
|
require.Equal(t, "", ExtractClientSessionID(&gin.Context{}))
|
|
}
|
|
|
|
func TestExtractClientSessionID_AbsentReturnsEmpty(t *testing.T) {
|
|
c := newSessionHeaderContext(t, nil)
|
|
require.Equal(t, "", ExtractClientSessionID(c))
|
|
}
|
|
|
|
func TestExtractClientSessionID_SupportedHeaders(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
header string
|
|
value string
|
|
}{
|
|
{"session_id", "session_id", "sess-A"},
|
|
{"conversation_id", "conversation_id", "conv-B"},
|
|
{"X-Session-Affinity", openCodeSessionAffinityHeader, "aff-C"},
|
|
{"X-Session-Id", openCodeSessionIDHeader, "sid-D"},
|
|
{"X-OpenCode-Session", openCodeNativeSessionHeader, "oc-E"},
|
|
{"X-Conversation-ID", codeBuddyConversationHeader, "cb-F"},
|
|
{"X-Claude-Code-Session-Id", claudeCodeSessionHeader, "cc-G"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c := newSessionHeaderContext(t, map[string]string{tc.header: tc.value})
|
|
require.Equal(t, tc.value, ExtractClientSessionID(c))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractClientSessionID_HeaderPrecedence(t *testing.T) {
|
|
// session_id ranks ahead of conversation_id and the X-* variants.
|
|
c := newSessionHeaderContext(t, map[string]string{
|
|
"session_id": "primary",
|
|
"conversation_id": "secondary",
|
|
openCodeSessionIDHeader: "tertiary",
|
|
codeBuddyConversationHeader: "quaternary",
|
|
})
|
|
require.Equal(t, "primary", ExtractClientSessionID(c))
|
|
}
|
|
|
|
func TestExtractClientSessionID_Sanitizes(t *testing.T) {
|
|
c := newSessionHeaderContext(t, map[string]string{openCodeSessionIDHeader: " clean-123 "})
|
|
require.Equal(t, "clean-123", ExtractClientSessionID(c))
|
|
}
|
|
|
|
func TestExtractClientSessionID_IgnoresNonSessionHeaders(t *testing.T) {
|
|
// prompt_cache_key, request/message ids, and a Grok conversation header on a
|
|
// non-Grok request are NOT persisted as session_id.
|
|
c := newSessionHeaderContext(t, map[string]string{
|
|
"prompt_cache_key": "cache-key-should-not-persist",
|
|
"X-Request-Id": "req-should-not-persist",
|
|
"x-grok-conv-id": "grok-conv-should-not-persist",
|
|
})
|
|
require.Equal(t, "", ExtractClientSessionID(c))
|
|
}
|
|
|
|
func TestExtractClientSessionID_GrokConversationHeader(t *testing.T) {
|
|
c := newSessionHeaderContext(t, map[string]string{
|
|
grokConversationIDHeader: "grok-native-session",
|
|
})
|
|
c.Set("api_key", &APIKey{
|
|
ID: 42,
|
|
Group: &Group{Platform: PlatformGrok},
|
|
})
|
|
|
|
require.Equal(t, "grok-native-session", ExtractClientSessionID(c))
|
|
}
|
|
|
|
func TestExtractClientSessionID_GrokConversationHeaderForCompositeRoute(t *testing.T) {
|
|
c := newSessionHeaderContext(t, map[string]string{
|
|
grokConversationIDHeader: "grok-composite-session",
|
|
})
|
|
c.Set("api_key", &APIKey{
|
|
ID: 43,
|
|
Group: &Group{Platform: PlatformComposite},
|
|
})
|
|
c.Request = c.Request.WithContext(WithResolvedTargetPlatform(context.Background(), PlatformGrok))
|
|
|
|
require.Equal(t, "grok-composite-session", ExtractClientSessionID(c))
|
|
}
|
|
|
|
func TestExtractClientSessionID_InjectionHeaderDropped(t *testing.T) {
|
|
// A supported header carrying a CRLF payload is rejected, not persisted mangled.
|
|
c := newSessionHeaderContext(t, map[string]string{"session_id": "abc"})
|
|
c.Request.Header.Set("session_id", "abc\r\nX-Injected: 1")
|
|
require.Equal(t, "", ExtractClientSessionID(c))
|
|
}
|