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