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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}