package redisconn_test import ( "context" "strings" "testing" "time" "galaxy/redisconn" "github.com/alicebob/miniredis/v2" "go.opentelemetry.io/otel/metric/noop" tracenoop "go.opentelemetry.io/otel/trace/noop" ) func TestDefaultConfigReturnsExpectedTuning(t *testing.T) { t.Parallel() cfg := redisconn.DefaultConfig() if cfg.OperationTimeout != redisconn.DefaultOperationTimeout { t.Fatalf("operation timeout = %v, want %v", cfg.OperationTimeout, redisconn.DefaultOperationTimeout) } if cfg.DB != redisconn.DefaultDB { t.Fatalf("db = %d, want %d", cfg.DB, redisconn.DefaultDB) } } func TestConfigValidateRejectsInvalidValues(t *testing.T) { t.Parallel() tests := []struct { name string mutate func(*redisconn.Config) wantSub string }{ { name: "missing master", mutate: func(c *redisconn.Config) { c.MasterAddr = "" }, wantSub: "master addr", }, { name: "missing password", mutate: func(c *redisconn.Config) { c.Password = "" }, wantSub: "password", }, { name: "blank replica entry", mutate: func(c *redisconn.Config) { c.ReplicaAddrs = []string{" "} }, wantSub: "replica addr", }, { name: "negative db", mutate: func(c *redisconn.Config) { c.DB = -1 }, wantSub: "db must not be negative", }, { name: "non-positive timeout", mutate: func(c *redisconn.Config) { c.OperationTimeout = 0 }, wantSub: "operation timeout", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() cfg := redisconn.DefaultConfig() cfg.MasterAddr = "127.0.0.1:6379" cfg.Password = "secret" tt.mutate(&cfg) err := cfg.Validate() if err == nil { t.Fatalf("expected validate error, got nil") } if !strings.Contains(err.Error(), tt.wantSub) { t.Fatalf("error %q does not contain %q", err, tt.wantSub) } }) } } func TestLoadFromEnvHappyPath(t *testing.T) { const prefix = "TESTSVC" t.Setenv(prefix+"_REDIS_MASTER_ADDR", "127.0.0.1:6379") t.Setenv(prefix+"_REDIS_REPLICA_ADDRS", "127.0.0.1:6380, 127.0.0.1:6381 ,") t.Setenv(prefix+"_REDIS_PASSWORD", "secret") t.Setenv(prefix+"_REDIS_DB", "3") t.Setenv(prefix+"_REDIS_OPERATION_TIMEOUT", "500ms") cfg, err := redisconn.LoadFromEnv(prefix) if err != nil { t.Fatalf("load from env: %v", err) } if cfg.MasterAddr != "127.0.0.1:6379" { t.Fatalf("master addr = %q", cfg.MasterAddr) } if cfg.Password != "secret" { t.Fatalf("password = %q", cfg.Password) } if got, want := cfg.DB, 3; got != want { t.Fatalf("db = %d, want %d", got, want) } if got, want := cfg.OperationTimeout, 500*time.Millisecond; got != want { t.Fatalf("operation timeout = %v, want %v", got, want) } if got, want := len(cfg.ReplicaAddrs), 2; got != want { t.Fatalf("replica count = %d, want %d", got, want) } } func TestLoadFromEnvRejectsDeprecatedTLSEnabled(t *testing.T) { const prefix = "TESTSVC" t.Setenv(prefix+"_REDIS_MASTER_ADDR", "127.0.0.1:6379") t.Setenv(prefix+"_REDIS_PASSWORD", "secret") t.Setenv(prefix+"_REDIS_TLS_ENABLED", "true") _, err := redisconn.LoadFromEnv(prefix) if err == nil { t.Fatal("expected error when TLS_ENABLED is set") } if !strings.Contains(err.Error(), "TLS_ENABLED") { t.Fatalf("error %q should name TLS_ENABLED", err) } if !strings.Contains(err.Error(), "ARCHITECTURE.md") { t.Fatalf("error %q should reference ARCHITECTURE.md", err) } } func TestLoadFromEnvRejectsDeprecatedUsername(t *testing.T) { const prefix = "TESTSVC" t.Setenv(prefix+"_REDIS_MASTER_ADDR", "127.0.0.1:6379") t.Setenv(prefix+"_REDIS_PASSWORD", "secret") t.Setenv(prefix+"_REDIS_USERNAME", "anything") _, err := redisconn.LoadFromEnv(prefix) if err == nil { t.Fatal("expected error when USERNAME is set") } if !strings.Contains(err.Error(), "USERNAME") { t.Fatalf("error %q should name USERNAME", err) } } func TestLoadFromEnvRequiresPassword(t *testing.T) { const prefix = "TESTSVC" t.Setenv(prefix+"_REDIS_MASTER_ADDR", "127.0.0.1:6379") t.Setenv(prefix+"_REDIS_PASSWORD", "") if _, err := redisconn.LoadFromEnv(prefix); err == nil { t.Fatal("expected error when password is empty") } } func TestNewMasterClientPingsMiniredis(t *testing.T) { t.Parallel() server := miniredis.RunT(t) server.RequireAuth("secret") cfg := redisconn.DefaultConfig() cfg.MasterAddr = server.Addr() cfg.Password = "secret" if err := cfg.Validate(); err != nil { t.Fatalf("validate: %v", err) } client := redisconn.NewMasterClient(cfg) t.Cleanup(func() { _ = client.Close() }) if err := redisconn.Ping(context.Background(), client, cfg.OperationTimeout); err != nil { t.Fatalf("ping miniredis: %v", err) } } func TestNewReplicaClientsReturnsExpectedLength(t *testing.T) { t.Parallel() server1 := miniredis.RunT(t) server2 := miniredis.RunT(t) cfg := redisconn.DefaultConfig() cfg.MasterAddr = "ignored:6379" cfg.Password = "secret" cfg.ReplicaAddrs = []string{server1.Addr(), server2.Addr()} clients := redisconn.NewReplicaClients(cfg) t.Cleanup(func() { for _, client := range clients { _ = client.Close() } }) if got, want := len(clients), 2; got != want { t.Fatalf("client count = %d, want %d", got, want) } } func TestNewReplicaClientsReturnsNilWhenUnconfigured(t *testing.T) { t.Parallel() cfg := redisconn.DefaultConfig() cfg.MasterAddr = "ignored:6379" cfg.Password = "secret" if clients := redisconn.NewReplicaClients(cfg); clients != nil { t.Fatalf("clients = %v, want nil", clients) } } func TestInstrumentAcceptsNoopProviders(t *testing.T) { t.Parallel() server := miniredis.RunT(t) server.RequireAuth("secret") cfg := redisconn.DefaultConfig() cfg.MasterAddr = server.Addr() cfg.Password = "secret" client := redisconn.NewMasterClient(cfg) t.Cleanup(func() { _ = client.Close() }) err := redisconn.Instrument( client, redisconn.WithTracerProvider(tracenoop.NewTracerProvider()), redisconn.WithMeterProvider(noop.NewMeterProvider()), ) if err != nil { t.Fatalf("instrument: %v", err) } if err := redisconn.Ping(context.Background(), client, cfg.OperationTimeout); err != nil { t.Fatalf("ping after instrument: %v", err) } } func TestInstrumentRejectsNilClient(t *testing.T) { t.Parallel() if err := redisconn.Instrument(nil); err == nil { t.Fatal("expected error for nil client") } }