package config import ( "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" ) func TestDefaults(t *testing.T) { cfg, err := Load(LoadOptions{}) if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.WeatherAPI.Units != "us" { t.Fatalf("Units = %q, want us", cfg.WeatherAPI.Units) } if cfg.WeatherAPI.Timezone != "America/Chicago" { t.Fatalf("Timezone = %q, want America/Chicago", cfg.WeatherAPI.Timezone) } if cfg.WeatherAPI.Format != "json" { t.Fatalf("Format = %q, want json", cfg.WeatherAPI.Format) } if cfg.Location.ID != "home" || cfg.Location.Name != "Brentwood" || cfg.Location.Region != "St. Louis Metro" { t.Fatalf("Location = %#v, want home/Brentwood/St. Louis Metro", cfg.Location) } 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.PipelineIDTemplate != "" { t.Fatalf("Notify.Distributor.PipelineIDTemplate = %q, want empty", cfg.Notify.Distributor.PipelineIDTemplate) } if cfg.Notify.Distributor.BundleIDTemplate != "weatherreporter.{location_id}.{report_id}" { t.Fatalf("Notify.Distributor.BundleIDTemplate = %q, want default", cfg.Notify.Distributor.BundleIDTemplate) } if cfg.Notify.Distributor.IdempotencyKeyTemplate != "{bundle_id}.{run_id}" { t.Fatalf("Notify.Distributor.IdempotencyKeyTemplate = %q, want default", cfg.Notify.Distributor.IdempotencyKeyTemplate) } wantReportPaths := []string{ "{valid_start_date}/{artifact_group}/{valid_start_date}-{artifact_group}-{run_id}.md", } if strings.Join(cfg.Notify.Distributor.ReportPathTemplates, "\n") != strings.Join(wantReportPaths, "\n") { t.Fatalf("Notify.Distributor.ReportPathTemplates = %#v, want %#v", cfg.Notify.Distributor.ReportPathTemplates, wantReportPaths) } if cfg.MissingSource.Default != MissingSourceWarn { t.Fatalf("MissingSource.Default = %q, want warn", cfg.MissingSource.Default) } } func TestLoadExampleConfig(t *testing.T) { cfg, err := LoadFile(filepath.Join("..", "..", "examples", "config.yml")) if err != nil { t.Fatalf("LoadFile() error = %v", err) } if cfg.WeatherAPI.BaseURL != "https://weather.api.rakestrawhome.com/" { t.Fatalf("BaseURL = %q, want configured example URL", cfg.WeatherAPI.BaseURL) } if cfg.WeatherAPI.Timeout != 15*time.Second { t.Fatalf("Timeout = %s, want 15s", cfg.WeatherAPI.Timeout) } if cfg.MissingSource.Sources["alerts"] != MissingSourceNone { t.Fatalf("alerts policy = %q, want none", cfg.MissingSource.Sources["alerts"]) } if cfg.Location.ID != "home" || cfg.Location.Name != "Brentwood" || cfg.Location.Region != "St. Louis Metro" { t.Fatalf("Location = %#v, want example location", cfg.Location) } if cfg.Notify.Distributor.PipelineIDTemplate != "weatherreporter.{artifact_group}" { t.Fatalf("PipelineIDTemplate = %q, want example pipeline template", cfg.Notify.Distributor.PipelineIDTemplate) } if len(cfg.Notify.Distributor.ReportPathTemplates) != 1 { t.Fatalf("ReportPathTemplates = %#v, want example archive path", cfg.Notify.Distributor.ReportPathTemplates) } } func TestLoadMinimalExampleConfig(t *testing.T) { cfg, err := LoadFile(filepath.Join("..", "..", "examples", "minimal-config.yml")) if err != nil { t.Fatalf("LoadFile() error = %v", err) } if cfg.WeatherAPI.BaseURL != "https://weather.api.example.com/" { t.Fatalf("BaseURL = %q, want example URL", cfg.WeatherAPI.BaseURL) } if cfg.WeatherAPI.Units != "us" { t.Fatalf("Units = %q, want default us", cfg.WeatherAPI.Units) } if cfg.Scriptorium.Binary != "scriptorium" { t.Fatalf("Scriptorium.Binary = %q, want default scriptorium", cfg.Scriptorium.Binary) } if cfg.Workspace.Root != "workspace" { t.Fatalf("Workspace.Root = %q, want default workspace", cfg.Workspace.Root) } if cfg.Workspace.NotificationsDir != "notifications" { t.Fatalf("Workspace.NotificationsDir = %q, want notifications", cfg.Workspace.NotificationsDir) } if cfg.Location.Name != "Brentwood" { t.Fatalf("Location.Name = %q, want default Brentwood", cfg.Location.Name) } } func TestLoadReportModuleOverrides(t *testing.T) { path := writeConfig(t, ` reports: daily: deterministic_modules: - metadata - current_conditions - id: area_forecast_discussion options: sections: - short_term `) cfg, err := LoadFile(path) if err != nil { t.Fatalf("LoadFile() error = %v", err) } overrides := cfg.ReportModuleOverrides() items := overrides[report.DailyToday] if len(items) != 3 { t.Fatalf("daily override length = %d, want 3", len(items)) } if items[0].ID != module.Metadata || items[1].ID != module.CurrentConditions || items[2].ID != module.AreaForecastDiscussion { t.Fatalf("daily override = %#v, want configured module order", items) } options, ok := items[2].Options.(module.AreaForecastDiscussionOptions) if !ok { t.Fatalf("AFD options type = %T, want AreaForecastDiscussionOptions", items[2].Options) } if strings.Join(options.Sections, ",") != "short_term" { t.Fatalf("AFD sections = %#v, want short_term", options.Sections) } } func TestReportModuleOverrideValidation(t *testing.T) { tests := []struct { name string yaml string wantErr string }{ { name: "UnknownReport", yaml: ` reports: moon: deterministic_modules: - metadata `, wantErr: "reports.moon", }, { name: "UnknownModule", yaml: ` reports: daily: deterministic_modules: - missing_module `, wantErr: `unknown module "missing_module"`, }, { name: "DuplicateModule", yaml: ` reports: daily: deterministic_modules: - metadata - metadata `, wantErr: `duplicate module "metadata"`, }, { name: "IncompatibleModule", yaml: ` reports: daily: deterministic_modules: - tomorrow_planning `, wantErr: `not compatible with report "daily_today"`, }, { name: "RemovedPlaceholderModule", yaml: ` reports: daily: deterministic_modules: - forecast_delta `, wantErr: `unknown module "forecast_delta"`, }, { name: "InvalidOptions", yaml: ` reports: daily: deterministic_modules: - id: metadata options: sections: - short_term `, wantErr: "options are invalid", }, { name: "DuplicateReportAlias", yaml: ` reports: daily: deterministic_modules: - metadata daily_today: deterministic_modules: - current_conditions `, wantErr: "duplicates report override", }, { name: "UnknownReportField", yaml: ` reports: daily: modules: - metadata `, wantErr: `unknown report entry field "modules"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := LoadFile(writeConfig(t, tt.yaml)) if err == nil { t.Fatal("LoadFile() error = nil, want validation error") } if !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("error = %q, want %q", err.Error(), tt.wantErr) } }) } } func TestExplicitMissingConfigReturnsError(t *testing.T) { _, err := LoadFile(filepath.Join(t.TempDir(), "missing.yml")) if err == nil { t.Fatal("LoadFile() error = nil, want missing file error") } if !strings.Contains(err.Error(), "read config") { t.Fatalf("error = %q, want read config context", err.Error()) } } func writeConfig(t *testing.T, contents string) string { t.Helper() path := filepath.Join(t.TempDir(), "config.yml") if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { t.Fatalf("write config fixture: %v", err) } return path } func TestInvalidConfigProducesActionableError(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yml") if err := os.WriteFile(path, []byte("missing_source:\n default: explode\n"), 0o600); err != nil { t.Fatalf("write config fixture: %v", err) } _, err := LoadFile(path) if err == nil { t.Fatal("LoadFile() error = nil, want validation error") } if !strings.Contains(err.Error(), "missing_source.default") { t.Fatalf("error = %q, want field path", err.Error()) } } func TestLoadAppliesOverrides(t *testing.T) { cfg, err := Load(LoadOptions{Units: "metric", Timezone: "+09:30"}) if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.WeatherAPI.Units != "metric" { t.Fatalf("Units = %q, want metric", cfg.WeatherAPI.Units) } if cfg.WeatherAPI.Timezone != "+09:30" { t.Fatalf("Timezone = %q, want +09:30", cfg.WeatherAPI.Timezone) } } 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: "PipelineTemplateEmpty", mutate: func(cfg *Config) { cfg.Notify.Distributor.PipelineIDTemplate = "" }, wantErr: "notify.distributor.pipeline_id_template", }, { name: "PipelineTemplateUnknown", mutate: func(cfg *Config) { cfg.Notify.Distributor.PipelineIDTemplate = "{unknown}" }, wantErr: "notify.distributor.pipeline_id_template", }, { name: "PipelineTemplateRenderedEmpty", mutate: func(cfg *Config) { cfg.Notify.Distributor.PipelineIDTemplate = " " }, wantErr: "notify.distributor.pipeline_id_template", }, { 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: "ReportPathTemplatesEmpty", mutate: func(cfg *Config) { cfg.Notify.Distributor.ReportPathTemplates = nil }, wantErr: "notify.distributor.report_path_templates", }, { name: "ReportPathTemplateUnknown", mutate: func(cfg *Config) { cfg.Notify.Distributor.ReportPathTemplates = []string{"{unknown}"} }, wantErr: "notify.distributor.report_path_templates", }, { name: "ReportPathTemplateInvalidPath", mutate: func(cfg *Config) { cfg.Notify.Distributor.ReportPathTemplates = []string{"/{batch_output_name}"} }, wantErr: "notify.distributor.report_path_templates", }, { name: "ReportPathTemplateDuplicatePath", mutate: func(cfg *Config) { cfg.Notify.Distributor.ReportPathTemplates = []string{"latest.md", "latest.md"} }, wantErr: "notify.distributor.report_path_templates", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Defaults() cfg.Notify.Distributor.Enabled = true cfg.Notify.Distributor.PipelineIDTemplate = "weatherreporter.{artifact_group}" 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", ValidStartDate: "2026-06-07", ValidEndDate: "2026-06-08", ValidStartTime: "1800", ValidEndTime: "0600", ValidStartStamp: "2026-06-07T1800", ValidEndStamp: "2026-06-08T0600", BundleID: "weatherreporter.home.daily", } bundleID, err := RenderDistributorBundleID("weatherreporter.{location_id}.{report_id}", values) if err != nil { t.Fatalf("RenderDistributorBundleID() error = %v", err) } if bundleID != "weatherreporter.home.daily" { t.Fatalf("bundleID = %q, want rendered value", bundleID) } pipelineID, err := RenderDistributorPipelineID("weatherreporter.{artifact_group}.{bundle_id}", values) if err != nil { t.Fatalf("RenderDistributorPipelineID() error = %v", err) } if pipelineID != "weatherreporter.daily.weatherreporter.home.daily" { t.Fatalf("pipelineID = %q, want rendered pipeline ID", pipelineID) } idempotencyKey, err := RenderDistributorIdempotencyKey("{bundle_id}.{run_id}", values) if err != nil { t.Fatalf("RenderDistributorIdempotencyKey() error = %v", err) } if idempotencyKey != "weatherreporter.home.daily.20260607T120000Z" { t.Fatalf("idempotencyKey = %q, want rendered run key", idempotencyKey) } reportPaths, err := RenderDistributorReportPaths([]string{ "{valid_start_date}/{artifact_group}/{valid_start_stamp}-{valid_end_stamp}-{run_id}.md", "{valid_start_date}/{artifact_group}/latest.md", }, values) if err != nil { t.Fatalf("RenderDistributorReportPaths() error = %v", err) } wantPaths := []string{ "2026-06-07/daily/2026-06-07T1800-2026-06-08T0600-20260607T120000Z.md", "2026-06-07/daily/latest.md", } if strings.Join(reportPaths, "\n") != strings.Join(wantPaths, "\n") { t.Fatalf("reportPaths = %#v, want %#v", reportPaths, wantPaths) } } 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("test.path", 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 := RenderDistributorReportPaths([]string{"{batch_output_name}"}, DistributorTemplateValues{ BatchOutputName: tt.batchOutputName, }) if err == nil { t.Fatal("RenderDistributorReportPaths() 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" + " pipeline_id_template: weatherreporter.{artifact_group}\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") if err := loadSecrets(SecretsConfig{}); err != nil { t.Fatalf("loadSecrets() error = %v", err) } if got := os.Getenv("WEATHERREPORTER_DISABLED_SECRET"); got != "original" { t.Fatalf("environment value = %q, want original", got) } } func TestLoadFileLoadsSecretsDirectory(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, "WEATHERREPORTER_SECRET"), []byte("from-file"), 0o600); err != nil { t.Fatalf("write secret: %v", err) } path := filepath.Join(dir, "config.yml") if err := os.WriteFile(path, []byte("secrets:\n directory: "+secretsDir+"\n"), 0o600); err != nil { t.Fatalf("write config fixture: %v", err) } t.Setenv("WEATHERREPORTER_SECRET", "") if _, err := LoadFile(path); err != nil { t.Fatalf("LoadFile() error = %v", err) } if got := os.Getenv("WEATHERREPORTER_SECRET"); got != "from-file" { t.Fatalf("environment value = %q, want from-file", got) } } func TestLoadSecretsOverwritesExistingEnvironment(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "WEATHERREPORTER_SECRET"), []byte("from-file"), 0o600); err != nil { t.Fatalf("write secret: %v", err) } t.Setenv("WEATHERREPORTER_SECRET", "existing") if err := loadSecrets(SecretsConfig{Directory: dir}); err != nil { t.Fatalf("loadSecrets() error = %v", err) } if got := os.Getenv("WEATHERREPORTER_SECRET"); got != "from-file" { t.Fatalf("environment value = %q, want from-file", got) } } func TestLoadSecretsTrimsOneTrailingLineEnding(t *testing.T) { tests := []struct { name string input string want string }{ {name: "LF", input: "value\n", want: "value"}, {name: "CRLF", input: "value\r\n", want: "value"}, {name: "TwoLF", input: "value\n\n", want: "value\n"}, {name: "LoneCR", input: "value\r", want: "value\r"}, {name: "NoNewline", input: "value", want: "value"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "WEATHERREPORTER_SECRET"), []byte(tt.input), 0o600); err != nil { t.Fatalf("write secret: %v", err) } t.Setenv("WEATHERREPORTER_SECRET", "") if err := loadSecrets(SecretsConfig{Directory: dir}); err != nil { t.Fatalf("loadSecrets() error = %v", err) } if got := os.Getenv("WEATHERREPORTER_SECRET"); got != tt.want { t.Fatalf("environment value = %q, want %q", got, tt.want) } }) } } func TestLoadSecretsRejectsInvalidDirectoryEntries(t *testing.T) { tests := []struct { name string setup func(t *testing.T, dir string) wantErr string }{ { name: "InvalidFilename", setup: func(t *testing.T, dir string) { if err := os.WriteFile(filepath.Join(dir, "1INVALID"), []byte("secret-value"), 0o600); err != nil { t.Fatalf("write invalid secret: %v", err) } }, wantErr: "invalid environment variable name", }, { name: "Subdirectory", setup: func(t *testing.T, dir string) { if err := os.Mkdir(filepath.Join(dir, "SUBDIR"), 0o700); err != nil { t.Fatalf("create subdirectory: %v", err) } }, wantErr: "not a directory", }, { name: "Symlink", setup: func(t *testing.T, dir string) { target := filepath.Join(dir, "TARGET") if err := os.WriteFile(target, []byte("secret-value"), 0o600); err != nil { t.Fatalf("write target: %v", err) } if err := os.Symlink(target, filepath.Join(dir, "SYMLINK")); err != nil { t.Fatalf("create symlink: %v", err) } }, wantErr: "not a symlink", }, { name: "Unreadable", setup: func(t *testing.T, dir string) { path := filepath.Join(dir, "UNREADABLE") if err := os.WriteFile(path, []byte("secret-value"), 0o600); err != nil { t.Fatalf("write unreadable secret: %v", err) } if err := os.Chmod(path, 0o000); err != nil { t.Fatalf("chmod unreadable secret: %v", err) } t.Cleanup(func() { _ = os.Chmod(path, 0o600) }) }, wantErr: "read secret file", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() tt.setup(t, dir) err := loadSecrets(SecretsConfig{Directory: dir}) if err == nil { t.Fatal("loadSecrets() error = nil, want error") } if !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("error = %q, want %q", err.Error(), tt.wantErr) } if strings.Contains(err.Error(), "secret-value") { t.Fatalf("error = %q, want no secret value", err.Error()) } }) } } func TestLoadSecretsRejectsMissingDirectory(t *testing.T) { err := loadSecrets(SecretsConfig{Directory: filepath.Join(t.TempDir(), "missing")}) if err == nil { t.Fatal("loadSecrets() error = nil, want missing directory error") } if !strings.Contains(err.Error(), "read secrets directory") { t.Fatalf("error = %q, want read secrets directory context", err.Error()) } }