tinyrelayfile
tinyrelay/internal/wiki/wiki_test.go/

internal/wiki/wiki_test.go raw
1package wiki
2
3import (
4 "fmt"
5 "strings"
6 "testing"
7)
8
9func TestNormalizeFollowsNIP54Examples(t *testing.T) {
10 cases := map[string]string{
11 "Wiki Article": "wiki-article",
12 "What's Up?": "whats-up",
13 " Hello World ": "hello-world",
14 "Article 1": "article-1",
15 "ウィキペディア": "ウィキペディア",
16 "Ñoño": "ñoño",
17 "Москва": "москва",
18 "日本語 Article": "日本語-article",
19 "proof-of-work": "proof-of-work",
20 "--Lightning--": "lightning",
21 "a -- b\tc": "a-b-c",
22 "C++ & C#": "c-c",
23 "e\u0301lan": "e\u0301lan",
24 "": "",
25 "!!!": "",
26 "snake_case_name": "snake-case-name",
27 "Tabs\tand\nbreaks": "tabs-and-breaks",
28 }
29 for input, want := range cases {
30 if got := Normalize(input); got != want {
31 t.Errorf("Normalize(%q) = %q, want %q", input, got, want)
32 }
33 }
34}
35
36func TestLinksFindWikilinksReferencesAndNostrTargets(t *testing.T) {
37 content := "Bitcoin is a [cryptocurrency][] invented by [Satoshi Nakamoto][].\n\n" +
38 "See also: [proof of work][] and [lightning network][Lightning Network], [[Mining|the miners]] and _[Биткойн][]_.\n\n" +
39 "- [Bob](nostr:npub1bob) wrote `[[not a link]]`\n" +
40 "- [web](https://example.com/[[x]])\n\n" +
41 "```\n[[ignored in code]]\n```\n\n" +
42 "[Satoshi Nakamoto]: nostr:npub1satoshi\n"
43 got := Links(content)
44 want := []string{"nostr:npub1satoshi", "cryptocurrency", "proof-of-work", "lightning-network", "mining", "биткойн", "nostr:npub1bob"}
45 if fmt.Sprint(got) != fmt.Sprint(want) {
46 t.Fatalf("Links = %v, want %v", got, want)
47 }
48 if links := Links("plain text"); len(links) != 0 {
49 t.Fatalf("plain text links = %v", links)
50 }
51}
52
53func TestRenderHTMLSupportedSyntax(t *testing.T) {
54 content := "# Title with _emphasis_\n\n" +
55 "A paragraph with *strong*, `code <b>` and a [link](https://example.com/a?b=1&c=2).\n" +
56 "Second line of the paragraph.\n\n" +
57 "## Lists\n\n" +
58 "- one [[Wiki Page]]\n- two [[Другая|shown]]\n\n" +
59 "1. first\n2) second\n\n" +
60 "``` go\nfunc main() {}\n<script>alert(1)</script>\n```\n\n" +
61 "[Bob](nostr:npub1bob) and [reference][] and [defined][site].\n\n" +
62 "[site]: https://example.org/\n"
63 got := string(RenderHTML(content))
64 for _, want := range []string{
65 "<h1>Title with <em>emphasis</em></h1>",
66 "<p>A paragraph with <strong>strong</strong>, <code>code &lt;b&gt;</code> and a <a href=\"https://example.com/a?b=1&amp;c=2\">link</a>.\nSecond line of the paragraph.</p>",
67 "<h2>Lists</h2>",
68 "<ul>\n<li>one <a href=\"/wiki/wiki-page\">Wiki Page</a></li>\n<li>two <a href=\"/wiki/%D0%B4%D1%80%D1%83%D0%B3%D0%B0%D1%8F\">shown</a></li>\n</ul>",
69 "<ol>\n<li>first</li>\n<li>second</li>\n</ol>",
70 "<pre><code data-lang=\"go\">func main() {}\n&lt;script&gt;alert(1)&lt;/script&gt;\n</code></pre>",
71 "<a href=\"/open?target=nostr%3Anpub1bob\">Bob</a>",
72 "<a href=\"/wiki/reference\">reference</a>",
73 "<a href=\"https://example.org/\">defined</a>",
74 } {
75 if !strings.Contains(got, want) {
76 t.Errorf("missing %q in\n%s", want, got)
77 }
78 }
79 if strings.Contains(got, "[site]") {
80 t.Errorf("definition line rendered:\n%s", got)
81 }
82}
83
84func TestRenderHTMLEscapesEverythingElse(t *testing.T) {
85 cases := []struct{ content, forbidden, required string }{
86 {"<script>alert(1)</script>", "<script", "&lt;script&gt;"},
87 {"[click](javascript:alert(1))", "javascript:", "click"},
88 {"[click](JavaScript:alert(1))", "avaScript:", "click"},
89 {"[click](data:text/html;base64,AAAA)", "data:", "click"},
90 {"[click](vbscript:msgbox)", "vbscript:", "click"},
91 {"[click](//evil.example/x)", "evil.example", "click"},
92 {"[click](/\\evil.example)", "href", "click"},
93 {"[[Page]] [x](\"><img src=x onerror=alert(1)>)", "<img", "x&gt;"},
94 {"[ref][]\n\n[ref]: javascript:alert(1)", "javascript:", "<p>ref</p>"},
95 {"<a href=\"https://x\">raw html</a>", "<a href", "&lt;a href"},
96 {"<img src=x onerror=alert(1)>", "<img", "&lt;img"},
97 {"_<b>bold</b>_", "<b>", "<em>&lt;b&gt;bold&lt;/b&gt;</em>"},
98 {"[[<script>]]", "<script", "/wiki/script"},
99 {"[[x|<script>]]", "<script>", "&lt;script&gt;"},
100 {"`<script>`", "<script>", "<code>&lt;script&gt;</code>"},
101 {"[text](https://example.com/\"onmouseover=\"alert(1))", "onmouseover=\"", "&#34;onmouseover=&#34;"},
102 }
103 for _, tc := range cases {
104 got := string(RenderHTML(tc.content))
105 if strings.Contains(got, tc.forbidden) {
106 t.Errorf("%q rendered %q:\n%s", tc.content, tc.forbidden, got)
107 }
108 if !strings.Contains(got, tc.required) {
109 t.Errorf("%q lacks %q:\n%s", tc.content, tc.required, got)
110 }
111 }
112}
113
114func TestRenderHTMLHandlesEdgeCases(t *testing.T) {
115 cases := map[string]string{
116 "": "",
117 "\n\n\n": "",
118 "```": "<pre><code>\n</code></pre>\n",
119 "```\nunterminated": "<pre><code>unterminated\n</code></pre>\n",
120 "#NoSpace": "<p>#NoSpace</p>\n",
121 "####### seven": "<p>####### seven</p>\n",
122 "* not strong": "<ul>\n<li>not strong</li>\n</ul>\n",
123 "a * b * c": "<p>a * b * c</p>\n",
124 "a_b_c": "<p>a<em>b</em>c</p>\n",
125 "\\*escaped\\*": "<p>*escaped*</p>\n",
126 "``code with ` inside``": "<p><code>code with ` inside</code></p>\n",
127 "[[]]": "<p>[[]]</p>\n",
128 "[unclosed": "<p>[unclosed</p>\n",
129 "[text](": "<p>[text](</p>\n",
130 "[mailto](mailto:a@b.example)": "<p><a href=\"mailto:a@b.example\">mailto</a></p>\n",
131 "[rel](/files)": "<p><a href=\"/files\">rel</a></p>\n",
132 "[bad](http://)": "<p>bad</p>\n",
133 "- item\nplain": "<ul>\n<li>item</li>\n</ul>\n<p>plain</p>\n",
134 "para\n- not a list": "<p>para\n- not a list</p>\n",
135 "Ünïcödé & <ampersand>": "<p>Ünïcödé &amp; &lt;ampersand&gt;</p>\n",
136 "line\r\nbreak": "<p>line\nbreak</p>\n",
137 }
138 for content, want := range cases {
139 if got := string(RenderHTML(content)); got != want {
140 t.Errorf("RenderHTML(%q) =\n%s\nwant\n%s", content, got, want)
141 }
142 }
143}
144
145func TestRenderHTMLPipeTables(t *testing.T) {
146 got := string(RenderHTML("Before.\n| Proven | Activity | Evidence |\n| --- | :---: | ---: |\n| yes | Discovery, *auth* | NIP-98 succeeded. |\n| no | Wiki fork \\| merge | remains |\nAfter."))
147 for _, want := range []string{
148 "<p>Before.</p>\n<table>\n<thead>\n<tr><th>Proven</th><th>Activity</th><th>Evidence</th></tr>\n</thead>\n<tbody>\n",
149 "<tr><td>yes</td><td>Discovery, <strong>auth</strong></td><td>NIP-98 succeeded.</td></tr>\n",
150 "<tr><td>no</td><td>Wiki fork | merge</td><td>remains</td></tr>\n</tbody>\n</table>\n<p>After.</p>\n",
151 } {
152 if !strings.Contains(got, want) {
153 t.Errorf("missing %q in %s", want, got)
154 }
155 }
156 // A table without a separator is all body rows; a pipe inside a paragraph is text.
157 got = string(RenderHTML("| a | b |\n| c | d |\n\ntext | with a pipe"))
158 if !strings.Contains(got, "<table>\n<tbody>\n<tr><td>a</td><td>b</td></tr>\n<tr><td>c</td><td>d</td></tr>\n</tbody>\n</table>") || !strings.Contains(got, "<p>text | with a pipe</p>") {
159 t.Fatalf("headerless table: %s", got)
160 }
161 if strings.Contains(string(RenderHTML("| <script>x</script> |\n| --- |")), "<script>") {
162 t.Fatal("table cells were not escaped")
163 }
164}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs