krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.3.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 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("![logo](https://example.com/logo.png)")
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("[![badge](https://example.com/badge.png)](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("&lt;b&gt;"))
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("![badge](https://sonarcloud.io/api/project_badges/measure?project=ccleberg_Hutch&metric=security_rating)")
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        [![builds.sr.ht status](https://builds.sr.ht/~ccleberg/Hutch.svg)](https://builds.sr.ht/~ccleberg/Hutch?)
191        [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ccleberg_Hutch&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ccleberg_Hutch)
192        [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=ccleberg_Hutch&metric=reliability_rating)](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] &lt;FILE&gt;</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}