| 1 | package tinyclient |
| 2 | |
| 3 | import ( |
| 4 | "html/template" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | |
| 8 | "golang.org/x/net/html" |
| 9 | ) |
| 10 | |
| 11 | func TestSourceHTMLIsEscapedAndAnchored(t *testing.T) { |
| 12 | got := sourceHTML(map[string]any{ |
| 13 | "path" : "internal/main.go" , |
| 14 | "content" : "package main\nvar x = \"<script>alert(1)</script>\"\n" , |
| 15 | }) |
| 16 | html := string(got) |
| 17 | for _, want := range []string{ |
| 18 | `<source-view lang="go"><table>` , |
| 19 | `id="L1"` , |
| 20 | `href="#L2"` , |
| 21 | `<b>var</b>` , |
| 22 | `<q>"<script>alert(1)</script>"</q>` , |
| 23 | `</table></source-view>` , |
| 24 | } { |
| 25 | if !strings.Contains(html, want) { |
| 26 | t.Fatalf("source output missing %q: %s" , want, html) |
| 27 | } |
| 28 | } |
| 29 | if strings.Contains(html, "<script>alert" ) || strings.Contains(html, "class=" ) { |
| 30 | t.Fatalf("source content was not escaped or used class hooks: %s" , html) |
| 31 | } |
| 32 | if _, ok := any(got).(template.HTML); !ok { |
| 33 | t.Fatalf("sourceHTML type = %T, want template.HTML" , got) |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | func TestSourceHTMLUsesPlaintextAndBoundsPreview(t *testing.T) { |
| 38 | content := strings.Repeat("<unsafe>\n" , sourcePreviewLimit/len("<unsafe>\n" )+10) |
| 39 | got := string(sourceHTML(map[string]any{"name" : "README.unknown" , "content" : content})) |
| 40 | if !strings.Contains(got, `<source-view lang="text">` ) { |
| 41 | t.Fatalf("unknown extension should use plaintext: %s" , got[:min(len(got), 200)]) |
| 42 | } |
| 43 | if !strings.Contains(got, `<tfoot><tr><td colspan="2">Preview truncated` ) { |
| 44 | t.Fatal("bounded preview should identify truncation" ) |
| 45 | } |
| 46 | if strings.Contains(got, "<unsafe>" ) { |
| 47 | t.Fatal("plaintext source was not escaped" ) |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | func TestDiffHTMLClassifiesLinesWithoutHTMLInjection(t *testing.T) { |
| 52 | got := string(diffHTML(map[string]any{"diff" : "@@ -1 +1 @@\n-old\n+<script>bad</script>\n context" })) |
| 53 | for _, want := range []string{`<diff-view><table>` , `<code><b>@@ -1 +1 @@</b></code>` , `<code><del>-old</del></code>` , `<code><ins>+<script>bad</script></ins></code>` , `<code> context</code>` , `</table></diff-view>` } { |
| 54 | if !strings.Contains(got, want) { |
| 55 | t.Fatalf("diff output missing %q: %s" , want, got) |
| 56 | } |
| 57 | } |
| 58 | if strings.Contains(got, "<script>bad" ) || strings.Contains(got, "class=" ) { |
| 59 | t.Fatal("diff content was not escaped or used class hooks" ) |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | func TestPreviewBoundsLineMarkupAndDisclosesOmission(t *testing.T) { |
| 64 | for name, render := range map[string]func(any) template.HTML{"source" : sourceHTML, "diff" : diffHTML} { |
| 65 | t.Run(name, func(t *testing.T) { |
| 66 | content := strings.Repeat("\n" , sourcePreviewLimit) |
| 67 | got := string(render(map[string]any{"content" : content, "diff" : content})) |
| 68 | if len(got) > 2*1024*1024 { |
| 69 | t.Fatalf("a short-line preview expanded to %d bytes" , len(got)) |
| 70 | } |
| 71 | if !strings.Contains(got, "<tfoot>" ) || !strings.Contains(got, "10,000 lines" ) { |
| 72 | t.Fatal("line preview omission was not disclosed" ) |
| 73 | } |
| 74 | }) |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | func TestDiffDisclosesBackendTruncation(t *testing.T) { |
| 79 | got := string(diffHTML(map[string]any{"diff" : "already bounded" , "truncated" : true})) |
| 80 | if !strings.Contains(got, "<tfoot>" ) || !strings.Contains(got, "truncated" ) { |
| 81 | t.Fatal("backend truncation was hidden" ) |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | func TestSourceDoesNotInventAnEmptyLineAtEndOfFile(t *testing.T) { |
| 86 | got := string(sourceHTML(map[string]any{"content" : "first\nsecond\n" })) |
| 87 | if strings.Contains(got, `id="L3"` ) || !strings.Contains(got, `id="L2"` ) { |
| 88 | t.Fatal("terminal newline added a phantom source row" ) |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | func TestSourceHTMLPreservesRenderedSourceText(t *testing.T) { |
| 93 | want := "/* block <tag> */\nfunc π(😀 string) {\n\treturn \"<script>\"\n}\n" |
| 94 | root, err := html.Parse(strings.NewReader(string(sourceHTML(map[string]any{ |
| 95 | "path" : "unicode.go" , |
| 96 | "content" : want, |
| 97 | })))) |
| 98 | if err != nil { |
| 99 | t.Fatal(err) |
| 100 | } |
| 101 | var got strings.Builder |
| 102 | var visit func(*html.Node) |
| 103 | visit = func(node *html.Node) { |
| 104 | if node.Type == html.ElementNode && node.Data == "td" && isCodeCell(node) { |
| 105 | appendText(&got, node) |
| 106 | got.WriteByte('\n' ) |
| 107 | return |
| 108 | } |
| 109 | for child := node.FirstChild; child != nil; child = child.NextSibling { |
| 110 | visit(child) |
| 111 | } |
| 112 | } |
| 113 | visit(root) |
| 114 | if got.String() != want { |
| 115 | t.Fatalf("rendered source changed content:\nwant %q\ngot %q" , want, got.String()) |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | // isCodeCell reports whether a table cell holds rendered source rather than |
| 120 | // a line number anchor. |
| 121 | func isCodeCell(node *html.Node) bool { |
| 122 | first := node.FirstChild |
| 123 | return first != nil && first.Type == html.ElementNode && first.Data == "code" |
| 124 | } |
| 125 | |
| 126 | func appendText(out *strings.Builder, node *html.Node) { |
| 127 | if node.Type == html.TextNode { |
| 128 | out.WriteString(node.Data) |
| 129 | return |
| 130 | } |
| 131 | for child := node.FirstChild; child != nil; child = child.NextSibling { |
| 132 | appendText(out, child) |
| 133 | } |
| 134 | } |