oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1package docs_test
2
3import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/docs/internal/docs"
11)
12
13func TestCreateListGetRoundTrip(t *testing.T) {
14 app, err := docs.NewForTest(t)
15 if err != nil {
16 t.Fatalf("NewForTest: %v", err)
17 }
18 ctx := context.Background()
19 owner := app.TestPerson(t, "owner@example.com")
20
21 made, err := app.Store().Create(ctx, owner, "First document")
22 if err != nil {
23 t.Fatalf("Create: %v", err)
24 }
25 if made.Sqid == "" {
26 t.Fatal("Create returned an empty sqid")
27 }
28 if made.GUID == "" || made.ClientID == 0 {
29 t.Fatal("Create must assign a GUID and client id, never draw them from yrs")
30 }
31
32 list, err := app.Store().List(ctx, owner)
33 if err != nil {
34 t.Fatalf("List: %v", err)
35 }
36 if len(list) != 1 || list[0].Title != "First document" {
37 t.Fatalf("List = %+v, want one document titled First document", list)
38 }
39
40 got, err := app.Store().Get(ctx, owner, made.Sqid)
41 if err != nil {
42 t.Fatalf("Get: %v", err)
43 }
44 if got.ID != made.ID {
45 t.Errorf("Get returned id %d, want %d", got.ID, made.ID)
46 }
47}
48
49func TestTextComesFromTheCore(t *testing.T) {
50 app, err := docs.NewForTest(t)
51 if err != nil {
52 t.Fatalf("NewForTest: %v", err)
53 }
54 ctx := context.Background()
55 owner := app.TestPerson(t, "owner@example.com")
56
57 doc, err := app.Store().Create(ctx, owner, "With content")
58 if err != nil {
59 t.Fatalf("Create: %v", err)
60 }
61
62 // The "hello" fragment fixture is a real Yjs update produced by
63 // real yjs (fixtures/generate-fragment.mjs), building a
64 // Y.XmlFragment on root "prosemirror" — the root Store.Text
65 // actually reads (core/src/abi.rs's text_string doc comment).
66 // ReadFixture's own fixtures/data "hello" applies just as cleanly
67 // but targets root "content", which Store.Text no longer reads —
68 // it would read back "", proving nothing about the CRDT.
69 update := docs.ReadFragmentFixture(t, "hello")
70 if _, err := app.Store().ApplyUpdate(ctx, doc, app.TestActorFor(t, doc), update); err != nil {
71 t.Fatalf("ApplyUpdate: %v", err)
72 }
73
74 text, err := app.Store().Text(ctx, doc)
75 if err != nil {
76 t.Fatalf("Text: %v", err)
77 }
78 if text != "hello world" {
79 t.Errorf("Text = %q, want %q — the text must come from the CRDT, not a column", text, "hello world")
80 }
81}
82
83// TestListOrdersByUpdatedAt is fix round 2's own regression test: List
84// used to order by CreatedAt while handlers.go's groupByRecency/
85// formatWhen bucket and display UpdatedAt, and ApplyUpdate never
86// touched UpdatedAt at all — the two clocks only ever agreed because
87// Store.Create sets both columns to the same timestamp, so nothing
88// exercised the divergence until an update actually moved UpdatedAt
89// away from CreatedAt. This creates an older document, then a newer
90// one, confirms the newer one lists first (ordinary creation order),
91// applies an update to the *older* document, and asserts it jumps to
92// the top — the assertion that would have caught the bug: it fails
93// against an order-by-CreatedAt List even though ApplyUpdate bumps
94// UpdatedAt, and it fails against an ApplyUpdate that does not bump
95// UpdatedAt even though List orders by it. Both halves of the fix are
96// required to pass it.
97func TestListOrdersByUpdatedAt(t *testing.T) {
98 app, err := docs.NewForTest(t)
99 if err != nil {
100 t.Fatalf("NewForTest: %v", err)
101 }
102 ctx := context.Background()
103 owner := app.TestPerson(t, "owner@example.com")
104
105 older, err := app.Store().Create(ctx, owner, "Older document")
106 if err != nil {
107 t.Fatalf("Create(older): %v", err)
108 }
109 // A real gap between the two rows' timestamps: two time.Now()
110 // calls back to back can land in the same tick on a coarse clock,
111 // and this test's whole point is ordering, not simultaneity.
112 time.Sleep(5 * time.Millisecond)
113 newer, err := app.Store().Create(ctx, owner, "Newer document")
114 if err != nil {
115 t.Fatalf("Create(newer): %v", err)
116 }
117
118 list, err := app.Store().List(ctx, owner)
119 if err != nil {
120 t.Fatalf("List: %v", err)
121 }
122 if len(list) != 2 || list[0].Sqid != newer.Sqid || list[1].Sqid != older.Sqid {
123 t.Fatalf("List before any update = %+v, want [newer, older] by plain creation order", list)
124 }
125
126 time.Sleep(5 * time.Millisecond)
127 update := docs.ReadFixture(t, "hello")
128 if _, err := app.Store().ApplyUpdate(ctx, older, app.TestActorFor(t, older), update); err != nil {
129 t.Fatalf("ApplyUpdate(older): %v", err)
130 }
131
132 list, err = app.Store().List(ctx, owner)
133 if err != nil {
134 t.Fatalf("List after update: %v", err)
135 }
136 if len(list) != 2 || list[0].Sqid != older.Sqid || list[1].Sqid != newer.Sqid {
137 t.Fatalf("List after applying an update to the older document = %+v, want [older, newer] — applying an update must move a document to the top of the list", list)
138 }
139}
140
141// TestADocumentYouDoNotOwnIsNotThere is the brief's own literal test
142// (Task 2, plan 3): Bob must not be able to fetch Alice's document by
143// sqid, and it must not appear in his own list either — both routed
144// through Store.Get/List's owner parameter and store.go's owned seam,
145// never a separate "is this yours" check that could leak a distinct
146// error shape.
147func TestADocumentYouDoNotOwnIsNotThere(t *testing.T) {
148 app, _ := docs.NewForTest(t)
149 ctx := context.Background()
150
151 alice := app.TestPerson(t, "alice@example.com")
152 bob := app.TestPerson(t, "bob@example.com")
153
154 doc, err := app.Store().Create(ctx, alice, "Alice's private notes")
155 if err != nil {
156 t.Fatal(err)
157 }
158
159 // Bob must not be able to fetch it, and must not be told it exists.
160 if _, err := app.Store().Get(ctx, bob, doc.Sqid); err == nil {
161 t.Fatal("Bob fetched Alice's document")
162 }
163
164 list, err := app.Store().List(ctx, bob)
165 if err != nil {
166 t.Fatal(err)
167 }
168 if len(list) != 0 {
169 t.Errorf("Bob's list has %d documents, want 0", len(list))
170 }
171}
172
173// TestUnownedDocumentOverHTTPIs404Not403 is the brief's own literal
174// test: the same isolation TestADocumentYouDoNotOwnIsNotThere proves
175// at the store, proven again over the real router, because a store
176// that scopes correctly and a handler that still leaks a 403 (or a
177// 200 on someone else's document) would both pass the store-level
178// test alone. Spec §4.2: a document you cannot see returns 404, so a
179// stranger cannot learn a sqid exists by probing it.
180func TestUnownedDocumentOverHTTPIs404Not403(t *testing.T) {
181 app, _ := docs.NewForTest(t)
182 ctx := context.Background()
183 alice := app.TestPerson(t, "alice@example.com")
184 doc, _ := app.Store().Create(ctx, alice, "Alice's private notes")
185
186 req := app.TestRequest(t, "bob@example.com", http.MethodGet, "/d/"+doc.Sqid, nil)
187 rec := httptest.NewRecorder()
188 app.Handler().ServeHTTP(rec, req)
189
190 if rec.Code != http.StatusNotFound {
191 t.Errorf("status %d, want 404 — a document you cannot see does not exist (spec §4.2)", rec.Code)
192 }
193}
194
195// TestApplyUpdateRefusesAnotherOwnersDocument is fix round 2's own
196// regression test (Finding 1, corrected): fix round 1's version of
197// this test was named as if it proved the write was refused, but it
198// only ever asserted that UpdatedAt held still — a green test
199// certifying a property nobody wanted, while the actual defect (the
200// content write) went unchecked and unfixed underneath it.
201//
202// The real bug: ApplyUpdate's doc_updates INSERT (and the MAX(seq)
203// query that sizes it) has always been keyed on doc.ID alone, with no
204// owner predicate at all. Only the trailing updated_at bump was ever
205// scoped through owned(doc.OwnerID), and it runs last — long after
206// the content has already been replayed, applied and persisted. A
207// gorm UPDATE matching zero rows returns no error, so a *Document
208// carrying a stranger's OwnerID used to replay and persist the
209// stranger's content into the real owner's document, then return nil:
210// only the timestamp was refused, silently, while "hello world"
211// landed in the victim's log.
212//
213// This asserts the actual contract: a mismatched owner must refuse
214// the whole operation, up front, with a real error — not partially
215// apply and then fail silently on the last write. It checks all four
216// legs: the call returns an error, the victim's content never changes
217// (via Store.Text, reading the real CRDT replay — not a column that
218// could look right while the log underneath it is wrong), the
219// victim's UpdatedAt never moves (fix round 1's original property,
220// kept), and — so this test cannot pass by making ApplyUpdate refuse
221// everything — the legitimate owner's own ApplyUpdate still works end
222// to end. SKILL.md §3, verbatim: "Scope the write, not just the read
223// that loaded the row."
224func TestApplyUpdateRefusesAnotherOwnersDocument(t *testing.T) {
225 app, err := docs.NewForTest(t)
226 if err != nil {
227 t.Fatalf("NewForTest: %v", err)
228 }
229 ctx := context.Background()
230 owner := app.TestPerson(t, "owner@example.com")
231
232 doc, err := app.Store().Create(ctx, owner, "Real owner's document")
233 if err != nil {
234 t.Fatalf("Create: %v", err)
235 }
236 before, err := app.Store().Get(ctx, owner, doc.Sqid)
237 if err != nil {
238 t.Fatalf("Get (before): %v", err)
239 }
240 textBefore, err := app.Store().Text(ctx, before)
241 if err != nil {
242 t.Fatalf("Text (before): %v", err)
243 }
244
245 // A real gap so a spurious bump would be visible against the
246 // clock, not hidden by two timestamps landing in the same tick.
247 time.Sleep(5 * time.Millisecond)
248
249 // Sharing re-pointed this test, and the re-pointing is the finding.
250 //
251 // It used to hand ApplyUpdate a *Document whose OwnerID had been
252 // tampered to a person who does not exist, and assert the write was
253 // refused. That worked because ApplyUpdate authorized the struct it was
254 // given. It no longer does: it authorizes the ACTING person against the
255 // row in the database, so a tampered OwnerID on a caller's copy is now
256 // simply ignored — which is the safer behaviour, and means the old
257 // assertion would pass for the wrong reason or fail for one.
258 //
259 // So the hazard is expressed the way it actually exists now: a stranger,
260 // holding a perfectly accurate *Document, is refused, and nothing is
261 // written. The tampered struct stays, unused by the authorization, to
262 // prove exactly that — a caller cannot make themselves the owner by
263 // editing a field on their own copy.
264 tampered := *doc
265 tampered.OwnerID = owner + 1_000_000 // guaranteed to belong to nobody
266 stranger := app.TestActorForPerson(t, app.TestPerson(t, "stranger@example.com"))
267
268 // Fragment fixture, not ReadFixture's plain-Y.Text one: this test's
269 // content assertions below go through Store.Text, which reads root
270 // "prosemirror" now — see TestTextComesFromTheCore's own comment.
271 update := docs.ReadFragmentFixture(t, "hello")
272 if _, err := app.Store().ApplyUpdate(ctx, &tampered, stranger, update); err == nil {
273 t.Fatal("ApplyUpdate returned nil for a stranger — want a refusal, before anything is written")
274 }
275
276 after, err := app.Store().Get(ctx, owner, doc.Sqid)
277 if err != nil {
278 t.Fatalf("Get (after): %v", err)
279 }
280 textAfter, err := app.Store().Text(ctx, after)
281 if err != nil {
282 t.Fatalf("Text (after): %v", err)
283 }
284 if textAfter != textBefore {
285 t.Errorf("victim document's content changed from %q to %q — the stranger's update landed despite the mismatched owner", textBefore, textAfter)
286 }
287 if !after.UpdatedAt.Equal(before.UpdatedAt) {
288 t.Errorf("UpdatedAt moved from %v to %v even though ApplyUpdate was called with a mismatched owner (%d instead of the document's real owner %d)",
289 before.UpdatedAt, after.UpdatedAt, tampered.OwnerID, owner)
290 }
291
292 // Prove the fix does not break the working path: the legitimate
293 // owner's own ApplyUpdate must still apply end to end.
294 if _, err := app.Store().ApplyUpdate(ctx, doc, app.TestActorFor(t, doc), update); err != nil {
295 t.Fatalf("ApplyUpdate (legitimate owner): %v", err)
296 }
297 legit, err := app.Store().Text(ctx, doc)
298 if err != nil {
299 t.Fatalf("Text (legitimate owner): %v", err)
300 }
301 if legit != "hello world" {
302 t.Errorf("legitimate owner's ApplyUpdate did not persist: Text = %q, want %q", legit, "hello world")
303 }
304}
305