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 "database/sql"
5 "path/filepath"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/release"
10 _ "modernc.org/sqlite"
11)
12
13// tempStoreAccount gives a bare store an account row, which AppRows and the
14// candidate queries join against.
15func tempStoreAccount(t *testing.T, s *Store) int64 {
16 t.Helper()
17 id := testAccountID()
18 if _, err := s.EnsureAccount(id, "tst", "Test", time.Unix(1700000000, 0)); err != nil {
19 t.Fatal(err)
20 }
21 return id
22}
23
24func mustLife(t *testing.T, s *Store, acct int64, app string) *AppLife {
25 t.Helper()
26 l, err := s.AppLife(acct, app)
27 if err != nil || l == nil {
28 t.Fatalf("AppLife(%s) = %v, %v", app, l, err)
29 }
30 return l
31}
32
33// The pre-accounts table is rebuilt by migrateAppsKey on the FIRST open, after
34// the ALTER list has added the new columns to the old table. The rebuild must
35// carry them, or the first boot after upgrade runs without them.
36func TestTemporaryColumnsSurviveTheAppsRebuild(t *testing.T) {
37 path := filepath.Join(t.TempDir(), "console.db")
38 raw, err := sql.Open("sqlite", path)
39 if err != nil {
40 t.Fatal(err)
41 }
42 if _, err := raw.Exec(`CREATE TABLE apps (app TEXT PRIMARY KEY, created_by TEXT NOT NULL, created_at INTEGER NOT NULL);`); err != nil {
43 t.Fatal(err)
44 }
45 if _, err := raw.Exec(`INSERT INTO apps (app, created_by, created_at) VALUES ('kass', 'paul@keymail.dev', 1700000000)`); err != nil {
46 t.Fatal(err)
47 }
48 raw.Close()
49 st, err := Open(path)
50 if err != nil {
51 t.Fatal(err)
52 }
53 defer st.Close()
54 var exp, restoring, after, forAt int64
55 if err := st.db.QueryRow(`SELECT expires_at, restoring, purge_after, purge_for FROM apps WHERE app = 'kass'`).
56 Scan(&exp, &restoring, &after, &forAt); err != nil {
57 t.Fatalf("new columns after the first open: %v", err)
58 }
59 if exp != 0 || restoring != 0 || after != 0 || forAt != 0 {
60 t.Fatalf("defaults = %d %d %d %d, want all 0 (permanent, not restoring, no early purge)", exp, restoring, after, forAt)
61 }
62}
63
64func TestCreateTemporaryAppInClaimsWithTheExpiry(t *testing.T) {
65 s := openTestStore(t)
66 acct := tempStoreAccount(t, s)
67 now := time.Unix(1700000000, 0)
68 exp := now.Add(7 * 24 * time.Hour)
69 if ok, err := s.CreateTemporaryAppIn(acct, "tmp-docs", "paul@keymail.dev", now, exp); err != nil || !ok {
70 t.Fatalf("CreateTemporaryAppIn = %v %v", ok, err)
71 }
72 if l := mustLife(t, s, acct, "tmp-docs"); l.ExpiresAt != exp.Unix() || !l.Temporary() || l.Trashed() {
73 t.Fatalf("row = %+v, want expires_at %d, live", l, exp.Unix())
74 }
75 // A racing permanent create of the same name loses (ON CONFLICT DO NOTHING).
76 if ok, err := s.CreateAppIn(acct, "tmp-docs", "eve@keymail.dev", now); err != nil || ok {
77 t.Fatalf("permanent create over a temporary row = %v %v, want false", ok, err)
78 }
79 if ok, _ := s.CreateTemporaryAppIn(acct, "tmp-docs", "eve@keymail.dev", now, exp); ok {
80 t.Fatal("a second temporary claim of the same name succeeded")
81 }
82 if _, err := s.CreateTemporaryAppIn(acct, "tmp-zero", "paul@keymail.dev", now, time.Time{}); err == nil {
83 t.Fatal("a temporary claim with no expiry was accepted")
84 }
85}
86
87func TestTemporaryConditionalUpdatesLoseCleanly(t *testing.T) {
88 s := openTestStore(t)
89 acct := tempStoreAccount(t, s)
90 now := time.Unix(1700000000, 0)
91 if _, err := s.CreateAppIn(acct, "perm", "paul@keymail.dev", now); err != nil {
92 t.Fatal(err)
93 }
94 if _, err := s.CreateTemporaryAppIn(acct, "tmp-a", "paul@keymail.dev", now, now.Add(time.Hour)); err != nil {
95 t.Fatal(err)
96 }
97 for name, fn := range map[string]func() (bool, error){
98 "keep permanent": func() (bool, error) { return s.KeepApp(acct, "perm") },
99 "ttl permanent": func() (bool, error) { return s.SetAppExpiry(acct, "perm", now.Add(2*time.Hour)) },
100 "expire permanent": func() (bool, error) { return s.ExpireAppRow(acct, "perm", now) },
101 "bump live": func() (bool, error) { return s.BumpTrashedExpiry(acct, "tmp-a", now) },
102 "restoring on live": func() (bool, error) { return s.SetRestoring(acct, "tmp-a", true) },
103 } {
104 if ok, err := fn(); err != nil || ok {
105 t.Errorf("%s = %v %v, want a lost update (false, nil)", name, ok, err)
106 }
107 }
108 // Expire wins once, and sets the 3-day window from the given clock.
109 at := now.Add(90 * time.Minute)
110 if ok, err := s.ExpireAppRow(acct, "tmp-a", at); err != nil || !ok {
111 t.Fatalf("ExpireAppRow = %v %v", ok, err)
112 }
113 l := mustLife(t, s, acct, "tmp-a")
114 if l.DeletedAt != at.Unix() || l.PurgeFor != at.Unix() || l.PurgeAfter != at.Unix()+72*3600 {
115 t.Fatalf("expired row = %+v, want deleted_at = purge_for = %d, purge_after = +72h", l, at.Unix())
116 }
117 for name, fn := range map[string]func() (bool, error){
118 "expire twice": func() (bool, error) { return s.ExpireAppRow(acct, "tmp-a", at.Add(time.Hour)) },
119 "keep trashed": func() (bool, error) { return s.KeepApp(acct, "tmp-a") },
120 "ttl on trashed": func() (bool, error) { return s.SetAppExpiry(acct, "tmp-a", now.Add(9*time.Hour)) },
121 } {
122 if ok, err := fn(); err != nil || ok {
123 t.Errorf("%s = %v %v, want a lost update", name, ok, err)
124 }
125 }
126}
127
128func TestTrashAppRowSetsTheWindowOnlyForTemporaryRows(t *testing.T) {
129 s := openTestStore(t)
130 acct := tempStoreAccount(t, s)
131 now := time.Unix(1700000000, 0)
132 s.CreateAppIn(acct, "perm", "paul@keymail.dev", now)
133 s.CreateTemporaryAppIn(acct, "tmp-a", "paul@keymail.dev", now, now.Add(30*24*time.Hour))
134 at := now.Add(time.Minute)
135 for _, app := range []string{"perm", "tmp-a"} {
136 if err := s.TrashAppRow(acct, app, at); err != nil {
137 t.Fatal(err)
138 }
139 }
140 if l := mustLife(t, s, acct, "perm"); l.PurgeAfter != 0 || l.PurgeFor != 0 || l.DeletedAt != at.Unix() {
141 t.Fatalf("permanent trash = %+v, want no early purge", l)
142 }
143 if l := mustLife(t, s, acct, "tmp-a"); l.PurgeFor != at.Unix() || l.PurgeAfter != at.Unix()+72*3600 {
144 t.Fatalf("temporary trash = %+v, want the 3-day window from %d", l, at.Unix())
145 }
146}
147
148func TestLiveTemporaryCountsARestoringRow(t *testing.T) {
149 s := openTestStore(t)
150 acct := tempStoreAccount(t, s)
151 now := time.Unix(1700000000, 0)
152 for _, app := range []string{"tmp-a", "tmp-b", "tmp-c"} {
153 s.CreateTemporaryAppIn(acct, app, "paul@keymail.dev", now, now.Add(time.Hour))
154 }
155 s.CreateAppIn(acct, "perm", "paul@keymail.dev", now)
156 s.TrashAppRow(acct, "tmp-b", now)
157 s.TrashAppRow(acct, "tmp-c", now)
158 if ok, err := s.SetRestoring(acct, "tmp-c", true); err != nil || !ok {
159 t.Fatalf("SetRestoring = %v %v", ok, err)
160 }
161 live, err := s.LiveTemporaryApps(acct)
162 if err != nil {
163 t.Fatal(err)
164 }
165 var names []string
166 for _, l := range live {
167 names = append(names, l.App)
168 }
169 if len(names) != 2 || names[0] != "tmp-a" || names[1] != "tmp-c" {
170 t.Fatalf("LiveTemporaryApps = %v, want [tmp-a tmp-c]: live, plus the one mid-restore", names)
171 }
172}
173
174func TestRestoreStepsOnTheRow(t *testing.T) {
175 s := openTestStore(t)
176 acct := tempStoreAccount(t, s)
177 now := time.Unix(1700000000, 0)
178 s.CreateTemporaryAppIn(acct, "tmp-a", "paul@keymail.dev", now.Add(-10*24*time.Hour), now.Add(-time.Minute))
179 s.TrashAppRow(acct, "tmp-a", now.Add(-time.Minute))
180 // Step 2: only while trashed, only when the expiry is under an hour away.
181 if ok, err := s.BumpTrashedExpiry(acct, "tmp-a", now); err != nil || !ok {
182 t.Fatalf("BumpTrashedExpiry = %v %v", ok, err)
183 }
184 if l := mustLife(t, s, acct, "tmp-a"); l.ExpiresAt != now.Add(temporaryTTLDefault).Unix() || !l.Trashed() || l.PurgeAfter == 0 {
185 t.Fatalf("after step 2 = %+v, want expiry now+7d, still trashed, deadline kept", l)
186 }
187 if ok, _ := s.BumpTrashedExpiry(acct, "tmp-a", now); ok {
188 t.Fatal("step 2 fired again on an expiry a week out")
189 }
190 // Step 5.
191 if err := s.FinishRestoreRow(acct, "tmp-a", now); err != nil {
192 t.Fatal(err)
193 }
194 if l := mustLife(t, s, acct, "tmp-a"); l.Trashed() || l.Restoring || l.PurgeAfter != 0 || l.PurgeFor != 0 {
195 t.Fatalf("after step 5 = %+v, want live with no deadline", l)
196 }
197}
198
199func TestEffectivePurgeAt(t *testing.T) {
200 del := int64(1700000000)
201 if got, want := effectivePurgeAt(del, 0), time.Unix(del, 0).UTC().Add(release.TrashCooloff); !got.Equal(want) {
202 t.Errorf("permanent = %v, want %v", got, want)
203 }
204 if got, want := effectivePurgeAt(del, del+72*3600), time.Unix(del+72*3600, 0).UTC(); !got.Equal(want) {
205 t.Errorf("temporary = %v, want %v", got, want)
206 }
207}
208
209func TestPurgeDueAndTemporaryRows(t *testing.T) {
210 s := openTestStore(t)
211 acct := tempStoreAccount(t, s)
212 now := time.Unix(1700000000, 0)
213 s.CreateAppIn(acct, "perm", "paul@keymail.dev", now)
214 s.CreateTemporaryAppIn(acct, "tmp-due", "paul@keymail.dev", now, now.Add(time.Hour))
215 s.CreateTemporaryAppIn(acct, "tmp-later", "paul@keymail.dev", now, now.Add(time.Hour))
216 s.TrashAppRow(acct, "perm", now.Add(-100*24*time.Hour))
217 s.TrashAppRow(acct, "tmp-due", now.Add(-72*time.Hour))
218 s.TrashAppRow(acct, "tmp-later", now.Add(-71*time.Hour))
219 due, err := s.PurgeDueApps(now)
220 if err != nil {
221 t.Fatal(err)
222 }
223 if len(due) != 1 || due[0].App != "tmp-due" || due[0].Sqid != "tst" || due[0].PurgeFor != now.Add(-72*time.Hour).Unix() {
224 t.Fatalf("PurgeDueApps = %+v, want only tmp-due (a permanent row never has purge_after)", due)
225 }
226 rows, err := s.TemporaryAppRows()
227 if err != nil {
228 t.Fatal(err)
229 }
230 if len(rows) != 2 {
231 t.Fatalf("TemporaryAppRows = %+v, want the two temporary rows", rows)
232 }
233}
234