package repo_test import ( "context" "fmt" "strings" "testing" "github.com/krow/krow-backend/go-api/internal/authctx" "github.com/krow/krow-backend/go-api/internal/repo" "github.com/krow/krow-backend/go-api/internal/testutil" ) // Version immutability. // // §3 states it in one sentence — "specs are immutable once published" — and the // whole value of it is what it makes possible downstream: a run records the // version it answered under, and that number is only worth recording if it can // still be resolved to the definition that actually answered. // // The tests below are mostly about the ways that guarantee can be lost quietly. func fixture(t *testing.T, slug string) (*testutil.Harness, authctx.Identity, *repo.VersionsRepo) { t.Helper() h := testutil.New(t) var orgID string if err := h.Pool.QueryRow(context.Background(), `INSERT INTO organizations (name, slug) VALUES ($1, $2) RETURNING id::text`, slug, slug).Scan(&orgID); err != nil { t.Fatalf("create org: %v", err) } var userID string if err := h.Pool.QueryRow(context.Background(), ` INSERT INTO users (org_id, email, full_name, role) VALUES ($1::uuid, $2, 'Author', 'admin') RETURNING id::text`, orgID, fmt.Sprintf("author-%s@example.test", slug)).Scan(&userID); err != nil { t.Fatalf("create user: %v", err) } ident := authctx.Identity{ UserID: userID, OrgID: orgID, Role: "admin", Email: fmt.Sprintf("author-%s@example.test", slug), } return h, ident, repo.NewVersionsRepo(h.Pool) } func snapshot(id string, version int, markdown string) repo.SnapshotInput { return repo.SnapshotInput{ Kind: repo.KindAgent, DefinitionID: id, Version: version, Markdown: markdown, Name: "Test Agent", Pages: []string{"activity"}, } } func TestAPublishedVersionCanBeReadBackExactly(t *testing.T) { // The property everything else rests on: a version number resolves to the // definition that answered under it. h, ident, versions := fixture(t, "ver-readback") ctx := context.Background() _ = h md := "---\nid: a\nname: Test Agent\nversion: 1\n---\n\n## Instructions\nOriginal." if err := versions.Snapshot(ctx, ident, snapshot("a", 1, md)); err != nil { t.Fatalf("snapshot: %v", err) } got, err := versions.Load(ctx, ident, repo.KindAgent, "a", 1) if err != nil { t.Fatalf("load: %v", err) } if got.Markdown != md { t.Errorf("the definition came back changed:\n want %q\n got %q", md, got.Markdown) } if got.Version != 1 { t.Errorf("version = %d", got.Version) } } func TestRepublishingTheSameVersionWithDifferentContentIsRefused(t *testing.T) { // The moment somebody would otherwise rewrite what a person approved. // Refused loudly, with the version number in the message, rather than // silently taking the newer text. h, ident, versions := fixture(t, "ver-rewrite") ctx := context.Background() _ = h if err := versions.Snapshot(ctx, ident, snapshot("a", 1, "original")); err != nil { t.Fatalf("first publish: %v", err) } err := versions.Snapshot(ctx, ident, snapshot("a", 1, "rewritten")) if err == nil { t.Fatal("republishing version 1 with different content was accepted") } if !strings.Contains(err.Error(), "1") { t.Errorf("the refusal does not name the version: %v", err) } // And the original survives. got, _ := versions.Load(ctx, ident, repo.KindAgent, "a", 1) if got == nil || got.Markdown != "original" { t.Errorf("the stored version changed: %+v", got) } } func TestRepublishingIdenticalContentIsANoOp(t *testing.T) { // "Publish version 1 again" when version 1 already says exactly this is not // an error — it is a restatement of a true thing. Treating it as a conflict // would make every idempotent import fail on its second run. h, ident, versions := fixture(t, "ver-idempotent") ctx := context.Background() _ = h for i := 0; i < 3; i++ { if err := versions.Snapshot(ctx, ident, snapshot("a", 1, "same")); err != nil { t.Fatalf("publish %d: %v", i+1, err) } } history, err := versions.History(ctx, ident, repo.KindAgent, "a", 10) if err != nil { t.Fatalf("history: %v", err) } if len(history) != 1 { t.Errorf("%d versions after three identical publishes, want 1", len(history)) } } func TestEditingPublishesANewVersionAndKeepsTheOld(t *testing.T) { // §3's sentence, asserted: editing publishes a NEW version, and the old one // is still there afterwards. h, ident, versions := fixture(t, "ver-newversion") ctx := context.Background() _ = h if err := versions.Snapshot(ctx, ident, snapshot("a", 1, "v1 text")); err != nil { t.Fatalf("v1: %v", err) } if err := versions.Snapshot(ctx, ident, snapshot("a", 2, "v2 text")); err != nil { t.Fatalf("v2: %v", err) } one, err := versions.Load(ctx, ident, repo.KindAgent, "a", 1) if err != nil { t.Fatalf("v1 is gone after publishing v2: %v", err) } if one.Markdown != "v1 text" { t.Errorf("v1 changed when v2 was published: %q", one.Markdown) } latest, err := versions.LatestVersion(ctx, ident, repo.KindAgent, "a") if err != nil || latest != 2 { t.Errorf("latest = %d (err %v), want 2", latest, err) } } func TestAnotherTenantsVersionIsAbsent(t *testing.T) { // I5. A version from another organization is not forbidden, it is absent — // the same rule every other row follows, and for the same reason. h, mine, versions := fixture(t, "ver-mine") ctx := context.Background() var otherOrg string if err := h.Pool.QueryRow(ctx, `INSERT INTO organizations (name, slug) VALUES ('Other', 'ver-other') RETURNING id::text`, ).Scan(&otherOrg); err != nil { t.Fatalf("create other org: %v", err) } theirs := authctx.Identity{UserID: "", OrgID: otherOrg, Role: "admin", Email: "x@other.test"} if err := versions.Snapshot(ctx, mine, snapshot("shared-id", 1, "mine")); err != nil { t.Fatalf("publish: %v", err) } if _, err := versions.Load(ctx, theirs, repo.KindAgent, "shared-id", 1); err == nil { t.Fatal("another tenant read a version that was not theirs") } // And the same id in their own tenant is a different definition entirely. if err := versions.Snapshot(ctx, theirs, snapshot("shared-id", 1, "theirs")); err != nil { t.Fatalf("their own publish was refused: %v", err) } got, _ := versions.Load(ctx, mine, repo.KindAgent, "shared-id", 1) if got == nil || got.Markdown != "mine" { t.Errorf("one tenant's publish overwrote another's: %+v", got) } } func TestTheDatabaseRefusesToRewriteAVersion(t *testing.T) { // The repository has no update path, but the repository is not the only // thing that can reach the table — a migration, a console session and a // future service all can. This asserts the guarantee where it actually // lives. h, ident, versions := fixture(t, "ver-trigger") ctx := context.Background() if err := versions.Snapshot(ctx, ident, snapshot("a", 1, "original")); err != nil { t.Fatalf("publish: %v", err) } _, err := h.Pool.Exec(ctx, `UPDATE definition_versions SET markdown = 'rewritten' WHERE definition_id = 'a'`) if err == nil { t.Fatal("the database allowed a published version to be rewritten") } if !strings.Contains(err.Error(), "append-only") { t.Errorf("the refusal does not explain itself: %v", err) } _, err = h.Pool.Exec(ctx, `DELETE FROM definition_versions WHERE definition_id = 'a'`) if err == nil { t.Fatal("the database allowed a published version to be deleted") } }