You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

541 lines
14 KiB

package protocol
import (
"net"
"reflect"
"testing"
)
func TestExtractClientHost(t *testing.T) {
tests := []struct {
name string
connCtx *ConnectionContext
expected string
}{
{
name: "Nil connection context",
connCtx: nil,
expected: "unknown",
},
{
name: "TCP address",
connCtx: &ConnectionContext{
RemoteAddr: &net.TCPAddr{
IP: net.ParseIP("192.168.1.100"),
Port: 54321,
},
},
expected: "192.168.1.100",
},
{
name: "TCP address with IPv6",
connCtx: &ConnectionContext{
RemoteAddr: &net.TCPAddr{
IP: net.ParseIP("::1"),
Port: 54321,
},
},
expected: "::1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExtractClientHost(tt.connCtx)
if result != tt.expected {
t.Errorf("ExtractClientHost() = %v, want %v", result, tt.expected)
}
})
}
}
func TestParseConsumerProtocolMetadata(t *testing.T) {
tests := []struct {
name string
metadata []byte
strategy string
want *ConsumerProtocolMetadata
wantErr bool
}{
{
name: "Empty metadata",
metadata: []byte{},
strategy: "range",
want: &ConsumerProtocolMetadata{
Version: 0,
Topics: []string{},
UserData: []byte{},
AssignmentStrategy: "range",
},
wantErr: false,
},
{
name: "Valid metadata with topics",
metadata: func() []byte {
data := make([]byte, 0)
// Version (2 bytes)
data = append(data, 0, 1)
// Topics count (4 bytes) - 2 topics
data = append(data, 0, 0, 0, 2)
// Topic 1: "topic-a"
data = append(data, 0, 7) // length
data = append(data, []byte("topic-a")...)
// Topic 2: "topic-b"
data = append(data, 0, 7) // length
data = append(data, []byte("topic-b")...)
// UserData length (4 bytes) - 5 bytes
data = append(data, 0, 0, 0, 5)
// UserData content
data = append(data, []byte("hello")...)
return data
}(),
strategy: "roundrobin",
want: &ConsumerProtocolMetadata{
Version: 1,
Topics: []string{"topic-a", "topic-b"},
UserData: []byte("hello"),
AssignmentStrategy: "roundrobin",
},
wantErr: false,
},
{
name: "Metadata too short for version (handled gracefully)",
metadata: []byte{0}, // Only 1 byte
strategy: "range",
want: &ConsumerProtocolMetadata{
Version: 0,
Topics: []string{},
UserData: []byte{},
AssignmentStrategy: "range",
},
wantErr: false, // Should handle gracefully, not error
},
{
name: "Unreasonable topics count",
metadata: func() []byte {
data := make([]byte, 0)
data = append(data, 0, 1) // version
data = append(data, 0xFF, 0xFF, 0xFF, 0xFF) // huge topics count
return data
}(),
strategy: "range",
want: nil,
wantErr: true,
},
{
name: "Topic name too long",
metadata: func() []byte {
data := make([]byte, 0)
data = append(data, 0, 1) // version
data = append(data, 0, 0, 0, 1) // 1 topic
data = append(data, 0xFF, 0xFF) // huge topic name length
return data
}(),
strategy: "sticky",
want: nil,
wantErr: true,
},
{
name: "Valid metadata with empty topic name (should skip)",
metadata: func() []byte {
data := make([]byte, 0)
data = append(data, 0, 1) // version
data = append(data, 0, 0, 0, 2) // 2 topics
// Topic 1: empty name
data = append(data, 0, 0) // length 0
// Topic 2: "valid-topic"
data = append(data, 0, 11) // length
data = append(data, []byte("valid-topic")...)
// UserData length (4 bytes) - 0 bytes
data = append(data, 0, 0, 0, 0)
return data
}(),
strategy: "range",
want: &ConsumerProtocolMetadata{
Version: 1,
Topics: []string{"valid-topic"},
UserData: []byte{},
AssignmentStrategy: "range",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseConsumerProtocolMetadata(tt.metadata, tt.strategy)
if (err != nil) != tt.wantErr {
t.Errorf("ParseConsumerProtocolMetadata() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseConsumerProtocolMetadata() = %v, want %v", got, tt.want)
}
})
}
}
func TestGenerateConsumerProtocolMetadata(t *testing.T) {
tests := []struct {
name string
topics []string
userData []byte
}{
{
name: "No topics, no user data",
topics: []string{},
userData: []byte{},
},
{
name: "Single topic, no user data",
topics: []string{"test-topic"},
userData: []byte{},
},
{
name: "Multiple topics with user data",
topics: []string{"topic-1", "topic-2", "topic-3"},
userData: []byte("user-data-content"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Generate metadata
generated := GenerateConsumerProtocolMetadata(tt.topics, tt.userData)
// Parse it back
parsed, err := ParseConsumerProtocolMetadata(generated, "test")
if err != nil {
t.Fatalf("Failed to parse generated metadata: %v", err)
}
// Verify topics match
if !reflect.DeepEqual(parsed.Topics, tt.topics) {
t.Errorf("Generated topics = %v, want %v", parsed.Topics, tt.topics)
}
// Verify user data matches
if !reflect.DeepEqual(parsed.UserData, tt.userData) {
t.Errorf("Generated user data = %v, want %v", parsed.UserData, tt.userData)
}
// Verify version is 1
if parsed.Version != 1 {
t.Errorf("Generated version = %v, want 1", parsed.Version)
}
})
}
}
func TestValidateAssignmentStrategy(t *testing.T) {
tests := []struct {
strategy string
valid bool
}{
{"range", true},
{"roundrobin", true},
{"sticky", true},
{"cooperative-sticky", false}, // Not implemented yet
{"unknown", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.strategy, func(t *testing.T) {
result := ValidateAssignmentStrategy(tt.strategy)
if result != tt.valid {
t.Errorf("ValidateAssignmentStrategy(%s) = %v, want %v", tt.strategy, result, tt.valid)
}
})
}
}
func TestExtractTopicsFromMetadata(t *testing.T) {
// Create test metadata for range protocol
rangeMetadata := GenerateConsumerProtocolMetadata([]string{"topic-a", "topic-b"}, []byte{})
roundrobinMetadata := GenerateConsumerProtocolMetadata([]string{"topic-x", "topic-y"}, []byte{})
invalidMetadata := []byte{0xFF, 0xFF} // Invalid metadata
tests := []struct {
name string
protocols []GroupProtocol
fallbackTopics []string
expectedTopics []string
}{
{
name: "Extract from range protocol",
protocols: []GroupProtocol{
{Name: "range", Metadata: rangeMetadata},
{Name: "roundrobin", Metadata: roundrobinMetadata},
},
fallbackTopics: []string{"fallback"},
expectedTopics: []string{"topic-a", "topic-b"},
},
{
name: "Invalid metadata, use fallback",
protocols: []GroupProtocol{
{Name: "range", Metadata: invalidMetadata},
},
fallbackTopics: []string{"fallback-topic"},
expectedTopics: []string{"fallback-topic"},
},
{
name: "No protocols, use fallback",
protocols: []GroupProtocol{},
fallbackTopics: []string{"fallback-topic"},
expectedTopics: []string{"fallback-topic"},
},
{
name: "No protocols, no fallback, use default",
protocols: []GroupProtocol{},
fallbackTopics: []string{},
expectedTopics: []string{"test-topic"},
},
{
name: "Unsupported protocol, use fallback",
protocols: []GroupProtocol{
{Name: "unsupported", Metadata: rangeMetadata},
},
fallbackTopics: []string{"fallback-topic"},
expectedTopics: []string{"fallback-topic"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExtractTopicsFromMetadata(tt.protocols, tt.fallbackTopics)
if !reflect.DeepEqual(result, tt.expectedTopics) {
t.Errorf("ExtractTopicsFromMetadata() = %v, want %v", result, tt.expectedTopics)
}
})
}
}
func TestSelectBestProtocol(t *testing.T) {
tests := []struct {
name string
clientProtocols []GroupProtocol
groupProtocols []string
expected string
}{
{
name: "Prefer sticky over roundrobin",
clientProtocols: []GroupProtocol{
{Name: "range", Metadata: []byte{}},
{Name: "roundrobin", Metadata: []byte{}},
{Name: "sticky", Metadata: []byte{}},
},
groupProtocols: []string{"range", "roundrobin", "sticky"},
expected: "sticky",
},
{
name: "Prefer roundrobin over range",
clientProtocols: []GroupProtocol{
{Name: "range", Metadata: []byte{}},
{Name: "roundrobin", Metadata: []byte{}},
},
groupProtocols: []string{"range", "roundrobin"},
expected: "roundrobin",
},
{
name: "Only range available",
clientProtocols: []GroupProtocol{
{Name: "range", Metadata: []byte{}},
},
groupProtocols: []string{"range"},
expected: "range",
},
{
name: "Client supports sticky but group doesn't",
clientProtocols: []GroupProtocol{
{Name: "sticky", Metadata: []byte{}},
{Name: "range", Metadata: []byte{}},
},
groupProtocols: []string{"range", "roundrobin"},
expected: "range",
},
{
name: "No group protocols specified (new group)",
clientProtocols: []GroupProtocol{
{Name: "sticky", Metadata: []byte{}},
{Name: "roundrobin", Metadata: []byte{}},
},
groupProtocols: []string{}, // Empty = new group
expected: "sticky",
},
{
name: "No supported protocols, fallback to range",
clientProtocols: []GroupProtocol{
{Name: "unsupported", Metadata: []byte{}},
},
groupProtocols: []string{"range"},
expected: "range", // Last resort fallback
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SelectBestProtocol(tt.clientProtocols, tt.groupProtocols)
if result != tt.expected {
t.Errorf("SelectBestProtocol() = %v, want %v", result, tt.expected)
}
})
}
}
func TestSanitizeConsumerGroupID(t *testing.T) {
tests := []struct {
name string
groupID string
want string
wantErr bool
}{
{
name: "Valid group ID",
groupID: "test-group",
want: "test-group",
wantErr: false,
},
{
name: "Group ID with spaces (trimmed)",
groupID: " spaced-group ",
want: "spaced-group",
wantErr: false,
},
{
name: "Empty group ID",
groupID: "",
want: "",
wantErr: true,
},
{
name: "Group ID too long",
groupID: string(make([]byte, 256)), // 256 characters
want: "",
wantErr: true,
},
{
name: "Group ID with control characters",
groupID: "test\x00group",
want: "",
wantErr: true,
},
{
name: "Group ID with tab character",
groupID: "test\tgroup",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := SanitizeConsumerGroupID(tt.groupID)
if (err != nil) != tt.wantErr {
t.Errorf("SanitizeConsumerGroupID() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("SanitizeConsumerGroupID() = %v, want %v", got, tt.want)
}
})
}
}
func TestAnalyzeProtocolMetadata(t *testing.T) {
// Create valid metadata
validMetadata := GenerateConsumerProtocolMetadata([]string{"topic-1", "topic-2"}, []byte("userdata"))
// Create invalid metadata
invalidMetadata := []byte{0xFF}
protocols := []GroupProtocol{
{Name: "range", Metadata: validMetadata},
{Name: "roundrobin", Metadata: invalidMetadata},
{Name: "sticky", Metadata: []byte{}}, // Empty but should not error
}
result := AnalyzeProtocolMetadata(protocols)
if len(result) != 3 {
t.Fatalf("Expected 3 protocol analyses, got %d", len(result))
}
// Check range protocol (should parse successfully)
rangeInfo := result[0]
if rangeInfo.Strategy != "range" {
t.Errorf("Expected strategy 'range', got '%s'", rangeInfo.Strategy)
}
if !rangeInfo.ParsedOK {
t.Errorf("Expected range protocol to parse successfully")
}
if rangeInfo.TopicCount != 2 {
t.Errorf("Expected 2 topics, got %d", rangeInfo.TopicCount)
}
// Check roundrobin protocol (with invalid metadata, handled gracefully)
roundrobinInfo := result[1]
if roundrobinInfo.Strategy != "roundrobin" {
t.Errorf("Expected strategy 'roundrobin', got '%s'", roundrobinInfo.Strategy)
}
// Note: We now handle invalid metadata gracefully, so it should parse successfully with empty topics
if !roundrobinInfo.ParsedOK {
t.Errorf("Expected roundrobin protocol to be handled gracefully")
}
if roundrobinInfo.TopicCount != 0 {
t.Errorf("Expected 0 topics for invalid metadata, got %d", roundrobinInfo.TopicCount)
}
// Check sticky protocol (empty metadata should not error but return empty topics)
stickyInfo := result[2]
if stickyInfo.Strategy != "sticky" {
t.Errorf("Expected strategy 'sticky', got '%s'", stickyInfo.Strategy)
}
if !stickyInfo.ParsedOK {
t.Errorf("Expected empty metadata to parse successfully")
}
if stickyInfo.TopicCount != 0 {
t.Errorf("Expected 0 topics for empty metadata, got %d", stickyInfo.TopicCount)
}
}
// Benchmark tests for performance validation
func BenchmarkParseConsumerProtocolMetadata(b *testing.B) {
// Create realistic metadata with multiple topics
topics := []string{"topic-1", "topic-2", "topic-3", "topic-4", "topic-5"}
userData := []byte("some-user-data-content")
metadata := GenerateConsumerProtocolMetadata(topics, userData)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = ParseConsumerProtocolMetadata(metadata, "range")
}
}
func BenchmarkExtractClientHost(b *testing.B) {
connCtx := &ConnectionContext{
RemoteAddr: &net.TCPAddr{
IP: net.ParseIP("192.168.1.100"),
Port: 54321,
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ExtractClientHost(connCtx)
}
}
func BenchmarkSelectBestProtocol(b *testing.B) {
protocols := []GroupProtocol{
{Name: "range", Metadata: []byte{}},
{Name: "roundrobin", Metadata: []byte{}},
{Name: "sticky", Metadata: []byte{}},
}
groupProtocols := []string{"range", "roundrobin", "sticky"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = SelectBestProtocol(protocols, groupProtocols)
}
}