krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v2.7.1: HutchTests/ReadmeViewTests.swift · raw
1import Foundation
2import Testing
3@testable import Hutch
4
5struct ReadmeViewTests {
6
7 @Test
8 func sanitizedReadmeLinkURLStringRejectsUnexpectedSchemes() {
9 #expect(sanitizedReadmeLinkURLString("javascript:alert(1)") == nil)
10 #expect(sanitizedReadmeLinkURLString("file:///tmp/readme") == nil)
11 #expect(sanitizedReadmeLinkURLString("data:text/html;base64,SGVsbG8=") == nil)
12 }
13
14 @Test
15 func processInlineDropsUnsafeMarkdownLinks() {
16 let rendered = processInline("[click me](javascript:alert)")
17
18 #expect(rendered == "click me")
19 #expect(!rendered.contains("href="))
20 #expect(!rendered.contains("javascript:"))
21 }
22
23 @Test
24 func sanitizedReadmeLinkURLStringAllowsExpectedDestinations() {
25 #expect(sanitizedReadmeLinkURLString("https://example.com/docs?q=1") == "https://example.com/docs?q=1")
26 #expect(sanitizedReadmeLinkURLString("mailto:test@example.com") == "mailto:test@example.com")
27 #expect(sanitizedReadmeLinkURLString("#readme") == "#readme")
28 }
29}
30
31struct MarkdownRenderingTests {
32
33 @Test
34 func markdownOrderedList() {
35 let html = markdownToHTML("1. First\n2. Second")
36
37 #expect(html.contains("<ol>"))
38 #expect(html.contains("<li>"))
39 }
40
41 @Test
42 func markdownBlockquote() {
43 let html = markdownToHTML("> This is a quote")
44
45 #expect(html.contains("<blockquote>"))
46 }
47
48 @Test
49 func markdownTable() {
50 let input = "| A | B |\n|---|---|\n| 1 | 2 |"
51 let html = markdownToHTML(input)
52
53 #expect(html.contains("<table>"))
54 #expect(html.contains("<th>"))
55 }
56
57 @Test
58 func markdownTableAlignment() {
59 let input = "| Left | Center | Right |\n|:-----|:------:|------:|\n| a | b | c |"
60 let html = markdownToHTML(input)
61
62 #expect(html.contains("text-align: left;"))
63 #expect(html.contains("text-align: center;"))
64 #expect(html.contains("text-align: right;"))
65 }
66
67 @Test
68 func markdownStrikethrough() {
69 let html = markdownToHTML("~~deleted~~")
70
71 #expect(html.contains("<del>"))
72 }
73
74 @Test
75 func markdownDeepHeadings() {
76 let html = markdownToHTML("#### Level 4")
77
78 #expect(html.contains("<h4>"))
79 }
80
81 @Test
82 func markdownHardWrapNormalization() {
83 let html = markdownToHTML("line one\nline two")
84
85 #expect(!html.contains("line one\nline two"))
86 #expect(html.contains("line one"))
87 #expect(html.contains("line two"))
88 }
89
90 @Test
91 func markdownSoftBreakIsSpace() {
92 let html = markdownToHTML("word one\nword two")
93
94 #expect(html.contains("word one word two") || (html.contains("word one") && html.contains("word two")))
95 #expect(!html.contains("<br>"))
96 }
97
98 @Test
99 func markdownUnsafeLinkDropped() {
100 let html = markdownToHTML("[click](javascript:alert(1))")
101
102 #expect(!html.contains("href="))
103 #expect(!html.contains("javascript:"))
104 }
105
106 @Test
107 func markdownImageRenders() {
108 let html = markdownToHTML("")
109
110 #expect(html.contains("<img src=\"https://example.com/logo.png\" alt=\"logo\">"))
111 #expect(!html.contains(#"\"#))
112 }
113
114 @Test
115 func markdownLinkedImageRendersAnchor() {
116 let html = markdownToHTML("[](https://example.com/build)")
117
118 #expect(html.contains("<a href=\"https://example.com/build\">"))
119 #expect(html.contains("<img src=\"https://example.com/badge.png\" alt=\"badge\">"))
120 #expect(!html.contains(#"\"#))
121 }
122
123 @Test
124 func markdownInlineCodeEscaping() {
125 let html = processInline("`<b>`")
126
127 #expect(html.contains("<code>"))
128 #expect(html.contains("<b>"))
129 #expect(!html.contains("<b>"))
130 }
131
132 @Test
133 func markdownPlainEmailAutolinks() {
134 let html = processInline("Contact hello@cleberg.net")
135
136 #expect(html.contains(#"href="mailto:hello@cleberg.net""#))
137 #expect(html.contains(">hello@cleberg.net</a>"))
138 }
139
140 @Test
141 func markdownImageQueryStringPreservesAmpersands() {
142 let html = processInline("")
143
144 #expect(html.contains("metric=security_rating"))
145 #expect(!html.contains("amp;metric"))
146 #expect(html.contains("<img"))
147 }
148
149 @Test
150 func markdownLinkedImagesWithQueryStringsRenderAllImages() {
151 let html = markdownToHTML("""
152 [](https://builds.sr.ht/~ccleberg/Hutch?)
153 [](https://sonarcloud.io/summary/new_code?id=ccleberg_Hutch)
154 [](https://sonarcloud.io/summary/new_code?id=ccleberg_Hutch)
155 """)
156
157 #expect(html.contains("Hutch.svg"))
158 #expect(html.contains("metric=security_rating"))
159 #expect(html.contains("metric=reliability_rating"))
160 #expect(!html.contains("amp;amp;"))
161 }
162
163 @Test
164 func markdownAllowsSafeInlineHTML() {
165 let html = processInline(#"<strong>Bold</strong> <a href="https://example.com">Link</a>"#)
166
167 #expect(html.contains("<strong>Bold</strong>"))
168 #expect(html.contains(#"<a href="https://example.com">Link</a>"#))
169 }
170
171 @Test
172 func markdownProtectedTokensDoNotLeak() {
173 let html = processInline(#"<a href="https://example.com">Back to top</a> `yoshi [ARG] <FILE>`"#)
174
175 #expect(!html.contains("ZZPROTECTED"))
176 #expect(html.contains(#"<a href="https://example.com">Back to top</a>"#))
177 #expect(html.contains("<code>yoshi [ARG] <FILE></code>"))
178 }
179}
180
181struct OrgRenderingTests {
182
183 @Test
184 func orgDeepHeading() {
185 let html = orgToHTML("**** Level 4 Heading")
186
187 #expect(html.contains("<h4>"))
188 }
189
190 @Test
191 func orgCommentLinesIgnored() {
192 let html = orgToHTML("# This is a comment\nNormal text")
193
194 #expect(!html.contains("This is a comment"))
195 #expect(html.contains("Normal text"))
196 }
197
198 @Test
199 func orgStrikethrough() {
200 let html = orgToHTML("+deleted text+")
201
202 #expect(html.contains("<del>"))
203 }
204
205 @Test
206 func orgExampleBlock() {
207 let html = orgToHTML("#+begin_example\nhello world\n#+end_example")
208
209 #expect(html.contains("<pre><code>"))
210 #expect(html.contains("hello world"))
211 }
212
213 @Test
214 func orgTitleKeyword() {
215 let html = orgToHTML("#+TITLE: My Document\nBody text")
216
217 #expect(html.contains("org-title"))
218 #expect(html.contains("My Document"))
219 }
220
221 @Test
222 func orgItalicDoesNotMatchURLPaths() {
223 let html = orgToHTML("[[https://example.com/path/to/file]]")
224
225 #expect(!html.contains("<em>"))
226 }
227
228 @Test
229 func orgWrappedBulletNormalizesLines() {
230 let html = orgToHTML("- First line\n continues here")
231
232 #expect(html.contains("<li>First line continues here</li>"))
233 }
234
235 @Test
236 func orgHeaderKeywordsIgnored() {
237 let html = orgToHTML("""
238 #+OPTIONS: toc:nil
239 #+PROPERTY: header-args :results output
240 Body text
241 """)
242
243 #expect(!html.contains("#+OPTIONS"))
244 #expect(!html.contains("#+PROPERTY"))
245 #expect(html.contains("<p>Body text</p>"))
246 }
247
248 @Test
249 func orgVerseBlock() {
250 let html = orgToHTML("""
251 #+begin_verse
252 There is a line.
253 And an indented line.
254 #+end_verse
255 """)
256
257 #expect(html.contains(#"<blockquote class="org-verse">"#))
258 #expect(html.contains("There is a line."))
259 #expect(html.contains("And an indented line."))
260 #expect(!html.contains("#+begin_verse"))
261 }
262
263 @Test
264 func orgNamedBlockRendersCaption() {
265 let html = orgToHTML("""
266 #+CAPTION: Build output
267 #+NAME: build-log
268 #+begin_example
269 hello world
270 #+end_example
271 """)
272
273 #expect(html.contains(#"<figure class="org-block" id="build-log">"#))
274 #expect(html.contains("<figcaption>Build output</figcaption>"))
275 #expect(!html.contains("#+CAPTION"))
276 #expect(!html.contains("#+NAME"))
277 }
278
279 @Test
280 func orgLinkedImageRenders() {
281 let html = orgToHTML("[[https://example.com][[https://img.cleberg.net/apps/hutch/screenshots/ipad/01_patch.jpg]]]")
282
283 #expect(html.contains(#"<a href="https://example.com">"#))
284 #expect(html.contains(#"<img src="https://img.cleberg.net/apps/hutch/screenshots/ipad/01_patch.jpg" alt="">"#))
285 }
286
287 @Test
288 func orgLinkedRelativeImageRenders() {
289 let html = orgToHTML(
290 "[[https://example.com/docs][[./images/badge.svg]]]",
291 imageURLResolver: { source in
292 source == "./images/badge.svg" ? "https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/images/badge.svg" : nil
293 }
294 )
295
296 #expect(html.contains(#"<a href="https://example.com/docs">"#))
297 #expect(html.contains(#"<img src="https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/images/badge.svg" alt="">"#))
298 }
299
300 @Test
301 func orgNestedBulletListRendersNestedMarkup() {
302 let html = orgToHTML("""
303 * Lists
304 ** Unordered
305 - First bullet
306 - Second bullet
307 - Third bullet with /italic/ and *bold*
308 - Bullet with wrapped
309 continuation line
310 - Bullet with nested list
311 - Nested child one
312 - Nested child two
313 - Bullet with inline code ~let x = 1~
314 """)
315
316 #expect(html.contains("<ul>"))
317 #expect(html.contains("<li>Bullet with nested list\n<ul>"))
318 #expect(html.contains("<li>Nested child one</li>"))
319 #expect(html.contains("<li>Nested child two</li>"))
320 #expect(html.contains("<li>Bullet with wrapped continuation line</li>"))
321 }
322
323 @Test
324 func orgTableAlignmentRendersStyles() {
325 let html = orgToHTML("""
326 | Left | Center | Right |
327 |:-----+:-----:+------:|
328 | a | b | c |
329 | 1 | 2 | 3 |
330 """)
331
332 #expect(html.contains("text-align: left;"))
333 #expect(html.contains("text-align: center;"))
334 #expect(html.contains("text-align: right;"))
335 }
336}
337
338struct RepositoryAssetURLTests {
339
340 @Test
341 func repositoryAssetURLPercentEncodesImagePaths() {
342 let url = resolveRepositoryAssetURL(
343 "images/My Logo.png",
344 owner: "~ccleberg",
345 repositoryName: "Hutch",
346 readmePath: "README.md"
347 )
348
349 #expect(url == "https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/images/My%20Logo.png")
350 }
351}