package browse import ( "context" "errors" "testing" ) func TestBranches(t *testing.T) { db := openFixture(t) ctx := context.Background() branches, err := db.Branches(ctx) if err != nil { t.Fatal(err) } if len(branches) != 2 { t.Fatalf("want 2 branches, got %d: %+v", len(branches), branches) } // main sorts first (default). if branches[0].Name != "main" { t.Errorf("want main first, got %q", branches[0].Name) } names := map[string]string{} for _, b := range branches { names[b.Name] = b.Head } if _, ok := names["dev"]; !ok { t.Errorf("missing dev branch: %+v", branches) } // main head is C4 (add items); dev head is C2 (insert users): distinct. if names["main"] == names["dev"] { t.Errorf("main and dev unexpectedly share a head %q", names["main"]) } if got, want := names["main"], commitHash(t, msgAddItem); got != want { t.Errorf("main head = %q, want C4 %q", got, want) } if got, want := names["dev"], commitHash(t, msgInsert); got != want { t.Errorf("dev head = %q, want C2 %q", got, want) } if got := DefaultBranch(branches); got != "main" { t.Errorf("DefaultBranch = %q, want main", got) } } func TestLogTopoOrder(t *testing.T) { db := openFixture(t) ctx := context.Background() commits, next, err := db.Log(ctx, "main", "", 100) if err != nil { t.Fatal(err) } if next != "" { t.Errorf("full log should have no next page, got %q", next) } wantOrder := []string{msgAddItem, msgModify, msgInsert, msgAddUser, msgInitial} if len(commits) != len(wantOrder) { t.Fatalf("want %d commits, got %d", len(wantOrder), len(commits)) } for i, want := range wantOrder { if commits[i].Message != want { t.Errorf("commit[%d] message = %q, want %q", i, commits[i].Message, want) } } // Author/email come from the CLI global config for the content commits. if commits[0].Author != "CLI Fixture" || commits[0].Email != "cli@fixture.test" { t.Errorf("head author/email = %q/%q", commits[0].Author, commits[0].Email) } // Parent linkage: each commit's first parent is the next-older commit; // the initial commit has no parent. for i := 0; i < len(commits)-1; i++ { if len(commits[i].ParentHashes) == 0 { t.Fatalf("commit[%d] %q unexpectedly has no parent", i, commits[i].Message) } if commits[i].ParentHashes[0] != commits[i+1].Hash { t.Errorf("commit[%d] parent = %q, want %q", i, commits[i].ParentHashes[0], commits[i+1].Hash) } } if last := commits[len(commits)-1]; len(last.ParentHashes) != 0 { t.Errorf("initial commit should have no parents, got %v", last.ParentHashes) } if commits[len(commits)-1].Date.IsZero() { t.Errorf("initial commit date should be set") } } func TestLogPagination(t *testing.T) { db := openFixture(t) ctx := context.Background() // Page through the full history two commits at a time. var got []string from := "" pages := 0 for { commits, next, err := db.Log(ctx, "main", from, 2) if err != nil { t.Fatal(err) } if len(commits) == 0 { t.Fatalf("empty page (from=%q)", from) } if len(commits) > 2 { t.Fatalf("page larger than limit: %d", len(commits)) } for _, c := range commits { got = append(got, c.Message) } pages++ if pages > 10 { t.Fatal("pagination did not terminate") } if next == "" { break } from = next } want := []string{msgAddItem, msgModify, msgInsert, msgAddUser, msgInitial} if len(got) != len(want) { t.Fatalf("paged %d commits, want %d: %v", len(got), len(want), got) } for i := range want { if got[i] != want[i] { t.Errorf("paged[%d] = %q, want %q", i, got[i], want[i]) } } } func TestLogFromHash(t *testing.T) { db := openFixture(t) ctx := context.Background() // Start the walk from C2 (insert users): should yield C2, C1, C0. commits, next, err := db.Log(ctx, "main", commitHash(t, msgInsert), 100) if err != nil { t.Fatal(err) } if next != "" { t.Errorf("want no next page, got %q", next) } want := []string{msgInsert, msgAddUser, msgInitial} if len(commits) != len(want) { t.Fatalf("want %d commits from C2, got %d", len(want), len(commits)) } for i, w := range want { if commits[i].Message != w { t.Errorf("from-hash commit[%d] = %q, want %q", i, commits[i].Message, w) } } } func TestLogInvalidLimit(t *testing.T) { db := openFixture(t) if _, _, err := db.Log(context.Background(), "main", "", 0); err == nil { t.Fatal("expected error for non-positive limit") } } func TestTables(t *testing.T) { db := openFixture(t) ctx := context.Background() tables, err := db.Tables(ctx, "main") if err != nil { t.Fatal(err) } byName := map[string]TableInfo{} for _, ti := range tables { byName[ti.Name] = ti } users, ok := byName["users"] if !ok { t.Fatalf("missing users table: %+v", tables) } if users.RowCount != 4 { t.Errorf("users row count = %d, want 4", users.RowCount) } if len(users.Columns) != 2 { t.Fatalf("users columns = %d, want 2", len(users.Columns)) } id := users.Columns[0] if id.Name != "id" || !id.PrimaryKey || id.Nullable { t.Errorf("users.id column = %+v", id) } if id.Type != "int" { t.Errorf("users.id type = %q, want int", id.Type) } name := users.Columns[1] if name.Name != "name" || name.PrimaryKey || !name.Nullable { t.Errorf("users.name column = %+v", name) } if name.Type != "varchar(64)" { t.Errorf("users.name type = %q, want varchar(64)", name.Type) } items, ok := byName["items"] if !ok { t.Fatalf("missing items table: %+v", tables) } if items.RowCount != 0 { t.Errorf("items row count = %d, want 0", items.RowCount) } } func TestRowsPagination(t *testing.T) { db := openFixture(t) ctx := context.Background() first, err := db.Rows(ctx, "main", "users", 0, 2) if err != nil { t.Fatal(err) } if first.Total != 4 { t.Errorf("total = %d, want 4", first.Total) } if first.Offset != 0 { t.Errorf("offset = %d, want 0", first.Offset) } wantCols := []string{"id", "name"} if len(first.Columns) != 2 || first.Columns[0] != wantCols[0] || first.Columns[1] != wantCols[1] { t.Errorf("columns = %v, want %v", first.Columns, wantCols) } // Ordered by primary key: rows 1,2. id=1 was renamed to alicia in C3. wantFirst := [][]string{{"1", "alicia"}, {"2", "bob"}} if !equalRows(first.Rows, wantFirst) { t.Errorf("page 1 rows = %v, want %v", first.Rows, wantFirst) } second, err := db.Rows(ctx, "main", "users", 2, 2) if err != nil { t.Fatal(err) } wantSecond := [][]string{{"3", "carol"}, {"4", "dave"}} if !equalRows(second.Rows, wantSecond) { t.Errorf("page 2 rows = %v, want %v", second.Rows, wantSecond) } // Offset at/after the end yields an empty page, not an error. past, err := db.Rows(ctx, "main", "users", 4, 2) if err != nil { t.Fatal(err) } if len(past.Rows) != 0 { t.Errorf("past-end page should be empty, got %v", past.Rows) } if past.Total != 4 { t.Errorf("past-end total = %d, want 4", past.Total) } } func TestRowsEmptyTable(t *testing.T) { db := openFixture(t) ctx := context.Background() page, err := db.Rows(ctx, "main", "items", 0, 10) if err != nil { t.Fatal(err) } if page.Total != 0 { t.Errorf("empty table total = %d, want 0", page.Total) } if len(page.Rows) != 0 { t.Errorf("empty table rows = %v, want none", page.Rows) } // Columns are still reported so a UI can render an empty grid. wantCols := []string{"sku", "qty"} if len(page.Columns) != 2 || page.Columns[0] != wantCols[0] || page.Columns[1] != wantCols[1] { t.Errorf("empty table columns = %v, want %v", page.Columns, wantCols) } } // TestRowsResolvesLongText guards the fix for out-of-line (address-encoded) // text: a >4KB longtext column must render its actual content, not the // "" placeholder that inline-only rendering produced. func TestRowsResolvesLongText(t *testing.T) { db := openFixture(t) ctx := context.Background() page, err := db.Rows(ctx, "main", "docs", 0, 10) if err != nil { t.Fatal(err) } if len(page.Rows) != 1 { t.Fatalf("docs rows = %d, want 1", len(page.Rows)) } body := page.Rows[0][1] // columns: id, body if body == placeholderBinary { t.Fatalf("longtext rendered as %q; address-encoded text was not resolved", body) } if body != longDocBody { t.Errorf("longtext body mismatch: got %d bytes, want %d bytes (prefix %.40q)", len(body), len(longDocBody), body) } } func TestCommitSummaryAddTable(t *testing.T) { db := openFixture(t) ctx := context.Background() // C1 creates the empty users table. diff, err := db.CommitSummary(ctx, commitHash(t, msgAddUser)) if err != nil { t.Fatal(err) } td := findTable(t, diff, "users") if !td.Added { t.Errorf("users should be Added in C1: %+v", td) } if td.Dropped { t.Errorf("users should not be Dropped: %+v", td) } if td.RowsAdded != 0 || td.RowsRemoved != 0 || td.RowsModified != 0 { t.Errorf("C1 (empty table) row counts = %+v, want all 0", td) } } func TestCommitSummaryInsertRows(t *testing.T) { db := openFixture(t) ctx := context.Background() // C2 inserts 3 rows. diff, err := db.CommitSummary(ctx, commitHash(t, msgInsert)) if err != nil { t.Fatal(err) } td := findTable(t, diff, "users") if td.Added || td.Dropped { t.Errorf("C2 users should be a data change, not add/drop: %+v", td) } if td.RowsAdded != 3 { t.Errorf("C2 RowsAdded = %d, want 3", td.RowsAdded) } if td.RowsRemoved != 0 || td.RowsModified != 0 { t.Errorf("C2 unexpected removed/modified: %+v", td) } } func TestCommitSummaryModifyRows(t *testing.T) { db := openFixture(t) ctx := context.Background() // C3 updates id=1 and inserts id=4. diff, err := db.CommitSummary(ctx, commitHash(t, msgModify)) if err != nil { t.Fatal(err) } td := findTable(t, diff, "users") if td.RowsAdded != 1 { t.Errorf("C3 RowsAdded = %d, want 1", td.RowsAdded) } if td.RowsModified != 1 { t.Errorf("C3 RowsModified = %d, want 1", td.RowsModified) } if td.RowsRemoved != 0 { t.Errorf("C3 RowsRemoved = %d, want 0", td.RowsRemoved) } } func TestCommitSummaryInitial(t *testing.T) { db := openFixture(t) ctx := context.Background() // C0 is the initial (parentless) commit, diffed against the empty root. // WriteEmptyRepo's initial commit has no tables, so the summary is empty // but must not error and must exercise the empty-root path. diff, err := db.CommitSummary(ctx, commitHash(t, msgInitial)) if err != nil { t.Fatalf("initial commit summary: %v", err) } if len(diff.Tables) != 0 { t.Errorf("initial commit should have no table changes, got %+v", diff.Tables) } if diff.Hash != commitHash(t, msgInitial) { t.Errorf("summary hash = %q, want %q", diff.Hash, commitHash(t, msgInitial)) } } func TestErrorsNonexistentRefAndTable(t *testing.T) { db := openFixture(t) ctx := context.Background() if _, err := db.Tables(ctx, "no-such-ref"); !errors.Is(err, ErrRefNotFound) { t.Errorf("Tables(bad ref) err = %v, want ErrRefNotFound", err) } if _, _, err := db.Log(ctx, "no-such-ref", "", 10); !errors.Is(err, ErrRefNotFound) { t.Errorf("Log(bad ref) err = %v, want ErrRefNotFound", err) } if _, err := db.Rows(ctx, "main", "no-such-table", 0, 10); !errors.Is(err, ErrTableNotFound) { t.Errorf("Rows(bad table) err = %v, want ErrTableNotFound", err) } } func findTable(t *testing.T, diff *CommitDiff, name string) TableDiff { t.Helper() for _, td := range diff.Tables { if td.Name == name { return td } } t.Fatalf("table %q not in diff %+v", name, diff.Tables) return TableDiff{} } func equalRows(got, want [][]string) bool { if len(got) != len(want) { return false } for i := range got { if len(got[i]) != len(want[i]) { return false } for j := range got[i] { if got[i][j] != want[i][j] { return false } } } return true }