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)
}
}