package config import ( "os" "path/filepath" "reflect" "testing" "time" ) func TestLoadOrEmpty_NoFile(t *testing.T) { cfg, err := LoadOrEmpty("/tmp/unitdore-nonexistent-test.yaml") if err != nil { t.Fatalf("expected no error for missing file, got: %v", err) } if len(cfg.Units) != 0 { t.Errorf("expected empty units, got %d", len(cfg.Units)) } } func TestSaveAndLoad(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "units.yaml") cfg := &Config{ Units: []Unit{ {Name: "nginx", Runtime: "podman", Order: 1, Enabled: true}, {Name: "myapp", Runtime: "docker", User: "hein", Order: 2, Enabled: true}, }, } if err := cfg.Save(path); err != nil { t.Fatalf("Save failed: %v", err) } loaded, err := Load(path) if err != nil { t.Fatalf("Load failed: %v", err) } if len(loaded.Units) != 2 { t.Errorf("expected 2 units, got %d", len(loaded.Units)) } if loaded.Units[0].Name != "nginx" { t.Errorf("expected nginx, got %s", loaded.Units[0].Name) } if loaded.Units[1].User != "hein" { t.Errorf("expected user hein, got %s", loaded.Units[1].User) } } func TestFindUnit(t *testing.T) { cfg := &Config{ Units: []Unit{ {Name: "nginx", Runtime: "podman"}, {Name: "myapp", Runtime: "docker"}, }, } u := cfg.FindUnit("nginx") if u == nil { t.Fatal("expected to find nginx, got nil") } if u.Runtime != "podman" { t.Errorf("expected podman, got %s", u.Runtime) } missing := cfg.FindUnit("nothere") if missing != nil { t.Errorf("expected nil for missing unit, got %+v", missing) } } func TestAddUnit(t *testing.T) { cfg := &Config{} added := cfg.AddUnit(Unit{Name: "nginx", Runtime: "podman", Enabled: true}) if !added { t.Error("expected AddUnit to return true for new unit") } if len(cfg.Units) != 1 { t.Errorf("expected 1 unit, got %d", len(cfg.Units)) } // Adding a duplicate should return false and not grow the list added = cfg.AddUnit(Unit{Name: "nginx", Runtime: "docker"}) if added { t.Error("expected AddUnit to return false for duplicate") } if len(cfg.Units) != 1 { t.Errorf("expected still 1 unit, got %d", len(cfg.Units)) } } func TestLoad_InvalidYAML(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "bad.yaml") // Use a tab character in a mapping key — this is genuinely invalid YAML os.WriteFile(path, []byte("units:\n - name: ok\n\t broken: tab-indent"), 0644) _, err := Load(path) if err == nil { t.Error("expected error for invalid YAML, got nil") } } func TestSave_CreatesParentDir(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "subdir", "nested", "units.yaml") cfg := &Config{Units: []Unit{{Name: "test", Runtime: "podman", Enabled: true}}} if err := cfg.Save(path); err != nil { t.Fatalf("Save failed to create parent dirs: %v", err) } if _, err := os.Stat(path); err != nil { t.Errorf("expected file to exist: %v", err) } } func TestDisabledReason_Roundtrip(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "units.yaml") cfg := &Config{ Units: []Unit{ { Name: "ghost", Runtime: "podman", Enabled: false, DisabledReason: "container not found", }, }, } if err := cfg.Save(path); err != nil { t.Fatalf("Save failed: %v", err) } loaded, err := Load(path) if err != nil { t.Fatalf("Load failed: %v", err) } if loaded.Units[0].DisabledReason != "container not found" { t.Errorf("expected disabled_reason to survive roundtrip, got: %q", loaded.Units[0].DisabledReason) } } func TestSaveAndLoad_Stack(t *testing.T) { path := filepath.Join(t.TempDir(), "units.yaml") cfg := &Config{ Users: []string{"hein"}, Units: []Unit{{ Name: "shop", Runtime: "podman", Kind: KindStack, Project: "shop", WorkingDir: "/srv/shop", ComposeFiles: []string{"compose.yaml"}, ComposeBin: "/usr/bin/podman-compose", User: "hein", Enabled: true, }}, } if err := cfg.Save(path); err != nil { t.Fatalf("Save failed: %v", err) } loaded, err := Load(path) if err != nil { t.Fatalf("Load failed: %v", err) } u := loaded.Units[0] if !u.IsStack() || u.Project != "shop" || u.WorkingDir != "/srv/shop" || len(u.ComposeFiles) != 1 || u.ComposeBin != "/usr/bin/podman-compose" { t.Errorf("stack fields not round-tripped: %+v", u) } if len(loaded.Users) != 1 || loaded.Users[0] != "hein" { t.Errorf("users not round-tripped: %v", loaded.Users) } } func TestIsStack_DefaultKind(t *testing.T) { if (Unit{Name: "x"}).IsStack() { t.Error("unit without kind must be a container") } } func TestUserConfigPath(t *testing.T) { if got := UserConfigPath("/home/hein"); got != "/home/hein/.config/unitdore/units.yaml" { t.Errorf("UserConfigPath() = %q", got) } } func TestResolveOrder(t *testing.T) { cfg := &Config{Units: []Unit{ {Name: "db", Order: 1, Enabled: true, Delay: "9s"}, {Name: "cache", Order: 1, Enabled: true}, {Name: "off", Order: 2, Enabled: false}, {Name: "web", Order: 3, Enabled: true, Delay: "5s"}, {Name: "web-user", Order: 3, Enabled: true, User: "hein"}, {Name: "user-db", Order: 1, Enabled: true, User: "hein"}, }} if err := cfg.ResolveOrder(); err != nil { t.Fatal(err) } get := func(n string) Unit { return *cfg.FindUnit(n) } if u := get("db"); len(u.After) != 0 || u.StartDelay != 0 { t.Errorf("first group must not wait: %+v", u) } if u := get("off"); len(u.After) != 0 || u.StartDelay != 0 { t.Errorf("disabled unit gets no ordering: %+v", u) } // skips the disabled order-2 unit, picks the enabled order-1 same-scope units if u := get("web"); !reflect.DeepEqual(u.After, []string{"cache", "db"}) || u.StartDelay != 5*time.Second { t.Errorf("web: %+v", u) } // user units only order after units of the same user if u := get("web-user"); !reflect.DeepEqual(u.After, []string{"user-db"}) || u.StartDelay != 0 { t.Errorf("web-user: %+v", u) } // re-resolving after a change resets stale values cfg.FindUnit("db").Order = 9 if err := cfg.ResolveOrder(); err != nil { t.Fatal(err) } if u := get("web"); !reflect.DeepEqual(u.After, []string{"cache"}) { t.Errorf("web after reorder: %+v", u) } } func TestResolveOrder_InvalidDelay(t *testing.T) { for _, d := range []string{"abc", "-2s"} { cfg := &Config{Units: []Unit{{Name: "a", Delay: d, Enabled: true}}} if err := cfg.ResolveOrder(); err == nil { t.Errorf("delay %q should be rejected", d) } } } func TestLoad_ResolvesOrderAndRejectsBadDelay(t *testing.T) { dir := t.TempDir() good := filepath.Join(dir, "good.yaml") os.WriteFile(good, []byte("units:\n - {name: a, order: 1, enabled: true}\n - {name: b, order: 2, delay: 4s, enabled: true}\n"), 0644) cfg, err := Load(good) if err != nil { t.Fatal(err) } if b := cfg.Units[1]; !reflect.DeepEqual(b.After, []string{"a"}) || b.StartDelay != 4*time.Second { t.Errorf("b = %+v", b) } bad := filepath.Join(dir, "bad.yaml") os.WriteFile(bad, []byte("units:\n - {name: a, delay: nope, enabled: true}\n"), 0644) if _, err := Load(bad); err == nil { t.Error("Load must reject invalid delay") } }