crossmate

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

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 }