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": "",
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}