mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
Add redis support for distributed caching (#83)
* Add redis support for distributed caching * Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * ... and another all nighter. * fixup! ... and another all nighter. * fixup! fixup! ... and another all nighter. * fixup! fixup! fixup! ... and another all nighter. * Resolve issue #85 by adding ability to set custom claims in JWT tokens * Remove redundant validation in auth middleware ( issue #89 ) * Add ability to set cookie prefix for session cookies ( #87 ) * fixup! Add ability to set cookie prefix for session cookies ( #87 ) * Add ability to set cookie max age - issue #91 * Potential fix for code scanning alert no. 10: Size computation for allocation may overflow Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixup! Merge main into 0.8.0-redis: resolve conflicts --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent
5fcbd54955
commit
e64fc7f730
@@ -0,0 +1,320 @@
|
||||
// Package compat provides backward compatibility layer during refactoring
|
||||
package compat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CompatibilityLayer provides backward compatibility during the migration
|
||||
type CompatibilityLayer struct {
|
||||
mappings map[string]string // old path -> new path
|
||||
converters map[string]Converter
|
||||
deprecations map[string]string // deprecated field -> warning message
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Converter is a function that converts old value format to new format
|
||||
type Converter func(oldValue interface{}) (newValue interface{}, err error)
|
||||
|
||||
// Global compatibility layer instance
|
||||
var (
|
||||
layer *CompatibilityLayer
|
||||
layerOnce sync.Once
|
||||
)
|
||||
|
||||
// GetLayer returns the global compatibility layer instance
|
||||
func GetLayer() *CompatibilityLayer {
|
||||
layerOnce.Do(func() {
|
||||
layer = &CompatibilityLayer{
|
||||
mappings: make(map[string]string),
|
||||
converters: make(map[string]Converter),
|
||||
deprecations: make(map[string]string),
|
||||
}
|
||||
layer.initialize()
|
||||
})
|
||||
return layer
|
||||
}
|
||||
|
||||
// initialize sets up default compatibility mappings
|
||||
func (c *CompatibilityLayer) initialize() {
|
||||
// Configuration path mappings (old -> new)
|
||||
c.RegisterMapping("ProviderURL", "Provider.IssuerURL")
|
||||
c.RegisterMapping("ClientID", "Provider.ClientID")
|
||||
c.RegisterMapping("ClientSecret", "Provider.ClientSecret")
|
||||
c.RegisterMapping("CallbackURL", "Provider.RedirectURL")
|
||||
c.RegisterMapping("LogoutURL", "Provider.LogoutURL")
|
||||
c.RegisterMapping("SessionEncryptionKey", "Session.EncryptionKey")
|
||||
c.RegisterMapping("Scopes", "Provider.Scopes")
|
||||
c.RegisterMapping("RateLimit", "Middleware.RateLimit")
|
||||
c.RegisterMapping("RefreshGracePeriodSeconds", "Token.RefreshGracePeriod")
|
||||
|
||||
// Redis configuration mappings
|
||||
c.RegisterMapping("RedisAddr", "Redis.Addresses[0]")
|
||||
c.RegisterMapping("RedisPassword", "Redis.Password")
|
||||
c.RegisterMapping("RedisDB", "Redis.DB")
|
||||
|
||||
// Session configuration mappings
|
||||
c.RegisterMapping("SessionName", "Session.Name")
|
||||
c.RegisterMapping("SessionMaxAge", "Session.MaxAge")
|
||||
c.RegisterMapping("SessionSecret", "Session.Secret")
|
||||
c.RegisterMapping("SessionChunkSize", "Session.ChunkSize")
|
||||
|
||||
// Security configuration mappings
|
||||
c.RegisterMapping("ForceHTTPS", "Security.ForceHTTPS")
|
||||
c.RegisterMapping("EnablePKCE", "Security.EnablePKCE")
|
||||
c.RegisterMapping("AllowedUsers", "Security.AllowedUsers")
|
||||
c.RegisterMapping("AllowedUserDomains", "Security.AllowedUserDomains")
|
||||
c.RegisterMapping("AllowedRolesAndGroups", "Security.AllowedRolesAndGroups")
|
||||
c.RegisterMapping("ExcludedURLs", "Security.ExcludedURLs")
|
||||
|
||||
// Register converters for complex transformations
|
||||
c.RegisterConverter("RefreshGracePeriodSeconds", func(oldValue interface{}) (interface{}, error) {
|
||||
// Convert seconds (int) to duration string
|
||||
if seconds, ok := oldValue.(int); ok {
|
||||
return fmt.Sprintf("%ds", seconds), nil
|
||||
}
|
||||
return oldValue, nil
|
||||
})
|
||||
|
||||
// Register deprecations
|
||||
c.RegisterDeprecation("LogLevel", "LogLevel is deprecated, use Logging.Level instead")
|
||||
c.RegisterDeprecation("HTTPClient", "HTTPClient is deprecated, configure via Transport settings")
|
||||
}
|
||||
|
||||
// RegisterMapping registers a field mapping from old to new path
|
||||
func (c *CompatibilityLayer) RegisterMapping(oldPath, newPath string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.mappings[oldPath] = newPath
|
||||
}
|
||||
|
||||
// RegisterConverter registers a value converter for a field
|
||||
func (c *CompatibilityLayer) RegisterConverter(field string, converter Converter) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.converters[field] = converter
|
||||
}
|
||||
|
||||
// RegisterDeprecation registers a deprecation warning for a field
|
||||
func (c *CompatibilityLayer) RegisterDeprecation(field, message string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.deprecations[field] = message
|
||||
}
|
||||
|
||||
// GetMapping returns the new path for an old configuration path
|
||||
func (c *CompatibilityLayer) GetMapping(oldPath string) (string, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
newPath, exists := c.mappings[oldPath]
|
||||
return newPath, exists
|
||||
}
|
||||
|
||||
// Convert applies conversion logic to a value
|
||||
func (c *CompatibilityLayer) Convert(field string, value interface{}) (interface{}, error) {
|
||||
c.mu.RLock()
|
||||
converter, exists := c.converters[field]
|
||||
c.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
return converter(value)
|
||||
}
|
||||
|
||||
// CheckDeprecation checks if a field is deprecated and returns warning message
|
||||
func (c *CompatibilityLayer) CheckDeprecation(field string) (string, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
message, deprecated := c.deprecations[field]
|
||||
return message, deprecated
|
||||
}
|
||||
|
||||
// MigrateMap migrates an old configuration map to new structure
|
||||
func (c *CompatibilityLayer) MigrateMap(oldConfig map[string]interface{}) (map[string]interface{}, []string) {
|
||||
newConfig := make(map[string]interface{})
|
||||
warnings := []string{}
|
||||
|
||||
for key, value := range oldConfig {
|
||||
// Check for deprecation
|
||||
if warning, deprecated := c.CheckDeprecation(key); deprecated {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
|
||||
// Get new path
|
||||
newPath, hasMappming := c.GetMapping(key)
|
||||
if !hasMappming {
|
||||
// No mapping, use as-is
|
||||
newConfig[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply converter if exists
|
||||
convertedValue, err := c.Convert(key, value)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("Failed to convert %s: %v", key, err))
|
||||
convertedValue = value
|
||||
}
|
||||
|
||||
// Set value at new path
|
||||
setNestedValue(newConfig, newPath, convertedValue)
|
||||
}
|
||||
|
||||
return newConfig, warnings
|
||||
}
|
||||
|
||||
// setNestedValue sets a value in a nested map structure using dot notation
|
||||
func setNestedValue(m map[string]interface{}, path string, value interface{}) {
|
||||
keys := splitPath(path)
|
||||
if len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
current := m
|
||||
for i := 0; i < len(keys)-1; i++ {
|
||||
key := keys[i]
|
||||
|
||||
// Check if this key has array notation
|
||||
if isArrayPath(key) {
|
||||
// Handle array notation (e.g., "Addresses[0]")
|
||||
continue // Skip array handling for now, will be handled in actual migration
|
||||
}
|
||||
|
||||
if _, exists := current[key]; !exists {
|
||||
current[key] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Ensure it's a map
|
||||
if next, ok := current[key].(map[string]interface{}); ok {
|
||||
current = next
|
||||
} else {
|
||||
// Can't traverse further, create new map
|
||||
newMap := make(map[string]interface{})
|
||||
current[key] = newMap
|
||||
current = newMap
|
||||
}
|
||||
}
|
||||
|
||||
// Set the final value
|
||||
finalKey := keys[len(keys)-1]
|
||||
current[finalKey] = value
|
||||
}
|
||||
|
||||
// splitPath splits a configuration path into segments
|
||||
func splitPath(path string) []string {
|
||||
segments := []string{}
|
||||
current := ""
|
||||
|
||||
for i := 0; i < len(path); i++ {
|
||||
if path[i] == '.' {
|
||||
if current != "" {
|
||||
segments = append(segments, current)
|
||||
current = ""
|
||||
}
|
||||
} else {
|
||||
current += string(path[i])
|
||||
}
|
||||
}
|
||||
|
||||
if current != "" {
|
||||
segments = append(segments, current)
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
// isArrayPath checks if a path segment contains array notation
|
||||
func isArrayPath(segment string) bool {
|
||||
for _, char := range segment {
|
||||
if char == '[' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ConfigAdapter provides an adapter interface for old code to work with new config
|
||||
type ConfigAdapter struct {
|
||||
newConfig interface{}
|
||||
oldPaths map[string]func() interface{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewConfigAdapter creates a new configuration adapter
|
||||
func NewConfigAdapter(newConfig interface{}) *ConfigAdapter {
|
||||
adapter := &ConfigAdapter{
|
||||
newConfig: newConfig,
|
||||
oldPaths: make(map[string]func() interface{}),
|
||||
}
|
||||
return adapter
|
||||
}
|
||||
|
||||
// RegisterGetter registers a getter function for an old path
|
||||
func (a *ConfigAdapter) RegisterGetter(oldPath string, getter func() interface{}) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.oldPaths[oldPath] = getter
|
||||
}
|
||||
|
||||
// Get retrieves a value using old path notation
|
||||
func (a *ConfigAdapter) Get(oldPath string) (interface{}, bool) {
|
||||
a.mu.RLock()
|
||||
getter, exists := a.oldPaths[oldPath]
|
||||
a.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
// Try to get from new config using reflection
|
||||
return a.getFromNewConfig(oldPath)
|
||||
}
|
||||
|
||||
return getter(), true
|
||||
}
|
||||
|
||||
// getFromNewConfig attempts to retrieve value from new config using reflection
|
||||
func (a *ConfigAdapter) getFromNewConfig(path string) (interface{}, bool) {
|
||||
// Check if there's a mapping for this path
|
||||
compat := GetLayer()
|
||||
if newPath, hasMappming := compat.GetMapping(path); hasMappming {
|
||||
return a.getNestedField(newPath)
|
||||
}
|
||||
|
||||
// Try direct access
|
||||
return a.getNestedField(path)
|
||||
}
|
||||
|
||||
// getNestedField retrieves a nested field value using reflection
|
||||
func (a *ConfigAdapter) getNestedField(path string) (interface{}, bool) {
|
||||
segments := splitPath(path)
|
||||
if len(segments) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(a.newConfig)
|
||||
|
||||
// Dereference pointer if needed
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
for _, segment := range segments {
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
field := v.FieldByName(segment)
|
||||
if !field.IsValid() {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
v = field
|
||||
}
|
||||
|
||||
if v.IsValid() && v.CanInterface() {
|
||||
return v.Interface(), true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
//go:build !yaegi
|
||||
|
||||
package compat
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetLayer_Singleton(t *testing.T) {
|
||||
// Reset global state
|
||||
layerOnce = sync.Once{}
|
||||
layer = nil
|
||||
|
||||
layer1 := GetLayer()
|
||||
layer2 := GetLayer()
|
||||
|
||||
if layer1 != layer2 {
|
||||
t.Error("Expected GetLayer to return same instance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLayer_Initialize(t *testing.T) {
|
||||
// Reset global state
|
||||
layerOnce = sync.Once{}
|
||||
layer = nil
|
||||
|
||||
l := GetLayer()
|
||||
|
||||
// Check default mappings exist
|
||||
if _, exists := l.GetMapping("ProviderURL"); !exists {
|
||||
t.Error("Expected ProviderURL mapping to exist")
|
||||
}
|
||||
|
||||
if _, exists := l.GetMapping("ClientID"); !exists {
|
||||
t.Error("Expected ClientID mapping to exist")
|
||||
}
|
||||
|
||||
// Check deprecations exist
|
||||
if _, deprecated := l.CheckDeprecation("LogLevel"); !deprecated {
|
||||
t.Error("Expected LogLevel to be marked deprecated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterMapping(t *testing.T) {
|
||||
l := &CompatibilityLayer{
|
||||
mappings: make(map[string]string),
|
||||
converters: make(map[string]Converter),
|
||||
deprecations: make(map[string]string),
|
||||
}
|
||||
|
||||
l.RegisterMapping("OldField", "New.Field")
|
||||
|
||||
newPath, exists := l.GetMapping("OldField")
|
||||
if !exists {
|
||||
t.Error("Expected mapping to exist")
|
||||
}
|
||||
|
||||
if newPath != "New.Field" {
|
||||
t.Errorf("Expected 'New.Field', got '%s'", newPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterConverter(t *testing.T) {
|
||||
l := &CompatibilityLayer{
|
||||
mappings: make(map[string]string),
|
||||
converters: make(map[string]Converter),
|
||||
deprecations: make(map[string]string),
|
||||
}
|
||||
|
||||
converter := func(oldValue interface{}) (interface{}, error) {
|
||||
if str, ok := oldValue.(string); ok {
|
||||
return str + "_converted", nil
|
||||
}
|
||||
return oldValue, nil
|
||||
}
|
||||
|
||||
l.RegisterConverter("TestField", converter)
|
||||
|
||||
result, err := l.Convert("TestField", "test")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result != "test_converted" {
|
||||
t.Errorf("Expected 'test_converted', got '%v'", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvert_NoConverter(t *testing.T) {
|
||||
l := &CompatibilityLayer{
|
||||
mappings: make(map[string]string),
|
||||
converters: make(map[string]Converter),
|
||||
deprecations: make(map[string]string),
|
||||
}
|
||||
|
||||
// No converter registered
|
||||
result, err := l.Convert("UnknownField", "value")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if result != "value" {
|
||||
t.Error("Expected original value when no converter exists")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterDeprecation(t *testing.T) {
|
||||
l := &CompatibilityLayer{
|
||||
mappings: make(map[string]string),
|
||||
converters: make(map[string]Converter),
|
||||
deprecations: make(map[string]string),
|
||||
}
|
||||
|
||||
l.RegisterDeprecation("OldField", "This field is deprecated")
|
||||
|
||||
message, deprecated := l.CheckDeprecation("OldField")
|
||||
if !deprecated {
|
||||
t.Error("Expected field to be deprecated")
|
||||
}
|
||||
|
||||
if message != "This field is deprecated" {
|
||||
t.Errorf("Expected deprecation message, got '%s'", message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDeprecation_NotDeprecated(t *testing.T) {
|
||||
l := &CompatibilityLayer{
|
||||
mappings: make(map[string]string),
|
||||
converters: make(map[string]Converter),
|
||||
deprecations: make(map[string]string),
|
||||
}
|
||||
|
||||
_, deprecated := l.CheckDeprecation("NewField")
|
||||
if deprecated {
|
||||
t.Error("Expected field not to be deprecated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateMap_BasicMapping(t *testing.T) {
|
||||
l := &CompatibilityLayer{
|
||||
mappings: make(map[string]string),
|
||||
converters: make(map[string]Converter),
|
||||
deprecations: make(map[string]string),
|
||||
}
|
||||
|
||||
l.RegisterMapping("OldField", "New.Field")
|
||||
|
||||
oldConfig := map[string]interface{}{
|
||||
"OldField": "value123",
|
||||
}
|
||||
|
||||
newConfig, warnings := l.MigrateMap(oldConfig)
|
||||
|
||||
if len(warnings) != 0 {
|
||||
t.Errorf("Expected no warnings, got %d", len(warnings))
|
||||
}
|
||||
|
||||
// Check nested structure
|
||||
if newMap, ok := newConfig["New"].(map[string]interface{}); ok {
|
||||
if val, exists := newMap["Field"]; !exists || val != "value123" {
|
||||
t.Errorf("Expected nested field value 'value123', got %v", val)
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected nested map structure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateMap_WithDeprecation(t *testing.T) {
|
||||
l := &CompatibilityLayer{
|
||||
mappings: make(map[string]string),
|
||||
converters: make(map[string]Converter),
|
||||
deprecations: make(map[string]string),
|
||||
}
|
||||
|
||||
l.RegisterMapping("DeprecatedField", "New.Field")
|
||||
l.RegisterDeprecation("DeprecatedField", "Field is deprecated")
|
||||
|
||||
oldConfig := map[string]interface{}{
|
||||
"DeprecatedField": "value",
|
||||
}
|
||||
|
||||
_, warnings := l.MigrateMap(oldConfig)
|
||||
|
||||
if len(warnings) != 1 {
|
||||
t.Errorf("Expected 1 warning, got %d", len(warnings))
|
||||
}
|
||||
|
||||
if warnings[0] != "Field is deprecated" {
|
||||
t.Errorf("Expected deprecation warning, got '%s'", warnings[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateMap_WithConverter(t *testing.T) {
|
||||
l := &CompatibilityLayer{
|
||||
mappings: make(map[string]string),
|
||||
converters: make(map[string]Converter),
|
||||
deprecations: make(map[string]string),
|
||||
}
|
||||
|
||||
l.RegisterMapping("Seconds", "Duration")
|
||||
l.RegisterConverter("Seconds", func(oldValue interface{}) (interface{}, error) {
|
||||
if seconds, ok := oldValue.(int); ok {
|
||||
return seconds * 1000, nil // Convert to milliseconds
|
||||
}
|
||||
return oldValue, nil
|
||||
})
|
||||
|
||||
oldConfig := map[string]interface{}{
|
||||
"Seconds": 60,
|
||||
}
|
||||
|
||||
newConfig, _ := l.MigrateMap(oldConfig)
|
||||
|
||||
if val, ok := newConfig["Duration"]; !ok || val != 60000 {
|
||||
t.Errorf("Expected Duration to be 60000, got %v", val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateMap_NoMapping(t *testing.T) {
|
||||
l := &CompatibilityLayer{
|
||||
mappings: make(map[string]string),
|
||||
converters: make(map[string]Converter),
|
||||
deprecations: make(map[string]string),
|
||||
}
|
||||
|
||||
oldConfig := map[string]interface{}{
|
||||
"UnmappedField": "value",
|
||||
}
|
||||
|
||||
newConfig, _ := l.MigrateMap(oldConfig)
|
||||
|
||||
if val, ok := newConfig["UnmappedField"]; !ok || val != "value" {
|
||||
t.Error("Expected unmapped field to be copied as-is")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
expected []string
|
||||
}{
|
||||
{"Simple", []string{"Simple"}},
|
||||
{"Nested.Path", []string{"Nested", "Path"}},
|
||||
{"Deep.Nested.Path", []string{"Deep", "Nested", "Path"}},
|
||||
{"", []string{}},
|
||||
{"Single", []string{"Single"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := splitPath(tt.path)
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("Path '%s': expected %d segments, got %d", tt.path, len(tt.expected), len(result))
|
||||
continue
|
||||
}
|
||||
|
||||
for i, segment := range result {
|
||||
if segment != tt.expected[i] {
|
||||
t.Errorf("Path '%s': segment %d expected '%s', got '%s'", tt.path, i, tt.expected[i], segment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsArrayPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
segment string
|
||||
expected bool
|
||||
}{
|
||||
{"Addresses[0]", true},
|
||||
{"Items[5]", true},
|
||||
{"Simple", false},
|
||||
{"NoArray", false},
|
||||
{"[start", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := isArrayPath(tt.segment)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Segment '%s': expected %v, got %v", tt.segment, tt.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetNestedValue_SingleLevel(t *testing.T) {
|
||||
m := make(map[string]interface{})
|
||||
setNestedValue(m, "Field", "value")
|
||||
|
||||
if val, ok := m["Field"]; !ok || val != "value" {
|
||||
t.Error("Expected single level field to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetNestedValue_MultiLevel(t *testing.T) {
|
||||
m := make(map[string]interface{})
|
||||
setNestedValue(m, "Parent.Child", "value")
|
||||
|
||||
parent, ok := m["Parent"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("Expected Parent to be a map")
|
||||
}
|
||||
|
||||
if val, ok := parent["Child"]; !ok || val != "value" {
|
||||
t.Error("Expected nested field to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetNestedValue_DeepNesting(t *testing.T) {
|
||||
m := make(map[string]interface{})
|
||||
setNestedValue(m, "Level1.Level2.Level3", "deep_value")
|
||||
|
||||
level1, ok := m["Level1"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("Expected Level1 to be a map")
|
||||
}
|
||||
|
||||
level2, ok := level1["Level2"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("Expected Level2 to be a map")
|
||||
}
|
||||
|
||||
if val, ok := level2["Level3"]; !ok || val != "deep_value" {
|
||||
t.Error("Expected deeply nested field to be set")
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigAdapter tests
|
||||
|
||||
func TestNewConfigAdapter(t *testing.T) {
|
||||
config := map[string]interface{}{"key": "value"}
|
||||
adapter := NewConfigAdapter(config)
|
||||
|
||||
if adapter == nil {
|
||||
t.Fatal("Expected adapter to be created")
|
||||
}
|
||||
|
||||
if adapter.newConfig == nil {
|
||||
t.Error("Expected config to be stored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigAdapter_RegisterGetter(t *testing.T) {
|
||||
adapter := NewConfigAdapter(nil)
|
||||
|
||||
called := false
|
||||
adapter.RegisterGetter("TestPath", func() interface{} {
|
||||
called = true
|
||||
return "test_value"
|
||||
})
|
||||
|
||||
val, exists := adapter.Get("TestPath")
|
||||
if !exists {
|
||||
t.Error("Expected getter to exist")
|
||||
}
|
||||
|
||||
if val != "test_value" {
|
||||
t.Errorf("Expected 'test_value', got %v", val)
|
||||
}
|
||||
|
||||
if !called {
|
||||
t.Error("Expected getter function to be called")
|
||||
}
|
||||
}
|
||||
|
||||
type TestConfig struct {
|
||||
Provider struct {
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
}
|
||||
Session struct {
|
||||
EncryptionKey string
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigAdapter_GetNestedField(t *testing.T) {
|
||||
config := &TestConfig{}
|
||||
config.Provider.IssuerURL = "https://test.com"
|
||||
config.Provider.ClientID = "test-client"
|
||||
config.Session.EncryptionKey = "secret123"
|
||||
|
||||
adapter := NewConfigAdapter(config)
|
||||
|
||||
// Test nested field access
|
||||
val, exists := adapter.getNestedField("Provider.IssuerURL")
|
||||
if !exists {
|
||||
t.Error("Expected field to exist")
|
||||
}
|
||||
|
||||
if val != "https://test.com" {
|
||||
t.Errorf("Expected 'https://test.com', got %v", val)
|
||||
}
|
||||
|
||||
// Test another nested field
|
||||
val2, exists2 := adapter.getNestedField("Provider.ClientID")
|
||||
if !exists2 || val2 != "test-client" {
|
||||
t.Error("Expected ClientID to be accessible")
|
||||
}
|
||||
|
||||
// Test non-existent field
|
||||
_, exists3 := adapter.getNestedField("NonExistent.Field")
|
||||
if exists3 {
|
||||
t.Error("Expected non-existent field to return false")
|
||||
}
|
||||
}
|
||||
|
||||
// Race condition tests
|
||||
|
||||
func TestCompatibilityLayer_ConcurrentAccess(t *testing.T) {
|
||||
l := &CompatibilityLayer{
|
||||
mappings: make(map[string]string),
|
||||
converters: make(map[string]Converter),
|
||||
deprecations: make(map[string]string),
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Concurrent registrations
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
l.RegisterMapping(string(rune('A'+idx%26)), "New.Field")
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Concurrent reads
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
_, _ = l.GetMapping(string(rune('A' + idx%26)))
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestCompatibilityLayer_ConcurrentMigrate(t *testing.T) {
|
||||
l := &CompatibilityLayer{
|
||||
mappings: make(map[string]string),
|
||||
converters: make(map[string]Converter),
|
||||
deprecations: make(map[string]string),
|
||||
}
|
||||
|
||||
l.RegisterMapping("OldField", "New.Field")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Concurrent migrations
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
oldConfig := map[string]interface{}{
|
||||
"OldField": "value",
|
||||
}
|
||||
_, _ = l.MigrateMap(oldConfig)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestConfigAdapter_ConcurrentAccess(t *testing.T) {
|
||||
config := &TestConfig{}
|
||||
config.Provider.IssuerURL = "https://test.com"
|
||||
|
||||
adapter := NewConfigAdapter(config)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Concurrent getter registrations
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
path := string(rune('A' + idx%26))
|
||||
adapter.RegisterGetter(path, func() interface{} {
|
||||
return "value"
|
||||
})
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Concurrent gets
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
path := string(rune('A' + idx%26))
|
||||
_, _ = adapter.Get(path)
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user