package config import ( "encoding/json" "os" "path/filepath" "testing" ) func tempConfig(t *testing.T) (*Config, string) { t.Helper() dir := t.TempDir() os.Setenv("XDG_CONFIG_HOME", dir) t.Cleanup(func() { os.Unsetenv("XDG_CONFIG_HOME") }) c := Load() // use the path the package actually resolves rather than guessing the layout return c, c.PathOf() } func TestIgnoredRoundTrips(t *testing.T) { c, path := tempConfig(t) if c.IsIgnored("linux") { t.Fatal("nothing should be ignored initially") } if !c.Toggle("linux") { t.Error("Toggle should report the new state as ignored") } if !c.Toggle("mesa") { t.Error("Toggle should add a second package") } if c.Toggle("linux") { t.Error("toggling twice should unignore") } if !c.Toggle("linux") { t.Error("and back again") } if err := c.Save(); err != nil { t.Fatalf("save: %v", err) } if _, err := os.Stat(path); err != nil { t.Fatalf("config not written to the resolved path %s: %v", path, err) } if filepath.Base(filepath.Dir(path)) != "updater" { t.Errorf("config should live under an app-named directory, got %s", path) } again := Load() if !again.IsIgnored("linux") || !again.IsIgnored("mesa") { t.Errorf("ignore list did not survive a round trip: %v", again.Ignored) } if again.Ignored[0] != "linux" { t.Errorf("ignore list should stay sorted: %v", again.Ignored) } } func TestUnknownKeysArePreserved(t *testing.T) { // the Python side owns the schema; saving here must not delete its fields dir := t.TempDir() os.Setenv("XDG_CONFIG_HOME", dir) defer os.Unsetenv("XDG_CONFIG_HOME") path := filepath.Join(dir, "updater", "config.json") if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } os.WriteFile(path, []byte(`{"color_accent":"13","something_python_added":{"a":1}}`), 0o644) c := Load() if c.AccentSlot() != "13" { t.Errorf("AccentSlot = %q, want 13", c.AccentSlot()) } c.Toggle("vim") if err := c.Save(); err != nil { t.Fatalf("save: %v", err) } body, _ := os.ReadFile(path) var raw map[string]any if err := json.Unmarshal(body, &raw); err != nil { t.Fatalf("config is no longer valid json: %v", err) } if raw["color_accent"] != "13" { t.Errorf("color_accent was clobbered: %v", raw["color_accent"]) } if _, ok := raw["something_python_added"]; !ok { t.Error("an unrecognised key was dropped on save") } } func TestCorruptConfigFallsBackToDefaults(t *testing.T) { dir := t.TempDir() os.Setenv("XDG_CONFIG_HOME", dir) defer os.Unsetenv("XDG_CONFIG_HOME") path := filepath.Join(dir, "updater", "config.json") os.MkdirAll(filepath.Dir(path), 0o755) os.WriteFile(path, []byte("{ not json"), 0o644) c := Load() if len(c.Ignored) != 0 { t.Errorf("expected an empty ignore list, got %v", c.Ignored) } if c.AccentSlot() != "10" { t.Errorf("expected the default accent, got %q", c.AccentSlot()) } } func TestDefaultsAreSane(t *testing.T) { c, _ := tempConfig(t) if c.CursorIcon() == "" { t.Error("cursor icon should never be empty") } if c.ExpandThreshold() < 1 { t.Errorf("expand threshold = %d", c.ExpandThreshold()) } if c.HideHints() { t.Error("hints should be on by default") } } func TestSaveIsAtomic(t *testing.T) { c, path := tempConfig(t) c.Toggle("x") if err := c.Save(); err != nil { t.Fatal(err) } if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) { t.Error("a temp file was left behind") } } func TestDirtyTracksChanges(t *testing.T) { c, _ := tempConfig(t) if c.Dirty() { t.Error("a freshly loaded config is not dirty") } c.Toggle("a") if !c.Dirty() { t.Error("toggling should mark it dirty") } c.Save() if c.Dirty() { t.Error("saving should clear the dirty flag") } } func TestAcknowledgementsRoundTrip(t *testing.T) { c, path := tempConfig(t) if len(c.AckedList()) != 0 { t.Fatal("nothing should be acknowledged initially") } c.Ack("AVG-1") c.Ack("AVG-2") c.Ack("AVG-1") // idempotent if err := c.Save(); err != nil { t.Fatalf("save: %v", err) } again := Load() got := again.AckedList() if len(got) != 2 || got[0] != "AVG-1" || got[1] != "AVG-2" { t.Errorf("acks did not survive: %v", got) } if _, err := os.Stat(path); err != nil { t.Fatalf("config not written: %v", err) } } func TestClearingAcknowledgementsReportsTheCount(t *testing.T) { c, _ := tempConfig(t) c.Ack("AVG-1") c.Ack("AVG-2") if n := c.ClearAcks(); n != 2 { t.Errorf("ClearAcks returned %d, want 2", n) } if len(c.AckedList()) != 0 { t.Error("acks were not cleared") } if err := c.Save(); err != nil { t.Fatal(err) } if len(Load().AckedList()) != 0 { t.Error("cleared acks came back after a reload") } } func TestAcksAndIgnoresCoexistWithoutClobbering(t *testing.T) { // both live in the same file and are written by different code paths c, _ := tempConfig(t) c.Toggle("linux") c.Ack("AVG-1") c.Ack("AVG-2") if err := c.Save(); err != nil { t.Fatal(err) } again := Load() if !again.IsIgnored("linux") { t.Error("the ignore list was lost when acks were written") } if len(again.AckedList()) != 2 { t.Errorf("acks lost when the ignore list was written: %v", again.AckedList()) } } func TestAGarbageAckListIsIgnored(t *testing.T) { c, _ := tempConfig(t) c.raw["acked"] = "not a list" if got := c.AckedList(); got != nil { t.Errorf("expected nil, got %v", got) } c.Ack("AVG-1") if got := c.AckedList(); len(got) != 1 { t.Errorf("recovering from a bad value failed: %v", got) } }