213 lines
7.2 KiB
Go
213 lines
7.2 KiB
Go
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")
|
|
}
|
|
}
|