Files
weatherreporter/internal/config/config_test.go

1821 lines
52 KiB
Go

package config
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gopkg.in/yaml.v3"
)
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)
}
if !cfg.Notify.Distributor.Batch.Enabled {
t.Fatalf("Notify.Distributor.Batch.Enabled = false, want true")
}
if cfg.Notify.Distributor.Batch.PipelineIDTemplate != "weatherreporter" {
t.Fatalf("Notify.Distributor.Batch.PipelineIDTemplate = %q, want weatherreporter", cfg.Notify.Distributor.Batch.PipelineIDTemplate)
}
if cfg.Notify.Distributor.Batch.BundleIDTemplate != "weatherreporter.{location_id}.{batch}" {
t.Fatalf("Notify.Distributor.Batch.BundleIDTemplate = %q, want default", cfg.Notify.Distributor.Batch.BundleIDTemplate)
}
if cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate != "{bundle_id}.{batch_run_id}" {
t.Fatalf("Notify.Distributor.Batch.IdempotencyKeyTemplate = %q, want default", cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate)
}
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.example.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.{report_id}" {
t.Fatalf("PipelineIDTemplate = %q, want example pipeline template", cfg.Notify.Distributor.PipelineIDTemplate)
}
if !cfg.Notify.Distributor.Batch.Enabled {
t.Fatalf("Notify.Distributor.Batch.Enabled = false, want true")
}
if cfg.Notify.Distributor.Batch.PipelineIDTemplate != "weatherreporter" {
t.Fatalf("Batch PipelineIDTemplate = %q, want weatherreporter", cfg.Notify.Distributor.Batch.PipelineIDTemplate)
}
if cfg.Notify.Distributor.Batch.BundleIDTemplate != "weatherreporter.{location_id}.{batch}" {
t.Fatalf("Batch BundleIDTemplate = %q, want example batch bundle template", cfg.Notify.Distributor.Batch.BundleIDTemplate)
}
if cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate != "{bundle_id}.{batch_run_id}" {
t.Fatalf("Batch IdempotencyKeyTemplate = %q, want example batch idempotency template", cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate)
}
overrides, err := cfg.ReportModuleOverrides()
if err != nil {
t.Fatalf("ReportModuleOverrides() error = %v", err)
}
hourly := overrides[report.Hourly]
if len(hourly) != 9 {
t.Fatalf("hourly example override length = %d, want 9", len(hourly))
}
if hourly[0].ID != module.Metadata ||
hourly[1].ID != module.CurrentConditions ||
hourly[2].ID != module.HourlyForecast ||
hourly[3].ID != module.PrecipTiming ||
hourly[4].ID != module.AlertDigest ||
hourly[5].ID != module.SPCConvectiveOutlooks ||
hourly[6].ID != module.AreaForecastDiscussion ||
hourly[7].ID != module.SPCConvectiveDiscussion ||
hourly[8].ID != module.WeatherStory {
t.Fatalf("hourly example override = %#v, want configured module order", hourly)
}
}
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
- alert_digest
- spc_convective_outlooks
- id: area_forecast_discussion
options:
sections:
- short_term
- spc_convective_discussion
`)
cfg, err := LoadFile(path)
if err != nil {
t.Fatalf("LoadFile() error = %v", err)
}
overrides, err := cfg.ReportModuleOverrides()
if err != nil {
t.Fatalf("ReportModuleOverrides() error = %v", err)
}
items := overrides[report.Daily]
if len(items) != 6 {
t.Fatalf("daily override length = %d, want 6", len(items))
}
if items[0].ID != module.Metadata ||
items[1].ID != module.CurrentConditions ||
items[2].ID != module.AlertDigest ||
items[3].ID != module.SPCConvectiveOutlooks ||
items[4].ID != module.AreaForecastDiscussion ||
items[5].ID != module.SPCConvectiveDiscussion {
t.Fatalf("daily override = %#v, want configured module order", items)
}
options, ok := items[4].Options.(module.AreaForecastDiscussionOptions)
if !ok {
t.Fatalf("AFD options type = %T, want AreaForecastDiscussionOptions", items[4].Options)
}
if strings.Join(options.Sections, ",") != "short_term" {
t.Fatalf("AFD sections = %#v, want short_term", options.Sections)
}
}
func TestLoadTodayReportModuleOverrides(t *testing.T) {
path := writeConfig(t, `
reports:
today:
deterministic_modules:
- metadata
- current_conditions
- derived_daily_summary
- derived_daypart_summaries
- today_planning
`)
cfg, err := LoadFile(path)
if err != nil {
t.Fatalf("LoadFile() error = %v", err)
}
overrides, err := cfg.ReportModuleOverrides()
if err != nil {
t.Fatalf("ReportModuleOverrides() error = %v", err)
}
items := overrides[report.Today]
if len(items) != 5 {
t.Fatalf("today override length = %d, want 5", len(items))
}
want := []module.ID{
module.Metadata,
module.CurrentConditions,
module.DerivedDailySummary,
module.DerivedDaypartSummaries,
module.TodayPlanning,
}
for i, id := range want {
if items[i].ID != id {
t.Fatalf("today override[%d] = %s, want %s", i, items[i].ID, id)
}
}
if _, ok := overrides[report.Daily]; ok {
t.Fatalf("daily override = %#v, want today override to stay distinct", overrides[report.Daily])
}
}
func TestLoadHourlyReportModuleOverrides(t *testing.T) {
path := writeConfig(t, `
reports:
hourly:
deterministic_modules:
- metadata
- hourly_forecast
- precip_timing
- alert_digest
`)
cfg, err := LoadFile(path)
if err != nil {
t.Fatalf("LoadFile() error = %v", err)
}
overrides, err := cfg.ReportModuleOverrides()
if err != nil {
t.Fatalf("ReportModuleOverrides() error = %v", err)
}
items := overrides[report.Hourly]
if len(items) != 4 {
t.Fatalf("hourly override length = %d, want 4", len(items))
}
if items[0].ID != module.Metadata ||
items[1].ID != module.HourlyForecast ||
items[2].ID != module.PrecipTiming ||
items[3].ID != module.AlertDigest {
t.Fatalf("hourly override = %#v, want configured module order", items)
}
}
func TestLoadReportModuleOverrideAliases(t *testing.T) {
path := writeConfig(t, `
reports:
three-day-outlook:
deterministic_modules:
- metadata
weekend_outlook:
deterministic_modules:
- metadata
storm_report:
deterministic_modules:
- metadata
`)
cfg, err := LoadFile(path)
if err != nil {
t.Fatalf("LoadFile() error = %v", err)
}
overrides, err := cfg.ReportModuleOverrides()
if err != nil {
t.Fatalf("ReportModuleOverrides() error = %v", err)
}
if len(overrides[report.ThreeDay]) != 1 || overrides[report.ThreeDay][0].ID != module.Metadata {
t.Fatalf("three-day alias override = %#v, want metadata override", overrides[report.ThreeDay])
}
if len(overrides[report.Weekend]) != 1 || overrides[report.Weekend][0].ID != module.Metadata {
t.Fatalf("weekend alias override = %#v, want metadata override", overrides[report.Weekend])
}
if len(overrides[report.Storm]) != 1 || overrides[report.Storm][0].ID != module.Metadata {
t.Fatalf("storm alias override = %#v, want metadata override", overrides[report.Storm])
}
}
func TestLoadReportDistributorPathOverrides(t *testing.T) {
path := writeConfig(t, `
reports:
daily:
distributor:
path_templates:
- "daily/{valid_start_date}/{run_id}.md"
- "daily/{valid_start_date}/index.md"
today:
deterministic_modules:
- metadata
`)
cfg, err := LoadFile(path)
if err != nil {
t.Fatalf("LoadFile() error = %v", err)
}
daily := cfg.Reports["daily"].Distributor
if !daily.PathTemplatesSet() {
t.Fatal("daily distributor path_templates set = false, want true")
}
want := []string{
"daily/{valid_start_date}/{run_id}.md",
"daily/{valid_start_date}/index.md",
}
if !reflect.DeepEqual(daily.PathTemplates, want) {
t.Fatalf("daily path templates = %#v, want %#v", daily.PathTemplates, want)
}
if cfg.Reports["today"].Distributor.PathTemplatesSet() {
t.Fatal("today distributor path_templates set = true, want false")
}
overrides, err := cfg.ReportDistributorPathOverrides()
if err != nil {
t.Fatalf("ReportDistributorPathOverrides() error = %v", err)
}
if !reflect.DeepEqual(overrides[report.Daily], want) {
t.Fatalf("daily distributor override = %#v, want %#v", overrides[report.Daily], want)
}
if _, ok := overrides[report.Today]; ok {
t.Fatalf("today distributor override = %#v, want omitted override absent", overrides[report.Today])
}
}
func TestReportDistributorPathTemplatesSetTracksExplicitEmptyList(t *testing.T) {
var cfg Config
if err := yaml.Unmarshal([]byte(`
reports:
daily:
distributor:
path_templates: []
today:
distributor: {}
`), &cfg); err != nil {
t.Fatalf("yaml.Unmarshal() error = %v", err)
}
if !cfg.Reports["daily"].Distributor.PathTemplatesSet() {
t.Fatal("daily distributor path_templates set = false, want true")
}
if len(cfg.Reports["daily"].Distributor.PathTemplates) != 0 {
t.Fatalf("daily path templates = %#v, want empty explicit list", cfg.Reports["daily"].Distributor.PathTemplates)
}
if cfg.Reports["today"].Distributor.PathTemplatesSet() {
t.Fatal("today distributor path_templates set = true, want false")
}
}
func TestLoadReportDistributorPathOverrideAliases(t *testing.T) {
path := writeConfig(t, `
reports:
three-day-outlook:
distributor:
path_templates:
- "three-day/{valid_start_date}/index.md"
weekend_outlook:
distributor:
path_templates:
- "weekend/{valid_start_date}/index.md"
`)
cfg, err := LoadFile(path)
if err != nil {
t.Fatalf("LoadFile() error = %v", err)
}
overrides, err := cfg.ReportDistributorPathOverrides()
if err != nil {
t.Fatalf("ReportDistributorPathOverrides() error = %v", err)
}
if !reflect.DeepEqual(overrides[report.ThreeDay], []string{"three-day/{valid_start_date}/index.md"}) {
t.Fatalf("three-day distributor override = %#v, want alias override", overrides[report.ThreeDay])
}
if !reflect.DeepEqual(overrides[report.Weekend], []string{"weekend/{valid_start_date}/index.md"}) {
t.Fatalf("weekend distributor override = %#v, want alias override", overrides[report.Weekend])
}
}
func TestValidateReportModuleKeysWithoutMutatingOptions(t *testing.T) {
cfg := Defaults()
rawOptions := map[string]any{
"sections": []any{"short_term"},
}
cfg.Reports = map[string]ReportConfig{
"daily": {
DeterministicModules: []ModuleConfigItem{
{ID: module.Metadata},
{ID: module.AreaForecastDiscussion, Options: rawOptions},
},
deterministicModulesSet: true,
},
}
if err := Validate(cfg); err != nil {
t.Fatalf("Validate() error = %v", err)
}
got, ok := cfg.Reports["daily"].DeterministicModules[1].Options.(map[string]any)
if !ok || !reflect.DeepEqual(got, rawOptions) {
t.Fatalf("Options after Validate = %#v, want original raw map", cfg.Reports["daily"].DeterministicModules[1].Options)
}
}
func TestReportModuleOverridesNormalizesConstructedOptionsWithoutMutatingConfig(t *testing.T) {
cfg := Defaults()
rawOptions := map[string]any{
"sections": []any{"short_term"},
}
cfg.Reports = map[string]ReportConfig{
"daily": {
DeterministicModules: []ModuleConfigItem{
{ID: module.Metadata},
{ID: module.AreaForecastDiscussion, Options: rawOptions},
},
deterministicModulesSet: true,
},
}
overrides, err := cfg.ReportModuleOverrides()
if err != nil {
t.Fatalf("ReportModuleOverrides() error = %v", err)
}
options, ok := overrides[report.Daily][1].Options.(module.AreaForecastDiscussionOptions)
if !ok {
t.Fatalf("override options type = %T, want AreaForecastDiscussionOptions", overrides[report.Daily][1].Options)
}
if strings.Join(options.Sections, ",") != "short_term" {
t.Fatalf("override sections = %#v, want short_term", options.Sections)
}
if got, ok := cfg.Reports["daily"].DeterministicModules[1].Options.(map[string]any); !ok || !reflect.DeepEqual(got, rawOptions) {
t.Fatalf("config options after ReportModuleOverrides = %#v, want original raw map", cfg.Reports["daily"].DeterministicModules[1].Options)
}
}
func TestValidateReportModuleAliasesDirectly(t *testing.T) {
retiredDailyKey := retiredDailyReportKeyForTest()
tests := []struct {
name string
reports map[string]ReportConfig
wantErr string
}{
{
name: "RetiredDailyReportID",
reports: map[string]ReportConfig{
"daily": {},
retiredDailyKey: {},
},
wantErr: "reports." + retiredDailyKey,
},
{
name: "TodayAndDailyAreDistinct",
reports: map[string]ReportConfig{
"daily": {},
"today": {},
},
},
{
name: "UnknownReport",
reports: map[string]ReportConfig{
"moon": {},
},
wantErr: "reports.moon",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := Defaults()
cfg.Reports = tt.reports
err := Validate(cfg)
if tt.wantErr == "" {
if err != nil {
t.Fatalf("Validate() error = %v", err)
}
return
}
if err == nil {
t.Fatal("Validate() error = nil, want report key error")
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("Validate() error = %q, want %q", err.Error(), tt.wantErr)
}
})
}
}
func TestReportModuleOverridesRejectsInvalidReportKeys(t *testing.T) {
cfg := Defaults()
cfg.Reports = map[string]ReportConfig{
"moon": {
DeterministicModules: []ModuleConfigItem{{ID: module.Metadata}},
deterministicModulesSet: true,
},
}
_, err := cfg.ReportModuleOverrides()
if err == nil {
t.Fatal("ReportModuleOverrides() error = nil, want invalid report key")
}
if !strings.Contains(err.Error(), "reports.moon") {
t.Fatalf("ReportModuleOverrides() error = %q, want report key context", err.Error())
}
}
func TestReportModuleOverrideValidation(t *testing.T) {
retiredDailyKey := retiredDailyReportKeyForTest()
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"`,
},
{
name: "RemovedPlaceholderModule",
yaml: `
reports:
daily:
deterministic_modules:
- forecast_delta
`,
wantErr: `unknown module "forecast_delta"`,
},
{
name: "RetiredTomorrowReportID",
yaml: `
reports:
daily_tomorrow:
deterministic_modules:
- metadata
`,
wantErr: "reports.daily_tomorrow",
},
{
name: "InvalidOptions",
yaml: `
reports:
daily:
deterministic_modules:
- id: metadata
options:
sections:
- short_term
`,
wantErr: "options are invalid",
},
{
name: "RetiredDailyReportID",
yaml: `
reports:
daily:
deterministic_modules:
- metadata
` + retiredDailyKey + `:
deterministic_modules:
- current_conditions
`,
wantErr: "reports." + retiredDailyKey,
},
{
name: "HourlyIncompatibleDailyModule",
yaml: `
reports:
hourly:
deterministic_modules:
- derived_daily_summary
`,
wantErr: `not compatible with report "hourly"`,
},
{
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 TestReportDistributorPathOverrideValidation(t *testing.T) {
tests := []struct {
name string
yaml string
wantErr string
}{
{
name: "UnknownReportField",
yaml: `
reports:
daily:
distributor_paths:
- latest.md
`,
wantErr: `unknown report entry field "distributor_paths"`,
},
{
name: "UnknownDistributorField",
yaml: `
reports:
daily:
distributor:
paths:
- latest.md
`,
wantErr: `unknown report distributor field "paths"`,
},
{
name: "DuplicateReportAlias",
yaml: `
reports:
three-day:
distributor:
path_templates:
- "three-day/{valid_start_date}/index.md"
three_day:
distributor:
path_templates:
- "three-day/latest.md"
`,
wantErr: "duplicates report override",
},
{
name: "UnknownTemplateVariable",
yaml: `
reports:
daily:
distributor:
path_templates:
- "{unknown}.md"
`,
wantErr: `reports.daily.distributor.path_templates[0] contains unknown template variable "unknown"`,
},
{
name: "AbsolutePath",
yaml: `
reports:
daily:
distributor:
path_templates:
- "/daily.md"
`,
wantErr: "reports.daily.distributor.path_templates[0] must render a relative path",
},
{
name: "ParentSegment",
yaml: `
reports:
daily:
distributor:
path_templates:
- "daily/../index.md"
`,
wantErr: "reports.daily.distributor.path_templates[0] must not render . or .. path segments",
},
{
name: "Manifest",
yaml: `
reports:
daily:
distributor:
path_templates:
- "daily/manifest.json"
`,
wantErr: `reports.daily.distributor.path_templates[0] must not render reserved path segment "manifest.json"`,
},
{
name: "DuplicateRenderedPath",
yaml: `
reports:
daily:
distributor:
path_templates:
- "daily/index.md"
- "daily/index.md"
`,
wantErr: `reports.daily.distributor.path_templates renders duplicate path "daily/index.md"`,
},
{
name: "NonStormStormIDEmptyPathSegment",
yaml: `
reports:
daily:
distributor:
path_templates:
- "daily/{storm_id}/index.md"
`,
wantErr: "reports.daily.distributor.path_templates[0] must not render empty path segments",
},
{
name: "EmptyOverrideList",
yaml: `
reports:
daily:
distributor:
path_templates: []
`,
wantErr: "reports.daily.distributor.path_templates must contain at least one entry",
},
}
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 TestReportDistributorPathOverrideStormIDValidation(t *testing.T) {
_, err := LoadFile(writeConfig(t, `
reports:
daily:
distributor:
path_templates:
- "daily/storm-{storm_id}.md"
storm:
distributor:
path_templates:
- "storm/{storm_id}/index.md"
`))
if err != nil {
t.Fatalf("LoadFile() error = %v", err)
}
}
func TestReportDistributorPathOverridesConsistentForLoadedAndConstructedConfig(t *testing.T) {
yaml := `
reports:
daily:
distributor:
path_templates:
- "daily/{valid_start_date}/index.md"
`
reports := map[string]ReportConfig{
"daily": {
Distributor: ReportDistributorConfig{
PathTemplates: []string{"daily/{valid_start_date}/index.md"},
pathTemplatesSet: true,
},
},
}
cfg, err := LoadFile(writeConfig(t, yaml))
if err != nil {
t.Fatalf("LoadFile() error = %v", err)
}
loaded, err := cfg.ReportDistributorPathOverrides()
if err != nil {
t.Fatalf("loaded ReportDistributorPathOverrides() error = %v", err)
}
cfg = Defaults()
cfg.Reports = reports
if err := Validate(cfg); err != nil {
t.Fatalf("Validate() error = %v", err)
}
constructed, err := cfg.ReportDistributorPathOverrides()
if err != nil {
t.Fatalf("constructed ReportDistributorPathOverrides() error = %v", err)
}
if !reflect.DeepEqual(loaded, constructed) {
t.Fatalf("loaded overrides = %#v, constructed = %#v", loaded, constructed)
}
}
func TestReportModuleValidationConsistentForLoadedAndConstructedConfig(t *testing.T) {
tests := []struct {
name string
yaml string
reports map[string]ReportConfig
wantErr string
}{
{
name: "UnknownReport",
yaml: `
reports:
moon:
deterministic_modules:
- metadata
`,
reports: map[string]ReportConfig{
"moon": {
DeterministicModules: []ModuleConfigItem{{ID: module.Metadata}},
deterministicModulesSet: true,
},
},
wantErr: "reports.moon",
},
{
name: "DuplicateReportAlias",
yaml: `
reports:
three-day:
deterministic_modules:
- metadata
three_day:
deterministic_modules:
- metadata
`,
reports: map[string]ReportConfig{
"three-day": {
DeterministicModules: []ModuleConfigItem{{ID: module.Metadata}},
deterministicModulesSet: true,
},
"three_day": {
DeterministicModules: []ModuleConfigItem{{ID: module.Metadata}},
deterministicModulesSet: true,
},
},
wantErr: "duplicates report override",
},
{
name: "UnknownModule",
yaml: `
reports:
daily:
deterministic_modules:
- missing_module
`,
reports: map[string]ReportConfig{
"daily": {
DeterministicModules: []ModuleConfigItem{{ID: module.ID("missing_module")}},
deterministicModulesSet: true,
},
},
wantErr: `unknown module "missing_module"`,
},
{
name: "DuplicateModule",
yaml: `
reports:
daily:
deterministic_modules:
- metadata
- metadata
`,
reports: map[string]ReportConfig{
"daily": {
DeterministicModules: []ModuleConfigItem{
{ID: module.Metadata},
{ID: module.Metadata},
},
deterministicModulesSet: true,
},
},
wantErr: `duplicate module "metadata"`,
},
{
name: "IncompatibleModule",
yaml: `
reports:
daily:
deterministic_modules:
- tomorrow_planning
`,
reports: map[string]ReportConfig{
"daily": {
DeterministicModules: []ModuleConfigItem{{ID: module.TomorrowPlanning}},
deterministicModulesSet: true,
},
},
wantErr: `not compatible with report "daily"`,
},
{
name: "InvalidOptions",
yaml: `
reports:
daily:
deterministic_modules:
- id: metadata
options:
sections:
- short_term
`,
reports: map[string]ReportConfig{
"daily": {
DeterministicModules: []ModuleConfigItem{
{
ID: module.Metadata,
Options: map[string]any{
"sections": []any{"short_term"},
},
},
},
deterministicModulesSet: true,
},
},
wantErr: "options are invalid",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, loadErr := LoadFile(writeConfig(t, tt.yaml))
assertReportModuleError(t, "LoadFile", loadErr, tt.wantErr)
cfg := Defaults()
cfg.Reports = tt.reports
assertReportModuleError(t, "Validate", Validate(cfg), tt.wantErr)
_, overrideErr := cfg.ReportModuleOverrides()
assertReportModuleError(t, "ReportModuleOverrides", overrideErr, tt.wantErr)
})
}
}
func assertReportModuleError(t *testing.T, operation string, err error, want string) {
t.Helper()
if err == nil {
t.Fatalf("%s error = nil, want %q", operation, want)
}
if !strings.Contains(err.Error(), want) {
t.Fatalf("%s error = %q, want %q", operation, err.Error(), want)
}
}
func retiredDailyReportKeyForTest() string {
return strings.Join([]string{"daily", "today"}, "_")
}
func TestReportModuleOverrideRejectsRetiredHourlyKeys(t *testing.T) {
for _, key := range []string{
strings.Join([]string{"near", "term"}, "_"),
strings.Join([]string{"near", "term"}, "-"),
} {
t.Run(key, func(t *testing.T) {
_, err := LoadFile(writeConfig(t, `
reports:
`+key+`:
deterministic_modules:
- metadata
`))
if err == nil {
t.Fatal("LoadFile() error = nil, want unknown report")
}
if !strings.Contains(err.Error(), "is not a known report") {
t.Fatalf("error = %q, want unknown report", err.Error())
}
})
}
}
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 TestDistributorNotifyRejectsRemovedGlobalReportPaths(t *testing.T) {
removedField := "report_path" + "_templates"
_, err := LoadFile(writeConfig(t, `
notify:
distributor:
`+removedField+`:
- index.md
`))
if err == nil {
t.Fatal("LoadFile() error = nil, want removed global path field error")
}
if !strings.Contains(err.Error(), `unknown notify distributor field "`+removedField+`"`) {
t.Fatalf("error = %q, want removed global path field rejection", err.Error())
}
}
func TestDistributorNotifyRejectsUnknownFields(t *testing.T) {
_, err := LoadFile(writeConfig(t, `
notify:
distributor:
paths:
- index.md
`))
if err == nil {
t.Fatal("LoadFile() error = nil, want unknown distributor field error")
}
if !strings.Contains(err.Error(), `unknown notify distributor field "paths"`) {
t.Fatalf("error = %q, want unknown field rejection", err.Error())
}
}
func TestDistributorBatchNotifyRejectsUnknownFields(t *testing.T) {
_, err := LoadFile(writeConfig(t, `
notify:
distributor:
batch:
paths:
- index.md
`))
if err == nil {
t.Fatal("LoadFile() error = nil, want unknown distributor batch field error")
}
if !strings.Contains(err.Error(), `unknown notify distributor batch field "paths"`) {
t.Fatalf("error = %q, want unknown batch field rejection", err.Error())
}
}
func TestDistributorBatchNotifyPartialConfigPreservesDefaults(t *testing.T) {
cfg, err := LoadFile(writeConfig(t, `
notify:
distributor:
batch:
enabled: false
`))
if err != nil {
t.Fatalf("LoadFile() error = %v", err)
}
if cfg.Notify.Distributor.Batch.Enabled {
t.Fatalf("Batch.Enabled = true, want false")
}
if cfg.Notify.Distributor.Batch.PipelineIDTemplate != "weatherreporter" {
t.Fatalf("Batch.PipelineIDTemplate = %q, want default", cfg.Notify.Distributor.Batch.PipelineIDTemplate)
}
if cfg.Notify.Distributor.Batch.BundleIDTemplate != "weatherreporter.{location_id}.{batch}" {
t.Fatalf("Batch.BundleIDTemplate = %q, want default", cfg.Notify.Distributor.Batch.BundleIDTemplate)
}
if cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate != "{bundle_id}.{batch_run_id}" {
t.Fatalf("Batch.IdempotencyKeyTemplate = %q, want default", cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate)
}
}
func TestDisabledDistributorNotifyAcceptsMalformedBatchTemplates(t *testing.T) {
cfg := Defaults()
cfg.Notify.Distributor.Enabled = false
cfg.Notify.Distributor.Batch.PipelineIDTemplate = "{unknown}"
cfg.Notify.Distributor.Batch.BundleIDTemplate = "{unknown}"
cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate = "{unknown}"
if err := Validate(cfg); err != nil {
t.Fatalf("Validate() error = %v", err)
}
}
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: "BatchTemplate",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.Batch.BundleIDTemplate = "{run_id}"
},
wantErr: "notify.distributor.batch.bundle_id_template",
},
}
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 TestEnabledDistributorBatchNotifyValidation(t *testing.T) {
tests := []struct {
name string
mutate func(*Config)
wantErr string
}{
{
name: "PipelineTemplateEmpty",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.Batch.PipelineIDTemplate = ""
},
wantErr: "notify.distributor.batch.pipeline_id_template",
},
{
name: "PipelineTemplateUnknown",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.Batch.PipelineIDTemplate = "{report_id}"
},
wantErr: "notify.distributor.batch.pipeline_id_template",
},
{
name: "PipelineTemplateRenderedEmpty",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.Batch.PipelineIDTemplate = " "
},
wantErr: "notify.distributor.batch.pipeline_id_template",
},
{
name: "BundleTemplateEmpty",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.Batch.BundleIDTemplate = ""
},
wantErr: "notify.distributor.batch.bundle_id_template",
},
{
name: "BundleTemplateUnknown",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.Batch.BundleIDTemplate = "{run_id}"
},
wantErr: "notify.distributor.batch.bundle_id_template",
},
{
name: "BundleTemplateRenderedEmpty",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.Batch.BundleIDTemplate = " "
},
wantErr: "notify.distributor.batch.bundle_id_template",
},
{
name: "IdempotencyTemplateEmpty",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate = ""
},
wantErr: "notify.distributor.batch.idempotency_key_template",
},
{
name: "IdempotencyTemplateUnknown",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate = "{report_id}"
},
wantErr: "notify.distributor.batch.idempotency_key_template",
},
{
name: "IdempotencyTemplateRenderedEmpty",
mutate: func(cfg *Config) {
cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate = " "
},
wantErr: "notify.distributor.batch.idempotency_key_template",
},
}
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 TestDisabledDistributorBatchNotifySkipsBatchTemplateValidation(t *testing.T) {
cfg := Defaults()
cfg.Notify.Distributor.Enabled = true
cfg.Notify.Distributor.PipelineIDTemplate = "weatherreporter.{artifact_group}"
cfg.Notify.Distributor.Batch.Enabled = false
cfg.Notify.Distributor.Batch.PipelineIDTemplate = "{unknown}"
cfg.Notify.Distributor.Batch.BundleIDTemplate = "{unknown}"
cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate = "{unknown}"
if err := Validate(cfg); err != nil {
t.Fatalf("Validate() error = %v", err)
}
}
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",
StormID: "2026-06-07T1800-2026-06-08T0600",
BundleID: "weatherreporter.home.daily",
}
bundleID, err := RenderDistributorBundleID("weatherreporter.{location_id}.{report_id}.{storm_id}", values)
if err != nil {
t.Fatalf("RenderDistributorBundleID() error = %v", err)
}
if bundleID != "weatherreporter.home.daily.2026-06-07T1800-2026-06-08T0600" {
t.Fatalf("bundleID = %q, want rendered value", bundleID)
}
values.BundleID = bundleID
pipelineID, err := RenderDistributorPipelineID("weatherreporter.{artifact_group}.{storm_id}.{bundle_id}", values)
if err != nil {
t.Fatalf("RenderDistributorPipelineID() error = %v", err)
}
if pipelineID != "weatherreporter.daily.2026-06-07T1800-2026-06-08T0600.weatherreporter.home.daily.2026-06-07T1800-2026-06-08T0600" {
t.Fatalf("pipelineID = %q, want rendered pipeline ID", pipelineID)
}
idempotencyKey, err := RenderDistributorIdempotencyKey("{bundle_id}.{storm_id}.{run_id}", values)
if err != nil {
t.Fatalf("RenderDistributorIdempotencyKey() error = %v", err)
}
if idempotencyKey != "weatherreporter.home.daily.2026-06-07T1800-2026-06-08T0600.2026-06-07T1800-2026-06-08T0600.20260607T120000Z" {
t.Fatalf("idempotencyKey = %q, want rendered run key", idempotencyKey)
}
reportPaths, err := RenderDistributorReportPaths("reports.daily.distributor.path_templates", []string{
"{valid_start_date}/{artifact_group}/{valid_start_stamp}-{valid_end_stamp}-{run_id}.md",
"storm/{storm_id}/index.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",
"storm/2026-06-07T1800-2026-06-08T0600/index.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 TestDistributorReportPathRenderingUsesCallerName(t *testing.T) {
values := DistributorTemplateValues{
BatchOutputName: "report.md",
}
tests := []struct {
name string
templates []string
wantErr string
}{
{
name: "UnknownVariable",
templates: []string{"{unknown}.md"},
wantErr: `report.daily.distributor_path_templates[0] contains unknown template variable "unknown"`,
},
{
name: "InvalidPath",
templates: []string{"/{batch_output_name}"},
wantErr: "report.daily.distributor_path_templates[0] must render a relative path",
},
{
name: "DuplicatePath",
templates: []string{"latest.md", "latest.md"},
wantErr: `report.daily.distributor_path_templates renders duplicate path "latest.md"`,
},
{
name: "Empty",
templates: nil,
wantErr: "report.daily.distributor_path_templates must contain at least one entry",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := RenderDistributorReportPaths("report.daily.distributor_path_templates", tt.templates, values)
if err == nil {
t.Fatal("RenderDistributorReportPaths() error = nil, want error")
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("error = %q, want %q", err.Error(), tt.wantErr)
}
})
}
}
func TestDistributorBatchTemplateRendering(t *testing.T) {
values := DistributorBatchTemplateValues{
LocationID: "home",
Batch: "evening",
BatchRunID: "20260617T235037.642224552Z_evening",
BatchStartedDate: "2026-06-17",
}
bundleID, err := RenderDistributorBatchBundleID("weatherreporter.{location_id}.{batch}", values)
if err != nil {
t.Fatalf("RenderDistributorBatchBundleID() error = %v", err)
}
if bundleID != "weatherreporter.home.evening" {
t.Fatalf("bundleID = %q, want batch bundle ID", bundleID)
}
values.BundleID = bundleID
pipelineID, err := RenderDistributorBatchPipelineID("weatherreporter", values)
if err != nil {
t.Fatalf("RenderDistributorBatchPipelineID() error = %v", err)
}
if pipelineID != "weatherreporter" {
t.Fatalf("pipelineID = %q, want weatherreporter", pipelineID)
}
idempotencyKey, err := RenderDistributorBatchIdempotencyKey("{bundle_id}.{batch_run_id}", values)
if err != nil {
t.Fatalf("RenderDistributorBatchIdempotencyKey() error = %v", err)
}
if idempotencyKey != "weatherreporter.home.evening.20260617T235037.642224552Z_evening" {
t.Fatalf("idempotencyKey = %q, want batch retry key", idempotencyKey)
}
bundleID, err = RenderDistributorBatchBundleID("weatherreporter.{batch_started_date}.{batch}", values)
if err != nil {
t.Fatalf("RenderDistributorBatchBundleID() with date error = %v", err)
}
if bundleID != "weatherreporter.2026-06-17.evening" {
t.Fatalf("bundleID = %q, want date-aware batch bundle ID", bundleID)
}
}
func TestDistributorBatchTemplateRejectsUnknownAndMalformedVariables(t *testing.T) {
tests := []struct {
name string
template string
}{
{name: "Unknown", template: "{report_id}"},
{name: "Unclosed", template: "{batch"},
{name: "Unopened", template: "batch}"},
{name: "Empty", template: "{}"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := RenderDistributorBatchBundleID(tt.template, DistributorBatchTemplateValues{})
if err == nil {
t.Fatal("RenderDistributorBatchBundleID() error = nil, want error")
}
})
}
}
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/" + distributorSidecarBasename(), 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: distributorSidecarBasename()},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := RenderDistributorReportPaths("report.daily.distributor_path_templates", []string{"{batch_output_name}"}, DistributorTemplateValues{
BatchOutputName: tt.batchOutputName,
})
if err == nil {
t.Fatal("RenderDistributorReportPaths() error = nil, want error")
}
if !strings.Contains(err.Error(), "report.daily.distributor_path_templates[0]") {
t.Fatalf("error = %q, want caller path name", err.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.{report_id}\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())
}
}