clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / internal / wiki / wiki_test.go
wiki_test.go 7.2K
1package wiki23import (4 "fmt"5 "strings"6 "testing"7)89func 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}3536func 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}5253func 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}8384func 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}113114func 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}144145func 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}