carlos / platform Public

test(domains): a trash marker never crosses accounts — same name, different tenant, untouched

App names are unique within an account and never globally: two tenants
legitimately both run a "derp". The removal key is one RouteAccount(rt)
away from being a name-only match, and that one word is the difference
between deleting an app and deleting a stranger's. Nothing pinned it.

Two accounts, one name, one marker, two passes: A's routes go, B's custom
route and apex survive with their stamp intact and byte-identical across
the second pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell ecdf5b885cd1ee052960648cb5094386abcd3299 parent 01569c3
1 file changed, +63 −0
  • internal/domains/reconcile_test.go +63 −0
diff --git a/internal/domains/reconcile_test.go b/internal/domains/reconcile_test.go
index 21cb9c3b6..370ecb56c 100644
--- a/internal/domains/reconcile_test.go
+++ b/internal/domains/reconcile_test.go
@@ -15,6 +15,7 @@ import (
"os"
"path/filepath"
"reflect"
+ "slices"
"testing"
"time"
@@ -724,6 +725,68 @@ func TestReconcileTrashedAppWithUnstampedRouteStillDrops(t *testing.T) {
}
}
+// THE safety property of the whole trash mechanism: app names are unique
+// within an account and never globally, so two tenants legitimately both run a
+// "derp". The removal key is (account, app) — one `release.RouteAccount(rt)`
+// away from being a name-only match, and that one word is the difference
+// between deleting an app and deleting a stranger's. Account A trashing its
+// derp must leave B's derp exactly as it found it.
+func TestReconcileTrashMarkerNeverCrossesAccounts(t *testing.T) {
+ a, b := domains.Sqid(793481920157), domains.Sqid(555000111222)
+ s, reg := dirStore(t), regWith(t,
+ registry.Route{Host: "derp.a.custom", Kind: "static", App: "derp",
+ Status: "ready", Channel: "stable", Account: a},
+ registry.Route{Host: "derp." + a + ".oncarlos.test", Kind: "static", App: "derp",
+ Status: "ready", Channel: "stable", Platform: true, Account: a},
+ registry.Route{Host: "derp.b.custom", Kind: "static", App: "derp",
+ Status: "ready", Channel: "stable", Account: b},
+ registry.Route{Host: "derp." + b + ".oncarlos.test", Kind: "static", App: "derp",
+ Status: "ready", Channel: "stable", Platform: true, Account: b},
+ )
+ if got := writeAccountManifest(t, s, 793481920157, "Ayy"); got != a {
+ t.Fatalf("account A sqid = %q, want %q", got, a)
+ }
+ if got := writeAccountManifest(t, s, 555000111222, "Bee"); got != b {
+ t.Fatalf("account B sqid = %q, want %q", got, b)
+ }
+ putKey(t, s, "apps/"+a+"/derp/channels/stable.json")
+ putKey(t, s, "apps/"+b+"/derp/channels/stable.json")
+ // Only A trashes its derp.
+ if err := release.TrashApp(ctx, s, a, "derp", "paul@keymail.dev", time.Unix(1753900000, 0)); err != nil {
+ t.Fatal(err)
+ }
+
+ wantHosts := []string{"derp.b.custom", "derp." + b + ".oncarlos.test"}
+ slices.Sort(wantHosts) // reg.List returns hosts in sorted order
+ var first []registry.Route
+ for pass := 1; pass <= 2; pass++ {
+ if err := domains.Reconcile(ctx, s, reg, cfg(t)); err != nil {
+ t.Fatalf("pass %d: %v", pass, err)
+ }
+ got := routesFor(t, reg, "derp")
+ var hosts []string
+ for _, rt := range got {
+ hosts = append(hosts, rt.Host)
+ if rt.Account != b {
+ t.Errorf("pass %d: surviving route %s stamped %q, want B (%s)", pass, rt.Host, rt.Account, b)
+ }
+ }
+ if !reflect.DeepEqual(hosts, wantHosts) {
+ t.Fatalf("pass %d: derp routes = %v, want exactly B's %v — A's marker must not reach B",
+ pass, hosts, wantHosts)
+ }
+ if pass == 1 {
+ first = got
+ continue
+ }
+ // Byte-identical across the second pass too: no re-stamp, no re-mint,
+ // no field quietly rewritten under B.
+ if !reflect.DeepEqual(first, got) {
+ t.Errorf("second pass changed B's routes:\nfirst %+v\nsecond %+v", first, got)
+ }
+ }
+}
+
// A marker we cannot fully believe must not destroy routes: ReadAppTombstone
// errors, reconcile logs it and treats the app as live, every pass, until
// someone fixes or removes the object.