diff --git a/README.md b/README.md index e1fbe41..ae81c84 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ if err != nil { `Load` reads, parses, and validates a file. `Parse` parses bytes when a caller owns file I/O; call `Config.Validate` before applying a parsed configuration. -The current schema defines one `pawsql` block with a listener, TLS certificate/key paths, and one or more `database` routes. A route has exactly one of: +The current schema defines one `pawsql` block with a listener, an optional TLS `cert`/`key` pair, and one or more `database` routes. A route has exactly one of: - `upstream ` for an external PostgreSQL server. - `postgres { ... }` for a managed PostgreSQL container. Its `image`, `volume`, and `password_env` directives are required. `idle_timeout` and `traffic_idle_timeout` accept Go duration strings. diff --git a/parser_test.go b/parser_test.go index 198f06d..3714675 100644 --- a/parser_test.go +++ b/parser_test.go @@ -81,13 +81,59 @@ func TestValidateMissingFieldsAndDuplicateHostname(t *testing.T) { if err == nil { t.Fatal("Validate() error = nil") } - for _, want := range []string{"TLS private key is required", "upstream or postgres is required", "duplicate hostname"} { + for _, want := range []string{"tls requires both cert and key", "upstream or postgres is required", "duplicate hostname"} { if !strings.Contains(err.Error(), want) { t.Errorf("Validate() error = %q, missing %q", err, want) } } } +func TestValidateAllowsConfigurationWithoutTLS(t *testing.T) { + cfg := Config{ + Listen: ":5432", + Databases: []DatabaseConfig{{ + Name: "analytics", + Upstream: "postgres-foo:5432", + }}, + } + if err := cfg.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } +} + +func TestParseAndValidateWithoutTLSBlock(t *testing.T) { + cfg, err := Parse([]byte(`pawsql { + listen :5432 + database analytics { + upstream postgres-foo:5432 + } +}`)) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + if cfg.TLS.CertFile != "" || cfg.TLS.KeyFile != "" { + t.Errorf("TLS = %#v, want unset", cfg.TLS) + } + if err := cfg.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } +} + +func TestValidateRejectsHalfConfiguredTLS(t *testing.T) { + cfg := Config{ + Listen: ":5432", + TLS: TLSConfig{CertFile: "cert.pem"}, + Databases: []DatabaseConfig{{ + Name: "analytics", + Upstream: "postgres-foo:5432", + }}, + } + err := cfg.Validate() + if err == nil || !strings.Contains(err.Error(), "tls requires both cert and key") { + t.Fatalf("Validate() error = %v, want tls requires both cert and key", err) + } +} + func TestValidateRejectsMalformedUpstreamAddress(t *testing.T) { cfg := Config{ Listen: ":5432", diff --git a/validate.go b/validate.go index 372e839..9a1f304 100644 --- a/validate.go +++ b/validate.go @@ -21,11 +21,10 @@ func (c Config) Validate() error { } else if err := validateListenAddress(c.Listen); err != nil { errs = append(errs, fmt.Errorf("listen address %q: %w", c.Listen, err)) } - if strings.TrimSpace(c.TLS.CertFile) == "" { - errs = append(errs, errors.New("TLS certificate is required")) - } - if strings.TrimSpace(c.TLS.KeyFile) == "" { - errs = append(errs, errors.New("TLS private key is required")) + certificateSet := strings.TrimSpace(c.TLS.CertFile) != "" + keySet := strings.TrimSpace(c.TLS.KeyFile) != "" + if certificateSet != keySet { + errs = append(errs, errors.New("tls requires both cert and key")) } if len(c.Databases) == 0 { errs = append(errs, errors.New("at least one database route is required"))