AppDelegatePushBufferTests.swift (6077B)
1 import Foundation 2 import Testing 3 import UIKit 4 5 @testable import Crossmate 6 7 @Suite("App delegate push buffering") 8 @MainActor 9 struct AppDelegatePushBufferTests { 10 private actor StartupProbe { 11 private(set) var started = false 12 private(set) var finished = false 13 private(set) var callbackReturned = false 14 private var continuation: CheckedContinuation<Void, Never>? 15 private var resumeRequested = false 16 17 func pause() async { 18 started = true 19 if resumeRequested { 20 finished = true 21 return 22 } 23 await withCheckedContinuation { continuation = $0 } 24 finished = true 25 } 26 27 func resume() { 28 if let continuation { 29 continuation.resume() 30 self.continuation = nil 31 } else { 32 resumeRequested = true 33 } 34 } 35 36 func noteCallbackReturned() { 37 callbackReturned = true 38 } 39 } 40 41 @Test("An APNs token that beats handler installation is replayed once") 42 func tokenReplayedOnce() { 43 let delegate = AppDelegate() 44 let token = Data([0x01, 0x02, 0x03]) 45 delegate.application( 46 UIApplication.shared, 47 didRegisterForRemoteNotificationsWithDeviceToken: token 48 ) 49 50 var received: [Data] = [] 51 delegate.onAPNsToken = { received.append($0) } 52 #expect(received == [token]) 53 54 // The buffer is consumed by the replay — reinstalling the handler 55 // must not deliver the token again. 56 delegate.onAPNsToken = { received.append($0) } 57 #expect(received == [token]) 58 } 59 60 @Test("The APNs token goes straight through once the handler is installed") 61 func tokenDeliveredDirectly() { 62 let delegate = AppDelegate() 63 var received: [Data] = [] 64 delegate.onAPNsToken = { received.append($0) } 65 66 let token = Data([0x0a, 0x0b]) 67 delegate.application( 68 UIApplication.shared, 69 didRegisterForRemoteNotificationsWithDeviceToken: token 70 ) 71 #expect(received == [token]) 72 } 73 74 @Test("A registration failure that beats handler installation is replayed") 75 func registrationFailureReplayed() { 76 let delegate = AppDelegate() 77 delegate.application( 78 UIApplication.shared, 79 didFailToRegisterForRemoteNotificationsWithError: NSError(domain: "test", code: 3) 80 ) 81 82 var messages: [String] = [] 83 delegate.onAPNsRegistrationResult = { messages.append($0) } 84 #expect(messages.count == 1) 85 #expect(messages.first?.contains("FAILED") == true) 86 } 87 88 @Test("Pre-start remote notifications are buffered, coalesced by source, and drained in order") 89 func remoteNotificationsBufferedAndDrained() async { 90 let delegate = AppDelegate() 91 let firstGame = UUID() 92 let secondGame = UUID() 93 94 // Two wakes from the same source coalesce to the newer one (which 95 // carries the presence deadline); a different game stays a separate 96 // entry. 97 _ = await delegate.application(UIApplication.shared, didReceiveRemoteNotification: [ 98 "kind": "presence", 99 "gameID": firstGame.uuidString, 100 "senderDeviceID": "alice-device", 101 ]) 102 _ = await delegate.application(UIApplication.shared, didReceiveRemoteNotification: [ 103 "kind": "presence", 104 "gameID": firstGame.uuidString, 105 "senderDeviceID": "alice-device", 106 "presenceUntil": "2026-07-19T10:00:00Z", 107 ]) 108 _ = await delegate.application(UIApplication.shared, didReceiveRemoteNotification: [ 109 "kind": "presence", 110 "gameID": secondGame.uuidString, 111 "senderDeviceID": "bob-device", 112 ]) 113 114 var drained: [(gameID: UUID?, presenceUntil: Date?)] = [] 115 delegate.onRemoteNotification = { _, _, _, gameID, _, _, presenceUntil, _ in 116 drained.append((gameID, presenceUntil)) 117 } 118 for _ in 0..<1000 where drained.count < 2 { await Task.yield() } 119 120 #expect(drained.count == 2) 121 #expect(drained.first?.gameID == firstGame) 122 #expect(drained.first?.presenceUntil != nil) 123 #expect(drained.last?.gameID == secondGame) 124 125 // The buffer is empty after the drain — a later push is delivered 126 // directly, not re-queued. 127 _ = await delegate.application(UIApplication.shared, didReceiveRemoteNotification: [ 128 "kind": "presence", 129 "gameID": firstGame.uuidString, 130 "senderDeviceID": "alice-device", 131 ]) 132 #expect(drained.count == 3) 133 } 134 135 @Test("A background wake waits for racing startup and the buffered drain") 136 func backgroundWakeAwaitsStartupAndDrain() async { 137 let delegate = AppDelegate() 138 let startup = StartupProbe() 139 let gameID = UUID() 140 var received: [UUID?] = [] 141 142 delegate.isInstalledApplicationDelegateForTesting = true 143 delegate.startServicesForTesting = { 144 await startup.pause() 145 delegate.onRemoteNotification = { _, _, _, gameID, _, _, _, _ in 146 received.append(gameID) 147 } 148 } 149 150 let resultTask = Task { @MainActor in 151 let result = await delegate.application(UIApplication.shared, didReceiveRemoteNotification: [ 152 "kind": "presence", 153 "gameID": gameID.uuidString, 154 "senderDeviceID": "alice-device", 155 ]) 156 await startup.noteCallbackReturned() 157 return result 158 } 159 160 for _ in 0..<1000 where !(await startup.started) { await Task.yield() } 161 #expect(await startup.started) 162 #expect(!(await startup.finished)) 163 #expect(!(await startup.callbackReturned)) 164 #expect(received.isEmpty) 165 166 await startup.resume() 167 #expect(await resultTask.value == .newData) 168 #expect(await startup.finished) 169 #expect(received == [gameID]) 170 } 171 }