carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "github.com/carlosframework/platform/internal/hostops"
8 "github.com/carlosframework/platform/internal/release"
9 "path/filepath"
10 "testing"
11 "time"
12)
13
14func receiptFixture(t *testing.T, s *Store) hostops.SQLReceiptScope {
15 t.Helper()
16 id, err := s.sqlDatabaseID()
17 if err != nil {
18 t.Fatal(err)
19 }
20 return hostops.SQLReceiptScope{Database: id, Account: "bab", App: "blog", AppID: gateScope.AppID, Operation: "ingest", Effect: "ingest", Epoch: 1, Owner: hostops.RegistrationOwner{Console: "c", Enrollment: "e", Controller: "i"}}
21}
22func preparedBatch(t *testing.T, s *Store, r *hostops.SQLReceiptScope, fn func(*Store) error) error {
23 t.Helper()
24 return s.withPreparedClaimSQL(t.Context(), []claimSQLScope{gateScope}, allowAllClaimSQL, allowAllClaimSQL, []hostops.SQLReceiptScope{*r}, true, fn)
25}
26func TestPlan9SQLRecoveryBeforeBeginRejectsDelayedWriter(t *testing.T) {
27 s := openGatedStore(t)
28 seedGateApp(t, s)
29 r := receiptFixture(t, s)
30 if err := s.FenceSQL(t.Context(), r); err != nil {
31 t.Fatal(err)
32 }
33 called := false
34 err := preparedBatch(t, s, &r, func(v *Store) error {
35 called = true
36 _, err := v.db.Exec(`UPDATE audit SET action='late' WHERE account_id=7 AND app='blog'`)
37 return err
38 })
39 if err == nil || called {
40 t.Fatalf("fenced transaction ran: called=%v err=%v", called, err)
41 }
42 other := r
43 other.Database = "other"
44 if s.FenceSQL(t.Context(), other) == nil {
45 t.Fatal("wrong database accepted")
46 }
47 if _, err = s.db.Exec(`DELETE FROM claim_sql_receipts`); !errors.Is(err, ErrClaimSQLForbidden) {
48 t.Fatalf("public receipt write: %v", err)
49 }
50}
51func TestPlan9SQLRecoveryWaitsForTransactionAndFencesReplay(t *testing.T) {
52 s := openGatedStore(t)
53 seedGateApp(t, s)
54 r := receiptFixture(t, s)
55 entered, release := make(chan struct{}), make(chan struct{})
56 done := make(chan error, 1)
57 go func() {
58 done <- preparedBatch(t, s, &r, func(v *Store) error {
59 _, err := v.db.Exec(`UPDATE audit SET action='joined' WHERE account_id=7 AND app='blog'`)
60 close(entered)
61 <-release
62 return err
63 })
64 }()
65 <-entered
66 recovery := make(chan error, 1)
67 go func() { recovery <- s.FenceSQL(t.Context(), r) }()
68 select {
69 case err := <-recovery:
70 t.Fatalf("recovery crossed running transaction: %v", err)
71 case <-time.After(30 * time.Millisecond):
72 }
73 close(release)
74 if err := <-done; err != nil {
75 t.Fatal(err)
76 }
77 if err := <-recovery; err != nil {
78 t.Fatal(err)
79 }
80 if countWhere(t, s, "audit", "action='joined'") == 0 {
81 t.Fatal("committed SQL disappeared")
82 }
83 if err := preparedBatch(t, s, &r, func(*Store) error { return nil }); err == nil {
84 t.Fatal("receipt replay accepted")
85 }
86}
87func TestPlan9SQLBatchRollbackAndCommitCancellation(t *testing.T) {
88 s := openGatedStore(t)
89 seedGateApp(t, s)
90 r := receiptFixture(t, s)
91 failure := errors.New("last write failed")
92 err := preparedBatch(t, s, &r, func(v *Store) error {
93 tx, err := v.db.Begin()
94 if err != nil {
95 return err
96 }
97 defer tx.Rollback()
98 if _, err = tx.Exec(`UPDATE audit SET action='rolled-back' WHERE account_id=7 AND app='blog'`); err != nil {
99 return err
100 }
101 if err = tx.Commit(); err != nil {
102 return err
103 }
104 return failure
105 })
106 if !errors.Is(err, failure) {
107 t.Fatal(err)
108 }
109 if countWhere(t, s, "audit", "action='rolled-back'") != 0 {
110 t.Fatal("nested commit escaped batch rollback")
111 }
112 // Simulate deadline passing immediately after the last statement.
113 err = preparedBatch(t, s, &r, func(v *Store) error {
114 _, err := v.db.Exec(`UPDATE audit SET action='expired' WHERE account_id=7 AND app='blog'`)
115 v.bound.deadline = time.Now().Add(-time.Second)
116 return err
117 })
118 if !errors.Is(err, context.DeadlineExceeded) {
119 t.Fatalf("commit deadline: %v", err)
120 }
121 if countWhere(t, s, "audit", "action='expired'") != 0 {
122 t.Fatal("expired commit landed")
123 }
124}
125func TestPlan9SQLReceiptCommitsWithRows(t *testing.T) {
126 s := openGatedStore(t)
127 seedGateApp(t, s)
128 r := receiptFixture(t, s)
129 if err := preparedBatch(t, s, &r, func(v *Store) error {
130 _, err := v.db.Exec(`UPDATE audit SET action='receipt' WHERE account_id=7 AND app='blog'`)
131 return err
132 }); err != nil {
133 t.Fatal(err)
134 }
135 var state string
136 if err := s.rawBoot.QueryRow(`SELECT state FROM claim_sql_receipts WHERE effect=?`, r.Effect).Scan(&state); err != nil || state != "committed" {
137 t.Fatalf("receipt=%s err=%v", state, err)
138 }
139 // Driver-only receipt door does not expose raw mutation to a Store callback.
140}
141
142type sqlNetworkCheck struct {
143 release.Store
144 s *Store
145}
146
147func (w *sqlNetworkCheck) Get(ctx context.Context, key string) ([]byte, string, error) {
148 if w.s.rawPool.Stats().InUse != 0 {
149 return nil, "", errors.New("network read under SQL connection")
150 }
151 return w.Store.Get(ctx, key)
152}
153func (w *sqlNetworkCheck) Put(ctx context.Context, key string, b []byte, tag string) (string, error) {
154 if w.s.rawPool.Stats().InUse != 0 {
155 return "", errors.New("network write under SQL connection")
156 }
157 return w.Store.Put(ctx, key, b, tag)
158}
159func TestPlan9SQLProductionAdapterNeverCallsNetworkUnderConnection(t *testing.T) {
160 s := openGatedStore(t)
161 seedGateApp(t, s)
162 if _, err := s.db.Exec(`UPDATE accounts SET sqid='bab' WHERE id=7`); err != nil {
163 t.Fatal(err)
164 }
165 raw := &sqlNetworkCheck{Store: release.NewDirStore(t.TempDir()), s: s}
166 w := claimTestWriter(t, raw)
167 seedClaimAdmission(t, raw, "bab", "blog", hostops.AppAdmission{V: 1, AppID: gateScope.AppID, Revision: 1, Epoch: 1, State: "open", Registrations: map[string]hostops.Registration{}, Effects: map[string]hostops.Effect{}})
168 bucket := claimTestBoundary(t, raw, w)
169 if err := s.bindClaimSQLMutations(bucket); err != nil {
170 t.Fatal(err)
171 }
172 err := s.withAutoAppSQL(t.Context(), gateAcct, "blog", "test", func(v *Store) error {
173 tx, err := v.db.Begin()
174 if err != nil {
175 return err
176 }
177 defer tx.Rollback()
178 for _, value := range []string{"one", "two"} {
179 if _, err = tx.Exec(`UPDATE audit SET action=? WHERE account_id=7 AND app='blog'`, value); err != nil {
180 return err
181 }
182 }
183 return tx.Commit()
184 })
185 if err != nil {
186 t.Fatal(err)
187 }
188 a, err := hostops.ReadCanonicalAdmission(t.Context(), raw, "bab", "blog")
189 if err != nil {
190 t.Fatal(err)
191 }
192 if a.Value.State != "open" || a.Value.Op != nil || len(a.Value.Registrations) != 0 {
193 t.Fatalf("ingest did not settle: %+v", a.Value)
194 }
195 if len(a.Value.Effects) != 1 {
196 t.Fatalf("effects=%d want one batch", len(a.Value.Effects))
197 }
198}
199
200func TestPlan9SQLCopiedDatabaseCannotProveOriginalTransaction(t *testing.T) {
201 s := openGatedStore(t)
202 seedGateApp(t, s)
203 r := receiptFixture(t, s)
204 copied := filepath.Join(t.TempDir(), "copied.db")
205 if _, err := s.rawBoot.Exec(`VACUUM INTO ?`, copied); err != nil {
206 t.Fatal(err)
207 }
208 other, err := OpenGated(copied)
209 if err != nil {
210 t.Fatal(err)
211 }
212 defer other.Close()
213 if err = other.FenceSQL(t.Context(), r); !errors.Is(err, hostops.ErrIdentity) {
214 t.Fatalf("backup claimed original lock: %v", err)
215 }
216}
217
218func TestPlan9SQLBatchClosesRetainedRowsBeforeCommit(t *testing.T) {
219 s := openGatedStore(t)
220 seedGateApp(t, s)
221 r := receiptFixture(t, s)
222 ctx, cancel := context.WithTimeout(t.Context(), time.Second)
223 defer cancel()
224 var retained *sql.Rows
225 err := s.withPreparedClaimSQL(ctx, []claimSQLScope{gateScope}, allowAllClaimSQL, allowAllClaimSQL, []hostops.SQLReceiptScope{r}, true, func(v *Store) error {
226 var err error
227 retained, err = v.db.Query(`SELECT action FROM audit`)
228 return err
229 })
230 if err != nil {
231 t.Fatal(err)
232 }
233 if retained.Next() {
234 t.Fatal("retained rows survived callback")
235 }
236}
237