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

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", "Gno​land", 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 "&amp;") 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}