crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

ShareLinkShortenerTests.swift (7569B)


      1 import Foundation
      2 import Testing
      3 
      4 @testable import Crossmate
      5 
      6 @Suite("Share link shortener")
      7 struct ShareLinkShortenerTests {
      8 
      9     private let base = URL(string: "https://crossmate.example.net")!
     10     private let token = "0a1BcDeFgHiJkLmNoPqRsTuVw"
     11     private var share: URL { URL(string: "https://www.icloud.com/share/\(token)")! }
     12 
     13     @Test("rewrites an iCloud share URL to the worker short form")
     14     func rewritesShareURL() {
     15         let short = ShareLinkShortener.shortURL(for: share, title: nil, shape: nil, baseURL: base)
     16         #expect(short.absoluteString == "https://crossmate.example.net/s/\(token)")
     17     }
     18 
     19     @Test("drops the title-slug fragment from the short link")
     20     func dropsFragment() {
     21         let fragmented = URL(string: "https://www.icloud.com/share/\(token)#Saturday_Stumper")!
     22         let short = ShareLinkShortener.shortURL(for: fragmented, title: nil, shape: nil, baseURL: base)
     23         #expect(short.absoluteString == "https://crossmate.example.net/s/\(token)")
     24     }
     25 
     26     @Test("encodes a custom game title as a t-tagged base64url segment")
     27     func encodesTitleSegment() {
     28         let short = ShareLinkShortener.shortURL(
     29             for: share, title: "Saturday Stumper", shape: nil, baseURL: base
     30         )
     31         #expect(
     32             short.absoluteString
     33                 == "https://crossmate.example.net/s/\(token)/tU2F0dXJkYXkgU3R1bXBlcg"
     34         )
     35     }
     36 
     37     @Test("maps base64 plus and slash into the URL-safe alphabet")
     38     func encodesTitleURLSafely() {
     39         // The em dash and π force '+' into the standard base64 encoding.
     40         let short = ShareLinkShortener.shortURL(
     41             for: share, title: "Sunday? Yes — π!", shape: nil, baseURL: base
     42         )
     43         #expect(
     44             short.absoluteString
     45                 == "https://crossmate.example.net/s/\(token)/tU3VuZGF5PyBZZXMg4oCUIM-AIQ"
     46         )
     47     }
     48 
     49     @Test("collapses a standard NYT weekday title to a single digit")
     50     func encodesWeekdayTitle() {
     51         let cases: [(String, String)] = [
     52             ("Sunday Crossword", "0"),
     53             ("Monday Crossword", "1"),
     54             ("Saturday Crossword", "6"),
     55             ("  Monday Crossword  ", "1")
     56         ]
     57         for (title, digit) in cases {
     58             let short = ShareLinkShortener.shortURL(for: share, title: title, shape: nil, baseURL: base)
     59             #expect(short.absoluteString == "https://crossmate.example.net/s/\(token)/\(digit)")
     60         }
     61     }
     62 
     63     @Test("a near-miss weekday title falls back to the base64url form")
     64     func nonWeekdayTitleStaysCustom() {
     65         // No trailing "Crossword", so it is not the NYT shortcut.
     66         let short = ShareLinkShortener.shortURL(for: share, title: "Monday", shape: nil, baseURL: base)
     67         #expect(
     68             short.absoluteString == "https://crossmate.example.net/s/\(token)/tTW9uZGF5"
     69         )
     70     }
     71 
     72     @Test("truncates an over-long custom title to the cap")
     73     func truncatesLongTitle() {
     74         let long = String(repeating: "A", count: ShareLinkShortener.maxCustomTitleLength + 12)
     75         let capped = String(long.prefix(ShareLinkShortener.maxCustomTitleLength))
     76         let expected = "t" + GridSilhouette.base64URLEncode([UInt8](capped.utf8))
     77         let short = ShareLinkShortener.shortURL(for: share, title: long, shape: nil, baseURL: base)
     78         #expect(short.absoluteString == "https://crossmate.example.net/s/\(token)/\(expected)")
     79     }
     80 
     81     @Test("appends the grid silhouette after the title")
     82     func appendsShapeAfterTitle() {
     83         // 3×3, centre cell blocked → symmetric → "s3CA".
     84         var blocks = [Bool](repeating: false, count: 9)
     85         blocks[4] = true
     86         let grid = GridSilhouette.Grid(side: 3, blocks: blocks)
     87         let short = ShareLinkShortener.shortURL(
     88             for: share, title: "Monday Crossword", shape: grid, baseURL: base
     89         )
     90         #expect(short.absoluteString == "https://crossmate.example.net/s/\(token)/1/s3CA")
     91     }
     92 
     93     @Test("emits the silhouette segment even when there is no title")
     94     func appendsShapeWithoutTitle() {
     95         var blocks = [Bool](repeating: false, count: 9)
     96         blocks[4] = true
     97         let grid = GridSilhouette.Grid(side: 3, blocks: blocks)
     98         let short = ShareLinkShortener.shortURL(for: share, title: nil, shape: grid, baseURL: base)
     99         #expect(short.absoluteString == "https://crossmate.example.net/s/\(token)/s3CA")
    100     }
    101 
    102     @Test("appends a rectangular silhouette under the uppercase form")
    103     func appendsRectangularShape() {
    104         // 2×3, all empty → symmetric → "S23AA".
    105         let grid = GridSilhouette.Grid(width: 2, height: 3, blocks: [Bool](repeating: false, count: 6))
    106         let short = ShareLinkShortener.shortURL(for: share, title: nil, shape: grid, baseURL: base)
    107         #expect(short.absoluteString == "https://crossmate.example.net/s/\(token)/S23AA")
    108     }
    109 
    110     @Test("omits a silhouette whose block count doesn't match its dimensions")
    111     func omitsMismatchedShape() {
    112         let grid = GridSilhouette.Grid(side: 3, blocks: [Bool](repeating: false, count: 6))
    113         let short = ShareLinkShortener.shortURL(for: share, title: nil, shape: grid, baseURL: base)
    114         #expect(short.absoluteString == "https://crossmate.example.net/s/\(token)")
    115     }
    116 
    117     @Test("omits the title segment for an empty or whitespace title")
    118     func omitsBlankTitle() {
    119         for title in [nil, "", "   ", "\n"] as [String?] {
    120             let short = ShareLinkShortener.shortURL(for: share, title: title, shape: nil, baseURL: base)
    121             #expect(short.absoluteString == "https://crossmate.example.net/s/\(token)")
    122         }
    123     }
    124 
    125     @Test("accepts the bare icloud.com host")
    126     func acceptsBareHost() {
    127         let bare = URL(string: "https://icloud.com/share/\(token)")!
    128         let short = ShareLinkShortener.shortURL(for: bare, title: nil, shape: nil, baseURL: base)
    129         #expect(short.absoluteString == "https://crossmate.example.net/s/\(token)")
    130     }
    131 
    132     @Test("passes through unchanged when no base URL is configured")
    133     func passesThroughWithoutBase() {
    134         let fragmented = URL(string: "https://www.icloud.com/share/\(token)#Title")!
    135         #expect(
    136             ShareLinkShortener.shortURL(for: fragmented, title: "Title", shape: nil, baseURL: nil)
    137                 == fragmented
    138         )
    139     }
    140 
    141     @Test(
    142         "passes through URLs that are not a CKShare link",
    143         arguments: [
    144             "https://example.com/share/0a1BcDeFgHiJkLmNoPqRsTuVw",
    145             "http://www.icloud.com/share/0a1BcDeFgHiJkLmNoPqRsTuVw",
    146             "https://www.icloud.com/notes/0a1BcDeFgHiJkLmNoPqRsTuVw",
    147             "https://www.icloud.com/share",
    148             "https://www.icloud.com/share/0a1BcDeF/extra"
    149         ]
    150     )
    151     func passesThroughForeignURLs(raw: String) {
    152         let url = URL(string: raw)!
    153         #expect(ShareLinkShortener.shortURL(for: url, title: nil, shape: nil, baseURL: base) == url)
    154     }
    155 
    156     @Test("rejects tokens outside the unreserved charset or length bounds")
    157     func rejectsMalformedTokens() {
    158         // The encoded slash decodes into the last path component; passing it
    159         // through to the worker would change the redirect target's shape.
    160         let smuggled = URL(string: "https://www.icloud.com/share/0a1BcDeF%2F..%2FbAd")!
    161         #expect(ShareLinkShortener.shortURL(for: smuggled, title: nil, shape: nil, baseURL: base) == smuggled)
    162 
    163         let tooShort = URL(string: "https://www.icloud.com/share/0a1Bc")!
    164         #expect(ShareLinkShortener.shortURL(for: tooShort, title: nil, shape: nil, baseURL: base) == tooShort)
    165     }
    166 }