AccountPushCoordinatorTests.swift (3351B)
1 import CloudKit 2 import CoreData 3 import Foundation 4 import Testing 5 6 @testable import Crossmate 7 8 /// Friend-invite encryption keys are minted/published only from the friendship 9 /// and invite-send paths (not the registration sweep), with a per-pair 10 /// "published" marker that dedupes the decision write. The marker must be 11 /// gated on the write actually being enqueued: an unstarted `SyncEngine` drops 12 /// the decision, and recording it as published anyway would strand the key — 13 /// it could never re-heal once the engine came up. 14 @Suite("AccountPushCoordinator friend invite keys", .isolatedNotificationState) 15 @MainActor 16 struct AccountPushCoordinatorTests { 17 18 // Persisted UserDefaults keys, mirrored here to pin the format the way the 19 // CloudKit-field tests pin record names. `AccountPushCoordinator` writes 20 // these under `UserDefaults.standard`, so tests use a unique pair key and 21 // clean up to stay isolated from siblings. 22 private static func keyDefaultsKey(_ pairKey: String) -> String { 23 "push.friendEncryptionKey." + pairKey 24 } 25 26 private static func publishedDefaultsKey(_ pairKey: String) -> String { 27 "push.friendEncryptionKeyPublished." + pairKey 28 } 29 30 private func makeCoordinator( 31 persistence: PersistenceController, 32 syncEngine: SyncEngine 33 ) -> AccountPushCoordinator { 34 let prefs = PlayerPreferences( 35 local: UserDefaults(suiteName: "test-pref-\(UUID().uuidString)")! 36 ) 37 prefs.isICloudSyncEnabled = true 38 return AccountPushCoordinator( 39 identity: AuthorIdentity(testing: "alice"), 40 preferences: prefs, 41 persistence: persistence, 42 store: makeTestStore(persistence: persistence), 43 syncEngine: syncEngine, 44 syncMonitor: SyncMonitor(log: EventLog()), 45 pushClient: nil 46 ) 47 } 48 49 @Test("a dropped friend-key enqueue mints the key but is not marked published") 50 func droppedEnqueueIsNotMarkedPublished() async { 51 let persistence = makeTestPersistence() 52 // Never started, so its CKSyncEngines are nil and the decision write is 53 // dropped — the exact case the publish gate must survive. 54 let engine = SyncEngine( 55 container: CloudContainer.container, 56 persistence: persistence 57 ) 58 let coordinator = makeCoordinator(persistence: persistence, syncEngine: engine) 59 let pairKey = "pair-\(UUID().uuidString)" 60 defer { 61 UserDefaults.standard.removeObject(forKey: Self.keyDefaultsKey(pairKey)) 62 UserDefaults.standard.removeObject(forKey: Self.publishedDefaultsKey(pairKey)) 63 } 64 65 await coordinator.ensureFriendInvitationKeyPublished( 66 pairKey: pairKey, 67 friendZoneID: CKRecordZone.ID(zoneName: "friend-\(pairKey)", ownerName: "owner"), 68 friendZoneScope: .private 69 ) 70 71 // The key is minted locally so this device can listen on its address... 72 #expect(UserDefaults.standard.string(forKey: Self.keyDefaultsKey(pairKey)) != nil) 73 // ...but the publish marker stays unset because nothing was enqueued, so 74 // a later call (engine up) still attempts the decision write. 75 #expect(UserDefaults.standard.string(forKey: Self.publishedDefaultsKey(pairKey)) == nil) 76 } 77 }