package installmeta import ( "errors" "os" "os/user" "path/filepath" "strconv" "testing" "time" ) func TestSaveLoadRoundTrip(t *testing.T) { path := filepath.Join(t.TempDir(), "install.toml") want := Metadata{ OwnerUser: "dev", OwnerUID: 1000, OwnerGID: 1000, OwnerHome: "/home/dev", InstalledAt: time.Unix(1710000000, 0).UTC(), Version: "v1.2.3", Commit: "abc123", BuiltAt: "2026-04-23T00:00:00Z", } if err := Save(path, want); err != nil { t.Fatalf("Save: %v", err) } got, err := Load(path) if err != nil { t.Fatalf("Load: %v", err) } if got != want { t.Fatalf("Load() = %+v, want %+v", got, want) } } func TestSaveCreatesParentDir(t *testing.T) { path := filepath.Join(t.TempDir(), "nested", "dir", "install.toml") meta := Metadata{OwnerUser: "dev", OwnerUID: 1, OwnerGID: 1, OwnerHome: "/home/dev"} if err := Save(path, meta); err != nil { t.Fatalf("Save: %v", err) } if _, err := os.Stat(path); err != nil { t.Fatalf("file not written: %v", err) } } func TestSaveRejectsInvalidMetadata(t *testing.T) { path := filepath.Join(t.TempDir(), "install.toml") if err := Save(path, Metadata{OwnerUID: 1, OwnerGID: 1, OwnerHome: "/home/dev"}); err == nil { t.Fatal("Save() = nil, want validation error") } if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { t.Fatalf("Save wrote a file despite validation error: stat err = %v", err) } } func TestLoadMissingFile(t *testing.T) { _, err := Load(filepath.Join(t.TempDir(), "missing.toml")) if !errors.Is(err, os.ErrNotExist) { t.Fatalf("Load() = %v, want os.ErrNotExist", err) } } func TestLoadInvalidTOML(t *testing.T) { path := filepath.Join(t.TempDir(), "install.toml") if err := os.WriteFile(path, []byte("not = valid = toml\n"), 0o644); err != nil { t.Fatal(err) } if _, err := Load(path); err == nil { t.Fatal("Load() = nil, want TOML parse error") } } func TestLoadRejectsInvalidPersistedMetadata(t *testing.T) { // File parses but fails Validate (no owner_user) — Load must surface // the validation error rather than returning a zero-value Metadata. path := filepath.Join(t.TempDir(), "install.toml") if err := os.WriteFile(path, []byte("owner_uid = 1\nowner_gid = 1\nowner_home = \"/home/dev\"\n"), 0o644); err != nil { t.Fatal(err) } if _, err := Load(path); err == nil { t.Fatal("Load() = nil, want validation error") } } func TestValidate(t *testing.T) { tests := []struct { name string m Metadata ok bool }{ {"valid", Metadata{OwnerUser: "dev", OwnerUID: 1, OwnerGID: 1, OwnerHome: "/home/dev"}, true}, {"missing owner_user", Metadata{OwnerUID: 1, OwnerGID: 1, OwnerHome: "/home/dev"}, false}, {"whitespace owner_user", Metadata{OwnerUser: " ", OwnerUID: 1, OwnerGID: 1, OwnerHome: "/home/dev"}, false}, {"negative uid", Metadata{OwnerUser: "dev", OwnerUID: -1, OwnerGID: 1, OwnerHome: "/home/dev"}, false}, {"negative gid", Metadata{OwnerUser: "dev", OwnerUID: 1, OwnerGID: -1, OwnerHome: "/home/dev"}, false}, {"empty home", Metadata{OwnerUser: "dev", OwnerUID: 1, OwnerGID: 1, OwnerHome: ""}, false}, {"relative home", Metadata{OwnerUser: "dev", OwnerUID: 1, OwnerGID: 1, OwnerHome: "home/dev"}, false}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { err := tc.m.Validate() if tc.ok && err != nil { t.Fatalf("Validate() = %v, want nil", err) } if !tc.ok && err == nil { t.Fatal("Validate() = nil, want error") } }) } } func TestLookupOwnerEmpty(t *testing.T) { if _, err := LookupOwner(""); err == nil { t.Fatal("LookupOwner(\"\") = nil, want error") } if _, err := LookupOwner(" "); err == nil { t.Fatal("LookupOwner(\" \") = nil, want error") } } func TestLookupOwnerMissing(t *testing.T) { if _, err := LookupOwner("definitely-no-such-user-banger-test"); err == nil { t.Fatal("LookupOwner(missing) = nil, want error") } } func TestLookupOwnerCurrentUser(t *testing.T) { cur, err := user.Current() if err != nil { t.Skipf("user.Current: %v", err) } got, err := LookupOwner(cur.Username) if err != nil { t.Fatalf("LookupOwner(%q): %v", cur.Username, err) } wantUID, _ := strconv.Atoi(cur.Uid) wantGID, _ := strconv.Atoi(cur.Gid) if got.OwnerUser != cur.Username || got.OwnerUID != wantUID || got.OwnerGID != wantGID || got.OwnerHome != cur.HomeDir { t.Fatalf("LookupOwner = %+v, want user=%s uid=%d gid=%d home=%s", got, cur.Username, wantUID, wantGID, cur.HomeDir) } } func TestUpdateBuildInfo(t *testing.T) { path := filepath.Join(t.TempDir(), "install.toml") original := Metadata{ OwnerUser: "dev", OwnerUID: 1000, OwnerGID: 1000, OwnerHome: "/home/dev", InstalledAt: time.Unix(1710000000, 0).UTC(), Version: "v0.1.0", Commit: "old", BuiltAt: "2026-01-01T00:00:00Z", } if err := Save(path, original); err != nil { t.Fatalf("Save: %v", err) } if err := UpdateBuildInfo(path, " v0.2.0 ", " new ", " 2026-04-30T00:00:00Z "); err != nil { t.Fatalf("UpdateBuildInfo: %v", err) } got, err := Load(path) if err != nil { t.Fatalf("Load: %v", err) } if got.Version != "v0.2.0" || got.Commit != "new" || got.BuiltAt != "2026-04-30T00:00:00Z" { t.Fatalf("build fields = %q/%q/%q, want trimmed values", got.Version, got.Commit, got.BuiltAt) } // Identity must be preserved. if got.OwnerUser != original.OwnerUser || got.OwnerUID != original.OwnerUID || got.OwnerGID != original.OwnerGID || got.OwnerHome != original.OwnerHome || !got.InstalledAt.Equal(original.InstalledAt) { t.Fatalf("identity changed: got %+v, want %+v", got, original) } } func TestUpdateBuildInfoMissingFile(t *testing.T) { err := UpdateBuildInfo(filepath.Join(t.TempDir(), "missing.toml"), "v1", "c", "t") if !errors.Is(err, os.ErrNotExist) { t.Fatalf("UpdateBuildInfo() = %v, want os.ErrNotExist", err) } } func TestValidateRejectsMissingOwner(t *testing.T) { err := Metadata{OwnerUID: 1000, OwnerGID: 1000, OwnerHome: "/home/dev"}.Validate() if err == nil { t.Fatal("Validate() = nil, want missing owner_user error") } }