Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}