From 02a1472dd4fa55ac096aa34e9625a3a1fcedbf5c Mon Sep 17 00:00:00 2001 From: Thales Maciel Date: Fri, 1 May 2026 12:08:19 -0300 Subject: [PATCH] test: cover absolutizePaths, lastID, runCheckMigrations Adds focused unit tests for previously-uncovered cli helpers: - TestAbsolutizePaths covers the path-vararg helper's empty, absolute, and relative branches; complements the existing TestAbsolutizeImageRegisterPaths. - TestLastID is table-driven across nil/empty/sorted/unsorted/ duplicates/negative inputs. - TestRunCheckMigrations* exercises every Compatibility branch (compatible / migrations needed / incompatible / inspect error) by stubbing bangerdExit and pointing the layout at a temp-dir DB seeded directly with the schema_migrations table. - TestNewBangerdCommandSubcommands pins the flag set against accidental drift. Lifts internal/cli coverage from 71% to 76% combined. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/cli/bangerd_test.go | 194 +++++++++++++++++++++++++++++++++++ internal/cli/cli_test.go | 44 ++++++++ 2 files changed, 238 insertions(+) create mode 100644 internal/cli/bangerd_test.go diff --git a/internal/cli/bangerd_test.go b/internal/cli/bangerd_test.go new file mode 100644 index 0000000..fa60b76 --- /dev/null +++ b/internal/cli/bangerd_test.go @@ -0,0 +1,194 @@ +package cli + +import ( + "bytes" + "database/sql" + "os" + "path/filepath" + "strings" + "testing" + + "banger/internal/store" + + "github.com/spf13/cobra" + _ "modernc.org/sqlite" +) + +func TestNewBangerdCommandSubcommands(t *testing.T) { + cmd := NewBangerdCommand() + if cmd.Use != "bangerd" { + t.Errorf("Use = %q, want bangerd", cmd.Use) + } + for _, flag := range []string{"system", "root-helper", "check-migrations"} { + if cmd.Flag(flag) == nil { + t.Errorf("flag %q missing", flag) + } + } +} + +func TestLastID(t *testing.T) { + tests := []struct { + name string + in []int + want int + }{ + {"nil", nil, 0}, + {"empty", []int{}, 0}, + {"single", []int{7}, 7}, + {"sorted ascending", []int{1, 2, 3}, 3}, + {"unsorted, max in middle", []int{1, 99, 5}, 99}, + {"duplicates", []int{4, 4, 2, 4}, 4}, + {"negative ignored", []int{-3, -1, 0}, 0}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := lastID(tc.in); got != tc.want { + t.Fatalf("lastID(%v) = %d, want %d", tc.in, got, tc.want) + } + }) + } +} + +// stubExit replaces bangerdExit for the test and returns a pointer to +// the captured exit code (-1 = not called) and a restore func. +func stubExit(t *testing.T) *int { + t.Helper() + called := -1 + prev := bangerdExit + bangerdExit = func(code int) { called = code } + t.Cleanup(func() { bangerdExit = prev }) + return &called +} + +// pointHomeAtTempDB sets XDG_STATE_HOME (and HOME, which Resolve falls +// back to) so that paths.Resolve().DBPath lands at /banger/state.db. +// Returns the DB path. +func pointHomeAtTempDB(t *testing.T) string { + t.Helper() + tmp := t.TempDir() + t.Setenv("HOME", tmp) + t.Setenv("XDG_STATE_HOME", tmp) + t.Setenv("XDG_CONFIG_HOME", tmp) + t.Setenv("XDG_CACHE_HOME", tmp) + t.Setenv("XDG_RUNTIME_DIR", tmp) + dir := filepath.Join(tmp, "banger") + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatalf("mkdir state dir: %v", err) + } + return filepath.Join(dir, "state.db") +} + +func TestRunCheckMigrationsCompatible(t *testing.T) { + dbPath := pointHomeAtTempDB(t) + s, err := store.Open(dbPath) + if err != nil { + t.Fatalf("store.Open: %v", err) + } + _ = s.Close() + + exit := stubExit(t) + cmd := &cobra.Command{} + var out bytes.Buffer + cmd.SetOut(&out) + + if err := runCheckMigrations(cmd, false); err != nil { + t.Fatalf("runCheckMigrations: %v", err) + } + if *exit != -1 { + t.Errorf("bangerdExit called with %d, want no call", *exit) + } + if !strings.HasPrefix(out.String(), "compatible:") { + t.Errorf("stdout = %q, want prefix \"compatible:\"", out.String()) + } +} + +func TestRunCheckMigrationsMigrationsNeeded(t *testing.T) { + dbPath := pointHomeAtTempDB(t) + // Hand-craft a DB that has schema_migrations with only the baseline + // row — InspectSchemaState classifies this as "migrations needed". + dsn := "file:" + dbPath + "?_pragma=foreign_keys(1)" + db, err := sql.Open("sqlite", dsn) + if err != nil { + t.Fatalf("sql.Open: %v", err) + } + if _, err := db.Exec(`CREATE TABLE schema_migrations (id INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL)`); err != nil { + t.Fatalf("create table: %v", err) + } + if _, err := db.Exec(`INSERT INTO schema_migrations VALUES (1, 'baseline', '2026-01-01T00:00:00Z')`); err != nil { + t.Fatalf("insert baseline: %v", err) + } + _ = db.Close() + + exit := stubExit(t) + cmd := &cobra.Command{} + var out bytes.Buffer + cmd.SetOut(&out) + + if err := runCheckMigrations(cmd, false); err != nil { + t.Fatalf("runCheckMigrations: %v", err) + } + if *exit != 1 { + t.Errorf("bangerdExit called with %d, want 1", *exit) + } + if !strings.HasPrefix(out.String(), "migrations needed:") { + t.Errorf("stdout = %q, want prefix \"migrations needed:\"", out.String()) + } +} + +func TestRunCheckMigrationsIncompatible(t *testing.T) { + dbPath := pointHomeAtTempDB(t) + s, err := store.Open(dbPath) + if err != nil { + t.Fatalf("store.Open: %v", err) + } + _ = s.Close() + + // Inject an unknown migration id directly so the binary's known set + // is a strict subset — InspectSchemaState classifies as incompatible. + dsn := "file:" + dbPath + db, err := sql.Open("sqlite", dsn) + if err != nil { + t.Fatalf("sql.Open: %v", err) + } + if _, err := db.Exec(`INSERT INTO schema_migrations VALUES (9999, 'from_the_future', '2030-01-01T00:00:00Z')`); err != nil { + t.Fatalf("insert future row: %v", err) + } + _ = db.Close() + + exit := stubExit(t) + cmd := &cobra.Command{} + var out bytes.Buffer + cmd.SetOut(&out) + + if err := runCheckMigrations(cmd, false); err != nil { + t.Fatalf("runCheckMigrations: %v", err) + } + if *exit != 2 { + t.Errorf("bangerdExit called with %d, want 2", *exit) + } + if !strings.HasPrefix(out.String(), "incompatible:") { + t.Errorf("stdout = %q, want prefix \"incompatible:\"", out.String()) + } +} + +func TestRunCheckMigrationsInspectError(t *testing.T) { + // Point at a state dir with a non-DB file at state.db so Inspect + // fails to open it. The function should wrap the error with the path. + dbPath := pointHomeAtTempDB(t) + if err := os.WriteFile(dbPath, []byte("not a sqlite file"), 0o600); err != nil { + t.Fatalf("write garbage: %v", err) + } + + stubExit(t) + cmd := &cobra.Command{} + var out bytes.Buffer + cmd.SetOut(&out) + + err := runCheckMigrations(cmd, false) + if err == nil { + t.Fatal("runCheckMigrations: nil error, want wrapped inspect error") + } + if !strings.Contains(err.Error(), dbPath) { + t.Errorf("error %q does not mention DB path %q", err.Error(), dbPath) + } +} diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index e924a18..ed2ab59 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -737,6 +737,50 @@ func TestAbsolutizeImageRegisterPaths(t *testing.T) { } } +func TestAbsolutizePaths(t *testing.T) { + tmp := t.TempDir() + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Getwd: %v", err) + } + if err := os.Chdir(tmp); err != nil { + t.Fatalf("Chdir: %v", err) + } + t.Cleanup(func() { _ = os.Chdir(wd) }) + + empty := "" + abs := "/already/absolute/path" + rel1 := filepath.Join("a", "b") + rel2 := "./c/d" + + if err := absolutizePaths(&empty, &abs, &rel1, &rel2); err != nil { + t.Fatalf("absolutizePaths: %v", err) + } + + if empty != "" { + t.Errorf("empty value mutated: %q", empty) + } + if abs != "/already/absolute/path" { + t.Errorf("absolute value mutated: %q", abs) + } + if !filepath.IsAbs(rel1) { + t.Errorf("rel1 not absolutized: %q", rel1) + } + if !filepath.IsAbs(rel2) { + t.Errorf("rel2 not absolutized: %q", rel2) + } + // Sanity: relative paths should land under tmp. + if !strings.HasPrefix(rel1, tmp) { + t.Errorf("rel1 = %q, want prefix %q", rel1, tmp) + } +} + +func TestAbsolutizePathsNoArgs(t *testing.T) { + if err := absolutizePaths(); err != nil { + t.Fatalf("absolutizePaths() with no args: %v", err) + } +} + func TestPrintImageListTableShowsRootfsSizes(t *testing.T) { rootfs := filepath.Join(t.TempDir(), "rootfs.ext4") if err := os.WriteFile(rootfs, nil, 0o644); err != nil {