crossmate

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

FriendZoneTests.swift (7530B)


      1 import CloudKit
      2 import Foundation
      3 import Testing
      4 
      5 @testable import Crossmate
      6 
      7 @Suite("FriendZone")
      8 struct FriendZoneTests {
      9 
     10     @Test("pairKey is symmetric in its arguments")
     11     func pairKeySymmetric() {
     12         let a = "_alice0000000000000000000000000000"
     13         let b = "_bob00000000000000000000000000000000"
     14         #expect(FriendZone.pairKey(a, b) == FriendZone.pairKey(b, a))
     15     }
     16 
     17     @Test("pairKey is deterministic across calls")
     18     func pairKeyDeterministic() {
     19         let a = "_alice"
     20         let b = "_bob"
     21         #expect(FriendZone.pairKey(a, b) == FriendZone.pairKey(a, b))
     22     }
     23 
     24     @Test("pairKey distinguishes different pairs")
     25     func pairKeyDistinct() {
     26         #expect(FriendZone.pairKey("_a", "_b") != FriendZone.pairKey("_a", "_c"))
     27     }
     28 
     29     @Test("pairKey is 64 lowercase hex characters")
     30     func pairKeyShape() {
     31         let key = FriendZone.pairKey("_a", "_b")
     32         #expect(key.count == 64)
     33         #expect(key.allSatisfy { "0123456789abcdef".contains($0) })
     34     }
     35 
     36     @Test("zoneName carries the prefix and is recognised as a friend zone")
     37     func zoneNameRoundTrip() {
     38         let key = FriendZone.pairKey("_a", "_b")
     39         let name = FriendZone.zoneName(pairKey: key)
     40         #expect(name == "friend-\(key)")
     41         #expect(FriendZone.isFriendZone(name))
     42         #expect(!FriendZone.isFriendZone("game-\(UUID().uuidString)"))
     43         #expect(!FriendZone.isFriendZone("account"))
     44     }
     45 
     46     @Test("owner election is deterministic, asymmetric, and excludes self")
     47     func ownerElection() {
     48         let small = "_aaa"
     49         let large = "_zzz"
     50         #expect(FriendZone.isOwner(localAuthorID: small, remoteAuthorID: large))
     51         #expect(!FriendZone.isOwner(localAuthorID: large, remoteAuthorID: small))
     52         // Exactly one side owns.
     53         #expect(
     54             FriendZone.isOwner(localAuthorID: small, remoteAuthorID: large)
     55                 != FriendZone.isOwner(localAuthorID: large, remoteAuthorID: small)
     56         )
     57         // Same user can never be a friend.
     58         #expect(!FriendZone.isOwner(localAuthorID: small, remoteAuthorID: small))
     59     }
     60 
     61     @Test("inbox and outbox share the pair's zone name but differ by owner")
     62     func mailboxDerivation() {
     63         let pairKey = FriendZone.pairKey("_alice", "_bob")
     64         let inbox = FriendZone.inboxZoneID(pairKey: pairKey)
     65         let outbox = FriendZone.outboxZoneID(pairKey: pairKey, friendAuthorID: "_bob")
     66         // Both mailboxes carry the same `friend-<pairKey>` name...
     67         #expect(inbox.zoneName == FriendZone.zoneName(pairKey: pairKey))
     68         #expect(outbox.zoneName == inbox.zoneName)
     69         // ...and are distinguished only by owner: our inbox is ours; the outbox
     70         // is the friend's inbox, owned by the friend.
     71         #expect(inbox.ownerName == CKCurrentUserDefaultName)
     72         #expect(outbox.ownerName == "_bob")
     73         #expect(inbox != outbox)
     74     }
     75 
     76     @Test("a pair's two mailboxes agree on the zone name that binds them")
     77     func mailboxSymmetry() {
     78         let pairKey = FriendZone.pairKey("_alice", "_bob")
     79         // Alice's outbox to Bob is the zone Bob owns; on Bob's device that same
     80         // zone is his inbox. They agree on the zone name — the pairwise binding.
     81         let aliceOutboxToBob = FriendZone.outboxZoneID(pairKey: pairKey, friendAuthorID: "_bob")
     82         let bobInbox = FriendZone.inboxZoneID(pairKey: pairKey)
     83         #expect(aliceOutboxToBob.zoneName == bobInbox.zoneName)
     84     }
     85 
     86     @Test("BootstrapPayload round-trips through its string encoding")
     87     func bootstrapPayloadRoundTrip() {
     88         let payload = FriendZone.BootstrapPayload(
     89             friendShareURL: "https://www.icloud.com/share/abc#xyz",
     90             pairKey: FriendZone.pairKey("_a", "_b"),
     91             ownerAuthorID: "_a"
     92         )
     93         let encoded = payload.encodedString()
     94         #expect(encoded != nil)
     95         #expect(FriendZone.BootstrapPayload.decode(encoded) == payload)
     96     }
     97 
     98     @Test("Bootstrap acceptor must be the other member of the pair")
     99     func bootstrapAcceptorMustBePairMember() {
    100         let payload = FriendZone.BootstrapPayload(
    101             friendShareURL: "https://www.icloud.com/share/abc#xyz",
    102             pairKey: FriendZone.pairKey("_alice", "_bob"),
    103             ownerAuthorID: "_alice"
    104         )
    105 
    106         #expect(FriendZone.canAcceptBootstrap(payload, localAuthorID: "_bob"))
    107         #expect(!FriendZone.canAcceptBootstrap(payload, localAuthorID: "_carol"))
    108         #expect(!FriendZone.canAcceptBootstrap(payload, localAuthorID: "_alice"))
    109         #expect(!FriendZone.canAcceptBootstrap(payload, localAuthorID: nil))
    110         #expect(!FriendZone.canAcceptBootstrap(payload, localAuthorID: ""))
    111     }
    112 
    113     @Test("BootstrapPayload.decode tolerates nil and malformed input")
    114     func bootstrapPayloadDecodeTolerant() {
    115         #expect(FriendZone.BootstrapPayload.decode(nil) == nil)
    116         #expect(FriendZone.BootstrapPayload.decode("") == nil)
    117         #expect(FriendZone.BootstrapPayload.decode("not json") == nil)
    118         #expect(FriendZone.BootstrapPayload.decode(#"{"friendShareURL":"x"}"#) == nil)
    119     }
    120 
    121     @Test("InvitePayload round-trips through its string encoding")
    122     func invitePayloadRoundTrip() {
    123         let payload = FriendZone.InvitePayload(
    124             gameShareURL: "https://www.icloud.com/share/xyz#abc"
    125         )
    126         let encoded = payload.encodedString()
    127         #expect(encoded != nil)
    128         #expect(FriendZone.InvitePayload.decode(encoded) == payload)
    129     }
    130 
    131     @Test("InvitePayload round-trips a grid silhouette segment")
    132     func invitePayloadRoundTripWithSilhouette() {
    133         let payload = FriendZone.InvitePayload(
    134             gameShareURL: "https://www.icloud.com/share/xyz#abc",
    135             gridSilhouette: "sf//AA"
    136         )
    137         let decoded = FriendZone.InvitePayload.decode(payload.encodedString())
    138         #expect(decoded == payload)
    139         #expect(decoded?.gridSilhouette == "sf//AA")
    140     }
    141 
    142     @Test("InvitePayload round-trips fast-accept game data")
    143     func invitePayloadRoundTripWithFastAcceptData() {
    144         let payload = FriendZone.InvitePayload(
    145             gameShareURL: "https://www.icloud.com/share/xyz#abc",
    146             gridSilhouette: "sf//AA",
    147             puzzleSource: "Title: Fast\n\n\nAB\n\n\nA1. _ ~ AB",
    148             notification: "encoded-notification-credential"
    149         )
    150         let decoded = FriendZone.InvitePayload.decode(payload.encodedString())
    151         #expect(decoded == payload)
    152         #expect(decoded?.puzzleSource == payload.puzzleSource)
    153         #expect(decoded?.notification == "encoded-notification-credential")
    154     }
    155 
    156     @Test("InvitePayload from an older sender decodes with a nil silhouette")
    157     func invitePayloadDecodesLegacyWithoutSilhouette() {
    158         // Payloads written before the silhouette field omit the key entirely.
    159         let legacy = #"{"gameShareURL":"https://www.icloud.com/share/xyz"}"#
    160         let decoded = FriendZone.InvitePayload.decode(legacy)
    161         #expect(decoded?.gameShareURL == "https://www.icloud.com/share/xyz")
    162         #expect(decoded?.gridSilhouette == nil)
    163         #expect(decoded?.puzzleSource == nil)
    164         #expect(decoded?.notification == nil)
    165     }
    166 
    167     @Test("InvitePayload.decode tolerates nil and malformed input")
    168     func invitePayloadDecodeTolerant() {
    169         #expect(FriendZone.InvitePayload.decode(nil) == nil)
    170         #expect(FriendZone.InvitePayload.decode("") == nil)
    171         #expect(FriendZone.InvitePayload.decode("garbage") == nil)
    172         #expect(FriendZone.InvitePayload.decode(#"{"x":1}"#) == nil)
    173     }
    174 }