package config import ( "os" "path/filepath" "strings" "testing" ) func write(t *testing.T, body string) string { t.Helper() path := filepath.Join(t.TempDir(), "config") if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } return path } func TestLoadMissingFileKeepsDefaults(t *testing.T) { cfg, err := Load(filepath.Join(t.TempDir(), "absent")) if err != nil { t.Fatalf("missing file should not be an error: %v", err) } if cfg.Hours != 12 || cfg.Icons != "nerd" { t.Fatalf("defaults not returned: %+v", cfg) } } func TestLoadOverridesOnlyWhatIsSet(t *testing.T) { cfg, err := Load(write(t, "hours=24\nicons=emoji\n")) if err != nil { t.Fatal(err) } if cfg.Hours != 24 { t.Errorf("hours = %d, want 24", cfg.Hours) } if cfg.Icons != "emoji" { t.Errorf("icons = %q, want emoji", cfg.Icons) } // Untouched keys must keep their defaults. if cfg.GraphHeight != 5 || !cfg.Graph { t.Errorf("unset keys lost their defaults: %+v", cfg) } } func TestLoadCommentsAndBlanks(t *testing.T) { cfg, err := Load(write(t, "\n# a comment\n\nhours=6 # trailing comment\n")) if err != nil { t.Fatal(err) } if cfg.Hours != 6 { t.Fatalf("hours = %d, want 6 (trailing comment must be stripped)", cfg.Hours) } } func TestLoadErrors(t *testing.T) { for name, body := range map[string]string{ "no equals": "hours 12\n", "unknown key": "colour=always\n", "not a number": "hours=soon\n", "not a bool": "graph=maybe\n", } { t.Run(name, func(t *testing.T) { if _, err := Load(write(t, body)); err == nil { t.Fatalf("expected an error for %q", body) } }) } } func TestPollenAllAndNone(t *testing.T) { cfg, _ := Load(write(t, "pollen=all\n")) if len(cfg.Pollen) != len(AllSpecies) { t.Errorf("pollen=all gave %v", cfg.Pollen) } cfg, _ = Load(write(t, "pollen=none\n")) if len(cfg.Pollen) != 0 { t.Errorf("pollen=none gave %v", cfg.Pollen) } } func TestValidateNamesTheOffender(t *testing.T) { cfg := Default() cfg.Columns = []string{"hour", "tempature"} err := cfg.Validate() if err == nil { t.Fatal("expected an error for an unknown column") } if !strings.Contains(err.Error(), "tempature") { t.Errorf("error must name the offending column, got: %v", err) } if !strings.Contains(err.Error(), "conditions") { t.Errorf("error must list valid columns, got: %v", err) } } func TestValidateRejectsBadEnums(t *testing.T) { for name, mutate := range map[string]func(*Config){ "icons": func(c *Config) { c.Icons = "pictures" }, "color": func(c *Config) { c.Color = "sometimes" }, "units": func(c *Config) { c.Units = "furlongs" }, "hours": func(c *Config) { c.Hours = 0 }, "graph_height": func(c *Config) { c.GraphHeight = 1 }, "pollen": func(c *Config) { c.Pollen = []string{"oak"} }, } { t.Run(name, func(t *testing.T) { cfg := Default() mutate(&cfg) if err := cfg.Validate(); err == nil { t.Fatalf("expected %s to be rejected", name) } }) } } func TestDefaultIsValid(t *testing.T) { if err := Default().Validate(); err != nil { t.Fatalf("the built-in default must be valid: %v", err) } } func TestFieldsRequestsOnlySelectedColumns(t *testing.T) { cfg := Default() cfg.Columns = []string{"hour", "temp", "wind"} got := strings.Join(cfg.Fields(), ",") want := "temperature_2m,wind_speed_10m" if got != want { t.Fatalf("Fields() = %q, want %q", got, want) } } func TestFieldsDeduplicates(t *testing.T) { cfg := Default() // icon and conditions both come from weather_code. cfg.Columns = []string{"icon", "conditions"} if got := cfg.Fields(); len(got) != 1 || got[0] != "weather_code" { t.Fatalf("Fields() = %v, want one weather_code", got) } } func TestWriteDefaultRoundTrips(t *testing.T) { path := filepath.Join(t.TempDir(), "sub", "config") want := Default() want.Location = "Krakow" if err := WriteDefault(path, want); err != nil { t.Fatal(err) } got, err := Load(path) if err != nil { t.Fatalf("the file we generate must parse: %v", err) } if got.Location != "Krakow" || got.Hours != want.Hours || got.Icons != want.Icons { t.Fatalf("round trip changed values:\n got %+v\nwant %+v", got, want) } if err := got.Validate(); err != nil { t.Fatalf("the file we generate must validate: %v", err) } }