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 }