title_test.gno
4.13 Kb · 106 lines
1package wiki
2
3import (
4 "testing"
5
6 "gno.land/p/nt/markdown/sanitize/v0"
7 "gno.land/p/nt/uassert/v0"
8)
9
10func TestParseTitle(t *testing.T) {
11 cases := []struct {
12 name string
13 in string
14 wantNS Namespace
15 want string // canonical String(), "" when an error is expected
16 wantErr error
17 }{
18 {"plain", "Gno land", NSMain, "Gno land", nil},
19 {"underscores become spaces", "Gno_land", NSMain, "Gno land", nil},
20 {"whitespace collapses", " Gno land ", NSMain, "Gno land", nil},
21 {"first letter is upper-cased", "gno land", NSMain, "Gno land", nil},
22 {"namespace prefix", "Help:Gno land", NSHelp, "Help:Gno land", nil},
23 {"namespace prefix is case-insensitive", "help:gno_land", NSHelp, "Help:Gno land", nil},
24 {"category", "Category:Chains", NSCategory, "Category:Chains", nil},
25 {"unknown prefix stays in the name", "Foo:Bar", NSMain, "Foo:Bar", nil},
26 {"unicode letters are allowed", "Élysée", NSMain, "Élysée", nil},
27 {"unicode is upper-cased too", "élysée", NSMain, "Élysée", nil},
28 {"zero-width characters are stripped", "Gnoland", NSMain, "Gnoland", nil},
29 {"empty", "", NSMain, "", ErrEmptyTitle},
30 {"whitespace only", " ", NSMain, "", ErrEmptyTitle},
31 {"namespace with no name", "Help:", NSMain, "", ErrEmptyTitle},
32 {"slash is rejected", "Gno/land", NSMain, "", ErrTitleChar},
33 {"pipe is rejected", "Gno|land", NSMain, "", ErrTitleChar},
34 {"markdown star is rejected", "Gno*land", NSMain, "", ErrTitleChar},
35 {"bracket is rejected", "Gno[land", NSMain, "", ErrTitleChar},
36 {"hash is rejected", "Gno#land", NSMain, "", ErrTitleChar},
37 }
38 for _, tc := range cases {
39 got, err := ParseTitle(tc.in)
40 if tc.wantErr != nil {
41 uassert.ErrorIs(t, err, tc.wantErr, tc.name)
42 continue
43 }
44 if !uassert.NoError(t, err, tc.name) {
45 continue
46 }
47 uassert.Equal(t, tc.want, got.String(), tc.name)
48 uassert.Equal(t, uint64(tc.wantNS), uint64(got.NS), tc.name+" (namespace)")
49 }
50}
51
52func TestTitleTooLong(t *testing.T) {
53 long := ""
54 for i := 0; i <= MaxTitleLen; i++ {
55 long += "a"
56 }
57 _, err := ParseTitle(long)
58 uassert.ErrorIs(t, err, ErrTitleTooLong)
59}
60
61func TestTitleKeyOrdersByNamespace(t *testing.T) {
62 // The avl key must group namespaces, and must keep doing so past nine
63 // namespaces, which is why the code is a fixed-width string rather than
64 // a ufmt-formatted number.
65 uassert.Equal(t, "00:Gno land", MustParseTitle("Gno land").Key())
66 uassert.Equal(t, "01:Gno land", MustParseTitle("User:Gno land").Key())
67 uassert.True(t, MustParseTitle("Gno land").Key() < MustParseTitle("User:Alpha").Key(),
68 "every main-namespace key must sort before every User key")
69}
70
71func TestTitleSlug(t *testing.T) {
72 uassert.Equal(t, "Gno_land", MustParseTitle("Gno land").Slug())
73 uassert.Equal(t, "User:Gno_land", MustParseTitle("User:Gno land").Slug())
74 uassert.Equal(t, "Mercury_(planet)", MustParseTitle("Mercury (planet)").Slug())
75}
76
77// TestTitleSurvivesTheRenderPipeline pins the coupling between the title
78// charset and the markdown sanitizer.
79//
80// Rendering escapes the whole body first and rewrites wikilinks second, so
81// every title character has to survive that round trip: sanitize escapes it,
82// ScanLinks finds the link in the escaped text, and unescapeInline hands
83// ParseTitle the bytes the author typed. The sanitizer escapes with
84// backslashes ("(" becomes "\(") which unescapeInline undoes, but a character
85// escaped as an entity ("&" becomes "&") would not survive, which is why
86// the charset excludes those. This test fails the day either side changes.
87func TestTitleSurvivesTheRenderPipeline(t *testing.T) {
88 // The ASCII allowlist plus a couple of accepted non-ASCII letters; the
89 // whole set has to fit in one title, so this is not every valid rune.
90 chars := "Élysée東京"
91 for r := rune(0); r < 0x80; r++ {
92 if validTitleRune(r) && r != ' ' {
93 chars += string(r)
94 }
95 }
96 want := MustParseTitle(chars)
97
98 escaped := sanitize.BlockRich("[[" + chars + "]]\n")
99 links := ScanLinks(escaped, `\[\[`, `\]\]`)
100 if !uassert.Equal(t, 1, len(links), "the escaped body must still contain exactly one link") {
101 return
102 }
103 got, err := ParseTitle(unescapeInline(links[0].Target))
104 uassert.NoError(t, err)
105 uassert.Equal(t, want.String(), got.String())
106}