Files
waitfordb/internal/config/config_test.go
T
unkin-agent a466b3e07f
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/push/pre-commit Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Add valkey/redis driver with operator-secret auto-configuration
Apps fronted by valkey-operator instances need the same wait-for-ready
initContainer postgres workloads already get, and wiring per-field
secretKeyRefs for operator-generated secrets is boilerplate. waitfordb
now speaks enough RESP to AUTH and PING, and natively understands the
secret shapes CNPG and valkey-operator generate so an initContainer is
just envFrom plus a mode variable.

- valkey driver (alias: redis): fresh TCP connection per attempt,
  optional AUTH (ACL user or default), PING, reusing the existing
  retry/backoff wait loop; redis:///valkey:// DSNs; default port 6379
- WAITFORDB_SECRET_FORMAT=cnpg|valkey for envFrom-injected operator
  secrets: CNPG <cluster>-app host/port/dbname/user/password keys, and
  valkey-operator key-per-username secrets (_operator preferred, or
  WAITFORDB_USER's same-named key)
- autodetection from injected keys (CNPG keys -> postgres, valkey keys
  -> valkey); explicit WAITFORDB_DRIVER/WAITFORDB_* always win, PG*
  fallback and all existing flags unchanged
- valkey needs no user/database to be valid (unauthenticated PING)
- tests: secret shape parsing/autodetect/mismatch, fake RESP server
  covering NOAUTH/WRONGPASS/ACL auth/DSN, retry-until-up wait
- README: envFrom initContainer snippets for CNPG and valkey-operator
2026-08-23 16:36:52 +10:00

330 lines
9.5 KiB
Go

package config
import (
"strings"
"testing"
"time"
)
// envFrom returns a Getenv backed by a map.
func envFrom(m map[string]string) Getenv {
return func(k string) string { return m[k] }
}
func TestLoadDefaults(t *testing.T) {
c, err := Load(envFrom(map[string]string{
"WAITFORDB_USER": "appuser",
"WAITFORDB_DATABASE": "appdb",
"WAITFORDB_HOST": "db",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Driver != "postgres" {
t.Errorf("driver = %q, want postgres", c.Driver)
}
if c.Port != "5432" {
t.Errorf("port = %q, want default 5432", c.Port)
}
if c.Interval != 2*time.Second {
t.Errorf("interval = %v, want 2s", c.Interval)
}
if c.ConnectTimeout != 5*time.Second {
t.Errorf("connect_timeout = %v, want 5s", c.ConnectTimeout)
}
if c.Timeout != 0 {
t.Errorf("timeout = %v, want 0 (forever)", c.Timeout)
}
}
func TestPrecedenceDSNWins(t *testing.T) {
c, err := Load(envFrom(map[string]string{
"WAITFORDB_DSN": "postgres://u:p@h:5432/d",
"WAITFORDB_HOST": "ignored",
"WAITFORDB_USER": "ignored",
"WAITFORDB_DATABASE": "ignored",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.DSN == "" {
t.Fatal("DSN should be set")
}
}
func TestPrecedenceWaitfordbOverPG(t *testing.T) {
c, err := Load(envFrom(map[string]string{
"WAITFORDB_HOST": "native-host",
"PGHOST": "pg-host",
"WAITFORDB_USER": "native-user",
"PGUSER": "pg-user",
"PGDATABASE": "pg-db", // only PG* set for database -> fallback used
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Host != "native-host" {
t.Errorf("host = %q, want native-host (WAITFORDB_* wins)", c.Host)
}
if c.User != "native-user" {
t.Errorf("user = %q, want native-user", c.User)
}
if c.Database != "pg-db" {
t.Errorf("database = %q, want pg-db (PG* fallback)", c.Database)
}
}
func TestPGFallbackPostgresOnly(t *testing.T) {
// For a non-postgres driver the PG* fallback must not apply.
_, err := Load(envFrom(map[string]string{
"WAITFORDB_DRIVER": "mysql",
"PGUSER": "pg-user",
"PGDATABASE": "pg-db",
// No WAITFORDB_USER/DATABASE -> must be a config error, PG* ignored.
}))
if err == nil {
t.Fatal("expected config error: PG* must not satisfy mysql config")
}
}
func TestMySQLDefaultPort(t *testing.T) {
c, err := Load(envFrom(map[string]string{
"WAITFORDB_DRIVER": "mysql",
"WAITFORDB_USER": "u",
"WAITFORDB_DATABASE": "d",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Port != "3306" {
t.Errorf("port = %q, want 3306", c.Port)
}
}
func TestValidationErrors(t *testing.T) {
cases := map[string]map[string]string{
"missing database": {"WAITFORDB_USER": "u"},
"missing user": {"WAITFORDB_DATABASE": "d"},
"bad driver": {"WAITFORDB_DRIVER": "oracle", "WAITFORDB_USER": "u", "WAITFORDB_DATABASE": "d"},
"bad timeout": {"WAITFORDB_USER": "u", "WAITFORDB_DATABASE": "d", "WAITFORDB_TIMEOUT": "nope"},
"zero interval": {"WAITFORDB_USER": "u", "WAITFORDB_DATABASE": "d", "WAITFORDB_INTERVAL": "0"},
}
for name, env := range cases {
t.Run(name, func(t *testing.T) {
if _, err := Load(envFrom(env)); err == nil {
t.Fatalf("expected error for %s", name)
} else if _, ok := err.(*Error); !ok {
t.Fatalf("expected *config.Error, got %T", err)
}
})
}
}
func TestRedactedHidesPassword(t *testing.T) {
c, err := Load(envFrom(map[string]string{
"WAITFORDB_HOST": "h",
"WAITFORDB_USER": "u",
"WAITFORDB_PASSWORD": "sup3r-s3cret",
"WAITFORDB_DATABASE": "d",
"WAITFORDB_TIMEOUT": "5m",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := c.Redacted()
if strings.Contains(got, "sup3r-s3cret") {
t.Fatalf("redacted output leaked password: %q", got)
}
for _, want := range []string{"driver=postgres", "database=d", "user=u", "password=***", "timeout=5m", "interval=2s"} {
if !strings.Contains(got, want) {
t.Errorf("redacted output missing %q: %q", want, got)
}
}
}
func TestRedactedForeverTimeout(t *testing.T) {
c, _ := Load(envFrom(map[string]string{"WAITFORDB_USER": "u", "WAITFORDB_DATABASE": "d"}))
if !strings.Contains(c.Redacted(), "timeout=forever") {
t.Errorf("want timeout=forever, got %q", c.Redacted())
}
}
func TestRedactedDSNMasksPassword(t *testing.T) {
cases := []struct {
name string
dsn string
}{
{"url", "postgres://user:topsecret@host:5432/db?sslmode=disable"},
{"keyword", "host=h user=u password=topsecret dbname=d"},
{"mysql", "user:topsecret@tcp(h:3306)/d?timeout=5s"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := Config{DSN: tc.dsn, Timeout: 0, Interval: time.Second, ConnectTimeout: time.Second, Driver: "postgres"}
got := c.Redacted()
if strings.Contains(got, "topsecret") {
t.Fatalf("leaked password: %q", got)
}
if !strings.Contains(got, "***") {
t.Errorf("expected redaction marker in %q", got)
}
})
}
}
func TestCNPGAutodetect(t *testing.T) {
// Shape of a CNPG <cluster>-app secret injected wholesale via envFrom.
c, err := Load(envFrom(map[string]string{
"host": "mydb-rw.ns.svc",
"port": "5432",
"dbname": "appdb",
"user": "appuser",
"username": "appuser",
"password": "pw",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Driver != "postgres" || c.SecretFormat != FormatCNPG {
t.Errorf("driver=%q format=%q, want postgres/cnpg", c.Driver, c.SecretFormat)
}
if c.Host != "mydb-rw.ns.svc" || c.Database != "appdb" || c.User != "appuser" || c.Password != "pw" {
t.Errorf("fields not filled from CNPG keys: %+v", c)
}
}
func TestCNPGExplicitFormat(t *testing.T) {
c, err := Load(envFrom(map[string]string{
"WAITFORDB_SECRET_FORMAT": "cnpg",
"host": "h",
"dbname": "d",
"username": "u",
"password": "pw",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.SecretFormat != FormatCNPG || c.User != "u" {
t.Errorf("format=%q user=%q, want cnpg/u", c.SecretFormat, c.User)
}
}
func TestWaitfordbOverridesCNPGKeys(t *testing.T) {
c, err := Load(envFrom(map[string]string{
"WAITFORDB_HOST": "explicit-host",
"host": "secret-host",
"dbname": "d",
"user": "u",
"password": "pw",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Host != "explicit-host" {
t.Errorf("host = %q, want explicit-host (WAITFORDB_* wins over secret keys)", c.Host)
}
if c.Database != "d" || c.Password != "pw" {
t.Errorf("unset fields should still fill from secret: %+v", c)
}
}
func TestValkeySystemSecretAutodetect(t *testing.T) {
// Shape of the valkey-operator system-passwords secret: key = username.
c, err := Load(envFrom(map[string]string{
"WAITFORDB_HOST": "myapp-valkey.ns.svc",
"_operator": "op-pass",
"_replication": "repl-pass",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Driver != "valkey" || c.SecretFormat != FormatValkey {
t.Errorf("driver=%q format=%q, want valkey/valkey", c.Driver, c.SecretFormat)
}
if c.User != "_operator" || c.Password != "op-pass" {
t.Errorf("user=%q password=%q, want _operator/op-pass", c.User, c.Password)
}
if c.Port != "6379" {
t.Errorf("port = %q, want default 6379", c.Port)
}
}
func TestValkeyACLUserSecret(t *testing.T) {
// An explicit ACL username whose password arrives as an env key of the
// same name (valkey-operator user secret via envFrom).
c, err := Load(envFrom(map[string]string{
"WAITFORDB_DRIVER": "valkey",
"WAITFORDB_USER": "appuser",
"appuser": "app-pass",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.SecretFormat != FormatValkey || c.Password != "app-pass" {
t.Errorf("format=%q password=%q, want valkey/app-pass", c.SecretFormat, c.Password)
}
}
func TestValkeyExplicitFlagsNoSecret(t *testing.T) {
c, err := Load(envFrom(map[string]string{
"WAITFORDB_DRIVER": "valkey",
"WAITFORDB_HOST": "h",
"WAITFORDB_PASSWORD": "pw",
}))
if err != nil {
t.Fatalf("valkey needs no user/database: %v", err)
}
if c.SecretFormat != "" {
t.Errorf("format = %q, want none without operator secret keys", c.SecretFormat)
}
}
func TestRedisDriverAlias(t *testing.T) {
c, err := Load(envFrom(map[string]string{"WAITFORDB_DRIVER": "redis"}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Driver != "valkey" || c.Port != "6379" {
t.Errorf("driver=%q port=%q, want valkey/6379", c.Driver, c.Port)
}
}
func TestExplicitDriverIgnoresMismatchedDetect(t *testing.T) {
// CNPG-shaped keys with an explicit valkey driver: format must not apply.
c, err := Load(envFrom(map[string]string{
"WAITFORDB_DRIVER": "valkey",
"host": "secret-host",
"dbname": "d",
"user": "u",
"password": "pw",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.SecretFormat != "" {
t.Errorf("format = %q, want none (cnpg does not apply to valkey)", c.SecretFormat)
}
if c.Host != DefaultHost {
t.Errorf("host = %q, want default (secret keys must not fill)", c.Host)
}
}
func TestSecretFormatErrors(t *testing.T) {
cases := map[string]map[string]string{
"unknown format": {"WAITFORDB_SECRET_FORMAT": "vault"},
"format/driver mismatch": {
"WAITFORDB_SECRET_FORMAT": "cnpg",
"WAITFORDB_DRIVER": "valkey",
},
}
for name, env := range cases {
t.Run(name, func(t *testing.T) {
if _, err := Load(envFrom(env)); err == nil {
t.Fatalf("expected error for %s", name)
} else if _, ok := err.(*Error); !ok {
t.Fatalf("expected *config.Error, got %T", err)
}
})
}
}