package db import ( "context" "errors" "testing" ) func TestDocumentRegistryLifecycle(t *testing.T) { s, _, cleanup := newTestStore(t) defer cleanup() ctx := context.Background() space := mkSpace(t, s, ctx, "bigbes", "rfcs") id := docID(t, "SPEC-0007") doc, err := s.RegisterDocID(ctx, space.ID, DocRef{ID: id, Path: "specs/0007-storage.md"}, "rev1") if err != nil { t.Fatalf("register: %v", err) } if doc.SpaceID != space.ID || doc.Path != "specs/0007-storage.md" || doc.UpdatedRev != "rev1" { t.Fatalf("unexpected registered doc: %+v", doc) } // The doc_id PRIMARY KEY makes a second claim unwritable, in this space or // any other. if _, err := s.RegisterDocID(ctx, space.ID, DocRef{ID: id, Path: "other.md"}, "rev2"); !errors.Is(err, ErrDocIDTaken) { t.Fatalf("re-register in the same space = %v, want ErrDocIDTaken", err) } other := mkSpace(t, s, ctx, "bigbes", "notes") if _, err := s.RegisterDocID(ctx, other.ID, DocRef{ID: id, Path: "n.md"}, "rev2"); !errors.Is(err, ErrDocIDTaken) { t.Fatalf("re-register in another space = %v, want ErrDocIDTaken", err) } got, err := s.DocByID(ctx, id) if err != nil { t.Fatalf("lookup: %v", err) } if got.ID != id || got.Path != "specs/0007-storage.md" { t.Fatalf("unexpected lookup: %+v", got) } if _, err := s.DocByID(ctx, docID(t, "SPEC-9999")); !errors.Is(err, ErrNotFound) { t.Fatalf("missing id = %v, want ErrNotFound", err) } // Rename: the ID is stable, the path moves. if err := s.SetDocPath(ctx, space.ID, DocRef{ID: id, Path: "specs/0007-proposals.md"}, "rev3"); err != nil { t.Fatalf("rename: %v", err) } got, err = s.DocByID(ctx, id) if err != nil { t.Fatalf("lookup after rename: %v", err) } if got.Path != "specs/0007-proposals.md" || got.UpdatedRev != "rev3" { t.Fatalf("rename did not stick: %+v", got) } // A rename scoped to the wrong space must not relocate the document. if err := s.SetDocPath(ctx, other.ID, DocRef{ID: id, Path: "stolen.md"}, "rev4"); !errors.Is(err, ErrNotFound) { t.Fatalf("cross-space rename = %v, want ErrNotFound", err) } if got, _ := s.DocByID(ctx, id); got.Path != "specs/0007-proposals.md" { t.Fatalf("cross-space rename moved the document: %+v", got) } docs, err := s.ListDocsBySpace(ctx, space.ID) if err != nil { t.Fatalf("list docs: %v", err) } if len(docs) != 1 || docs[0].ID != id { t.Fatalf("unexpected doc list: %+v", docs) } // Deletion (human push only) frees the ID again. if err := s.UnregisterDocID(ctx, other.ID, id); !errors.Is(err, ErrNotFound) { t.Fatalf("unregister from the wrong space = %v, want ErrNotFound", err) } if err := s.UnregisterDocID(ctx, space.ID, id); err != nil { t.Fatalf("unregister: %v", err) } if _, err := s.DocByID(ctx, id); !errors.Is(err, ErrNotFound) { t.Fatalf("after unregister = %v, want ErrNotFound", err) } } func TestDocIDCollisionCheck(t *testing.T) { s, _, cleanup := newTestStore(t) defer cleanup() ctx := context.Background() specs := mkSpace(t, s, ctx, "bigbes", "rfcs") notes := mkSpace(t, s, ctx, "bigbes", "notes") mine := docID(t, "SPEC-1") theirs := docID(t, "NOTE-1") if _, err := s.RegisterDocID(ctx, specs.ID, DocRef{ID: mine, Path: "a.md"}, "rev1"); err != nil { t.Fatalf("register: %v", err) } if _, err := s.RegisterDocID(ctx, notes.ID, DocRef{ID: theirs, Path: "b.md"}, "rev1"); err != nil { t.Fatalf("register: %v", err) } // Pushing into specs: SPEC-1 is ours (a rename at worst), NOTE-1 is not. batch := []DocRef{ {ID: mine, Path: "a-renamed.md"}, {ID: theirs, Path: "c.md"}, {ID: docID(t, "SPEC-2"), Path: "d.md"}, } collisions, err := s.CheckDocIDCollisions(ctx, specs.ID, batch) if err != nil { t.Fatalf("collision check: %v", err) } if len(collisions) != 1 { t.Fatalf("expected 1 collision, got %+v", collisions) } if collisions[0].DocID != theirs || collisions[0].Existing.SpaceID != notes.ID { t.Fatalf("unexpected collision: %+v", collisions[0]) } } func TestUpsertDocIDsRejectsCrossSpaceClaim(t *testing.T) { s, _, cleanup := newTestStore(t) defer cleanup() ctx := context.Background() specs := mkSpace(t, s, ctx, "bigbes", "rfcs") notes := mkSpace(t, s, ctx, "bigbes", "notes") stolen := docID(t, "NOTE-1") if _, err := s.RegisterDocID(ctx, notes.ID, DocRef{ID: stolen, Path: "n.md"}, "rev1"); err != nil { t.Fatalf("register: %v", err) } // A fresh ID plus one owned by another space: the statement's ON CONFLICT // guard leaves the foreign row alone and the missing row surfaces as a // CollisionError. fresh := docID(t, "SPEC-1") err := s.UpsertDocIDs(ctx, specs.ID, []DocRef{ {ID: fresh, Path: "a.md"}, {ID: stolen, Path: "b.md"}, }, "rev2") if !errors.Is(err, ErrDocIDTaken) { t.Fatalf("cross-space upsert = %v, want ErrDocIDTaken", err) } var ce *CollisionError if !errors.As(err, &ce) || len(ce.Collisions) != 1 || ce.Collisions[0].DocID != stolen { t.Fatalf("expected a CollisionError naming %s, got %v", stolen, err) } // The foreign row is untouched. got, err := s.DocByID(ctx, stolen) if err != nil { t.Fatalf("lookup foreign doc: %v", err) } if got.SpaceID != notes.ID || got.Path != "n.md" || got.UpdatedRev != "rev1" { t.Fatalf("foreign row was modified: %+v", got) } // A clean batch inserts and then updates in place. if err := s.UpsertDocIDs(ctx, specs.ID, []DocRef{ {ID: fresh, Path: "a.md"}, {ID: docID(t, "SPEC-2"), Path: "b.md"}, }, "rev3"); err != nil { t.Fatalf("clean upsert: %v", err) } if err := s.UpsertDocIDs(ctx, specs.ID, []DocRef{ {ID: fresh, Path: "a-moved.md"}, }, "rev4"); err != nil { t.Fatalf("upsert rename: %v", err) } got, err = s.DocByID(ctx, fresh) if err != nil { t.Fatalf("lookup: %v", err) } if got.Path != "a-moved.md" || got.UpdatedRev != "rev4" { t.Fatalf("upsert rename did not stick: %+v", got) } }