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 "testing"
6 "time"
7)
8
9// TestOwnedScopesByOwner tests store.go's owned function directly —
10// the plan's own requirement (Task 2: "Write the scoping as one
11// function that every query goes through, and test that function
12// directly"), not only indirectly through Store.List/Store.Get, which
13// would prove the seam is wired in but not pin what the seam itself
14// actually does. Deliberately not a test of rastrillo's scope.Owned
15// (which this app does not use — see owned's own doc comment and
16// spec §4.2): this exercises Docs' own function, in this package, so
17// the day it grows a grants join the test above is what has to keep
18// passing.
19func TestOwnedScopesByOwner(t *testing.T) {
20 app, err := NewForTest(t)
21 if err != nil {
22 t.Fatalf("NewForTest: %v", err)
23 }
24 ctx := context.Background()
25 store := app.Store()
26
27 alice, err := store.EnsurePerson(ctx, "alice@example.com")
28 if err != nil {
29 t.Fatalf("EnsurePerson(alice): %v", err)
30 }
31 bob, err := store.EnsurePerson(ctx, "bob@example.com")
32 if err != nil {
33 t.Fatalf("EnsurePerson(bob): %v", err)
34 }
35
36 doc, err := store.Create(ctx, alice, "Alice's document")
37 if err != nil {
38 t.Fatalf("Create: %v", err)
39 }
40
41 var aliceRows []*Document
42 if err := owned(store.db.WithContext(ctx), alice).Find(&aliceRows).Error; err != nil {
43 t.Fatalf("owned(alice): %v", err)
44 }
45 if len(aliceRows) != 1 || aliceRows[0].ID != doc.ID {
46 t.Errorf("owned(alice) = %+v, want exactly [%d]", aliceRows, doc.ID)
47 }
48
49 var bobRows []*Document
50 if err := owned(store.db.WithContext(ctx), bob).Find(&bobRows).Error; err != nil {
51 t.Fatalf("owned(bob): %v", err)
52 }
53 if len(bobRows) != 0 {
54 t.Errorf("owned(bob) returned %d row(s), want 0 — bob does not own alice's document", len(bobRows))
55 }
56
57 // Owner 0 is scoped to nothing — not an unused id that merely
58 // never happens to match a row (999999 would prove that, but is a
59 // value that never existed), but the one real value
60 // 0002_owners.sql's migration actually writes onto every
61 // pre-ownership document, and the one owned() refuses
62 // structurally regardless of what the table holds (fix round 1,
63 // Finding 2). TestOwnedRejectsOwnerZero and
64 // TestOrphanedDocumentsAreVisibleToNobody below cover the same
65 // refusal against a row that genuinely has owner_id = 0; this
66 // assertion holds even with no such row present at all.
67 var noRows []*Document
68 if err := owned(store.db.WithContext(ctx), 0).Find(&noRows).Error; err != nil {
69 t.Fatalf("owned(0): %v", err)
70 }
71 if len(noRows) != 0 {
72 t.Errorf("owned(0) returned %d row(s), want 0", len(noRows))
73 }
74}
75
76// TestOwnedRejectsOwnerZero is fix round 1's own regression test
77// (Finding 2): a reviewer found owned(g, 0), Store.List(ctx, 0) and
78// Store.Get(ctx, 0, ...) all happily returned a row whose owner_id is
79// 0 — exactly what 0002_owners.sql's DEFAULT 0 backfill produces for
80// a document that predates ownership (see that migration's own
81// comment). This inserts such a row directly, bypassing Store.Create
82// (which only ever receives a real Person id from its caller), to
83// reproduce that exact shape, then confirms owned(g, 0) still matches
84// nothing even though a matching row genuinely exists. Negative
85// owners are checked too — the same "not a real person" reasoning,
86// not a special case for the literal value 0.
87func TestOwnedRejectsOwnerZero(t *testing.T) {
88 app, err := NewForTest(t)
89 if err != nil {
90 t.Fatalf("NewForTest: %v", err)
91 }
92 ctx := context.Background()
93 store := app.Store()
94
95 now := time.Now().UTC()
96 orphan := &Document{
97 Sqid: "orphanzero1",
98 Title: "Pre-ownership document",
99 OwnerID: 0,
100 GUID: "orphan-guid-zero",
101 ClientID: 1,
102 CreatedAt: now,
103 UpdatedAt: now,
104 }
105 if err := store.db.WithContext(ctx).Create(orphan).Error; err != nil {
106 t.Fatalf("insert orphaned document: %v", err)
107 }
108
109 var zeroRows []*Document
110 if err := owned(store.db.WithContext(ctx), 0).Find(&zeroRows).Error; err != nil {
111 t.Fatalf("owned(0): %v", err)
112 }
113 if len(zeroRows) != 0 {
114 t.Errorf("owned(0) returned %d row(s), want 0 — owner 0 is never a real person, even when a row with owner_id = 0 exists", len(zeroRows))
115 }
116
117 var negRows []*Document
118 if err := owned(store.db.WithContext(ctx), -1).Find(&negRows).Error; err != nil {
119 t.Fatalf("owned(-1): %v", err)
120 }
121 if len(negRows) != 0 {
122 t.Errorf("owned(-1) returned %d row(s), want 0", len(negRows))
123 }
124}
125
126// TestOrphanedDocumentsAreVisibleToNobody is fix round 1's own
127// regression test (Finding 3): 0002_owners.sql's DEFAULT 0 backfill
128// is a deliberate decision — a document that existed before ownership
129// did becomes permanently invisible — but that property was
130// previously asserted nowhere. This pins it through the same Store
131// API a real request uses (List and Get), not just owned() directly:
132// an orphaned document must not appear in anyone's list, and must not
133// be fetchable by anyone via its sqid, exactly as if it did not
134// exist — spec §4.2's own contract, applied to the migration's own
135// backfill rather than only to a document someone else owns.
136func TestOrphanedDocumentsAreVisibleToNobody(t *testing.T) {
137 app, err := NewForTest(t)
138 if err != nil {
139 t.Fatalf("NewForTest: %v", err)
140 }
141 ctx := context.Background()
142 store := app.Store()
143
144 now := time.Now().UTC()
145 orphan := &Document{
146 Sqid: "orphanzero2",
147 Title: "Another pre-ownership document",
148 OwnerID: 0,
149 GUID: "orphan-guid-visible",
150 ClientID: 2,
151 CreatedAt: now,
152 UpdatedAt: now,
153 }
154 if err := store.db.WithContext(ctx).Create(orphan).Error; err != nil {
155 t.Fatalf("insert orphaned document: %v", err)
156 }
157
158 alice, err := store.EnsurePerson(ctx, "alice-orphan-check@example.com")
159 if err != nil {
160 t.Fatalf("EnsurePerson(alice): %v", err)
161 }
162 bob, err := store.EnsurePerson(ctx, "bob-orphan-check@example.com")
163 if err != nil {
164 t.Fatalf("EnsurePerson(bob): %v", err)
165 }
166
167 for _, owner := range []int64{alice, bob} {
168 list, err := store.List(ctx, owner)
169 if err != nil {
170 t.Fatalf("List(%d): %v", owner, err)
171 }
172 for _, d := range list {
173 if d.Sqid == orphan.Sqid {
174 t.Errorf("owner %d's list contains the orphaned document %q", owner, orphan.Sqid)
175 }
176 }
177
178 if _, err := store.Get(ctx, owner, orphan.Sqid); err == nil {
179 t.Errorf("owner %d fetched the orphaned document %q", owner, orphan.Sqid)
180 }
181 }
182
183 // And owner 0 itself — the value the row actually carries — must
184 // not see it either; owned() refuses it outright rather than
185 // treating it as "the orphan's own owner."
186 if _, err := store.Get(ctx, 0, orphan.Sqid); err == nil {
187 t.Error("owner 0 fetched the orphaned document — owned() must refuse owner 0 unconditionally")
188 }
189}
190
191// TestEnsurePersonIsStableAndCaseInsensitive pins EnsurePerson's own
192// contract, since it is what turns a session's verified address into
193// the owner id every scoped query above keys on: the same address
194// (however it is cased) must always resolve to the same Person id, or
195// two requests from the same person could silently own different
196// libraries.
197func TestEnsurePersonIsStableAndCaseInsensitive(t *testing.T) {
198 app, err := NewForTest(t)
199 if err != nil {
200 t.Fatalf("NewForTest: %v", err)
201 }
202 ctx := context.Background()
203 store := app.Store()
204
205 first, err := store.EnsurePerson(ctx, "Alice@Example.com")
206 if err != nil {
207 t.Fatalf("EnsurePerson: %v", err)
208 }
209 second, err := store.EnsurePerson(ctx, " alice@example.com ")
210 if err != nil {
211 t.Fatalf("EnsurePerson (again): %v", err)
212 }
213 if first != second {
214 t.Errorf("EnsurePerson(%q) = %d, EnsurePerson(%q) = %d, want the same id", "Alice@Example.com", first, " alice@example.com ", second)
215 }
216}
217