Files
pekoe/internal/config/config_test.go
2026-04-03 00:09:40 +03:00

436 lines
11 KiB
Go

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")
}
}