oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1package docs
2
3import (
4 "context"
5 "path/filepath"
6 "sync"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/docs/internal/core"
11)
12
13// newSampleDocumentsInternalTestApp is this file's own copy of
14// sample_documents_test.go's newSampleDocumentsTestApp: that one
15// lives in package docs_test and cannot be reused from here, and the
16// tests below need package-internal access (a *Store's own db handle,
17// to soft-delete rows the way a future "delete document" handler
18// would; a second, deliberately broken *core.Runtime) that only a
19// docs-package test can reach.
20func newSampleDocumentsInternalTestApp(t *testing.T, sampleDocuments string) *App {
21 t.Helper()
22 app, err := New(Config{
23 DBPath: filepath.Join(t.TempDir(), "docs.db"),
24 Env: "development",
25 SampleDocuments: sampleDocuments,
26 })
27 if err != nil {
28 t.Fatalf("New: %v", err)
29 }
30 t.Cleanup(func() {
31 if cerr := app.Close(); cerr != nil {
32 t.Errorf("App.Close: %v", cerr)
33 }
34 })
35 return app
36}
37
38// TestPersonWhoDeletesSampleDocumentsGetsNoneBackOnNextSignIn is the
39// property EnsurePerson's own doc comment calls out by name: seeding
40// is tied to "this Person row was just created", never to "this
41// person's list happens to be empty". There is no delete handler yet
42// (spec's later work), so this reaches into store.db directly to
43// soft-delete every one of the seeded rows exactly the way such a
44// handler would (setting deleted_at, the same column List's own
45// "deleted_at IS NULL" filter checks) — then signs the same address
46// in again and asserts nothing comes back.
47func TestPersonWhoDeletesSampleDocumentsGetsNoneBackOnNextSignIn(t *testing.T) {
48 app := newSampleDocumentsInternalTestApp(t, "1")
49 ctx := context.Background()
50 store := app.Store()
51
52 owner, err := store.EnsurePerson(ctx, "deletes-everything@example.com")
53 if err != nil {
54 t.Fatalf("EnsurePerson (first): %v", err)
55 }
56 first, err := store.List(ctx, owner)
57 if err != nil {
58 t.Fatalf("List (first): %v", err)
59 }
60 if len(first) != 6 {
61 t.Fatalf("got %d seeded documents, want 6", len(first))
62 }
63
64 now := time.Now().UTC()
65 if err := owned(store.db.WithContext(ctx), owner).
66 Model(&Document{}).
67 Update("deleted_at", now).Error; err != nil {
68 t.Fatalf("soft-delete owner's documents: %v", err)
69 }
70
71 afterDelete, err := store.List(ctx, owner)
72 if err != nil {
73 t.Fatalf("List (after delete): %v", err)
74 }
75 if len(afterDelete) != 0 {
76 t.Fatalf("got %d documents after deleting all of them, want 0", len(afterDelete))
77 }
78
79 // The person row already exists, so this hits EnsurePerson's
80 // found branch, not its create branch — seedIfEnabled is never
81 // called, and List must stay at zero.
82 ownerAgain, err := store.EnsurePerson(ctx, "deletes-everything@example.com")
83 if err != nil {
84 t.Fatalf("EnsurePerson (second): %v", err)
85 }
86 if ownerAgain != owner {
87 t.Fatalf("EnsurePerson(again) = %d, want %d", ownerAgain, owner)
88 }
89
90 after, err := store.List(ctx, owner)
91 if err != nil {
92 t.Fatalf("List (after second sign-in): %v", err)
93 }
94 if len(after) != 0 {
95 t.Fatalf("got %d documents back after signing in again, want 0 — sample documents were resurrected", len(after))
96 }
97}
98
99// TestEnsurePersonSurvivesSeedingFailure is the care point the task
100// calls out explicitly: seeding failure must never break sign-in. To
101// force Seed itself to fail without touching the Person row
102// EnsurePerson creates first (which would defeat the point of the
103// test — the person must exist BEFORE seeding is even attempted),
104// this builds a second, deliberately broken *core.Runtime — closed
105// immediately after it compiles — and hands it to a Store sharing the
106// real, working database underneath app's own Store.
107//
108// Confirmed directly (see the probe this test's design was checked
109// against): calling a wazero module method through a Runtime whose
110// underlying wazero runtime has been Close'd returns a clean error
111// ("runtime closed with exit_code(0)"), not a panic — Store.replay's
112// s.core.NewDoc call, the first thing Seed's loop does for its first
113// document's ApplyUpdate, fails this way every time, deterministically,
114// with no flakiness from timing.
115func TestEnsurePersonSurvivesSeedingFailure(t *testing.T) {
116 app := newSampleDocumentsInternalTestApp(t, "1")
117 ctx := context.Background()
118
119 brokenCore, err := core.New(ctx)
120 if err != nil {
121 t.Fatalf("core.New: %v", err)
122 }
123 if err := brokenCore.Close(ctx); err != nil {
124 t.Fatalf("brokenCore.Close: %v", err)
125 }
126
127 broken := NewStore(app.store.db, brokenCore, true, app.logger)
128
129 id, err := broken.EnsurePerson(ctx, "seed-fails@example.com")
130 if err != nil {
131 t.Fatalf("EnsurePerson must succeed even when seeding fails, got: %v", err)
132 }
133 if id <= 0 {
134 t.Fatalf("EnsurePerson returned a non-positive id: %d", id)
135 }
136
137 // The person is durably there: a fresh EnsurePerson call for the
138 // same address finds the same row rather than creating (or
139 // re-seeding) it.
140 id2, err := broken.EnsurePerson(ctx, "seed-fails@example.com")
141 if err != nil {
142 t.Fatalf("EnsurePerson (again): %v", err)
143 }
144 if id2 != id {
145 t.Fatalf("EnsurePerson(again) = %d, want %d (same person, not recreated)", id2, id)
146 }
147
148 // And the same person, looked up through the app's own working
149 // Store, is a real, usable person: List works and returns without
150 // error (whatever partial content Seed managed before it failed —
151 // this asserts "does not break", not "produced zero documents",
152 // since Seed's per-document loop may have left one partial row
153 // behind before its first ApplyUpdate failed).
154 docs, err := app.Store().List(ctx, id)
155 if err != nil {
156 t.Fatalf("List for the person seeding failed for: %v", err)
157 }
158
159 // Assert the seeding actually FAILED. Without this the test is
160 // vacuous: "EnsurePerson succeeded and List did not error" holds
161 // just as well if seeding silently succeeded, or was skipped
162 // because a future refactor stopped calling seedIfEnabled at all
163 // — in which case this test would keep passing while proving
164 // nothing about surviving a failure. A complete seed is
165 // len(seedDocs) documents, so anything short of that is
166 // positive evidence the broken core was reached and the loop died
167 // partway. Deliberately not asserting an exact count: where
168 // precisely Seed dies is an implementation detail, but that it
169 // died at all is the whole property under test.
170 if len(docs) >= len(seedDocs) {
171 t.Fatalf("got %d documents, want fewer than %d — seeding was expected to FAIL against the closed core, so this test is not proving what it claims", len(docs), len(seedDocs))
172 }
173}
174
175// TestConcurrentFirstSignInsProduceOnePersonAndOneSampleSet is the
176// concurrency requirement stated explicitly: two simultaneous first
177// sign-ins for the same brand-new address must not produce two
178// people or two sets of documents. EnsurePerson's own duplicate-key
179// race handling (store.go) already guarantees one Person row; this
180// pins that seeding rides along with it — exactly one winner reaches
181// seedIfEnabled, so the address ends up with exactly six documents,
182// never twelve.
183func TestConcurrentFirstSignInsProduceOnePersonAndOneSampleSet(t *testing.T) {
184 app := newSampleDocumentsInternalTestApp(t, "1")
185 store := app.Store()
186 const address = "concurrent-first-signin@example.com"
187
188 const n = 8
189 ids := make([]int64, n)
190 errs := make([]error, n)
191 var wg sync.WaitGroup
192 wg.Add(n)
193 for i := 0; i < n; i++ {
194 go func(i int) {
195 defer wg.Done()
196 ids[i], errs[i] = store.EnsurePerson(context.Background(), address)
197 }(i)
198 }
199 wg.Wait()
200
201 for i, err := range errs {
202 if err != nil {
203 t.Fatalf("EnsurePerson[%d]: %v", i, err)
204 }
205 }
206 for i := 1; i < n; i++ {
207 if ids[i] != ids[0] {
208 t.Fatalf("EnsurePerson returned different ids across concurrent calls: %d and %d — two people were created for one address", ids[0], ids[i])
209 }
210 }
211
212 list, err := store.List(context.Background(), ids[0])
213 if err != nil {
214 t.Fatalf("List: %v", err)
215 }
216 if len(list) != 6 {
217 t.Fatalf("got %d documents after %d concurrent first sign-ins, want exactly 6 (one seeded set, not %d)", len(list), n, n*6)
218 }
219}
220