From 370545997f25e30721973463194778cb52bf4665 Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 7 Aug 2026 17:01:16 +0000 Subject: [PATCH] test: explicit null config values must abort startup (PR #53 rework) An explicitly-null key (port: null, bare port:, debug: ~, and every other config key including metrics subkeys) is a SET value under the no-silent-fallback rule and must abort startup naming the key, instead of silently taking the default as it does today. All 13 subtests fail against the current behavior; the fix follows. --- internal/config/config_validation_test.go | 100 ++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/internal/config/config_validation_test.go b/internal/config/config_validation_test.go index 8b1ef87..f1a58f6 100644 --- a/internal/config/config_validation_test.go +++ b/internal/config/config_validation_test.go @@ -295,6 +295,106 @@ func TestSetButInvalidValueAbortsStartup(t *testing.T) { } } +// TestExplicitNullValueAbortsStartup verifies that a key explicitly +// set to null (including the bare "key:" form and the "~" alias) aborts +// startup naming the key. An explicit null is a SET value: it must +// never silently fall back to the default the way an omitted key does. +func TestExplicitNullValueAbortsStartup(t *testing.T) { + signingKeyLine := "signing_key: " + validTestSigningKey + "\n" + + cases := []struct { + name string + yaml string + // wantErrSubstrings must all appear in the error message. + wantErrSubstrings []string + }{ + { + name: "port explicit null", + yaml: signingKeyLine + "port: null\n", + wantErrSubstrings: []string{"port", "null"}, + }, + { + name: "port bare key no value", + yaml: signingKeyLine + "port:\n", + wantErrSubstrings: []string{"port", "null"}, + }, + { + name: "debug tilde null", + yaml: signingKeyLine + "debug: ~\n", + wantErrSubstrings: []string{"debug", "null"}, + }, + { + name: "maintenance_mode null", + yaml: signingKeyLine + "maintenance_mode: null\n", + wantErrSubstrings: []string{"maintenance_mode", "null"}, + }, + { + name: "allow_http null", + yaml: signingKeyLine + "allow_http: null\n", + wantErrSubstrings: []string{"allow_http", "null"}, + }, + { + name: "state_dir null", + yaml: signingKeyLine + "state_dir: null\n", + wantErrSubstrings: []string{"state_dir", "null"}, + }, + { + name: "db_url null", + yaml: signingKeyLine + "db_url: null\n", + wantErrSubstrings: []string{"db_url", "null"}, + }, + { + name: "sentry_dsn null", + yaml: signingKeyLine + "sentry_dsn: null\n", + wantErrSubstrings: []string{"sentry_dsn", "null"}, + }, + { + name: "upstream_connections_per_host null", + yaml: signingKeyLine + "upstream_connections_per_host: null\n", + wantErrSubstrings: []string{"upstream_connections_per_host", "null"}, + }, + { + name: "allowlist_hosts null", + yaml: signingKeyLine + "allowlist_hosts: null\n", + wantErrSubstrings: []string{"allowlist_hosts", "null"}, + }, + { + name: "signing_key null", + yaml: "signing_key: null\n", + wantErrSubstrings: []string{"signing_key", "null"}, + }, + { + name: "metrics null", + yaml: signingKeyLine + "metrics: null\n", + wantErrSubstrings: []string{"metrics", "null"}, + }, + { + name: "metrics subkeys null", + yaml: signingKeyLine + "metrics:\n username: null\n password: null\n", + wantErrSubstrings: []string{ + "metrics.username", "metrics.password", "null", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c, err := configFromYAML(t, tc.yaml) + if err == nil { + t.Fatalf("config with %s must abort startup, got config: %+v", tc.name, c) + } + + t.Logf("got expected error: %v", err) + + for _, want := range tc.wantErrSubstrings { + if !strings.Contains(err.Error(), want) { + t.Errorf("error %q does not mention %q", err.Error(), want) + } + } + }) + } +} + func TestUnknownTopLevelKeyAbortsStartup(t *testing.T) { yamlContent := `signing_key: ` + validTestSigningKey + ` whitelist_hosts: