instance policy: nothing set is byte-identical (plan 1's part)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+139
−0
internal/provision/apppolicy_reconcile_test.go+139 −0
diff --git a/internal/provision/apppolicy_reconcile_test.go b/internal/provision/apppolicy_reconcile_test.go| index f1dc43ae0..a43c3c75a 100644 |
| --- a/internal/provision/apppolicy_reconcile_test.go |
| +++ b/internal/provision/apppolicy_reconcile_test.go |
| @@ -3,9 +3,11 @@ package provision |
| import ( |
| "context" |
| "crypto/ed25519" |
| + "database/sql" |
| "errors" |
| "fmt" |
| "os" |
| + "slices" |
| "strconv" |
| "strings" |
| "sync" |
| @@ -471,3 +473,140 @@ func TestReconcileHeldMintCarriesThePolicy(t *testing.T) { |
| } |
| wantCols(t, rt, "10.0.5.20", "large", fx.now.Add(apppolicy.Lifetime).Unix()) |
| } |
| + |
| +// Nothing set is byte-identical (spec §7, plan 1's part, ruling PF-1): with |
| +// no policy object anywhere, reconcile mints the rows main's mint does, both |
| +// registry digests equal main's, a later pass writes nothing, and the policy |
| +// reads stay inside spec §3a's backstop allowance (one listing per backstop, |
| +// no GET). |
| +func TestNoPolicyLeavesRowsAndFingerprintsAsMain(t *testing.T) { |
| + fx := newPolicyFixture(t) |
| + if err := PutManifest(ctx, fx.s, fx.sqid, "blog", Manifest{Schema: 1, App: "blog", Domain: "example.com", Seed: []string{"seed"}}); err != nil { |
| + t.Fatal(err) |
| + } |
| + blog := fx.record("blog-a.example.com") |
| + blog.App = "blog" |
| + recs := []Record{blog, fx.record(policyHost), fx.record("shop-b.example.com")} |
| + for _, rec := range recs { |
| + fx.putRecord(rec) |
| + } |
| + // The production shape: one cache for every pass, over a store that |
| + // counts the policy listings and reads. |
| + fx.cfg.Policies = NewPolicyCache(nil) |
| + cs := &countingStore{DirStore: fx.s, gets: map[string]int{}} |
| + |
| + fx.runOn(cs) // pass 1 mints, and is the cache's first backstop |
| + if c := cs.take(); c.lists != 0 || c.listETags != 1 || c.totalGets() != 0 { |
| + t.Fatalf("minting pass: %d LISTs, %d ETag listings, %d GETs of policy keys, want 0, 1, 0", c.lists, c.listETags, c.totalGets()) |
| + } |
| + minted, err := fx.reg.List() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(minted) != len(recs) { |
| + t.Fatalf("minted %d rows, want %d", len(minted), len(recs)) |
| + } |
| + for _, rt := range minted { |
| + wantCols(t, rt, "", "", 0) |
| + } |
| + |
| + // Main's mint, which carries neither column, upserted in the order this |
| + // pass minted (Fingerprint hashes the rowid, and the pass walks its |
| + // records in map order, on main as here). |
| + byHost := map[string]Record{} |
| + for _, rec := range recs { |
| + byHost[rec.Host] = rec |
| + } |
| + mainReg := regWith(t) |
| + for _, host := range mintOrder(t, fx.reg) { |
| + rec := byHost[host] |
| + m, err := GetManifest(ctx, fx.s, rec.Account, rec.App) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + rt := RouteFor(rec, "/data", seedJSON(m), false, nil) |
| + // Upsert stamps CreatedAt from the wall clock when it is zero; not |
| + // part of the contract under test (TestReconcileMints). |
| + rt.CreatedAt = fx.row(rec.Host).CreatedAt |
| + if err := mainReg.Upsert(rt); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + mainRows, err := mainReg.List() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if !slices.Equal(minted, mainRows) { |
| + t.Fatalf("minted rows differ from main's mint:\n got %+v\nwant %+v", minted, mainRows) |
| + } |
| + digests := func(reg *registry.Registry) (string, string) { |
| + t.Helper() |
| + f, err := reg.Fingerprint() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + s, err := reg.SnapshotFingerprint() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return f, s |
| + } |
| + f, s := digests(fx.reg) |
| + mainF, mainS := digests(mainReg) |
| + if f != mainF { |
| + t.Fatalf("Fingerprint = %s, main's mint %s", f, mainF) |
| + } |
| + if s != mainS { |
| + t.Fatalf("SnapshotFingerprint = %s, main's mint %s", s, mainS) |
| + } |
| + |
| + // Pass 2, between backstops: no policy request, nothing written. |
| + fx.runOn(cs) |
| + if c := cs.take(); c.lists != 0 || c.listETags != 0 || c.totalGets() != 0 { |
| + t.Fatalf("second pass: %d LISTs, %d ETag listings, %d GETs of policy keys, want none", c.lists, c.listETags, c.totalGets()) |
| + } |
| + if f2, s2 := digests(fx.reg); f2 != f || s2 != s { |
| + t.Fatalf("second pass moved the digests: Fingerprint %s -> %s, SnapshotFingerprint %s -> %s", f, f2, s, s2) |
| + } |
| + |
| + // The next backstop: its one listing, still nothing written. |
| + fx.now = fx.now.Add(34 * time.Minute) |
| + fx.runOn(cs) |
| + if c := cs.take(); c.lists != 0 || c.listETags != 1 || c.totalGets() != 0 { |
| + t.Fatalf("backstop pass: %d LISTs, %d ETag listings, %d GETs of policy keys, want 0, 1, 0", c.lists, c.listETags, c.totalGets()) |
| + } |
| + if f3, s3 := digests(fx.reg); f3 != f || s3 != s { |
| + t.Fatalf("backstop pass moved the digests: Fingerprint %s -> %s, SnapshotFingerprint %s -> %s", f, f3, s, s3) |
| + } |
| + if got := fx.changedHosts(); len(got) != 0 { |
| + t.Fatalf("PolicyChanged with no policy: %v", got) |
| + } |
| + fx.wantState("") |
| +} |
| + |
| +// mintOrder is reg's hosts in rowid order: the order they were minted. |
| +func mintOrder(t *testing.T, reg *registry.Registry) []string { |
| + t.Helper() |
| + db, err := sql.Open("sqlite", reg.Path()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer db.Close() |
| + rows, err := db.Query(`SELECT host FROM routes ORDER BY rowid`) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer rows.Close() |
| + var hosts []string |
| + for rows.Next() { |
| + var h string |
| + if err := rows.Scan(&h); err != nil { |
| + t.Fatal(err) |
| + } |
| + hosts = append(hosts, h) |
| + } |
| + if err := rows.Err(); err != nil { |
| + t.Fatal(err) |
| + } |
| + return hosts |
| +} |