carlos / platform Public

Add read-only sponsored candidate inventory prerequisite

claude agent, operated by paul@keymail.dev 9fa0af01f7d581b2cfc2fe3df22a1ab1964953c5 parent b58b661
2 files changed, +698 −0
  • internal/console/sponsoredinventory.go +241 −0
  • internal/console/sponsoredinventory_test.go +457 −0

CI passed — run details

diff --git a/internal/console/sponsoredinventory.go b/internal/console/sponsoredinventory.go
new file mode 100644
index 000000000..bceee6139
--- /dev/null
+++ b/internal/console/sponsoredinventory.go
@@ -0,0 +1,241 @@
+package console
+
+import (
+ "context"
+ "database/sql"
+ "os"
+
+ "github.com/carlosframework/platform/internal/domains"
+)
+
+type sponsoredCandidateInventory struct {
+ Rows []sponsoredCandidateRow
+}
+
+type sponsoredCandidateRow struct {
+ ID int64
+ Sqid, Name string
+ StoredCreatedAt, DeletedAt int64
+ Owners []string
+ Billing sponsoredCandidateBilling
+ CheckoutPresent, BillingRetirement bool
+ Outbox sponsoredCandidateOutbox
+ OpenBillingReviews, EntitlementReviews int64
+}
+
+type sponsoredCandidateBilling struct {
+ Present, House, LiveMode, CustomerPresent bool
+ Subscription, Status string
+}
+
+type sponsoredCandidateOutbox struct {
+ Present, LiveMode, SerialOwed bool
+ Revision, PublishedRevision int64
+ Digest, State string
+}
+
+// readSponsoredCandidates observes console-local facts only. It neither
+// decides the sponsored cohort nor grants anything. ReadOnly is a convention
+// with the current SQLite driver, so every statement below is deliberately a
+// SELECT and tests compare the complete logical database state before/after.
+func (s *Store) readSponsoredCandidates(ctx context.Context) (sponsoredCandidateInventory, error) {
+ var zero sponsoredCandidateInventory
+ if s == nil || s.db == nil || ctx == nil {
+ return zero, os.ErrInvalid
+ }
+ tx, err := s.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
+ if err != nil {
+ return zero, err
+ }
+ defer tx.Rollback()
+ out, err := readSponsoredCandidatesTx(ctx, tx, nil)
+ if err != nil {
+ return zero, err
+ }
+ if err := tx.Commit(); err != nil {
+ return zero, err
+ }
+ return out, nil
+}
+
+// readSponsoredCandidatesTx is split out only for the deterministic
+// two-handle snapshot test. afterAccounts runs after the account rows have
+// fixed the WAL snapshot and been closed; production always passes nil.
+func readSponsoredCandidatesTx(ctx context.Context, tx *sql.Tx, afterAccounts func() error) (sponsoredCandidateInventory, error) {
+ var zero sponsoredCandidateInventory
+ if ctx == nil || tx == nil {
+ return zero, os.ErrInvalid
+ }
+ out := sponsoredCandidateInventory{Rows: make([]sponsoredCandidateRow, 0)}
+ byID := make(map[int64]int)
+
+ // Open configures file-backed console databases for WAL. With SQLite's
+ // deferred transaction, this first SELECT fixes the snapshot used by every
+ // later SELECT in this transaction.
+ rows, err := tx.QueryContext(ctx, `SELECT id,sqid,name,created_at,deleted_at FROM accounts ORDER BY id`)
+ if err != nil {
+ return zero, err
+ }
+ for rows.Next() {
+ var row sponsoredCandidateRow
+ if err := rows.Scan(&row.ID, &row.Sqid, &row.Name, &row.StoredCreatedAt, &row.DeletedAt); err != nil {
+ rows.Close()
+ return zero, err
+ }
+ if row.ID < 1 || row.Sqid != domains.Sqid(row.ID) {
+ rows.Close()
+ return zero, os.ErrInvalid
+ }
+ row.Owners = make([]string, 0)
+ byID[row.ID] = len(out.Rows)
+ out.Rows = append(out.Rows, row)
+ }
+ if err := closeSponsoredRows(rows); err != nil {
+ return zero, err
+ }
+ if afterAccounts != nil {
+ if err := afterAccounts(); err != nil {
+ return zero, err
+ }
+ }
+
+ rows, err = tx.QueryContext(ctx, `SELECT account_id,address FROM memberships WHERE role=? ORDER BY account_id,address`, RoleOwner)
+ if err != nil {
+ return zero, err
+ }
+ for rows.Next() {
+ var accountID int64
+ var address string
+ if err := rows.Scan(&accountID, &address); err != nil {
+ rows.Close()
+ return zero, err
+ }
+ if i, ok := byID[accountID]; ok {
+ out.Rows[i].Owners = append(out.Rows[i].Owners, address)
+ }
+ }
+ if err := closeSponsoredRows(rows); err != nil {
+ return zero, err
+ }
+
+ rows, err = tx.QueryContext(ctx, `SELECT account_id FROM billing_retirements ORDER BY account_id`)
+ if err != nil {
+ return zero, err
+ }
+ for rows.Next() {
+ var accountID int64
+ if err := rows.Scan(&accountID); err != nil {
+ rows.Close()
+ return zero, err
+ }
+ if i, ok := byID[accountID]; ok {
+ out.Rows[i].BillingRetirement = true
+ }
+ }
+ if err := closeSponsoredRows(rows); err != nil {
+ return zero, err
+ }
+
+ rows, err = tx.QueryContext(ctx, `SELECT account_id,house,live_mode,(customer<>''),subscription,status FROM billing_accounts ORDER BY account_id`)
+ if err != nil {
+ return zero, err
+ }
+ for rows.Next() {
+ var accountID int64
+ var billing sponsoredCandidateBilling
+ if err := rows.Scan(&accountID, &billing.House, &billing.LiveMode, &billing.CustomerPresent, &billing.Subscription, &billing.Status); err != nil {
+ rows.Close()
+ return zero, err
+ }
+ if i, ok := byID[accountID]; ok {
+ billing.Present = true
+ out.Rows[i].Billing = billing
+ }
+ }
+ if err := closeSponsoredRows(rows); err != nil {
+ return zero, err
+ }
+
+ rows, err = tx.QueryContext(ctx, `SELECT account_id FROM billing_checkout_intents ORDER BY account_id`)
+ if err != nil {
+ return zero, err
+ }
+ for rows.Next() {
+ var accountID int64
+ if err := rows.Scan(&accountID); err != nil {
+ rows.Close()
+ return zero, err
+ }
+ if i, ok := byID[accountID]; ok {
+ out.Rows[i].CheckoutPresent = true
+ }
+ }
+ if err := closeSponsoredRows(rows); err != nil {
+ return zero, err
+ }
+
+ rows, err = tx.QueryContext(ctx, `SELECT account_id,revision,digest,state,live_mode,published_revision,serial_owed FROM entitlement_outbox ORDER BY account_id`)
+ if err != nil {
+ return zero, err
+ }
+ for rows.Next() {
+ var accountID int64
+ var outbox sponsoredCandidateOutbox
+ if err := rows.Scan(&accountID, &outbox.Revision, &outbox.Digest, &outbox.State, &outbox.LiveMode, &outbox.PublishedRevision, &outbox.SerialOwed); err != nil {
+ rows.Close()
+ return zero, err
+ }
+ if i, ok := byID[accountID]; ok {
+ outbox.Present = true
+ out.Rows[i].Outbox = outbox
+ }
+ }
+ if err := closeSponsoredRows(rows); err != nil {
+ return zero, err
+ }
+
+ rows, err = tx.QueryContext(ctx, `SELECT account_id,count(*) FROM billing_reviews WHERE resolved_at=0 GROUP BY account_id ORDER BY account_id`)
+ if err != nil {
+ return zero, err
+ }
+ for rows.Next() {
+ var accountID, count int64
+ if err := rows.Scan(&accountID, &count); err != nil {
+ rows.Close()
+ return zero, err
+ }
+ if i, ok := byID[accountID]; ok {
+ out.Rows[i].OpenBillingReviews = count
+ }
+ }
+ if err := closeSponsoredRows(rows); err != nil {
+ return zero, err
+ }
+
+ rows, err = tx.QueryContext(ctx, `SELECT account_id,count(*) FROM entitlement_reviews GROUP BY account_id ORDER BY account_id`)
+ if err != nil {
+ return zero, err
+ }
+ for rows.Next() {
+ var accountID, count int64
+ if err := rows.Scan(&accountID, &count); err != nil {
+ rows.Close()
+ return zero, err
+ }
+ if i, ok := byID[accountID]; ok {
+ out.Rows[i].EntitlementReviews = count
+ }
+ }
+ if err := closeSponsoredRows(rows); err != nil {
+ return zero, err
+ }
+ return out, nil
+}
+
+func closeSponsoredRows(rows *sql.Rows) error {
+ if err := rows.Err(); err != nil {
+ rows.Close()
+ return err
+ }
+ return rows.Close()
+}
diff --git a/internal/console/sponsoredinventory_test.go b/internal/console/sponsoredinventory_test.go
new file mode 100644
index 000000000..9cbaf2c55
--- /dev/null
+++ b/internal/console/sponsoredinventory_test.go
@@ -0,0 +1,457 @@
+package console
+
+import (
+ "context"
+ "database/sql"
+ "encoding/hex"
+ "fmt"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/domains"
+)
+
+func TestSponsoredCandidatesCompleteDeterministicRawFacts(t *testing.T) {
+ empty := readSponsoredTestInventory(t, openSponsoredTestStore(t, "empty"))
+ if empty.Rows == nil || len(empty.Rows) != 0 {
+ t.Fatalf("empty Rows=%#v, want non-nil empty slice", empty.Rows)
+ }
+
+ first := openSponsoredTestStore(t, "first")
+ second := openSponsoredTestStore(t, "second")
+ seedSponsoredCoverage(t, first, false)
+ seedSponsoredCoverage(t, second, true)
+
+ got := readSponsoredTestInventory(t, first)
+ gotReverse := readSponsoredTestInventory(t, second)
+ if !reflect.DeepEqual(got, gotReverse) {
+ t.Fatalf("insertion order changed capture:\nfirst: %#v\nsecond: %#v", got, gotReverse)
+ }
+ want := sponsoredCandidateInventory{Rows: []sponsoredCandidateRow{
+ {
+ ID: 1, Sqid: domains.Sqid(1), Name: "ops", StoredCreatedAt: 100,
+ Owners: []string{"a-owner@example.test", "z-owner@example.test"},
+ },
+ {
+ ID: 2, Sqid: domains.Sqid(2), Name: "member-only", StoredCreatedAt: 0,
+ Owners: []string{},
+ },
+ {
+ ID: 3, Sqid: domains.Sqid(3), Name: "ownerless", StoredCreatedAt: -99,
+ Owners: []string{},
+ },
+ {
+ ID: 4, Sqid: domains.Sqid(4), Name: "deleted", StoredCreatedAt: 40, DeletedAt: 400,
+ Owners: []string{},
+ Billing: sponsoredCandidateBilling{Present: true, House: true, Subscription: "sub_deleted", Status: "canceled"},
+ },
+ {
+ ID: 5, Sqid: domains.Sqid(5), Name: "retiring", StoredCreatedAt: 50,
+ Owners: []string{},
+ BillingRetirement: true,
+ },
+ {
+ ID: 6, Sqid: domains.Sqid(6), Name: "full", StoredCreatedAt: 60,
+ Owners: []string{"full-owner@example.test"},
+ Billing: sponsoredCandidateBilling{
+ Present: true, LiveMode: true, CustomerPresent: true,
+ Subscription: "sub_full", Status: "active",
+ },
+ CheckoutPresent: true,
+ Outbox: sponsoredCandidateOutbox{
+ Present: true, LiveMode: true, SerialOwed: true,
+ Revision: 7, PublishedRevision: 5, Digest: "digest-full", State: "pending",
+ },
+ OpenBillingReviews: 2,
+ EntitlementReviews: 2,
+ },
+ }}
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("capture:\n got: %#v\nwant: %#v", got, want)
+ }
+ for _, row := range got.Rows {
+ if row.Owners == nil {
+ t.Fatalf("account %d has nil Owners", row.ID)
+ }
+ if !sort.StringsAreSorted(row.Owners) {
+ t.Fatalf("account %d Owners are not sorted: %q", row.ID, row.Owners)
+ }
+ }
+ var receipts int
+ if err := first.db.QueryRow(`SELECT count(*) FROM account_birth_receipts`).Scan(&receipts); err != nil || receipts != 0 {
+ t.Fatalf("birth receipts=%d err=%v", receipts, err)
+ }
+}
+
+func TestSponsoredCandidatesReadDoesNotMutateAnyTable(t *testing.T) {
+ s := openSponsoredTestStore(t, "immutable")
+ seedSponsoredCoverage(t, s, false)
+ before := sponsoredLogicalSnapshot(t, s.db)
+
+ got := readSponsoredTestInventory(t, s)
+ if len(got.Rows) == 0 {
+ t.Fatal("empty capture")
+ }
+ after := sponsoredLogicalSnapshot(t, s.db)
+ if !reflect.DeepEqual(after, before) {
+ t.Fatalf("database changed:\nbefore: %#v\nafter: %#v", before, after)
+ }
+ var authority int
+ if err := s.db.QueryRow(`SELECT count(*) FROM entitlement_authority`).Scan(&authority); err != nil || authority != 0 {
+ t.Fatalf("entitlement authority rows=%d err=%v", authority, err)
+ }
+}
+
+func TestSponsoredCandidatesWALSnapshotAcrossEveryRead(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "snapshot.db")
+ reader := openSponsoredStoreAt(t, path)
+ writer := openSponsoredStoreAt(t, path)
+ sponsoredInsertAccount(t, reader.db, 2, "before", 20, 0)
+ sponsoredExec(t, reader.db, `INSERT INTO memberships(account_id,address,role,added_at) VALUES(2,'old-owner@example.test',?,1)`, RoleOwner)
+ sponsoredExec(t, reader.db, `INSERT INTO billing_accounts(account_id,customer,subscription,status,live_mode) VALUES(2,'cus_old','sub_old','active',0)`)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ tx, err := reader.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer tx.Rollback()
+ got, err := readSponsoredCandidatesTx(ctx, tx, func() error {
+ writeTx, err := writer.db.BeginTx(ctx, nil)
+ if err != nil {
+ return err
+ }
+ defer writeTx.Rollback()
+ for _, step := range []struct {
+ query string
+ args []any
+ }{
+ {`DELETE FROM memberships WHERE account_id=2`, nil},
+ {`INSERT INTO memberships(account_id,address,role,added_at) VALUES(2,'new-owner@example.test',?,2)`, []any{RoleOwner}},
+ {`UPDATE billing_accounts SET customer='cus_new',subscription='sub_new',status='canceled',live_mode=1 WHERE account_id=2`, nil},
+ {`INSERT INTO billing_checkout_intents(account_id,id,plan,terms,band,price,quantity,created_at) VALUES(2,'ci_new','personal','terms','band','price',1,2)`, nil},
+ {`INSERT INTO billing_retirements(account_id,started_at) VALUES(2,2)`, nil},
+ {`INSERT INTO entitlement_outbox(account_id,account,authority,revision,body,digest,live_mode,state,published_revision,published_digest,serial_owed,attempts,next_attempt_at,last_error_code,updated_at) VALUES(2,?,'new-authority',3,?,'new-digest',1,'pending',2,'new-published',1,0,0,'',2)`, []any{domains.Sqid(2), []byte("new-body")}},
+ {`INSERT INTO billing_reviews(account_id,subscription,reason,created_at) VALUES(2,'sub_new','new-review',2)`, nil},
+ {`INSERT INTO entitlement_reviews(account_id,reason,revision,created_at) VALUES(2,'new-hold',3,2)`, nil},
+ {`UPDATE accounts SET deleted_at=200 WHERE id=2`, nil},
+ {`INSERT INTO accounts(id,sqid,name,created_at) VALUES(3,?,'added',30)`, []any{domains.Sqid(3)}},
+ } {
+ if _, err := writeTx.ExecContext(ctx, step.query, step.args...); err != nil {
+ return err
+ }
+ }
+ return writeTx.Commit()
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := tx.Commit(); err != nil {
+ t.Fatal(err)
+ }
+ want := sponsoredCandidateInventory{Rows: []sponsoredCandidateRow{{
+ ID: 2, Sqid: domains.Sqid(2), Name: "before", StoredCreatedAt: 20,
+ Owners: []string{"old-owner@example.test"},
+ Billing: sponsoredCandidateBilling{
+ Present: true, CustomerPresent: true, Subscription: "sub_old", Status: "active",
+ },
+ }}}
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("mixed snapshot:\n got: %#v\nwant: %#v", got, want)
+ }
+ latest := readSponsoredTestInventory(t, writer)
+ if len(latest.Rows) != 2 || latest.Rows[0].DeletedAt != 200 || latest.Rows[0].Owners[0] != "new-owner@example.test" || !latest.Rows[0].CheckoutPresent || !latest.Rows[0].Billing.LiveMode || !latest.Rows[0].BillingRetirement || !latest.Rows[0].Outbox.Present || latest.Rows[0].OpenBillingReviews != 1 || latest.Rows[0].EntitlementReviews != 1 {
+ t.Fatalf("writer changes did not commit as expected: %#v", latest)
+ }
+}
+
+func TestSponsoredCandidatesFailuresReturnZero(t *testing.T) {
+ t.Run("cancelled before capture", func(t *testing.T) {
+ s := openSponsoredTestStore(t, "cancel-before")
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel()
+ assertSponsoredCaptureError(t, func() (sponsoredCandidateInventory, error) {
+ return s.readSponsoredCandidates(ctx)
+ })
+ })
+
+ t.Run("cancelled after accounts", func(t *testing.T) {
+ s := openSponsoredTestStore(t, "cancel-after")
+ sponsoredInsertAccount(t, s.db, 2, "account", 20, 0)
+ ctx, cancel := context.WithCancel(context.Background())
+ tx, err := s.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer tx.Rollback()
+ assertSponsoredCaptureError(t, func() (sponsoredCandidateInventory, error) {
+ return readSponsoredCandidatesTx(ctx, tx, func() error {
+ cancel()
+ return nil
+ })
+ })
+ })
+
+ t.Run("missing schema", func(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "missing.db")
+ reader := openSponsoredStoreAt(t, path)
+ writer := openSponsoredStoreAt(t, path)
+ sponsoredExec(t, writer.db, `DROP TABLE entitlement_reviews`)
+ assertSponsoredCaptureError(t, func() (sponsoredCandidateInventory, error) {
+ return reader.readSponsoredCandidates(sponsoredDeadline(t))
+ })
+ })
+
+ t.Run("text created_at", func(t *testing.T) {
+ s := openSponsoredTestStore(t, "text-created")
+ sponsoredExec(t, s.db, `INSERT INTO accounts(id,sqid,name,created_at) VALUES(2,?,'bad','x')`, domains.Sqid(2))
+ assertSponsoredCaptureError(t, func() (sponsoredCandidateInventory, error) {
+ return s.readSponsoredCandidates(sponsoredDeadline(t))
+ })
+ })
+
+ t.Run("null created_at", func(t *testing.T) {
+ s := openSponsoredTestStore(t, "null-created")
+ sponsoredExec(t, s.db, `ALTER TABLE accounts RENAME TO accounts_strict`)
+ sponsoredExec(t, s.db, `CREATE TABLE accounts(id INTEGER PRIMARY KEY,sqid TEXT NOT NULL UNIQUE,name TEXT NOT NULL,created_at INTEGER,deleted_at INTEGER NOT NULL DEFAULT 0,tier TEXT NOT NULL DEFAULT '')`)
+ sponsoredExec(t, s.db, `INSERT INTO accounts(id,sqid,name,created_at) VALUES(2,?,'bad',NULL)`, domains.Sqid(2))
+ assertSponsoredCaptureError(t, func() (sponsoredCandidateInventory, error) {
+ return s.readSponsoredCandidates(sponsoredDeadline(t))
+ })
+ })
+
+ t.Run("missing created_at column", func(t *testing.T) {
+ s := openSponsoredTestStore(t, "missing-created")
+ sponsoredExec(t, s.db, `ALTER TABLE accounts DROP COLUMN created_at`)
+ assertSponsoredCaptureError(t, func() (sponsoredCandidateInventory, error) {
+ return s.readSponsoredCandidates(sponsoredDeadline(t))
+ })
+ })
+}
+
+func TestSponsoredCandidatesRefuseInvalidAccountIdentity(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ id int64
+ sqid string
+ }{
+ {name: "nonpositive id", id: 0, sqid: domains.Sqid(0)},
+ {name: "mismatched sqid", id: 2, sqid: domains.Sqid(3)},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ s := openSponsoredTestStore(t, strings.ReplaceAll(tc.name, " ", "-"))
+ sponsoredExec(t, s.db, `INSERT INTO accounts(id,sqid,name,created_at) VALUES(?,?,?,1)`, tc.id, tc.sqid, tc.name)
+ assertSponsoredCaptureError(t, func() (sponsoredCandidateInventory, error) {
+ return s.readSponsoredCandidates(sponsoredDeadline(t))
+ })
+ })
+ }
+}
+
+func seedSponsoredCoverage(t *testing.T, s *Store, reverse bool) {
+ t.Helper()
+ accounts := []struct {
+ id, created, deleted int64
+ name string
+ }{
+ {1, 100, 0, "ops"},
+ {2, 0, 0, "member-only"},
+ {3, -99, 0, "ownerless"},
+ {4, 40, 400, "deleted"},
+ {5, 50, 0, "retiring"},
+ {6, 60, 0, "full"},
+ }
+ if reverse {
+ sort.Slice(accounts, func(i, j int) bool { return accounts[i].id > accounts[j].id })
+ }
+ for _, account := range accounts {
+ sponsoredInsertAccount(t, s.db, account.id, account.name, account.created, account.deleted)
+ }
+ memberships := []struct {
+ id int64
+ address string
+ role string
+ }{
+ {1, "z-owner@example.test", RoleOwner},
+ {1, "member@example.test", RoleMember},
+ {1, "a-owner@example.test", RoleOwner},
+ {2, "only-member@example.test", RoleMember},
+ {6, "full-owner@example.test", RoleOwner},
+ {999, "orphan-owner@example.test", RoleOwner},
+ }
+ if reverse {
+ sort.Slice(memberships, func(i, j int) bool { return memberships[i].address > memberships[j].address })
+ }
+ for i, member := range memberships {
+ sponsoredExec(t, s.db, `INSERT INTO memberships(account_id,address,role,added_at) VALUES(?,?,?,?)`, member.id, member.address, member.role, i+1)
+ }
+
+ sponsoredExec(t, s.db, `INSERT INTO billing_retirements(account_id,started_at) VALUES(5,50),(999,99)`)
+ sponsoredExec(t, s.db, `INSERT INTO billing_accounts(account_id,house,customer,subscription,status,live_mode) VALUES
+ (4,1,'','sub_deleted','canceled',0),
+ (6,0,'cus_full','sub_full','active',1),
+ (999,1,'cus_orphan','sub_orphan','active',1)`)
+ sponsoredExec(t, s.db, `INSERT INTO billing_checkout_intents(account_id,id,plan,terms,band,price,quantity,created_at) VALUES
+ (6,'ci_full','personal','terms','band','price',1,60),
+ (999,'ci_orphan','personal','terms','band','price',1,99)`)
+ sponsoredExec(t, s.db, `INSERT INTO entitlement_outbox(account_id,account,authority,revision,body,digest,live_mode,state,published_revision,published_digest,serial_owed,attempts,next_attempt_at,last_error_code,updated_at) VALUES
+ (6,?,'raw-authority',7,?, 'digest-full',1,'pending',5,'published-digest',1,2,70,'raw-error',71),
+ (999,?,'orphan-authority',1,?, 'digest-orphan',0,'held',0,'',0,0,0,'',99)`, domains.Sqid(6), []byte("not-json"), domains.Sqid(999), []byte("also-not-json"))
+ sponsoredExec(t, s.db, `INSERT INTO billing_reviews(account_id,subscription,reason,created_at,resolved_at) VALUES
+ (6,'sub_full','open-a',60,0),
+ (6,'sub_full','open-b',61,0),
+ (6,'sub_full','resolved',62,63),
+ (999,'sub_orphan','open',99,0)`)
+ sponsoredExec(t, s.db, `INSERT INTO entitlement_reviews(account_id,reason,revision,created_at) VALUES
+ (6,'hold-a',7,60),
+ (6,'hold-b',8,61),
+ (999,'orphan-hold',1,99)`)
+}
+
+func openSponsoredTestStore(t *testing.T, name string) *Store {
+ t.Helper()
+ return openSponsoredStoreAt(t, filepath.Join(t.TempDir(), name+".db"))
+}
+
+func openSponsoredStoreAt(t *testing.T, path string) *Store {
+ t.Helper()
+ s, err := Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := s.Close(); err != nil {
+ t.Errorf("close store: %v", err)
+ }
+ })
+ return s
+}
+
+func sponsoredInsertAccount(t *testing.T, db *sql.DB, id int64, name string, created, deleted int64) {
+ t.Helper()
+ sponsoredExec(t, db, `INSERT INTO accounts(id,sqid,name,created_at,deleted_at) VALUES(?,?,?,?,?)`, id, domains.Sqid(id), name, created, deleted)
+}
+
+func sponsoredExec(t *testing.T, db interface {
+ Exec(string, ...any) (sql.Result, error)
+}, query string, args ...any) {
+ t.Helper()
+ if _, err := db.Exec(query, args...); err != nil {
+ t.Fatalf("exec %q: %v", query, err)
+ }
+}
+
+func sponsoredDeadline(t *testing.T) context.Context {
+ t.Helper()
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ t.Cleanup(cancel)
+ return ctx
+}
+
+func readSponsoredTestInventory(t *testing.T, s *Store) sponsoredCandidateInventory {
+ t.Helper()
+ got, err := s.readSponsoredCandidates(sponsoredDeadline(t))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return got
+}
+
+func assertSponsoredCaptureError(t *testing.T, capture func() (sponsoredCandidateInventory, error)) {
+ t.Helper()
+ got, err := capture()
+ if err == nil {
+ t.Fatalf("capture succeeded: %#v", got)
+ }
+ if !reflect.DeepEqual(got, sponsoredCandidateInventory{}) {
+ t.Fatalf("partial result on error: %#v", got)
+ }
+}
+
+type sponsoredTableSnapshot struct {
+ Name, Schema string
+ Rows []string
+}
+
+func sponsoredLogicalSnapshot(t *testing.T, db *sql.DB) []sponsoredTableSnapshot {
+ t.Helper()
+ rows, err := db.Query(`SELECT name,sql FROM sqlite_schema WHERE type='table' ORDER BY name`)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var tables []sponsoredTableSnapshot
+ for rows.Next() {
+ var table sponsoredTableSnapshot
+ if err := rows.Scan(&table.Name, &table.Schema); err != nil {
+ rows.Close()
+ t.Fatal(err)
+ }
+ tables = append(tables, table)
+ }
+ if err := rows.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if err := rows.Err(); err != nil {
+ t.Fatal(err)
+ }
+ for i := range tables {
+ name := `"` + strings.ReplaceAll(tables[i].Name, `"`, `""`) + `"`
+ data, err := db.Query(`SELECT * FROM ` + name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ columns, err := data.Columns()
+ if err != nil {
+ data.Close()
+ t.Fatal(err)
+ }
+ for data.Next() {
+ values := make([]any, len(columns))
+ pointers := make([]any, len(columns))
+ for j := range values {
+ pointers[j] = &values[j]
+ }
+ if err := data.Scan(pointers...); err != nil {
+ data.Close()
+ t.Fatal(err)
+ }
+ parts := make([]string, len(values))
+ for j, value := range values {
+ parts[j] = sponsoredSnapshotValue(value)
+ }
+ tables[i].Rows = append(tables[i].Rows, strings.Join(parts, "|"))
+ }
+ if err := data.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if err := data.Err(); err != nil {
+ t.Fatal(err)
+ }
+ sort.Strings(tables[i].Rows)
+ }
+ return tables
+}
+
+func sponsoredSnapshotValue(value any) string {
+ switch value := value.(type) {
+ case nil:
+ return "null"
+ case int64:
+ return fmt.Sprintf("int:%d", value)
+ case float64:
+ return fmt.Sprintf("float:%016x", value)
+ case bool:
+ return fmt.Sprintf("bool:%t", value)
+ case string:
+ return fmt.Sprintf("string:%q", value)
+ case []byte:
+ return "bytes:" + hex.EncodeToString(value)
+ default:
+ return fmt.Sprintf("%T:%v", value, value)
+ }
+}