wiki_test.gno
10.64 Kb · 347 lines
1package wiki
2
3import (
4 "testing"
5 "time"
6
7 "gno.land/p/nt/testutils/v0"
8 "gno.land/p/nt/uassert/v0"
9)
10
11var (
12 alice = testutils.TestAddress("alice")
13 bob = testutils.TestAddress("bob")
14 mod = testutils.TestAddress("mod")
15)
16
17// at returns a deterministic timestamp and height for step n, so tests never
18// depend on wall-clock time or on where a previous test left the block height.
19func at(n int) (time.Time, int64) {
20 return time.Unix(int64(1757000000+n*60), 0).UTC(), int64(100 + n)
21}
22
23func edit(t *testing.T, w *Wiki, n int, author address, title, body, summary string) *Revision {
24 t.Helper()
25 now, h := at(n)
26 rev, err := w.Edit(author, now, h, title, body, summary, false)
27 uassert.NoError(t, err, title)
28 return rev
29}
30
31func TestEditCreatesAndUpdates(t *testing.T) {
32 w := New(DefaultRetention, DefaultMaxBody)
33
34 r1 := edit(t, w, 1, alice, "gno_land", "The first draft.\n", "create")
35 uassert.Equal(t, uint64(1), r1.ID)
36 uassert.Equal(t, uint64(0), r1.Prev)
37 uassert.Equal(t, string(KindCreate), string(r1.Kind))
38
39 r2 := edit(t, w, 2, bob, "Gno land", "The second draft.\n", "expand")
40 uassert.Equal(t, uint64(1), r2.Prev)
41 uassert.Equal(t, string(KindEdit), string(r2.Kind))
42
43 p, err := w.Page("Gno land")
44 uassert.NoError(t, err)
45 uassert.Equal(t, 2, p.NumRevisions())
46 body, kept := p.Body()
47 uassert.True(t, kept)
48 uassert.Equal(t, "The second draft.\n", body)
49
50 s := w.Stats()
51 uassert.Equal(t, 1, s.Pages, "the two edits are one page")
52 uassert.Equal(t, 2, s.Revisions)
53}
54
55func TestEditRejectsNoOpAndOversize(t *testing.T) {
56 w := New(DefaultRetention, 32)
57 now, h := at(1)
58
59 _, err := w.Edit(alice, now, h, "A", "", "empty", false)
60 uassert.ErrorIs(t, err, ErrEmptyBody)
61
62 _, err = w.Edit(alice, now, h, "A", "0123456789012345678901234567890123", "too big", false)
63 uassert.ErrorIs(t, err, ErrBodyTooLarge)
64
65 edit(t, w, 1, alice, "A", "same\n", "create")
66 _, err = w.Edit(alice, now, h, "A", "same\n", "again", false)
67 uassert.ErrorIs(t, err, ErrNoChange, "an identical body must not create a revision")
68
69 _, err = w.Edit(alice, now, h, "Special:Anything", "x\n", "", false)
70 uassert.ErrorIs(t, err, ErrSpecial)
71}
72
73// TestRetentionEvictsOldBodies is the storage design in one test: the spine
74// grows by a fixed amount per edit, the held bytes do not.
75func TestRetentionEvictsOldBodies(t *testing.T) {
76 w := New(2, DefaultMaxBody)
77 bodies := []string{"aaaa\n", "bbbbbb\n", "cccccccc\n", "dddddddddd\n"}
78 for i, b := range bodies {
79 edit(t, w, i+1, alice, "A", b, "edit")
80 }
81
82 p, err := w.Page("A")
83 uassert.NoError(t, err)
84 uassert.Equal(t, 4, p.NumRevisions(), "every revision stays in the history")
85
86 kept := 0
87 for _, r := range p.History(0, 10) {
88 if r.Kept() {
89 kept++
90 }
91 }
92 uassert.Equal(t, 2, kept, "only the retention window keeps bodies")
93 uassert.Equal(t, len(bodies[2])+len(bodies[3]), w.Stats().BytesHeld)
94
95 // The spine of an evicted revision is intact, hash included.
96 oldest := p.History(0, 10)[3]
97 uassert.False(t, oldest.Kept())
98 uassert.Equal(t, hashBody(bodies[0]), oldest.Hash)
99 uassert.Equal(t, len(bodies[0]), oldest.Size)
100}
101
102func TestRevert(t *testing.T) {
103 w := New(3, DefaultMaxBody)
104 first := edit(t, w, 1, alice, "A", "good\n", "create")
105 edit(t, w, 2, bob, "A", "VANDALISM\n", "oops")
106
107 now, h := at(3)
108 rev, err := w.Revert(mod, now, h, "A", first.ID, "rv")
109 uassert.NoError(t, err)
110 uassert.Equal(t, string(KindRevert), string(rev.Kind))
111
112 p, _ := w.Page("A")
113 body, _ := p.Body()
114 uassert.Equal(t, "good\n", body)
115 uassert.Equal(t, 3, p.NumRevisions(), "the vandalism stays in the history")
116}
117
118func TestRevertRefusesEvictedAndUnknown(t *testing.T) {
119 w := New(1, DefaultMaxBody)
120 first := edit(t, w, 1, alice, "A", "one\n", "")
121 edit(t, w, 2, alice, "A", "two\n", "")
122 now, h := at(3)
123
124 _, err := w.Revert(alice, now, h, "A", first.ID, "")
125 uassert.ErrorIs(t, err, ErrBodyEvicted,
126 "reverting past the retention window must fail loudly, not silently")
127
128 _, err = w.Revert(alice, now, h, "A", 9999, "")
129 uassert.ErrorIs(t, err, ErrNoSuchRevision)
130}
131
132func TestBlankAndPurge(t *testing.T) {
133 w := New(5, DefaultMaxBody)
134 edit(t, w, 1, alice, "A", "content\n", "")
135 edit(t, w, 2, alice, "A", "more content\n", "")
136 uassert.Equal(t, 1, w.Stats().Pages)
137
138 now, h := at(3)
139 _, err := w.Blank(mod, now, h, "A", "policy")
140 uassert.NoError(t, err)
141 uassert.Equal(t, 0, w.Stats().Pages, "a blanked page stops counting as a page")
142
143 p, _ := w.Page("A")
144 uassert.True(t, p.Blanked)
145 uassert.Equal(t, 3, p.NumRevisions(), "blanking is a revision, not an erasure")
146
147 released, err := w.Purge("A")
148 uassert.NoError(t, err)
149 uassert.Equal(t, len("content\n")+len("more content\n"), released)
150 uassert.Equal(t, 0, w.Stats().BytesHeld, "purge releases every retained byte")
151 uassert.Equal(t, 3, p.NumRevisions(), "purge leaves the spine intact")
152}
153
154func TestBlankThenEditRestoresThePage(t *testing.T) {
155 w := New(5, DefaultMaxBody)
156 edit(t, w, 1, alice, "A", "content\n", "")
157 now, h := at(2)
158 w.Blank(mod, now, h, "A", "")
159 edit(t, w, 3, alice, "A", "back\n", "restore")
160 uassert.Equal(t, 1, w.Stats().Pages)
161 p, _ := w.Page("A")
162 uassert.False(t, p.Blanked)
163}
164
165func TestBacklinksAndRedlinks(t *testing.T) {
166 w := New(3, DefaultMaxBody)
167 edit(t, w, 1, alice, "A", "links to [[B]] and [[C]]\n", "")
168 edit(t, w, 2, alice, "D", "also links to [[B]]\n", "")
169
170 b := MustParseTitle("B")
171 in := w.Backlinks(b)
172 uassert.Equal(t, 2, len(in))
173 uassert.Equal(t, "A", in[0].String())
174 uassert.Equal(t, "D", in[1].String())
175
176 // C does not exist: a redlink is still indexed, so the page knows who is
177 // waiting for it the moment it is created.
178 uassert.False(t, w.Exists(MustParseTitle("C")))
179 uassert.Equal(t, 1, len(w.Backlinks(MustParseTitle("C"))))
180
181 // Editing A to drop the link must un-index it.
182 edit(t, w, 3, alice, "A", "links to nothing\n", "")
183 uassert.Equal(t, 1, len(w.Backlinks(b)))
184 uassert.Equal(t, 0, len(w.Backlinks(MustParseTitle("C"))))
185}
186
187func TestCategories(t *testing.T) {
188 w := New(3, DefaultMaxBody)
189 edit(t, w, 1, alice, "A", "text\n[[Category:Chains]]\n", "")
190 edit(t, w, 2, alice, "B", "text\n[[Category:Chains]]\n[[Category:Tools]]\n", "")
191
192 chains := MustParseTitle("Category:Chains")
193 uassert.Equal(t, 2, len(w.CategoryMembers(chains)))
194 uassert.Equal(t, 2, len(w.Categories()))
195
196 // An explicit [[:Category:X]] links to the category instead of joining it.
197 edit(t, w, 3, alice, "C", "see [[:Category:Chains]]\n", "")
198 uassert.Equal(t, 2, len(w.CategoryMembers(chains)))
199 uassert.Equal(t, 1, len(w.Backlinks(chains)))
200}
201
202func TestRedirect(t *testing.T) {
203 w := New(3, DefaultMaxBody)
204 edit(t, w, 1, alice, "Gno", "#REDIRECT [[Gno land]]\n", "")
205 edit(t, w, 2, alice, "Gno land", "The article.\n", "")
206
207 dest, asked, err := w.Resolve("Gno")
208 uassert.NoError(t, err)
209 uassert.Equal(t, "Gno land", dest.Title.String())
210 uassert.Equal(t, "Gno", asked.Title.String())
211
212 // A dangling redirect renders itself rather than 404ing.
213 edit(t, w, 3, alice, "Nowhere", "#REDIRECT [[Absent]]\n", "")
214 dest, _, err = w.Resolve("Nowhere")
215 uassert.NoError(t, err)
216 uassert.Equal(t, "Nowhere", dest.Title.String())
217}
218
219func TestMoveKeepsHistoryAndLeavesARedirect(t *testing.T) {
220 w := New(3, DefaultMaxBody)
221 edit(t, w, 1, alice, "Old name", "body linking to [[B]]\n", "")
222 edit(t, w, 2, alice, "Old name", "body linking to [[B]], revised\n", "")
223
224 now, h := at(3)
225 uassert.NoError(t, w.Move(mod, now, h, "Old name", "New name", "rename"))
226
227 moved, err := w.Page("New name")
228 uassert.NoError(t, err)
229 uassert.Equal(t, 3, moved.NumRevisions(), "history follows the page")
230 body, kept := moved.Body()
231 uassert.True(t, kept)
232 uassert.Equal(t, "body linking to [[B]], revised\n", body, "a move does not touch the text")
233
234 stub, err := w.Page("Old name")
235 uassert.NoError(t, err)
236 uassert.Equal(t, "New name", stub.Redirect())
237
238 // The backlink index must follow the rename, not point at the old key.
239 in := w.Backlinks(MustParseTitle("B"))
240 uassert.Equal(t, 1, len(in))
241 uassert.Equal(t, "New name", in[0].String())
242}
243
244func TestMoveRefusesAnExistingTarget(t *testing.T) {
245 w := New(3, DefaultMaxBody)
246 edit(t, w, 1, alice, "A", "a\n", "")
247 edit(t, w, 2, alice, "B", "b\n", "")
248 now, h := at(3)
249 uassert.ErrorIs(t, w.Move(mod, now, h, "A", "B", ""), ErrPageExists)
250}
251
252func TestProtectionIsRecordedInTheHistory(t *testing.T) {
253 w := New(3, DefaultMaxBody)
254 edit(t, w, 1, alice, "A", "a\n", "")
255 now, h := at(2)
256 uassert.NoError(t, w.SetProtection(mod, now, h, "A", "locked"))
257
258 p, _ := w.Page("A")
259 uassert.Equal(t, "locked", p.Protection.String())
260 uassert.Equal(t, 2, p.NumRevisions())
261
262 // The protection entry must not become the current text.
263 body, kept := p.Body()
264 uassert.True(t, kept)
265 uassert.Equal(t, "a\n", body)
266
267 // It carries the current hash, so the history does not read as a blanking.
268 last := p.History(0, 1)[0]
269 uassert.Equal(t, string(KindProtect), string(last.Kind))
270 uassert.Equal(t, hashBody("a\n"), last.Hash)
271}
272
273func TestMetaRevisionsDoNotEvictRealBodies(t *testing.T) {
274 w := New(2, DefaultMaxBody)
275 edit(t, w, 1, alice, "A", "one\n", "")
276 edit(t, w, 2, alice, "A", "two\n", "")
277 now, h := at(3)
278 w.SetProtection(mod, now, h, "A", "locked")
279 w.SetProtection(mod, now, h, "A", "open")
280
281 p, _ := w.Page("A")
282 kept := 0
283 for _, r := range p.History(0, 10) {
284 if r.Kept() {
285 kept++
286 }
287 }
288 uassert.Equal(t, 2, kept, "two bodyless entries must not push both bodies out of a window of 2")
289}
290
291func TestRecentChangesIsNewestFirst(t *testing.T) {
292 w := New(3, DefaultMaxBody)
293 edit(t, w, 1, alice, "A", "a\n", "first")
294 edit(t, w, 2, bob, "B", "b\n", "second")
295
296 recent := w.Recent(10)
297 uassert.Equal(t, 2, len(recent))
298 uassert.Equal(t, "B", recent[0].Title.String())
299 uassert.Equal(t, "A", recent[1].Title.String())
300}
301
302func TestTitlesWalkOneNamespace(t *testing.T) {
303 w := New(3, DefaultMaxBody)
304 edit(t, w, 1, alice, "Beta", "b\n", "")
305 edit(t, w, 2, alice, "Alpha", "a\n", "")
306 edit(t, w, 3, alice, "Help:Alpha", "t\n", "")
307
308 main := w.Titles(NSMain.Prefix(), 0, 10)
309 uassert.Equal(t, 2, len(main))
310 uassert.Equal(t, "Alpha", main[0].String())
311 uassert.Equal(t, "Beta", main[1].String())
312
313 help := w.Titles(NSHelp.Prefix(), 0, 10)
314 uassert.Equal(t, 1, len(help))
315 uassert.Equal(t, "Help:Alpha", help[0].String())
316
317 uassert.Equal(t, 1, len(w.Titles(NSMain.Prefix(), 1, 10)), "offset skips")
318}
319
320func TestContributors(t *testing.T) {
321 w := New(3, DefaultMaxBody)
322 edit(t, w, 1, alice, "A", "1\n", "")
323 edit(t, w, 2, bob, "A", "2\n", "")
324 edit(t, w, 3, alice, "A", "3\n", "")
325
326 p, _ := w.Page("A")
327 c := p.Contributors()
328 uassert.Equal(t, 2, len(c))
329 uassert.Equal(t, alice.String(), c[0].String())
330 uassert.Equal(t, bob.String(), c[1].String())
331}
332
333func TestFormatGNOT(t *testing.T) {
334 cases := []struct {
335 in int
336 want string
337 }{
338 {0, "0 GNOT"},
339 {1000000, "1 GNOT"},
340 {1500000, "1.5 GNOT"},
341 {100, "0.0001 GNOT"},
342 {123456, "0.123456 GNOT"},
343 }
344 for _, tc := range cases {
345 uassert.Equal(t, tc.want, formatGNOT(tc.in))
346 }
347}