krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.7.0: 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 markdownRelativeLinkWithoutResolverDropped() {
108 let html = markdownToHTML("[LICENSE](LICENSE)")
109
110 #expect(!html.contains("href="))
111 #expect(html.contains("LICENSE"))
112 }
113
114 @Test
115 func markdownRelativeLinkWithResolverRendersAnchor() {
116 let html = markdownToHTML(
117 "[LICENSE](LICENSE)",
118 linkURLResolver: { source in
119 source == "LICENSE" ? "https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/LICENSE" : nil
120 }
121 )
122
123 #expect(html.contains(#"href="https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/LICENSE""#))
124 #expect(html.contains(">LICENSE</a>"))
125 }
126
127 @Test
128 func markdownFragmentLinkWithResolverPreservesFragment() {
129 let html = markdownToHTML(
130 "[section](#install)",
131 linkURLResolver: { source in
132 resolveRepositoryLinkURL(
133 source,
134 owner: "~ccleberg",
135 repositoryName: "Hutch",
136 readmePath: "README.md"
137 )
138 }
139 )
140
141 #expect(html.contains("href=\"#install\""))
142 }
143
144 @Test
145 func markdownImageRenders() {
146 let html = markdownToHTML("")
147
148 #expect(html.contains("<img src=\"https://example.com/logo.png\" alt=\"logo\">"))
149 #expect(!html.contains(#"\"#))
150 }
151
152 @Test
153 func markdownLinkedImageRendersAnchor() {
154 let html = markdownToHTML("[](https://example.com/build)")
155
156 #expect(html.contains("<a href=\"https://example.com/build\">"))
157 #expect(html.contains("<img src=\"https://example.com/badge.png\" alt=\"badge\">"))
158 #expect(!html.contains(#"\"#))
159 }
160
161 @Test
162 func markdownInlineCodeEscaping() {
163 let html = processInline("`<b>`")
164
165 #expect(html.contains("<code>"))
166 #expect(html.contains("<b>"))
167 #expect(!html.contains("<b>"))
168 }
169
170 @Test
171 func markdownPlainEmailAutolinks() {
172 let html = processInline("Contact hello@cleberg.net")
173
174 #expect(html.contains(#"href="mailto:hello@cleberg.net""#))
175 #expect(html.contains(">hello@cleberg.net</a>"))
176 }
177
178 @Test
179 func markdownImageQueryStringPreservesAmpersands() {
180 let html = processInline("")
181
182 // `&` is the correct encoding for `&` in an attribute value, so the
183 // failure mode to guard against is double-escaping, which would make the
184 // browser request a literal "&" in the query string.
185 #expect(html.contains("metric=security_rating"))
186 #expect(!html.contains("&amp;"))
187 #expect(html.contains("<img"))
188 }
189
190 @Test
191 func markdownLinkedImagesWithQueryStringsRenderAllImages() {
192 let html = markdownToHTML("""
193 [](https://builds.sr.ht/~ccleberg/Hutch?)
194 [](https://sonarcloud.io/summary/new_code?id=ccleberg_Hutch)
195 [](https://sonarcloud.io/summary/new_code?id=ccleberg_Hutch)
196 """)
197
198 #expect(html.contains("Hutch.svg"))
199 #expect(html.contains("metric=security_rating"))
200 #expect(html.contains("metric=reliability_rating"))
201 #expect(!html.contains("amp;amp;"))
202 }
203
204 @Test
205 func markdownAllowsSafeInlineHTML() {
206 let html = processInline(#"<strong>Bold</strong> <a href="https://example.com">Link</a>"#)
207
208 #expect(html.contains("<strong>Bold</strong>"))
209 #expect(html.contains(#"<a href="https://example.com">Link</a>"#))
210 }
211
212 @Test
213 func markdownProtectedTokensDoNotLeak() {
214 let html = processInline(#"<a href="https://example.com">Back to top</a> `yoshi [ARG] <FILE>`"#)
215
216 #expect(!html.contains("ZZPROTECTED"))
217 #expect(html.contains(#"<a href="https://example.com">Back to top</a>"#))
218 #expect(html.contains("<code>yoshi [ARG] <FILE></code>"))
219 }
220}
221
222struct OrgRenderingTests {
223
224 @Test
225 func orgDeepHeading() {
226 let html = orgToHTML("**** Level 4 Heading")
227
228 #expect(html.contains("<h4>"))
229 }
230
231 @Test
232 func orgCommentLinesIgnored() {
233 let html = orgToHTML("# This is a comment\nNormal text")
234
235 #expect(!html.contains("This is a comment"))
236 #expect(html.contains("Normal text"))
237 }
238
239 @Test
240 func orgStrikethrough() {
241 let html = orgToHTML("+deleted text+")
242
243 #expect(html.contains("<del>"))
244 }
245
246 @Test
247 func orgExampleBlock() {
248 let html = orgToHTML("#+begin_example\nhello world\n#+end_example")
249
250 #expect(html.contains("<pre><code>"))
251 #expect(html.contains("hello world"))
252 }
253
254 @Test
255 func orgTitleKeyword() {
256 let html = orgToHTML("#+TITLE: My Document\nBody text")
257
258 #expect(html.contains("org-title"))
259 #expect(html.contains("My Document"))
260 }
261
262 @Test
263 func orgItalicDoesNotMatchURLPaths() {
264 let html = orgToHTML("[[https://example.com/path/to/file]]")
265
266 #expect(!html.contains("<em>"))
267 }
268
269 @Test
270 func orgWrappedBulletNormalizesLines() {
271 let html = orgToHTML("- First line\n continues here")
272
273 #expect(html.contains("<li>First line continues here</li>"))
274 }
275
276 @Test
277 func orgHeaderKeywordsIgnored() {
278 let html = orgToHTML("""
279 #+OPTIONS: toc:nil
280 #+PROPERTY: header-args :results output
281 Body text
282 """)
283
284 #expect(!html.contains("#+OPTIONS"))
285 #expect(!html.contains("#+PROPERTY"))
286 #expect(html.contains("<p>Body text</p>"))
287 }
288
289 @Test
290 func orgVerseBlock() {
291 let html = orgToHTML("""
292 #+begin_verse
293 There is a line.
294 And an indented line.
295 #+end_verse
296 """)
297
298 #expect(html.contains(#"<blockquote class="org-verse">"#))
299 #expect(html.contains("There is a line."))
300 #expect(html.contains("And an indented line."))
301 #expect(!html.contains("#+begin_verse"))
302 }
303
304 @Test
305 func orgNamedBlockRendersCaption() {
306 let html = orgToHTML("""
307 #+CAPTION: Build output
308 #+NAME: build-log
309 #+begin_example
310 hello world
311 #+end_example
312 """)
313
314 #expect(html.contains(#"<figure class="org-block" id="build-log">"#))
315 #expect(html.contains("<figcaption>Build output</figcaption>"))
316 #expect(!html.contains("#+CAPTION"))
317 #expect(!html.contains("#+NAME"))
318 }
319
320 @Test
321 func orgLinkedImageRenders() {
322 let html = orgToHTML("[[https://example.com][[https://img.cleberg.net/apps/hutch/screenshots/ipad/01_patch.jpg]]]")
323
324 #expect(html.contains(#"<a href="https://example.com">"#))
325 #expect(html.contains(#"<img src="https://img.cleberg.net/apps/hutch/screenshots/ipad/01_patch.jpg" alt="">"#))
326 }
327
328 @Test
329 func orgLinkedRelativeImageRenders() {
330 let html = orgToHTML(
331 "[[https://example.com/docs][[./images/badge.svg]]]",
332 imageURLResolver: { source in
333 source == "./images/badge.svg" ? "https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/images/badge.svg" : nil
334 }
335 )
336
337 #expect(html.contains(#"<a href="https://example.com/docs">"#))
338 #expect(html.contains(#"<img src="https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/images/badge.svg" alt="">"#))
339 }
340
341 @Test
342 func orgNestedBulletListRendersNestedMarkup() {
343 let html = orgToHTML("""
344 * Lists
345 ** Unordered
346 - First bullet
347 - Second bullet
348 - Third bullet with /italic/ and *bold*
349 - Bullet with wrapped
350 continuation line
351 - Bullet with nested list
352 - Nested child one
353 - Nested child two
354 - Bullet with inline code ~let x = 1~
355 """)
356
357 #expect(html.contains("<ul>"))
358 #expect(html.contains("<li>Bullet with nested list\n<ul>"))
359 #expect(html.contains("<li>Nested child one</li>"))
360 #expect(html.contains("<li>Nested child two</li>"))
361 #expect(html.contains("<li>Bullet with wrapped continuation line</li>"))
362 }
363
364 @Test
365 func orgTableAlignmentRendersStyles() {
366 let html = orgToHTML("""
367 | Left | Center | Right |
368 |:-----+:-----:+------:|
369 | a | b | c |
370 | 1 | 2 | 3 |
371 """)
372
373 #expect(html.contains("text-align: left;"))
374 #expect(html.contains("text-align: center;"))
375 #expect(html.contains("text-align: right;"))
376 }
377}
378
379struct RepositoryAssetURLTests {
380
381 @Test
382 func repositoryAssetURLPercentEncodesImagePaths() {
383 let url = resolveRepositoryAssetURL(
384 "images/My Logo.png",
385 owner: "~ccleberg",
386 repositoryName: "Hutch",
387 readmePath: "README.md"
388 )
389
390 #expect(url == "https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/images/My%20Logo.png")
391 }
392}
393
394struct RepositoryLinkURLTests {
395
396 @Test
397 func repositoryLinkURLResolvesRelativePath() {
398 let url = resolveRepositoryLinkURL(
399 "LICENSE",
400 owner: "~ccleberg",
401 repositoryName: "Hutch",
402 readmePath: "README.md"
403 )
404
405 #expect(url == "https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/LICENSE")
406 }
407
408 @Test
409 func repositoryLinkURLPassesThroughAbsoluteURL() {
410 let url = resolveRepositoryLinkURL(
411 "https://example.com/page",
412 owner: "~ccleberg",
413 repositoryName: "Hutch",
414 readmePath: "README.md"
415 )
416
417 #expect(url == "https://example.com/page")
418 }
419
420 @Test
421 func repositoryLinkURLPassesThroughFragment() {
422 let url = resolveRepositoryLinkURL(
423 "#install",
424 owner: "~ccleberg",
425 repositoryName: "Hutch",
426 readmePath: "README.md"
427 )
428
429 #expect(url == "#install")
430 }
431
432 @Test
433 func repositoryLinkURLPassesThroughMailto() {
434 let url = resolveRepositoryLinkURL(
435 "mailto:hello@example.com",
436 owner: "~ccleberg",
437 repositoryName: "Hutch",
438 readmePath: "README.md"
439 )
440
441 #expect(url == "mailto:hello@example.com")
442 }
443
444 @Test
445 func repositoryLinkURLResolvesSubdirectoryRelativePath() {
446 let url = resolveRepositoryLinkURL(
447 "docs/SECURITY.md",
448 owner: "~ccleberg",
449 repositoryName: "Hutch",
450 readmePath: "README.md"
451 )
452
453 #expect(url == "https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/docs/SECURITY.md")
454 }
455}