package console // claim_invariant_world_test.go builds the console TestClaimConsoleMutationInvariant // runs against, and records every effect that reaches a raw writer. // // The console is built the way cmd/console builds it with CARLOS_HOST_CLAIM // on: OpenGated, then NewServer with HostClaim already true, then // ConfigureClaimRuntime. Nothing switches the flag after construction. The // raw bucket, the raw versioned store and the four raw backends handed to // Config, and the outbound mail HTTP transport, are recorders. The test sees what got past every // boundary the production constructor installs over them, and nothing else. // // Recorders sit BELOW the boundaries. A write the boundary refused never // reaches them, so a refusal can never be counted as an effect, and an // effect can never be hidden by a refusal elsewhere in the same request. import ( "context" "crypto/ed25519" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "slices" "sort" "strings" "sync" "testing" "time" "github.com/carlosframework/platform/internal/dnsrec" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/lease" "github.com/carlosframework/platform/internal/lease/leasetest" "github.com/carlosframework/platform/internal/mail" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/release" ) const ( claimFxConsole = "console-a" claimFxBucket = "bucket" claimFxOwner = "paul@keymail.dev" claimFxNamespace = "platform" claimFxForeignOp = "claim-fixture-foreign" claimFxRouterSHA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" claimFxConsoleSHA = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" ) var claimFxConsoleEnrollment = strings.Repeat("c", 32) // claimAppRef is one app incarnation a door affects. Fresh marks a name that // has no incarnation before the request: the door creates it. type claimAppRef struct { Account, App string Fresh bool } func (r claimAppRef) String() string { return r.Account + "/" + r.App } func (r claimAppRef) scope() hostops.AppScope { return hostops.AppScope{Account: r.Account, App: r.App} } // ---- the recorder ----------------------------------------------------------- // claimRawEvent is one effect that reached a raw writer. type claimRawEvent struct { Surface string // "bucket", "versioned:", or a backend interface such as "objstore.Provisioner" Method string Key string Scopes []hostops.AppScope // the production classifier's affected apps Control bool // a canonical control object (hostops.IsControlOnlyKey) NonApp string // the classifier's reviewed non-app reason Err error // the classifier refuses this key: production should never have let it through // Unscoped is a backend call or a data-bucket version write: no bucket // classifier names its app, so it counts against every affected app. Unscoped bool } func (e claimRawEvent) String() string { return fmt.Sprintf("%s %s %s", e.Surface, e.Method, e.Key) } func (e claimRawEvent) touches(refs []claimAppRef) bool { for _, s := range e.Scopes { for _, r := range refs { if s == r.scope() { return true } } } return false } // admissionWrite reports a write to one of ref's canonical control objects. func (e claimRawEvent) admissionWrite(ref claimAppRef) bool { return e.Control && e.Key == "apps/"+ref.Account+"/"+ref.App+"/admission.json" } type claimRecorder struct { mu sync.Mutex events []claimRawEvent read release.Store // the raw bucket, read-only, for the classifier // control is the production control writer the classifier verifies lifecycle // roots against. It exists only once ConfigureClaimRuntime has run, so it // is installed after construction (setControl), never nil afterwards. control *hostops.ControlWriter } func (r *claimRecorder) setControl(w *hostops.ControlWriter) { r.mu.Lock() r.control = w r.mu.Unlock() } func (r *claimRecorder) controlWriter() *hostops.ControlWriter { r.mu.Lock() defer r.mu.Unlock() return r.control } func (r *claimRecorder) add(e claimRawEvent) { r.mu.Lock() r.events = append(r.events, e) r.mu.Unlock() } func (r *claimRecorder) mark() int { r.mu.Lock() defer r.mu.Unlock() return len(r.events) } func (r *claimRecorder) since(i int) []claimRawEvent { r.mu.Lock() defer r.mu.Unlock() return append([]claimRawEvent(nil), r.events[i:]...) } // classifyBucket runs the production classifier over one raw bucket write, // BEFORE it lands, so owner-record families see the old owner too. func (r *claimRecorder) classifyBucket(ctx context.Context, surface, method, key, etag string, after []byte) claimRawEvent { e := claimRawEvent{Surface: surface, Method: method, Key: key} if hostops.IsControlOnlyKey(key) { e.Control = true if p := strings.Split(key, "/"); len(p) == 4 && p[0] == "apps" { e.Scopes = []hostops.AppScope{{Account: p[1], App: p[2]}} } return e } ctx = context.WithoutCancel(ctx) before, _, err := r.read.Get(ctx, key) if err != nil { before = nil } c, err := hostops.ClassifyMutation(ctx, r.read, r.controlWriter(), hostops.MutationTarget{Surface: "bucket", Method: method, Key: key, ETag: etag, Before: before, After: after}) e.Scopes, e.NonApp, e.Err = c.Scopes, c.NonApp, err return e } // backend is the check the generated backend boundaries run before the raw // fake: it records the attempt and lets it through. These boundaries are the // RAW side here; the console installs its own wrappers above them. func (r *claimRecorder) backend(_ context.Context, t hostops.MutationTarget) error { r.add(claimRawEvent{Surface: t.Surface, Method: t.Method, Key: t.Key, Unscoped: true}) return nil } // claimRawStore is the raw deployment bucket handed to Config.Releases. // Named private fields; it forwards the optional interfaces the directory // store really has and records each mutation that succeeds. type claimRawStore struct { inner release.Store // deletableStore: Store plus DeleteIfMatch dir *release.DirStore // the same directory, for the optional listers rec *claimRecorder } var ( _ release.Store = (*claimRawStore)(nil) _ release.ConditionalDeleter = (*claimRawStore)(nil) _ release.Opener = (*claimRawStore)(nil) _ release.ModLister = (*claimRawStore)(nil) _ release.SizeLister = (*claimRawStore)(nil) _ release.PageLister = (*claimRawStore)(nil) ) func (s *claimRawStore) Get(ctx context.Context, key string) ([]byte, string, error) { return s.inner.Get(ctx, key) } func (s *claimRawStore) List(ctx context.Context, prefix string) ([]string, error) { return s.inner.List(ctx, prefix) } func (s *claimRawStore) ListDirs(ctx context.Context, prefix string) ([]string, error) { return s.inner.ListDirs(ctx, prefix) } func (s *claimRawStore) Open(ctx context.Context, key string) (io.ReadCloser, string, error) { return s.dir.Open(ctx, key) } func (s *claimRawStore) ListMod(ctx context.Context, prefix string) ([]release.KeyMod, error) { return s.dir.ListMod(ctx, prefix) } func (s *claimRawStore) ListSizes(ctx context.Context, prefix string) ([]release.KeySize, error) { return s.dir.ListSizes(ctx, prefix) } func (s *claimRawStore) ListPage(ctx context.Context, prefix, startAfter string, limit int) ([]string, error) { return s.dir.ListPage(ctx, prefix, startAfter, limit) } func (s *claimRawStore) Put(ctx context.Context, key string, body []byte, tag string) (string, error) { e := s.rec.classifyBucket(ctx, "bucket", "Put", key, tag, body) out, err := s.inner.Put(ctx, key, body, tag) if err == nil { s.rec.add(e) } return out, err } func (s *claimRawStore) Delete(ctx context.Context, key string) error { e := s.rec.classifyBucket(ctx, "bucket", "Delete", key, "", nil) err := s.inner.Delete(ctx, key) if err == nil { s.rec.add(e) } return err } func (s *claimRawStore) DeleteIfMatch(ctx context.Context, key, tag string) error { e := s.rec.classifyBucket(ctx, "bucket", "DeleteIfMatch", key, tag, nil) err := release.DeleteIfMatch(ctx, s.inner, key, tag) if err == nil { s.rec.add(e) } return err } // claimRawVersions is the raw exact-version store handed to Config.DBVersions. type claimRawVersions struct { inner *leasetest.VersionedBucket rec *claimRecorder } var ( _ lease.VersionedStore = (*claimRawVersions)(nil) _ lease.VersionRangeOpener = (*claimRawVersions)(nil) ) func (v *claimRawVersions) ListVersions(ctx context.Context, bucket, prefix string) ([]lease.ObjectVersion, error) { return v.inner.ListVersions(ctx, bucket, prefix) } func (v *claimRawVersions) OpenVersion(ctx context.Context, bucket, key, versionID string) (io.ReadCloser, error) { return v.inner.OpenVersion(ctx, bucket, key, versionID) } func (v *claimRawVersions) GetVersion(ctx context.Context, bucket, key, versionID string) ([]byte, error) { return v.inner.GetVersion(ctx, bucket, key, versionID) } func (v *claimRawVersions) OpenVersionRange(ctx context.Context, bucket, key, versionID string, offset, length int64) (io.ReadCloser, error) { return v.inner.OpenVersionRange(ctx, bucket, key, versionID, offset, length) } func (v *claimRawVersions) event(ctx context.Context, bucket, method, key string, body []byte) claimRawEvent { if bucket == claimFxBucket { return v.rec.classifyBucket(ctx, "versioned:"+bucket, method, key, "", body) } // A data bucket has no bucket classifier: the effect is unscoped, and an // unscoped effect counts against every affected app (strict). return claimRawEvent{Surface: "versioned:" + bucket, Method: method, Key: key, Unscoped: true} } func (v *claimRawVersions) CopyVersion(ctx context.Context, srcBucket, srcKey, srcVersionID, dstBucket, dstKey string, createOnly bool) (string, string, error) { e := v.event(ctx, dstBucket, "CopyVersion", dstKey, nil) id, tag, err := v.inner.CopyVersion(ctx, srcBucket, srcKey, srcVersionID, dstBucket, dstKey, createOnly) if err == nil { v.rec.add(e) } return id, tag, err } func (v *claimRawVersions) DeleteVersion(ctx context.Context, bucket, key, versionID string) error { e := v.event(ctx, bucket, "DeleteVersion", key, nil) err := v.inner.DeleteVersion(ctx, bucket, key, versionID) if err == nil { v.rec.add(e) } return err } func (v *claimRawVersions) PutIfAbsent(ctx context.Context, bucket, key string, body []byte) (string, error) { e := v.event(ctx, bucket, "PutIfAbsent", key, body) id, err := v.inner.PutIfAbsent(ctx, bucket, key, body) if err == nil { v.rec.add(e) } return id, err } // ---- the host-actor world --------------------------------------------------- // claimFixtureEvidence answers the census the way a healthy fleet would: one // router per roster box and this console, all on the published system // release. Identity and termination fences are the real ExactEvidence reads. // It replaces only fleet REPORTS; admission, permits and every boundary stay // the production ones. type claimFixtureEvidence struct { exact hostactors.ExactEvidence floor string } var _ hostactors.Evidence = claimFixtureEvidence{} func (e claimFixtureEvidence) report(component, actor, enrollment, instance, sha string) hostactors.Report { return hostactors.Report{Component: component, Actor: actor, Enrollment: enrollment, InstanceID: instance, ProcessID: "claim-fixture-process", GeneratedAt: time.Now().UTC(), Release: "v1.0.0", ReleaseTag: "v1.0.0", Platform: "linux-amd64", BinarySHA256: sha, MovesProtocol: 1, BootID: "claim-fixture-boot", FloorSHA256: e.floor, FloorGuardReady: true, HostClaimEnabled: true} } func (e claimFixtureEvidence) BoxReport(_ context.Context, id string, b hostactors.Box) (hostactors.Report, error) { return e.report("router", id, b.Enrollment, b.Cloud.InstanceID, claimFxRouterSHA), nil } func (e claimFixtureEvidence) ConsoleReport(_ context.Context, id string, c hostactors.Console) (hostactors.Report, error) { return e.report("console", id, c.Enrollment, c.InstanceID, claimFxConsoleSHA), nil } func (e claimFixtureEvidence) LocalConsole(context.Context) (hostactors.Report, error) { return e.report("console", claimFxConsole, claimFxConsoleEnrollment, claimFxConsole, claimFxConsoleSHA), nil } func (e claimFixtureEvidence) Identity(ctx context.Context, tag string) ([]byte, error) { return e.exact.Identity(ctx, tag) } func (e claimFixtureEvidence) Fence(ctx context.Context, o hostactors.Observed, ref string, now time.Time) (bool, error) { return e.exact.Fence(ctx, o, ref, now) } // claimActorWorld publishes the system release, the floor and a roster with // this console and one active box per pool in region "ie". It writes the raw // bucket before the console exists. It returns the floor digest. func claimActorWorld(t *testing.T, store release.Store, key ed25519.PrivateKey, pub ed25519.PublicKey) string { t.Helper() ctx := context.Background() sg := release.RootSigner(key) sys := release.SystemRelease{Version: "v1.0.0", Source: "release:v1.0.0", ReleaseTag: "v1.0.0", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{"linux-amd64": { Key: release.SystemArtifactKey("v1.0.0", "linux-amd64"), SHA256: claimFxRouterSHA}}, ConsoleArtifacts: map[string]release.SystemArtifact{"linux-amd64": { Key: "bootstrap/console/v1.0.0/carlos-linux-amd64", SHA256: claimFxConsoleSHA}}} if _, err := release.PromoteSystem(ctx, store, sg, release.SystemComponentRouter, "stable", sys, release.SystemPromoteOptions{}); err != nil { t.Fatalf("actor world: promote system release: %v", err) } pointer, _, err := store.Get(ctx, release.SystemPointerKey(release.SystemComponentRouter, "stable")) if err != nil { t.Fatal(err) } if err := release.PublishSystemIdentity(ctx, store, pub, pointer); err != nil { t.Fatalf("actor world: publish identity: %v", err) } if err := hostfloor.Publish(ctx, store, sg, claimFxBucket, hostfloor.Floor{Min: 1, MinRelease: "v1.0.0"}); err != nil { t.Fatalf("actor world: publish floor: %v", err) } floor, err := hostfloor.Read(ctx, store, pub, claimFxBucket) if err != nil { t.Fatal(err) } box := func(id, pool, enrollment, instance string) hostactors.Box { return hostactors.Box{BoxID: id, Namespace: claimFxNamespace, Label: id, Region: "ie", Pool: pool, Cloud: hostactors.Cloud{Provider: "aws", Account: "123456789012", Region: "eu-west-1", InstanceID: instance}, Enrollment: enrollment, EvidenceSHA256: strings.Repeat("e", 64), State: "active"} } roster := hostactors.Roster{V: 1, DeploymentBucket: claimFxBucket, Revision: 1, Publication: "published", FloorSHA256: floor.Digest, Regions: map[string]hostactors.Region{"ie": {Namespaces: []string{claimFxNamespace}, Pools: []string{"default", "tito"}, Account: "123456789012", AWSRegion: "eu-west-1"}}, Boxes: map[string]hostactors.Box{ "box-ie-default": box("box-ie-default", "default", strings.Repeat("b", 32), "i-claim-default"), "box-ie-tito": box("box-ie-tito", "tito", strings.Repeat("d", 32), "i-claim-tito"), }, Consoles: map[string]hostactors.Console{claimFxConsole: {Enrollment: claimFxConsoleEnrollment, InstanceID: claimFxConsole, Namespaces: []string{claimFxNamespace}, DeploymentBucket: claimFxBucket}}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} envelope, err := release.SignRootEnvelope(sg, roster) if err != nil { t.Fatal(err) } if _, err := store.Put(ctx, hostactors.Key, envelope, ""); err != nil { t.Fatal(err) } return floor.Digest } // ---- the case --------------------------------------------------------------- // claimCase is one enabled console and everything a fixture needs to drive // it. It embeds the ordinary consoleFixture so the existing helpers // (signUp, cliToken, postGated, completeStepUp, declareEmail, grantStore, // enableInstances, ...) work unchanged. fx.rel is the RAW bucket: helpers // that seed through it stage the world outside every measured window. type claimCase struct { *consoleFixture rec *claimRecorder rawStore *claimRawStore rawVersions *claimRawVersions versions *leasetest.VersionedBucket storeFake *objstore.MemProvisioner scanFake *objstore.MemScanner mailFake *mail.MemProvisioner dnsFake *dnsrec.MemWriter rawProv objstore.Provisioner rawScanner objstore.Scanner rawMail mail.Provisioner rawDNS dnsrec.Writer secretsPriv string browser *http.Client csrf string bearer string vars map[string]string appIDs map[claimAppRef]string sessions int // hits records which mux pattern each request through the listener // matched, so a probe proves its request reached the entry it covers // (and not another route, a 404 or the front page). hits *claimHits } // claimHits is the list of mux patterns the listener's requests matched, in // arrival order. type claimHits struct { mu sync.Mutex seen []string } func (h *claimHits) add(pattern string) { h.mu.Lock() h.seen = append(h.seen, pattern) h.mu.Unlock() } func (h *claimHits) mark() int { h.mu.Lock() defer h.mu.Unlock() return len(h.seen) } func (h *claimHits) since(i int) []string { h.mu.Lock() defer h.mu.Unlock() return append([]string(nil), h.seen[i:]...) } // newClaimCase builds the enabled console. Config.HostClaim is true at // NewServer; ConfigureClaimRuntime runs exactly as cmd/console runs it. func newClaimCase(t *testing.T) *claimCase { return newClaimCaseWith(t) } // newClaimCaseWith is newClaimCase with the Config edited before NewServer, // for entries that exist only behind a switch (CARLOS_MOVES_PHASE1). func newClaimCaseWith(t *testing.T, opts ...func(*Config)) *claimCase { t.Helper() ctx := context.Background() regPath, inner, priv, pub := buildFixtures(t) deletable, ok := inner.(*deletableStore) if !ok { t.Fatalf("buildFixtures returned %T, want *deletableStore", inner) } dir, ok := deletable.Store.(*release.DirStore) if !ok { t.Fatalf("buildFixtures' bucket is %T, want *release.DirStore", deletable.Store) } floor := claimActorWorld(t, inner, priv, pub) rec := &claimRecorder{read: inner} rawStore := &claimRawStore{inner: inner, dir: dir, rec: rec} versions := leasetest.NewVersionedBucket() versions.AddBucket(claimFxBucket) rawVersions := &claimRawVersions{inner: versions, rec: rec} storeFake := objstore.NewMemProvisioner("eu-west-1") scanFake := objstore.NewMemScanner() mailFake := mail.NewMemProvisioner("1") dnsFake := dnsrec.NewMemWriter() rawProv, err := NewStoreProvisionerBoundary(storeFake, rec.backend) if err != nil { t.Fatal(err) } rawScanner, err := NewScannerBoundary(scanFake, rec.backend) if err != nil { t.Fatal(err) } rawMail, err := NewMailProvisionerBoundary(mailFake, rec.backend) if err != nil { t.Fatal(err) } rawDNS, err := NewDNSWriterBoundary(dnsFake, rec.backend) if err != nil { t.Fatal(err) } secretsPub, secretsPriv, _ := sealedFor(t, "irrelevant") st, err := OpenGated(filepath.Join(t.TempDir(), "console.db")) if err != nil { t.Fatalf("OpenGated: %v", err) } t.Cleanup(func() { st.Close() }) fk := newFakeKeymail(t, claimFxOwner) var srv *Server hits := &claimHits{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, pattern := srv.mux.Handler(r) hits.add(pattern) srv.ServeHTTP(w, r) })) t.Cleanup(ts.Close) operators := []string{claimFxOwner} cfg := Config{ HostClaim: true, HostConsoleID: claimFxConsole, Bucket: claimFxBucket, DBVersions: rawVersions, Origin: ts.URL, Keymail: fk.srv.URL, Registry: regPath, Releases: rawStore, ReleasePub: pub, Key: priv, AppScheme: "http", AppsDomain: "oncarlos.com", PlatformDomain: "oncarlos.com", SecretsPub: secretsPub, TemporaryApps: true, StoreProvisioner: rawProv, StoreScanner: rawScanner, CallbackSigner: callbackTestSigner(t, "claim-fixture-webhook-master", ""), CallbackTransport: claimOKTransport{rec: rec}, StoreDeployment: objstore.Deployment{Backend: "mem", Region: "eu-west-1", BucketPrefix: "carlos-"}, MailProvisioner: rawMail, DNSWriter: rawDNS, MailDeployment: mail.Deployment{Region: "eu-west-1", PlatformSuffix: "oncarlos.com", SNSTopicARN: "arn:aws:sns:eu-west-1:1:carlos-mail"}, RelayRegions: []string{"eu-west-1"}, StoreOperators: operators, MailOperators: operators, RelayOperators: operators, SystemOperators: operators, LookupTXT: func(context.Context, string) ([]string, error) { return nil, errors.New("claim fixture: no DNS") }, } for _, opt := range opts { opt(&cfg) } if home := cfg.dbClaimHomeRegion(); home != "" { d := hostactors.Directory{Store: rawStore, PublicKey: pub, Bucket: claimFxBucket} o, e := d.Read(ctx) if e != nil { t.Fatal(e) } r := o.Roster r.HomeRegion = home if _, e = d.Replace(ctx, release.RootSigner(priv), o, r); e != nil { t.Fatal(e) } } srv, err = NewServer(cfg, st) if err != nil { t.Fatalf("NewServer with HostClaim: %v", err) } if err := srv.ConfigureClaimRuntime(ctx); err != nil { t.Fatalf("ConfigureClaimRuntime: %v", err) } if srv.cfg.ClaimAuthority == nil { t.Fatal("ConfigureClaimRuntime installed no claim authority") } if srv.cfg.ClaimAuthority.Control == nil { t.Fatal("ConfigureClaimRuntime installed no control writer") } rec.setControl(srv.cfg.ClaimAuthority.Control) srv.cfg.ClaimAuthority.Evidence = claimFixtureEvidence{ exact: hostactors.ExactEvidence{PublicKey: pub, Store: inner}, floor: floor} srv.instances.timeout = 100 * time.Millisecond srv.lookupCNAME = func(context.Context, string) (string, error) { return "", errors.New("claim fixture: no DNS") } srv.storeListerFor = func(context.Context, *objstore.Provisioned) (objstore.ObjectLister, error) { return claimOneObjectLister{}, nil } if err := st.AddMember(OpsAccountID, claimFxOwner, RoleOwner, time.Now()); err != nil { t.Fatal(err) } if err := st.SetOperator(claimFxOwner, true); err != nil { t.Fatal(err) } cx := &claimCase{ consoleFixture: &consoleFixture{ts: ts, srv: srv, st: st, fk: fk, rel: inner, regPath: regPath, priv: priv, pub: pub, sqid: release.OpsAccount}, rec: rec, rawStore: rawStore, rawVersions: rawVersions, versions: versions, storeFake: storeFake, scanFake: scanFake, mailFake: mailFake, dnsFake: dnsFake, rawProv: rawProv, rawScanner: rawScanner, rawMail: rawMail, rawDNS: rawDNS, secretsPriv: secretsPriv, vars: map[string]string{}, appIDs: map[claimAppRef]string{}, hits: hits, } cx.enroll(t, release.OpsAccount, "kass") cx.enroll(t, release.OpsAccount, "titogo") cx.browser = newBrowser(t) signUp(t, cx.consoleFixture, cx.browser, "code-claim-"+claimSafeName(t.Name())) cx.csrf = sessionToken(t, cx.consoleFixture, cx.browser) cx.bearer = mintUserToken(t, st, claimFxOwner, time.Now()) cx.quiesce() return cx } func claimSafeName(s string) string { return strings.Map(func(r rune) rune { if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' { return r } return '-' }, s) } // claimOneObjectLister lists one plaintext object, so a sweep reaches the // scanner's Scan rather than finishing on an empty bucket. type claimOneObjectLister struct{} func (claimOneObjectLister) ListPage(_ context.Context, _, startAfter string, _ int32) ([]objstore.ScanObject, bool, error) { if startAfter != "" { return nil, false, nil } return []objstore.ScanObject{{Key: "claim-fixture.txt", ETag: `"claim-fixture"`, Size: 3, Modified: time.Now().Add(-time.Hour)}}, false, nil } // enroll stages an existing, open incarnation: the console's row bound to // an app_id (through the test-only seed scope, the SQL gate's own fixture // door) and the canonical admission object in the raw bucket. func (cx *claimCase) enroll(t *testing.T, account, app string) string { t.Helper() acct, err := cx.st.AccountBySqid(account) if err != nil || acct == nil { t.Fatalf("enroll %s/%s: account %+v %v", account, app, acct, err) } appID := "claim-" + account + "-" + app scope := []claimSQLScope{{AccountID: acct.ID, App: app, AppID: appID, Mode: claimModeEffect}} err = cx.st.withClaimSQL(gateCtx(t), scope, allowAllClaimSQL, func(v *Store) error { if _, err := v.CreateAppIn(acct.ID, app, claimFxOwner, time.Now()); err != nil { return err } return v.BindClaimAppIdentity(acct.ID, app, appID) }) if err != nil { t.Fatalf("enroll %s/%s: %v", account, app, err) } cx.putAdmission(t, account, app, hostops.AppAdmission{V: 1, AppID: appID, Revision: 1, Epoch: 1, State: "open", Registrations: map[string]hostops.Registration{}, Effects: map[string]hostops.Effect{}}) cx.appIDs[claimAppRef{Account: account, App: app}] = appID return appID } func admissionKeyOf(account, app string) string { return "apps/" + account + "/" + app + "/admission.json" } // putAdmission writes the canonical admission object through the RAW bucket: // world staging, never a door under test. func (cx *claimCase) putAdmission(t *testing.T, account, app string, a hostops.AppAdmission) { t.Helper() ctx := context.Background() key := admissionKeyOf(account, app) _, tag, err := cx.rel.Get(ctx, key) if errors.Is(err, os.ErrNotExist) { tag = "" } else if err != nil { t.Fatal(err) } body, err := json.Marshal(a) if err != nil { t.Fatal(err) } if _, err := cx.rel.Put(ctx, key, body, tag); err != nil { t.Fatalf("stage admission %s: %v", key, err) } } // foreign closes ref under an exclusive operation this console does not own, // and returns the function that puts the observed admission back. A fresh // name gets a predecessor incarnation closed under that operation. func (cx *claimCase) foreign(t *testing.T, ref claimAppRef) func() { t.Helper() ctx := context.Background() key := admissionKeyOf(ref.Account, ref.App) body, _, err := cx.rel.Get(ctx, key) if errors.Is(err, os.ErrNotExist) { appID := "claim-foreign-" + ref.Account + "-" + ref.App cx.putAdmission(t, ref.Account, ref.App, hostops.AppAdmission{V: 1, AppID: appID, Revision: 1, Epoch: 1, State: "closed", Op: &hostops.AppOperation{ID: claimFxForeignOp, Kind: hostops.OpAppMutation, AppID: appID, Phase: "issued", Intent: map[string]string{"action": claimFxForeignOp}}, Registrations: map[string]hostops.Registration{}, Effects: map[string]hostops.Effect{}}) return func() { if err := cx.rel.Delete(ctx, key); err != nil { t.Fatalf("remove the foreign predecessor of %s: %v", ref, err) } } } if err != nil { t.Fatal(err) } var original hostops.AppAdmission if err := json.Unmarshal(body, &original); err != nil { t.Fatalf("admission %s: %v", key, err) } closed := original closed.Revision++ closed.State = "closed" closed.Op = &hostops.AppOperation{ID: claimFxForeignOp, Kind: hostops.OpAppMutation, AppID: original.AppID, Phase: "issued", Intent: map[string]string{"action": claimFxForeignOp}} closed.Registrations = map[string]hostops.Registration{} cx.putAdmission(t, ref.Account, ref.App, closed) return func() { back := original back.Revision = closed.Revision + 1 cx.putAdmission(t, ref.Account, ref.App, back) } } // quiesce waits until no raw effect has arrived for two consecutive 100ms // windows (at most 5s), so a goroutine a handler started is measured with its // request. Two windows, not one: the invariant runs its doors in parallel // (claimCaseParallelism), and a goroutine starved of CPU for one window must // still land in its own request's measurement, not the next one's. func (cx *claimCase) quiesce() { const window = 100 * time.Millisecond deadline := time.Now().Add(5 * time.Second) n, quiet := cx.rec.mark(), 0 for time.Now().Before(deadline) { time.Sleep(window) m := cx.rec.mark() if m != n { n, quiet = m, 0 continue } if quiet++; quiet == 2 { return } } } // ---- SQL digests ------------------------------------------------------------ // sqlDigest is one hash per gated table over ref's rows, read through the // gated handle (reads need no permit). Only tables the census classified as // app-scoped are read: they are exactly the tables the triggers guard. func (cx *claimCase) sqlDigest(t *testing.T, ref claimAppRef) map[string]string { t.Helper() out := map[string]string{} acct, err := cx.st.AccountBySqid(ref.Account) if err != nil { t.Fatal(err) } if acct == nil { return out } names := make([]string, 0, len(claimSQLTables)) for name := range claimSQLTables { names = append(names, name) } sort.Strings(names) for _, name := range names { tb := claimSQLTables[name] if tb.NonApp != "" { continue } where := []string{tb.col("t", tb.App) + " = ?"} args := []any{ref.App} if tb.Account != "" { where = append(where, tb.col("t", tb.Account)+" = ?") args = append(args, acct.ID) } if tb.Sqid != "" { where = append(where, tb.col("t", tb.Sqid)+" = ?") args = append(args, ref.Account) } rows, err := cx.st.db.QueryContext(context.Background(), `SELECT * FROM `+claimQ(name)+` t WHERE `+strings.Join(where, " AND "), args...) if err != nil { if strings.Contains(err.Error(), "no such table") { continue } t.Fatalf("digest %s: %v", name, err) } cols, err := rows.Columns() if err != nil { rows.Close() t.Fatal(err) } var lines []string for rows.Next() { vals := make([]any, len(cols)) ptrs := make([]any, len(cols)) for i := range vals { ptrs[i] = &vals[i] } if err := rows.Scan(ptrs...); err != nil { rows.Close() t.Fatal(err) } parts := make([]string, len(vals)) for i, v := range vals { if b, ok := v.([]byte); ok { v = string(b) } parts[i] = fmt.Sprintf("%v", v) } lines = append(lines, strings.Join(parts, "\x1f")) } err = rows.Err() rows.Close() if err != nil { t.Fatal(err) } sort.Strings(lines) sum := sha256.Sum256([]byte(strings.Join(lines, "\x1e"))) out[name] = hex.EncodeToString(sum[:]) } return out } func claimChangedTables(before, after map[string]string) []string { var out []string for name, a := range after { if before[name] != a { out = append(out, name) } } for name := range before { if _, ok := after[name]; !ok { out = append(out, name) } } sort.Strings(out) return slices.Compact(out) } // ---- requests --------------------------------------------------------------- // api sends one authenticated CLI request as the owner and returns its status. func (cx *claimCase) api(t *testing.T, method, path, body string) int { t.Helper() status, _, _ := rawReq(t, method, cx.ts.URL+path, cx.bearer, body) return status } // mustAPI is api for staging: anything but a wanted status stops the case. func (cx *claimCase) mustAPI(t *testing.T, method, path, body string, want ...int) string { t.Helper() status, _, out := rawReq(t, method, cx.ts.URL+path, cx.bearer, body) if len(want) == 0 { want = []int{http.StatusOK, http.StatusCreated, http.StatusAccepted} } if !slices.Contains(want, status) { t.Fatalf("stage %s %s = %d %s", method, path, status, out) } return out } // form posts one browser form as the signed-in owner, with the session's // CSRF token, and walks the passkey step-up when the door asks for one. func (cx *claimCase) form(t *testing.T, path string, kv ...string) int { t.Helper() v := url.Values{"csrf": {cx.csrf}} for i := 0; i+1 < len(kv); i += 2 { v.Add(kv[i], kv[i+1]) } status, _, _ := postGated(t, cx.consoleFixture, cx.browser, cx.ts.URL+path, v) return status } func (cx *claimCase) mustForm(t *testing.T, path string, kv ...string) { t.Helper() if status := cx.form(t, path, kv...); status == 0 || status >= 400 { t.Fatalf("stage form %s = %d", path, status) } } // jsonField reads one string field out of a staging reply. func claimReplyField(t *testing.T, body, field string) string { t.Helper() var m map[string]any if err := json.Unmarshal([]byte(body), &m); err != nil { t.Fatalf("reply %q: %v", body, err) } v, _ := m[field].(string) if v == "" { t.Fatalf("reply %q has no %s", body, field) } return v }