Add distributor notification config validation

This commit is contained in:
2026-06-07 23:18:39 +00:00
parent 7c8d9191c1
commit a2ba6f5382
7 changed files with 544 additions and 0 deletions

View File

@@ -29,6 +29,30 @@ func TestDefaults(t *testing.T) {
if cfg.Secrets.Directory != "" {
t.Fatalf("Secrets.Directory = %q, want empty", cfg.Secrets.Directory)
}
if cfg.Notify.Distributor.Enabled {
t.Fatalf("Notify.Distributor.Enabled = true, want false")
}
if cfg.Notify.Distributor.Endpoint != "https://distributor.example.com" {
t.Fatalf("Notify.Distributor.Endpoint = %q, want default endpoint", cfg.Notify.Distributor.Endpoint)
}
if cfg.Notify.Distributor.TokenEnv != "DISTRIBUTOR_UPLOAD_TOKEN" {
t.Fatalf("Notify.Distributor.TokenEnv = %q, want DISTRIBUTOR_UPLOAD_TOKEN", cfg.Notify.Distributor.TokenEnv)
}
if cfg.Notify.Distributor.Timeout != 30*time.Second {
t.Fatalf("Notify.Distributor.Timeout = %s, want 30s", cfg.Notify.Distributor.Timeout)
}
if cfg.Notify.Distributor.FailurePolicy != NotifyFailureError {
t.Fatalf("Notify.Distributor.FailurePolicy = %q, want error", cfg.Notify.Distributor.FailurePolicy)
}
if cfg.Notify.Distributor.BundleIDTemplate != "weatherreporter.{location_id}.{report_id}.{run_id}" {
t.Fatalf("Notify.Distributor.BundleIDTemplate = %q, want default", cfg.Notify.Distributor.BundleIDTemplate)
}
if cfg.Notify.Distributor.IdempotencyKeyTemplate != "{bundle_id}" {
t.Fatalf("Notify.Distributor.IdempotencyKeyTemplate = %q, want default", cfg.Notify.Distributor.IdempotencyKeyTemplate)
}
if cfg.Notify.Distributor.ReportPathTemplate != "{batch_output_name}" {
t.Fatalf("Notify.Distributor.ReportPathTemplate = %q, want default", cfg.Notify.Distributor.ReportPathTemplate)
}
if cfg.MissingSource.Default != MissingSourceWarn {
t.Fatalf("MissingSource.Default = %q, want warn", cfg.MissingSource.Default)
}
@@ -116,6 +140,256 @@ func TestLoadAppliesOverrides(t *testing.T) {
}
}
func TestDisabledDistributorNotifyAcceptsOmittedFields(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yml")
if err := os.WriteFile(path, []byte("notify:\n distributor:\n enabled: false\n"), 0o600); err != nil {
t.Fatalf("write config fixture: %v", err)
}
cfg, err := LoadFile(path)
if err != nil {
t.Fatalf("LoadFile() error = %v", err)
}
if cfg.Notify.Distributor.Enabled {
t.Fatalf("Notify.Distributor.Enabled = true, want false")
}
}
func TestEnabledDistributorNotifyValidation(t *testing.T) {
tests := []struct {
name string
mutate func(*Config)
wantErr string
}{
{
name: "Endpoint",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.Endpoint = "distributor.example.com"
},
wantErr: "notify.distributor.endpoint",
},
{
name: "TokenEnvEmpty",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.TokenEnv = ""
},
wantErr: "notify.distributor.token_env",
},
{
name: "TokenEnvInvalid",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.TokenEnv = "1TOKEN"
},
wantErr: "notify.distributor.token_env",
},
{
name: "Timeout",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.Timeout = 0
},
wantErr: "notify.distributor.timeout",
},
{
name: "FailurePolicy",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.FailurePolicy = "warn"
},
wantErr: "notify.distributor.failure_policy",
},
{
name: "BundleTemplate",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.BundleIDTemplate = "{unknown}"
},
wantErr: "notify.distributor.bundle_id_template",
},
{
name: "IdempotencyTemplate",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.IdempotencyKeyTemplate = "{unknown}"
},
wantErr: "notify.distributor.idempotency_key_template",
},
{
name: "ReportPathTemplateUnknown",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.ReportPathTemplate = "{bundle_id}"
},
wantErr: "notify.distributor.report_path_template",
},
{
name: "ReportPathTemplateInvalidPath",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.ReportPathTemplate = "/{batch_output_name}"
},
wantErr: "notify.distributor.report_path_template",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := Defaults()
cfg.Notify.Distributor.Enabled = true
tt.mutate(&cfg)
err := Validate(cfg)
if err == nil {
t.Fatal("Validate() error = nil, want error")
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("error = %q, want %q", err.Error(), tt.wantErr)
}
})
}
}
func TestDistributorTemplateRendering(t *testing.T) {
values := DistributorTemplateValues{
LocationID: "home",
ReportID: "daily",
RunID: "20260607T120000Z",
ArtifactGroup: "daily",
BatchOutputName: "daily.md",
BundleID: "weatherreporter.home.daily.20260607T120000Z",
}
bundleID, err := RenderDistributorBundleID("weatherreporter.{location_id}.{report_id}.{run_id}", values)
if err != nil {
t.Fatalf("RenderDistributorBundleID() error = %v", err)
}
if bundleID != "weatherreporter.home.daily.20260607T120000Z" {
t.Fatalf("bundleID = %q, want rendered value", bundleID)
}
idempotencyKey, err := RenderDistributorIdempotencyKey("{bundle_id}", values)
if err != nil {
t.Fatalf("RenderDistributorIdempotencyKey() error = %v", err)
}
if idempotencyKey != "weatherreporter.home.daily.20260607T120000Z" {
t.Fatalf("idempotencyKey = %q, want rendered bundle ID", idempotencyKey)
}
reportPath, err := RenderDistributorReportPath("reports/{batch_output_name}", values)
if err != nil {
t.Fatalf("RenderDistributorReportPath() error = %v", err)
}
if reportPath != "reports/daily.md" {
t.Fatalf("reportPath = %q, want reports/daily.md", reportPath)
}
}
func TestDistributorTemplateRejectsUnknownAndMalformedVariables(t *testing.T) {
tests := []struct {
name string
template string
}{
{name: "Unknown", template: "{unknown}"},
{name: "Unclosed", template: "{location_id"},
{name: "Unopened", template: "location_id}"},
{name: "Empty", template: "{}"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := RenderDistributorBundleID(tt.template, DistributorTemplateValues{})
if err == nil {
t.Fatal("RenderDistributorBundleID() error = nil, want error")
}
})
}
}
func TestDistributorReportPathValidation(t *testing.T) {
tests := []struct {
name string
path string
ok bool
}{
{name: "Simple", path: "daily.md", ok: true},
{name: "Nested", path: "reports/daily.md", ok: true},
{name: "Empty", path: "", ok: false},
{name: "Absolute", path: "/reports/daily.md", ok: false},
{name: "WindowsAbsolute", path: "C:/reports/daily.md", ok: false},
{name: "Backslash", path: `reports\daily.md`, ok: false},
{name: "CurrentSegment", path: "reports/./daily.md", ok: false},
{name: "ParentSegment", path: "reports/../daily.md", ok: false},
{name: "EmptySegment", path: "reports//daily.md", ok: false},
{name: "Manifest", path: "reports/manifest.json", ok: false},
{name: "DistributorMetadata", path: "reports/.distributor.json", ok: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateDistributorReportPath(tt.path)
if tt.ok && err != nil {
t.Fatalf("ValidateDistributorReportPath() error = %v", err)
}
if !tt.ok && err == nil {
t.Fatal("ValidateDistributorReportPath() error = nil, want error")
}
})
}
}
func TestDistributorReportPathRenderingRejectsInvalidValues(t *testing.T) {
tests := []struct {
name string
batchOutputName string
}{
{name: "Absolute", batchOutputName: "/daily.md"},
{name: "Backslash", batchOutputName: `reports\daily.md`},
{name: "CurrentSegment", batchOutputName: "./daily.md"},
{name: "ParentSegment", batchOutputName: "../daily.md"},
{name: "EmptySegment", batchOutputName: "reports//daily.md"},
{name: "Manifest", batchOutputName: "manifest.json"},
{name: "DistributorMetadata", batchOutputName: ".distributor.json"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := RenderDistributorReportPath("{batch_output_name}", DistributorTemplateValues{
BatchOutputName: tt.batchOutputName,
})
if err == nil {
t.Fatal("RenderDistributorReportPath() error = nil, want error")
}
})
}
}
func TestLoadFileLoadsSecretsBeforeReturningNotifyConfig(t *testing.T) {
dir := t.TempDir()
secretsDir := filepath.Join(dir, "secrets")
if err := os.Mkdir(secretsDir, 0o700); err != nil {
t.Fatalf("create secrets directory: %v", err)
}
if err := os.WriteFile(filepath.Join(secretsDir, "DISTRIBUTOR_UPLOAD_TOKEN"), []byte("loaded-token"), 0o600); err != nil {
t.Fatalf("write secret: %v", err)
}
path := filepath.Join(dir, "config.yml")
configYAML := "secrets:\n" +
" directory: " + secretsDir + "\n" +
"notify:\n" +
" distributor:\n" +
" enabled: true\n"
if err := os.WriteFile(path, []byte(configYAML), 0o600); err != nil {
t.Fatalf("write config fixture: %v", err)
}
t.Setenv("DISTRIBUTOR_UPLOAD_TOKEN", "")
cfg, err := LoadFile(path)
if err != nil {
t.Fatalf("LoadFile() error = %v", err)
}
if cfg.Notify.Distributor.TokenEnv != "DISTRIBUTOR_UPLOAD_TOKEN" {
t.Fatalf("TokenEnv = %q, want DISTRIBUTOR_UPLOAD_TOKEN", cfg.Notify.Distributor.TokenEnv)
}
if got := os.Getenv(cfg.Notify.Distributor.TokenEnv); got != "loaded-token" {
t.Fatalf("environment value = %q, want loaded-token", got)
}
}
func TestLoadSecretsDisabledLeavesEnvironmentUnchanged(t *testing.T) {
t.Setenv("WEATHERREPORTER_DISABLED_SECRET", "original")