mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 14:00:40 +00:00
143 lines
5.4 KiB
Go
143 lines
5.4 KiB
Go
package store
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/PerpetualSoftware/pad/internal/models"
|
|
)
|
|
|
|
func TestImportWorkspacePreservesReferenceTarget(t *testing.T) {
|
|
t.Parallel()
|
|
for _, deleteFirst := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("deleted_first=%v", deleteFirst), func(t *testing.T) {
|
|
s := testStore(t)
|
|
owner := createTestUser(t, s, "audit@example.com", "Audit", "password123")
|
|
ws := createTestWorkspace(t, s, "Reference source")
|
|
coll, err := s.CreateCollection(ws.ID, models.CollectionCreate{Name: "Tasks", Prefix: "TASK"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first := createTestItem(t, s, ws.ID, coll.ID, "Obsolete task", "")
|
|
target := createTestItem(t, s, ws.ID, coll.ID, "Deploy the release", "")
|
|
note := createTestItem(t, s, ws.ID, coll.ID, "Release instructions", "Follow [[TASK-2]] before continuing.")
|
|
comment, err := s.CreateComment(ws.ID, note.ID, "", models.CommentCreate{Body: "Review [[TASK-2]] first."})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Deterministic fixture for ordinary items created one second apart.
|
|
for i, item := range []*models.Item{first, target, note} {
|
|
_, err := s.db.Exec(s.q("UPDATE items SET created_at = ? WHERE id = ?"), fmt.Sprintf("2026-09-01T12:00:0%dZ", i), item.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
before, err := s.ResolveItem(ws.ID, "TASK-2")
|
|
if err != nil || before == nil || before.ID != target.ID {
|
|
t.Fatalf("source control: item=%+v err=%v", before, err)
|
|
}
|
|
if deleteFirst {
|
|
if err := s.DeleteItem(first.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
bundle, err := s.ExportWorkspace(ws.Slug)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dst, err := s.ImportWorkspace(bundle, "Reference restored", owner.ID, "cli")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, err := s.ResolveItem(dst.ID, "TASK-2")
|
|
if err != nil || after == nil {
|
|
t.Fatalf("restored ref lookup: item=%+v err=%v", after, err)
|
|
}
|
|
importedNote, err := s.ResolveItem(dst.ID, note.Slug)
|
|
if err != nil || importedNote == nil {
|
|
t.Fatalf("restored note lookup: item=%+v err=%v", importedNote, err)
|
|
}
|
|
t.Logf("before TASK-2=%q; after TASK-2=%q; restored body=%q; export/import succeeded", before.Title, after.Title, importedNote.Content)
|
|
if after.Title != target.Title {
|
|
t.Errorf("reference changed target: want %q, got %q", target.Title, after.Title)
|
|
}
|
|
if importedNote.Content != note.Content {
|
|
t.Errorf("content changed: %q", importedNote.Content)
|
|
}
|
|
comments, err := s.ListComments(importedNote.ID)
|
|
if err != nil || len(comments) != 1 || comments[0].Body != comment.Body {
|
|
t.Errorf("comments did not round-trip: %+v, err=%v", comments, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestImportWorkspaceItemNumberCompatibility(t *testing.T) {
|
|
t.Parallel()
|
|
for _, tc := range []struct {
|
|
name string
|
|
numbers []int
|
|
want []int
|
|
orphan bool
|
|
}{
|
|
{name: "out of order with gaps", numbers: []int{7, 2}, want: []int{7, 2}},
|
|
{name: "legacy duplicate", numbers: []int{1, 1}, want: []int{1, 2}},
|
|
{name: "missing number", numbers: []int{0, 2}, want: []int{1, 2}},
|
|
{name: "negative number", numbers: []int{-1, 2}, want: []int{1, 2}},
|
|
{name: "unnumbered archive", numbers: []int{0, 0}, want: []int{1, 2}},
|
|
{name: "orphan duplicate ignored", numbers: []int{7, 2}, want: []int{7, 2}, orphan: true},
|
|
{name: "empty archive"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
s := testStore(t)
|
|
owner := createTestUser(t, s, "import@example.com", "Importer", "password123")
|
|
const timestamp = "2026-09-01T12:00:00Z"
|
|
bundle := &models.WorkspaceExport{
|
|
Version: 1,
|
|
Workspace: models.WorkspaceExportMeta{Name: "Number archive", Slug: "number-archive"},
|
|
Collections: []models.CollectionExport{
|
|
{ID: "tasks", Name: "Tasks", Slug: "tasks", Prefix: "TASK", Schema: `{"fields":[]}`, CreatedAt: timestamp, UpdatedAt: timestamp},
|
|
{ID: "plans", Name: "Plans", Slug: "plans", Prefix: "PLAN", Schema: `{"fields":[]}`, CreatedAt: timestamp, UpdatedAt: timestamp},
|
|
},
|
|
}
|
|
for i, number := range tc.numbers {
|
|
slug := fmt.Sprintf("item-%d", i)
|
|
bundle.Items = append(bundle.Items, models.ItemExport{
|
|
ID: slug, CollectionID: bundle.Collections[i].ID, Title: slug, Slug: slug,
|
|
ItemNumber: number, CreatedAt: timestamp, UpdatedAt: timestamp,
|
|
})
|
|
}
|
|
if tc.orphan {
|
|
bundle.Items = append(bundle.Items, models.ItemExport{ID: "orphan", CollectionID: "missing", ItemNumber: 7})
|
|
}
|
|
dst, err := s.ImportWorkspace(bundle, "Imported numbers", owner.ID, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
imported, err := s.ExportWorkspace(dst.Slug)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(imported.Items) != len(tc.want) {
|
|
t.Fatalf("imported %d items, want %d", len(imported.Items), len(tc.want))
|
|
}
|
|
maxNumber := 0
|
|
for i, number := range tc.want {
|
|
item, err := s.GetItemBySlug(dst.ID, bundle.Items[i].Slug)
|
|
if err != nil || item == nil || item.ItemNumber == nil {
|
|
t.Fatalf("read imported item: %+v, err=%v", item, err)
|
|
}
|
|
if *item.ItemNumber != number {
|
|
t.Errorf("%s number=%d, want %d", item.Slug, *item.ItemNumber, number)
|
|
}
|
|
maxNumber = max(maxNumber, number)
|
|
}
|
|
created := createTestItem(t, s, dst.ID, imported.Collections[0].ID, "After import", "")
|
|
if created.ItemNumber == nil || *created.ItemNumber != maxNumber+1 {
|
|
t.Errorf("next number=%v, want %d", created.ItemNumber, maxNumber+1)
|
|
}
|
|
})
|
|
}
|
|
}
|