package barkfile import ( "errors" "strings" "testing" "time" ) func TestParseValidConfiguration(t *testing.T) { cfg, err := Parse([]byte(`pawsql { listen :5432 tls { cert "./certs/fullchain.pem" key ./certs/privkey.pem } database gramps { hostname Gramps.PawSQL.Barkstack.Dev upstream 192.168.27.10:5432 } }`)) if err != nil { t.Fatalf("Parse() error = %v", err) } if cfg.Listen != ":5432" { t.Errorf("Listen = %q", cfg.Listen) } if cfg.TLS.CertFile != "./certs/fullchain.pem" || cfg.TLS.KeyFile != "./certs/privkey.pem" { t.Errorf("TLS = %#v", cfg.TLS) } if len(cfg.Databases) != 1 { t.Fatalf("databases = %d", len(cfg.Databases)) } if got := cfg.Databases[0]; got.Name != "gramps" || got.Hostname != "Gramps.PawSQL.Barkstack.Dev" || got.Upstream != "192.168.27.10:5432" { t.Errorf("database = %#v", got) } if err := cfg.Validate(); err != nil { t.Fatalf("Validate() error = %v", err) } } func TestParseCommentsAndMultipleDatabases(t *testing.T) { cfg, err := Parse([]byte(`# external comment pawsql { listen :5432 # client port tls { cert cert.pem # inline key key.pem } database one { hostname one.pawsql.test upstream postgres-one:5432 } # route another application database two { hostname two.pawsql.test upstream 127.0.0.1:55432 } }`)) if err != nil { t.Fatalf("Parse() error = %v", err) } if len(cfg.Databases) != 2 { t.Fatalf("databases = %d, want 2", len(cfg.Databases)) } if cfg.Databases[1].Name != "two" || cfg.Databases[1].Upstream != "127.0.0.1:55432" { t.Errorf("second database = %#v", cfg.Databases[1]) } } func TestValidateMissingFieldsAndDuplicateHostname(t *testing.T) { cfg, err := Parse([]byte(`pawsql { listen :5432 tls { cert cert.pem } database one { hostname FOO.pawsql.test } database two { hostname foo.pawsql.test upstream postgres-two:5432 } }`)) if err != nil { t.Fatalf("Parse() error = %v", err) } err = cfg.Validate() if err == nil { t.Fatal("Validate() error = nil") } for _, want := range []string{"TLS private key is required", "upstream or postgres is required", "duplicate hostname"} { if !strings.Contains(err.Error(), want) { t.Errorf("Validate() error = %q, missing %q", err, want) } } } func TestValidateRejectsMalformedUpstreamAddress(t *testing.T) { cfg := Config{ Listen: ":5432", TLS: TLSConfig{CertFile: "cert.pem", KeyFile: "key.pem"}, Databases: []DatabaseConfig{{ Name: "foo", Hostname: "foo.pawsql.test", Upstream: "postgres-foo", }}, } err := cfg.Validate() if err == nil || !strings.Contains(err.Error(), "missing port") { t.Fatalf("Validate() error = %v, want malformed upstream address", err) } } func TestValidateAllowsDatabaseWithoutHostname(t *testing.T) { cfg := Config{ Listen: ":5432", TLS: TLSConfig{CertFile: "cert.pem", KeyFile: "key.pem"}, Databases: []DatabaseConfig{{ Name: "analytics", Upstream: "postgres-foo:5432", }}, } if err := cfg.Validate(); err != nil { t.Fatalf("Validate() error = %v", err) } } func TestValidateRejectsDuplicateDatabaseName(t *testing.T) { cfg := Config{ Listen: ":5432", TLS: TLSConfig{CertFile: "cert.pem", KeyFile: "key.pem"}, Databases: []DatabaseConfig{ {Name: "analytics", Upstream: "postgres-foo:5432"}, {Name: "analytics", Upstream: "postgres-bar:5432"}, }, } err := cfg.Validate() if err == nil || !strings.Contains(err.Error(), "duplicate database name") { t.Fatalf("Validate() error = %v, want duplicate database name", err) } } func TestParsePostgresContainer(t *testing.T) { cfg, err := Parse([]byte(`pawsql { listen :5432 tls { cert cert.pem key key.pem } database analytics { postgres { image postgres:18 volume analytics-data password_env ANALYTICS_POSTGRES_PASSWORD idle_timeout 15m traffic_idle_timeout 1h } } }`)) if err != nil { t.Fatal(err) } database := cfg.Databases[0] if database.Postgres == nil || database.Postgres.Image != "postgres:18" || database.Postgres.Volume != "analytics-data" || database.Postgres.PasswordEnv != "ANALYTICS_POSTGRES_PASSWORD" || database.Postgres.IdleTimeout != 15*time.Minute || database.Postgres.TrafficIdleTimeout != time.Hour { t.Errorf("Postgres = %#v", database.Postgres) } if err := cfg.Validate(); err != nil { t.Fatalf("Validate() error = %v", err) } } func TestParseRejectsInvalidPostgresIdleTimeout(t *testing.T) { _, err := Parse([]byte(`pawsql { listen :5432 tls { cert cert.pem key key.pem } database analytics { postgres { image postgres:18 volume analytics-data password_env ANALYTICS_POSTGRES_PASSWORD idle_timeout whenever } } }`)) if err == nil || !strings.Contains(err.Error(), "invalid idle_timeout") { t.Fatalf("Parse() error = %v, want invalid idle_timeout", err) } } func TestParseRejectsInvalidPostgresTrafficIdleTimeout(t *testing.T) { _, err := Parse([]byte(`pawsql { listen :5432 tls { cert cert.pem key key.pem } database analytics { postgres { image postgres:18 volume analytics-data password_env ANALYTICS_POSTGRES_PASSWORD traffic_idle_timeout whenever } } }`)) if err == nil || !strings.Contains(err.Error(), "invalid traffic_idle_timeout") { t.Fatalf("Parse() error = %v, want invalid traffic_idle_timeout", err) } } func TestValidateAllowsSupportedPostgresImages(t *testing.T) { for _, image := range []string{"postgres:16", "postgres:17", "postgres:18"} { cfg := Config{ Listen: ":5432", TLS: TLSConfig{CertFile: "cert.pem", KeyFile: "key.pem"}, Databases: []DatabaseConfig{{ Name: "analytics", Postgres: &PostgresConfig{Image: image, Volume: "analytics-data", PasswordEnv: "ANALYTICS_POSTGRES_PASSWORD"}, }}, } if err := cfg.Validate(); err != nil { t.Errorf("Validate() image %q error = %v", image, err) } } } func TestValidateRejectsUnsupportedPostgresImage(t *testing.T) { cfg := Config{ Listen: ":5432", TLS: TLSConfig{CertFile: "cert.pem", KeyFile: "key.pem"}, Databases: []DatabaseConfig{{ Name: "analytics", Postgres: &PostgresConfig{Image: "postgres:15", Volume: "analytics-data", PasswordEnv: "ANALYTICS_POSTGRES_PASSWORD"}, }}, } err := cfg.Validate() if err == nil || !strings.Contains(err.Error(), "postgres:16") { t.Fatalf("Validate() error = %v, want unsupported PostgreSQL image", err) } } func TestParseMalformedBlocksReportLine(t *testing.T) { _, err := Parse([]byte("pawsql {\n tls {\n cert cert.pem\n")) if err == nil { t.Fatal("Parse() error = nil") } if !strings.Contains(err.Error(), "line 4") { t.Errorf("error = %q, want line number", err) } _, err = Parse([]byte("pawsql {\n listen :5432 unexpected\n}")) if err == nil { t.Fatal("Parse() error = nil for trailing directive") } if errors.Is(err, nil) { t.Fatal("unexpected nil error") } }