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.

text_test.gno

8.39 Kb · 196 lines
  1package bolo
  2
  3import (
  4	"strings"
  5	"testing"
  6	"unicode"
  7)
  8
  9func TestNormalize(t *testing.T) {
 10	for _, c := range []struct {
 11		in        string
 12		multiline bool
 13		want      string
 14	}{
 15		{"  Clean water  ", false, "Clean water"},
 16		{"a\u200bb\u202ec\ufeffd\u2066e", false, "abcde"},
 17		{"one\r\ntwo\rthree", true, "one\ntwo\nthree"},
 18		// Every line break is one; outside multiline text, it and a tab become a
 19		// space, so words never join.
 20		{"one\r\ntwo\nthree", false, "one two three"},
 21		{"bell\a and\ttab", false, "bell and tab"},
 22		{"rub\x7fout", false, "rubout"},
 23		{"a\vb\fc\x1cd\x1fe", false, "a b c d e"},
 24		{"a\tb\vc", true, "a b c"},
 25		{"a\u0085b\u2028c\u2029d", true, "a\nb\nc\nd"},
 26		{"a\u0085b\u2028c\u2029d", false, "a b c d"},
 27		{"word\u2060joiner, soft\u00adhyphen", false, "wordjoiner, softhyphen"},
 28		// Fillers that show nothing go: Hangul fillers, the combining grapheme
 29		// joiner, the Braille blank. A variation selector stays, for emoji.
 30		{"a\u3164b\uffa0c\u115fd\u034fe\u2800f\U0001D159g", false, "abcdefg"},
 31		{"I \u2764\ufe0f you", false, "I \u2764\ufe0f you"},
 32		{"\n\n story \n\n", true, "story"},
 33	} {
 34		if got := normalize(c.in, c.multiline); got != c.want {
 35			t.Errorf("normalize(%q, %t) = %q, want %q", c.in, c.multiline, got, c.want)
 36		}
 37	}
 38}
 39
 40// TestLatin1 holds the Latin-1 shortcut of invisible and visible to the Unicode
 41// tables they read beyond it: invisible is Cc, Cf or Other_Default_Ignorable; a
 42// character shows on its own (counts alone) if it is L, Mc, N, P or S; a space (Zs)
 43// counts only between two of those.
 44func TestLatin1(t *testing.T) {
 45	for r := rune(0); r <= unicode.MaxLatin1; r++ {
 46		if want := unicode.In(r, unicode.Cc, unicode.Cf, unicode.Other_Default_Ignorable_Code_Point); invisible(r) != want {
 47			t.Errorf("invisible(%U) = %t, the tables say %t", r, !want, want)
 48		}
 49		shows := unicode.In(r, unicode.L, unicode.Mc, unicode.N, unicode.P, unicode.S)
 50		if got := visible(string(r), 1); got != shows {
 51			t.Errorf("visible(%U, 1) = %t, the tables say %t", r, got, shows)
 52		}
 53		between := shows || unicode.Is(unicode.Zs, r)
 54		if got := visible("x"+string(r)+"x", 3); got != between {
 55			t.Errorf("visible(x %U x, 3) = %t, the tables say %t", r, got, between)
 56		}
 57	}
 58}
 59
 60// TestVisible checks visible's threshold: at least min, the spaces between two
 61// shown characters counted, marks and spaces elsewhere not.
 62func TestVisible(t *testing.T) {
 63	for _, c := range []struct {
 64		s    string
 65		min  int
 66		want bool
 67	}{
 68		{"", 0, true},
 69		{"", 1, false},
 70		{"abc", 4, false},
 71		{"abcd", 4, true},
 72		{"ab cd", 5, true},
 73		{"ab  ", 3, false},
 74		{"a\u0301\u0301\u0301", 2, false},
 75	} {
 76		if got := visible(c.s, c.min); got != c.want {
 77			t.Errorf("visible(%q, %d) = %t, want %t", c.s, c.min, got, c.want)
 78		}
 79	}
 80}
 81
 82func TestCheckText(t *testing.T) {
 83	if got := checkText("  Clean water for Ndiaganiao ", titleMin, titleMax, false, ErrTitle); got != "Clean water for Ndiaganiao" {
 84		t.Errorf("checkText = %q", got)
 85	}
 86	// 80 characters of four bytes each: exactly the byte cap.
 87	if got := checkText(strings.Repeat("💧", 80), titleMin, titleMax, false, ErrTitle); len(got) != 320 {
 88		t.Errorf("80 four-byte characters: got %d bytes", len(got))
 89	}
 90	// Four letters, one with a combining accent: four characters show. So do
 91	// emoji with a skin tone, flags, CJK, Arabic, a Devanagari word with a spacing
 92	// vowel sign, and two letters with the spaces between them.
 93	for _, s := range []string{"Cafe\u0301", "\U0001F44D\U0001F3FD\U0001F44D\U0001F3FD", "\U0001F1F8\U0001F1F3\U0001F1F8\U0001F1F3", "水井项目", "بئر ماء", "कमला", "a  b"} {
 94		if got := checkText(s, titleMin, titleMax, false, ErrTitle); got != s {
 95			t.Errorf("checkText(%q) = %q", s, got)
 96		}
 97	}
 98	for name, s := range map[string]string{
 99		"too short":                 "abc",
100		"four spaces":               "    ",
101		"four invisible characters": "\u200b\u200b\u200b\u200b",
102		"four word joiners":         "\u2060\u2060\u2060\u2060",
103		"four soft hyphens":         "\u00ad\u00ad\u00ad\u00ad",
104		"four Hangul fillers":       "\u3164\u3164\u3164\u3164",
105		"four halfwidth fillers":    "\uffa0\uffa0\uffa0\uffa0",
106		"four grapheme joiners":     "\u034f\u034f\u034f\u034f",
107		"four Braille blanks":       "\u2800\u2800\u2800\u2800",
108		"four variation selectors":  "\ufe0f\ufe0f\ufe0f\ufe0f",
109		"one letter, three accents": "a\u0301\u0301\u0301",
110		"four null noteheads":       "\U0001D159\U0001D159\U0001D159\U0001D159",
111		"four private-use":          "\ue000\ue000\ue000\ue000",
112		"four noncharacters":        "\uffff\uffff\uffff\uffff",
113		// Spaces count only between shown characters: wrapped in characters
114		// that don't count, they would escape the trim and pass as a title.
115		"spaces in variation selectors":  "\ufe0f    \ufe0f",
116		"spaces in accents":              "\u0301    \u0301",
117		"spaces in private-use":          "\ue000    \ue000",
118		"spaces in noncharacters":        "\uffff    \uffff",
119		"no-break spaces in selectors":   "\U000E0100\u00a0\u00a0\u00a0\u00a0\U000E0100",
120		"ideographic spaces":             "\ufe0f\u3000\u3000\u3000\u3000\ufe0f",
121		"one letter and trailing spaces": "a\ufe0f   \ufe0f",
122		"leading spaces and one letter":  "\ufe0f    a",
123		"too long":                       strings.Repeat("a", 81),
124		"invalid UTF-8":                  "\xff\xfeabcd",
125		// Fits once normalized, but is more than four bytes per allowed character
126		// as sent: the byte cap applies to the argument (API §2.3).
127		"padded past the byte cap": "abcd" + strings.Repeat("\u200b", 110),
128	} {
129		if !panicsWith(func() { checkText(s, titleMin, titleMax, false, ErrTitle) }, ErrTitle) {
130			t.Errorf("%s: not refused", name)
131		}
132	}
133}
134
135func TestCheckStory(t *testing.T) {
136	// Accepted, and stored normalized; a refusal of any kind fails the test.
137	for _, c := range []struct{ in, want string }{
138		{"We need **1,500 GNOT** for the pump.\n\nSee [the quote](https://example.org/q).", "We need **1,500 GNOT** for the pump.\n\nSee [the quote](https://example.org/q)."},
139		{"Thanks <3 to everyone", "Thanks <3 to everyone"},
140		{"a < b, and x<5", "a < b, and x<5"},
141		{"", ""},
142		{" Line one\r\n\r\nline\u2028two\t ", "Line one\n\nline\ntwo"},
143	} {
144		if got := checkStory(c.in, 0, descriptionMax, ErrDescription); got != c.want {
145			t.Errorf("checkStory(%q) = %q, want %q", c.in, got, c.want)
146		}
147	}
148	for name, s := range map[string]string{
149		"an image":                    "![the pump](https://imgur.com/p.png)",
150		"an image behind zero-width":  "!\u200b[the pump](https://imgur.com/p.png)",
151		"an image behind a bidi mark": "!\u202e[the pump](https://imgur.com/p.png)",
152		"a tag":                       "<script>alert(1)</script>",
153		"an upper-case tag":           "<IMG SRC=x>",
154		"a closing tag":               "fine </p>",
155		"a comment":                   "visible <!-- hidden from the page -->",
156		"a declaration":               "<!DOCTYPE html>",
157		"a processing instruction":    "<?php echo 1 ?>",
158	} {
159		if !panicsWith(func() { checkStory(s, 0, descriptionMax, ErrDescription) }, ErrMarkup) {
160			t.Errorf("%s: not refused", name)
161		}
162	}
163	if !panicsWith(func() { checkStory(strings.Repeat("a", descriptionMax+1), 0, descriptionMax, ErrDescription) }, ErrDescription) {
164		t.Errorf("a story one character too long: not refused")
165	}
166}
167
168func TestCheckImage(t *testing.T) {
169	const v0 = "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"
170	const v1 = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
171	if cid, alt := checkImage("", ""); cid != "" || alt != "" {
172		t.Errorf("no photo: got %q, %q", cid, alt)
173	}
174	for _, cid := range []string{v0, v1} {
175		if _, alt := checkImage(cid, "  The village well, dry since March "); alt != "The village well, dry since March" {
176			t.Errorf("%s: alt = %q", cid, alt)
177		}
178	}
179	for name, c := range map[string][2]string{
180		"alt text without a photo": {"", "a well"},
181		"a photo without alt text": {v0, ""},
182		"blank alt text":           {v0, "   "},
183		"alt text of a space":      {v0, "\ufe0f \ufe0f"},
184		"alt text too long":        {v0, strings.Repeat("a", altMax+1)},
185		"v0 with a 0":              {"Qm0wAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG", "a well"},
186		"v0 too short":             {v0[:45], "a well"},
187		"v1 in upper case":         {strings.ToUpper(v1), "a well"},
188		"v1 breaking out":          {"bafy](javascript:alert(1))", "a well"},
189		"v1 too long":              {"b" + strings.Repeat("a", cidMaxBytes), "a well"},
190		"not a CID":                {"https://imgur.com/p.png", "a well"},
191	} {
192		if !panicsWith(func() { checkImage(c[0], c[1]) }, ErrImage) {
193			t.Errorf("%s: not refused", name)
194		}
195	}
196}