package config import ( "os" "testing" ) // nolint: cyclop func TestSetDefaults(t *testing.T) { tests := []struct { name string cfg *Config check func(*Config) }{ { name: "empty config gets all defaults", cfg: &Config{}, check: func(c *Config) { if c.Bind != DefaultBind { t.Errorf("Bind = %q, want %q", c.Bind, DefaultBind) } if c.Port != DefaultPort { t.Errorf("Port = %d, want %d", c.Port, DefaultPort) } if c.MaxClients != DefaultMaxClients { t.Errorf("MaxClients = %d, want %d", c.MaxClients, DefaultMaxClients) } if c.MaxMsgBytes != DefaultMaxMsgBytes { t.Errorf("MaxMsgBytes = %d, want %d", c.MaxMsgBytes, DefaultMaxMsgBytes) } if c.LogLevel != DefaultLogLevel { t.Errorf("LogLevel = %q, want %q", c.LogLevel, DefaultLogLevel) } }, }, { name: "port zero uses default", cfg: &Config{Port: 0}, check: func(c *Config) { if c.Port != DefaultPort { t.Errorf("Port = %d, want %d", c.Port, DefaultPort) } }, }, { name: "port out of range uses default", cfg: &Config{Port: 70000}, check: func(c *Config) { if c.Port != DefaultPort { t.Errorf("Port = %d, want %d", c.Port, DefaultPort) } }, }, { name: "negative port uses default", cfg: &Config{Port: -1}, check: func(c *Config) { if c.Port != DefaultPort { t.Errorf("Port = %d, want %d", c.Port, DefaultPort) } }, }, { name: "negative timeout becomes zero", cfg: &Config{ReadTimeout: -5, WriteTimeout: -10}, check: func(c *Config) { if c.ReadTimeout != 0 { t.Errorf("ReadTimeout = %d, want 0", c.ReadTimeout) } if c.WriteTimeout != 0 { t.Errorf("WriteTimeout = %d, want 0", c.WriteTimeout) } }, }, { name: "zero max_clients uses default", cfg: &Config{MaxClients: 0}, check: func(c *Config) { if c.MaxClients != DefaultMaxClients { t.Errorf("MaxClients = %d, want %d", c.MaxClients, DefaultMaxClients) } }, }, { name: "zero max_message_bytes uses default", cfg: &Config{MaxMsgBytes: 0}, check: func(c *Config) { if c.MaxMsgBytes != DefaultMaxMsgBytes { t.Errorf("MaxMsgBytes = %d, want %d", c.MaxMsgBytes, DefaultMaxMsgBytes) } }, }, { name: "empty log_level uses default", cfg: &Config{LogLevel: ""}, check: func(c *Config) { if c.LogLevel != DefaultLogLevel { t.Errorf("LogLevel = %q, want %q", c.LogLevel, DefaultLogLevel) } }, }, { name: "valid values are preserved", cfg: &Config{ Bind: "127.0.0.1", Port: 6380, MaxClients: 500, ReadTimeout: 30, WriteTimeout: 60, MaxMsgBytes: 1024, LogLevel: "debug", }, check: func(c *Config) { if c.Bind != "127.0.0.1" { t.Errorf("Bind = %q, want 127.0.0.1", c.Bind) } if c.Port != 6380 { t.Errorf("Port = %d, want 6380", c.Port) } if c.MaxClients != 500 { t.Errorf("MaxClients = %d, want 500", c.MaxClients) } if c.ReadTimeout != 30 { t.Errorf("ReadTimeout = %d, want 30", c.ReadTimeout) } if c.WriteTimeout != 60 { t.Errorf("WriteTimeout = %d, want 60", c.WriteTimeout) } if c.MaxMsgBytes != 1024 { t.Errorf("MaxMsgBytes = %d, want 1024", c.MaxMsgBytes) } if c.LogLevel != "debug" { t.Errorf("LogLevel = %q, want debug", c.LogLevel) } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.cfg.SetDefaults() tt.check(tt.cfg) }) } } func TestValidate(t *testing.T) { tests := []struct { name string cfg *Config wantErr bool errMsg string }{ { name: "valid config passes", cfg: &Config{Bind: "0.0.0.0", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"}, wantErr: false, }, { name: "valid IPv6 bind passes", cfg: &Config{Bind: "::", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"}, wantErr: false, }, { name: "valid IPv6 loopback passes", cfg: &Config{Bind: "::1", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"}, wantErr: false, }, { name: "empty bind fails", cfg: &Config{Bind: "", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"}, wantErr: true, errMsg: "bind address cannot be empty", }, { name: "invalid bind fails", cfg: &Config{Bind: "not-an-ip", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"}, wantErr: true, errMsg: "bind must be a valid IP address (e.g. 0.0.0.0, ::, ::1, 192.168.1.1)", }, { name: "port zero fails", cfg: &Config{Bind: "0.0.0.0", Port: 0, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"}, wantErr: true, errMsg: "port must be between 1 and 65535", }, { name: "port out of range fails", cfg: &Config{Bind: "0.0.0.0", Port: 70000, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"}, wantErr: true, errMsg: "port must be between 1 and 65535", }, { name: "negative port fails", cfg: &Config{Bind: "0.0.0.0", Port: -1, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"}, wantErr: true, errMsg: "port must be between 1 and 65535", }, { name: "zero max_clients fails", cfg: &Config{Bind: "0.0.0.0", Port: 6379, MaxClients: 0, MaxMsgBytes: 1024, LogLevel: "info"}, wantErr: true, errMsg: "max_clients must be positive", }, { name: "negative max_clients fails", cfg: &Config{Bind: "0.0.0.0", Port: 6379, MaxClients: -5, MaxMsgBytes: 1024, LogLevel: "info"}, wantErr: true, errMsg: "max_clients must be positive", }, { name: "zero max_message_bytes fails", cfg: &Config{Bind: "0.0.0.0", Port: 6379, MaxClients: 100, MaxMsgBytes: 0, LogLevel: "info"}, wantErr: true, errMsg: "max_message_bytes must be positive", }, { name: "invalid log_level fails", cfg: &Config{Bind: "0.0.0.0", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "invalid"}, wantErr: true, errMsg: "invalid log_level: invalid (must be debug, info, warn, or error)", }, { name: "valid log levels pass", cfg: &Config{Bind: "0.0.0.0", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "debug"}, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.cfg.Validate() if tt.wantErr { if err == nil { t.Errorf("Validate() expected error, got nil") return } if err.Error() != tt.errMsg { t.Errorf("Validate() error = %q, want %q", err.Error(), tt.errMsg) } } else if err != nil { t.Errorf("Validate() unexpected error: %v", err) } }) } } // nolint: cyclop func TestLoad(t *testing.T) { tmpfile, err := os.CreateTemp("", "config*.json") if err != nil { t.Fatal(err) } defer func() { _ = os.Remove(tmpfile.Name()) }() content := `{ "bind": "127.0.0.1", "port": 6380, "max_clients": 500, "read_timeout": 30, "write_timeout": 60, "max_message_bytes": 1048576, "log_level": "warn" }` if _, err := tmpfile.WriteString(content); err != nil { t.Fatal(err) } if err := tmpfile.Close(); err != nil { t.Fatal(err) } cfg, err := Load(tmpfile.Name()) if err != nil { t.Fatalf("Load() error: %v", err) } if cfg.Bind != "127.0.0.1" { t.Errorf("Bind = %q, want 127.0.0.1", cfg.Bind) } if cfg.Port != 6380 { t.Errorf("Port = %d, want 6380", cfg.Port) } if cfg.MaxClients != 500 { t.Errorf("MaxClients = %d, want 500", cfg.MaxClients) } if cfg.ReadTimeout != 30 { t.Errorf("ReadTimeout = %d, want 30", cfg.ReadTimeout) } if cfg.WriteTimeout != 60 { t.Errorf("WriteTimeout = %d, want 60", cfg.WriteTimeout) } if cfg.MaxMsgBytes != 1048576 { t.Errorf("MaxMsgBytes = %d, want 1048576", cfg.MaxMsgBytes) } if cfg.LogLevel != "warn" { t.Errorf("LogLevel = %q, want warn", cfg.LogLevel) } } func TestLoadInvalidFile(t *testing.T) { _, err := Load("nonexistent.json") if err == nil { t.Error("Load() expected error for nonexistent file") } } func TestLoadInvalidJSON(t *testing.T) { tmpfile, err := os.CreateTemp("", "config*.json") if err != nil { t.Fatal(err) } defer func() { _ = os.Remove(tmpfile.Name()) }() if _, err := tmpfile.WriteString("not valid json"); err != nil { t.Fatal(err) } if err := tmpfile.Close(); err != nil { t.Fatal(err) } _, err = Load(tmpfile.Name()) if err == nil { t.Error("Load() expected error for invalid JSON") } } func TestLoadAppliesDefaults(t *testing.T) { tmpfile, err := os.CreateTemp("", "config*.json") if err != nil { t.Fatal(err) } defer func() { _ = os.Remove(tmpfile.Name()) }() content := `{"bind": "127.0.0.1", "port": 6379}` if _, err := tmpfile.WriteString(content); err != nil { t.Fatal(err) } if err := tmpfile.Close(); err != nil { t.Fatal(err) } cfg, err := Load(tmpfile.Name()) if err != nil { t.Fatalf("Load() error: %v", err) } if cfg.MaxClients != DefaultMaxClients { t.Errorf("MaxClients = %d, want default %d", cfg.MaxClients, DefaultMaxClients) } if cfg.LogLevel != DefaultLogLevel { t.Errorf("LogLevel = %q, want default %q", cfg.LogLevel, DefaultLogLevel) } } func TestAddr(t *testing.T) { tests := []struct { cfg *Config want string }{ // IPv4 {&Config{Bind: "0.0.0.0", Port: 6379}, "0.0.0.0:6379"}, {&Config{Bind: "127.0.0.1", Port: 6380}, "127.0.0.1:6380"}, {&Config{Bind: "192.168.1.100", Port: 8080}, "192.168.1.100:8080"}, // IPv6 — must be wrapped in brackets {&Config{Bind: "::", Port: 6379}, "[::]:6379"}, {&Config{Bind: "::1", Port: 6379}, "[::1]:6379"}, {&Config{Bind: "2001:db8::1", Port: 8080}, "[2001:db8::1]:8080"}, {&Config{Bind: "fe80::1", Port: 6379}, "[fe80::1]:6379"}, } for _, tt := range tests { got := tt.cfg.Addr() if got != tt.want { t.Errorf("Addr(bind=%q, port=%d) = %q, want %q", tt.cfg.Bind, tt.cfg.Port, got, tt.want) } } } func TestReadTimeoutDuration(t *testing.T) { tests := []struct { name string cfg *Config expected string }{ {"zero returns zero duration", &Config{ReadTimeout: 0}, "0s"}, {"30 seconds", &Config{ReadTimeout: 30}, "30s"}, {"120 seconds", &Config{ReadTimeout: 120}, "2m0s"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.cfg.ReadTimeoutDuration().String() if got != tt.expected { t.Errorf("ReadTimeoutDuration() = %s, want %s", got, tt.expected) } }) } } func TestWriteTimeoutDuration(t *testing.T) { tests := []struct { name string cfg *Config expected string }{ {"zero returns zero duration", &Config{WriteTimeout: 0}, "0s"}, {"60 seconds", &Config{WriteTimeout: 60}, "1m0s"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.cfg.WriteTimeoutDuration().String() if got != tt.expected { t.Errorf("WriteTimeoutDuration() = %s, want %s", got, tt.expected) } }) } } func TestString(t *testing.T) { cfg := &Config{ Bind: "0.0.0.0", Port: 6379, MaxClients: 10000, ReadTimeout: 0, WriteTimeout: 0, MaxMsgBytes: 536870912, LogLevel: "info", } str := cfg.String() if len(str) == 0 { t.Error("String() returned empty string") } }