krz/hutch

an ios client for sourcehut

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

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