package console import ( "context" "errors" "fmt" "path/filepath" "sync/atomic" "testing" "time" "github.com/carlosframework/platform/internal/hostops" ) // useSQLInstanceSources runs the production identity reader against stubbed // enrollment and live-instance sources, so no test touches /etc or the // network. func useSQLInstanceSources(t *testing.T, enrolled, dmi, imds string) { t.Helper() restore := useSQLInstanceIdentity(readSQLInstanceIdentity) oldE, oldD, oldI := sqlEnrolledInstance, sqlDMIInstance, sqlIMDSInstance sqlEnrolledInstance = func() (string, error) { return enrolled, nil } sqlDMIInstance = func() string { return dmi } sqlIMDSInstance = func() string { return imds } t.Cleanup(func() { sqlEnrolledInstance, sqlDMIInstance, sqlIMDSInstance = oldE, oldD, oldI restore() }) } // D1: a disk image carries root's host-actor.json with the original's // instance ID. Only the live machine's ID can tell a clone apart, so the // enrollment must match it and a copied file alone opens nothing. func TestPlan9Fix1SQLIdentityRefusesCopiedEnrollment(t *testing.T) { path := filepath.Join(t.TempDir(), "console.db") t.Run("original", func(t *testing.T) { useSQLInstanceSources(t, "i-0a1b2c3d4e5f60001", "i-0a1b2c3d4e5f60001", "") s, err := OpenGated(path) if err != nil { t.Fatal(err) } s.Close() }) t.Run("clone", func(t *testing.T) { useSQLInstanceSources(t, "i-0a1b2c3d4e5f60001", "i-0c10e0c10e0c10e01", "") if s, err := OpenGated(path); err == nil { s.Close() t.Fatal("clone opened the original's receipt database from a copied enrollment") } else if !errors.Is(err, hostops.ErrIdentity) { t.Fatalf("clone refusal: %v", err) } }) t.Run("clone via IMDS", func(t *testing.T) { useSQLInstanceSources(t, "i-0a1b2c3d4e5f60001", "", "i-0c10e0c10e0c10e01") if s, err := OpenGated(path); err == nil { s.Close() t.Fatal("clone opened the original's receipt database (IMDS source)") } }) t.Run("no live instance", func(t *testing.T) { useSQLInstanceSources(t, "i-0a1b2c3d4e5f60001", "", "") if s, err := OpenGated(filepath.Join(t.TempDir(), "fresh.db")); err == nil { s.Close() t.Fatal("gated DB opened on the enrollment file alone") } }) } func TestPlan9Fix1SQLLiveInstanceSources(t *testing.T) { for _, c := range []struct { name, dmi, imds, want string }{ {"nitro asset tag", "i-0123456789abcdef0", "i-0imds", "i-0123456789abcdef0"}, {"non-instance asset tag falls back", "Default string", "i-0imds", "i-0imds"}, {"nothing", "", "", ""}, } { t.Run(c.name, func(t *testing.T) { useSQLInstanceSources(t, "unused", c.dmi, c.imds) if got := sqlLiveInstance(); got != c.want { t.Fatalf("live instance = %q, want %q", got, c.want) } }) } } // L4: a public transaction whose own context ends is rolled back by // database/sql behind the callback's back. If the callback then ignores the // failed Commit and returns nil, the batch must still abort: its receipt // would otherwise claim writes that were never made. func TestPlan9Fix1BatchRefusesContextRolledBackTransaction(t *testing.T) { s := openGatedStore(t) seedGateApp(t, s) r := receiptFixture(t, s) err := preparedBatch(t, s, &r, func(v *Store) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() tx, err := v.db.BeginTx(ctx, nil) if err != nil { return err } if _, err = tx.Exec(`UPDATE audit SET action='ctx-lost' WHERE account_id=7 AND app='blog'`); err != nil { return err } cancel() // Wait until database/sql's own rollback reached the driver: the // write is gone from this connection's view. deadline := time.Now().Add(10 * time.Second) for { var n int if err := v.db.QueryRow(`SELECT count(*) FROM audit WHERE action='ctx-lost'`).Scan(&n); err != nil { return err } if n == 0 { break } if time.Now().After(deadline) { return errors.New("database/sql never rolled the transaction back") } time.Sleep(5 * time.Millisecond) } _ = tx.Commit() // the misuse under test: the failure is ignored return nil }) if err == nil { t.Fatal("batch reported success after its transaction was rolled back by its context") } var n int if qerr := s.rawBoot.QueryRow(`SELECT count(*) FROM claim_sql_receipts WHERE effect=? AND state='committed'`, r.Effect).Scan(&n); qerr != nil || n != 0 { t.Fatalf("receipt committed for lost writes: n=%d err=%v", n, qerr) } // An explicit rollback under a live context is the callback's own choice // and still lets the batch commit. r2 := r r2.Effect = "ingest-explicit-rollback" if err = preparedBatch(t, s, &r2, func(v *Store) error { tx, err := v.db.BeginTx(context.Background(), nil) if err != nil { return err } if _, err = tx.Exec(`UPDATE audit SET action='rolled' WHERE account_id=7 AND app='blog'`); err != nil { tx.Rollback() return err } return tx.Rollback() }); err != nil { t.Fatalf("explicit rollback aborted the batch: %v", err) } } // L4, unbatched: the same lost transaction must not let withClaimSQL report // success either. func TestPlan9Fix1ScopeRefusesContextRolledBackTransaction(t *testing.T) { s := openGatedStore(t) seedGateApp(t, s) err := s.withClaimSQL(t.Context(), []claimSQLScope{gateScope}, allowAllClaimSQL, func(v *Store) error { ctx, cancel := context.WithCancel(context.Background()) tx, err := v.db.BeginTx(ctx, nil) if err != nil { cancel() return err } if _, err = tx.Exec(`UPDATE audit SET action='ctx-lost' WHERE account_id=7 AND app='blog'`); err != nil { cancel() return err } cancel() _ = tx.Rollback() // after its context ended: the writes are lost either way return nil }) if err == nil { t.Fatal("scope reported success after losing a written transaction") } if countWhere(t, s, "audit", "action='ctx-lost'") != 0 { t.Fatal("lost write landed") } } // L4, failed commit: in a batch, a public commit refused because one scope // died must abort the batch even if the callback swallows the refusal and // another scope is still live. func TestPlan9Fix1BatchRefusesSwallowedFailedCommit(t *testing.T) { s := openGatedStore(t) seedGateApp(t, s) r := receiptFixture(t, s) other := claimSQLScope{AccountID: gateAcct, App: "blog-b", AppID: "app-B", Mode: claimModeEffect} var killed atomic.Bool local := func(_ context.Context, sc claimSQLScope) error { if killed.Load() && sc == gateScope { return errors.New("permit expired") } return nil } err := s.withPreparedClaimSQL(t.Context(), []claimSQLScope{gateScope, other}, allowAllClaimSQL, local, []hostops.SQLReceiptScope{r}, true, func(v *Store) error { tx, err := v.db.Begin() if err != nil { return err } if _, err = tx.Exec(`UPDATE audit SET action='refused' WHERE account_id=7 AND app='blog'`); err != nil { return err } killed.Store(true) if err = tx.Commit(); !errors.Is(err, ErrClaimSQLDenied) { return fmt.Errorf("commit err = %v, want ErrClaimSQLDenied", err) } return nil // the misuse under test }) if err == nil { t.Fatal("batch reported success after a swallowed failed commit") } var n int if qerr := s.rawBoot.QueryRow(`SELECT count(*) FROM claim_sql_receipts WHERE effect=? AND state='committed'`, r.Effect).Scan(&n); qerr != nil || n != 0 { t.Fatalf("receipt committed for lost writes: n=%d err=%v", n, qerr) } }