main
rb 114 lines 3.65 KB
Raw
1 # frozen_string_literal: true
2
3 require "rails_helper"
4
5 RSpec.describe MarkdownRenderer do
6 def render(md, context: nil)
7 described_class.render(md, context: context)
8 end
9
10 describe "sanitization (the security boundary)" do
11 it "strips <script> tags and their contents" do
12 html = render("Hello\n\n<script>alert('xss')</script>")
13 expect(html).not_to include("<script")
14 expect(html).not_to include("alert('xss')")
15 end
16
17 it "strips <iframe> elements" do
18 expect(render("<iframe src='https://evil.example'></iframe>")).not_to include("<iframe")
19 end
20
21 it "strips inline event handlers" do
22 html = render("<img src='x' onerror='alert(1)'>")
23 expect(html).not_to include("onerror")
24 expect(html).not_to include("alert(1)")
25 end
26
27 it "strips javascript: scheme links" do
28 expect(render("[click](javascript:alert(1))")).not_to include("javascript:")
29 end
30
31 it "keeps safe http(s) links" do
32 expect(render("[siGit](https://sigit.si)")).to include('href="https://sigit.si"')
33 end
34 end
35
36 describe "GitHub-Flavored Markdown" do
37 it "renders tables" do
38 html = render("| a | b |\n|---|---|\n| 1 | 2 |")
39 expect(html).to include("<table").and include("<th").and include("<td")
40 end
41
42 it "renders task lists as disabled checkboxes" do
43 html = render("- [ ] todo\n- [x] done")
44 expect(html.scan(/<input[^>]*type="checkbox"/).length).to eq(2)
45 expect(html).to include("checked")
46 expect(html).to include("task-list-item")
47 end
48
49 it "gives headings anchor ids and links" do
50 html = render("# Hello World")
51 expect(html).to include('id="hello-world"').and include('href="#hello-world"')
52 end
53
54 it "makes duplicate heading ids unique" do
55 html = render("# Title\n\n# Title")
56 expect(html).to include('id="title"').and include('id="title-1"')
57 end
58
59 it "syntax-highlights fenced code" do
60 html = render("```ruby\nputs 1\n```")
61 expect(html).to include('class="highlight code-block"').and include("<span")
62 end
63
64 it "autolinks bare URLs" do
65 expect(render("see https://sigit.si for more")).to include('href="https://sigit.si"')
66 end
67 end
68
69 describe "relative link/image resolution" do
70 let(:ctx) { MarkdownRenderer::Context.new(username: "sigit", repository: "demo", ref: "main", dir: dir) }
71 let(:dir) { "" }
72
73 it "resolves a relative link to the blob view" do
74 expect(render("[docs](docs/guide.md)", context: ctx))
75 .to include('href="/sigit/demo/blob/main/docs/guide.md"')
76 end
77
78 it "resolves a relative image to the raw endpoint" do
79 expect(render("![logo](assets/logo.png)", context: ctx))
80 .to include('src="/sigit/demo/raw/main/assets/logo.png"')
81 end
82
83 context "from a nested directory" do
84 let(:dir) { "docs" }
85
86 it "resolves against the current directory" do
87 expect(render("[sibling](other.md)", context: ctx))
88 .to include('href="/sigit/demo/blob/main/docs/other.md"')
89 end
90 end
91
92 context "with parent traversal" do
93 let(:dir) { "docs/api" }
94
95 it "collapses .. against the current directory" do
96 expect(render("[up](../intro.md)", context: ctx))
97 .to include('href="/sigit/demo/blob/main/docs/intro.md"')
98 end
99 end
100
101 it "leaves absolute URLs untouched" do
102 expect(render("[x](https://example.com/a)", context: ctx)).to include('href="https://example.com/a"')
103 end
104
105 it "leaves in-page anchors untouched" do
106 expect(render("[top](#intro)", context: ctx)).to include('href="#intro"')
107 end
108 end
109
110 it "returns an empty string for blank input" do
111 expect(render("")).to eq("")
112 expect(render(nil)).to eq("")
113 end
114 end