CloudJoinFailureTests.swift (4519B)
1 import CloudKit 2 import Foundation 3 import Testing 4 5 @testable import Crossmate 6 7 @Suite("Cloud join failure handling") 8 struct CloudJoinFailureTests { 9 @Test("Accepted-game extraction propagates a per-record error") 10 func acceptedGameRecordPropagatesFailure() throws { 11 let zoneID = CKRecordZone.ID(zoneName: "game-test", ownerName: "_owner") 12 let recordID = CKRecord.ID(recordName: "game-test", zoneID: zoneID) 13 let quotaError = NSError( 14 domain: CKErrorDomain, 15 code: CKError.quotaExceeded.rawValue 16 ) 17 let results: [CKRecord.ID: Result<CKRecord, Error>] = [ 18 recordID: .failure(quotaError) 19 ] 20 21 do { 22 _ = try SyncEngine.acceptedGameRecord(from: results, recordID: recordID) 23 Issue.record("Expected the record-level CloudKit error") 24 } catch { 25 #expect((error as NSError).domain == CKErrorDomain) 26 #expect((error as NSError).code == CKError.quotaExceeded.rawValue) 27 } 28 } 29 30 @Test("Accepted-game extraction returns the requested record") 31 func acceptedGameRecordReturnsSuccess() throws { 32 let zoneID = CKRecordZone.ID(zoneName: "game-test", ownerName: "_owner") 33 let recordID = CKRecord.ID(recordName: "game-test", zoneID: zoneID) 34 let record = CKRecord(recordType: "Game", recordID: recordID) 35 36 let extracted = try SyncEngine.acceptedGameRecord( 37 from: [recordID: .success(record)], 38 recordID: recordID 39 ) 40 41 #expect(extracted?.recordID == recordID) 42 } 43 44 @Test("Join retry policy distinguishes transient and terminal errors") 45 func joinRetryClassification() { 46 let rateLimit = NSError( 47 domain: CKErrorDomain, 48 code: CKError.requestRateLimited.rawValue, 49 userInfo: [CKErrorRetryAfterKey: NSNumber(value: 12.5)] 50 ) 51 let quota = NSError( 52 domain: CKErrorDomain, 53 code: CKError.quotaExceeded.rawValue 54 ) 55 56 #expect(CloudService.isRetryableJoinError(rateLimit)) 57 #expect(CloudService.retryAfter(rateLimit) == 12.5) 58 #expect(CloudService.joinFailureKind(for: rateLimit) == .temporarilyUnavailable) 59 #expect(!CloudService.isRetryableJoinError(quota)) 60 #expect(CloudService.joinFailureKind(for: quota) == .unavailable) 61 let acceptedQuota = AcceptedShareError( 62 kind: .unavailable, 63 underlyingError: quota 64 ) 65 #expect(acceptedQuota.errorDescription == CloudFailureCopy.quotaJoinBody) 66 } 67 68 @Test("Removed accepted shares retain their distinct outcome") 69 func removedJoinClassification() { 70 for code in [CKError.unknownItem, .zoneNotFound, .userDeletedZone] { 71 let error = NSError(domain: CKErrorDomain, code: code.rawValue) 72 #expect(CloudService.joinFailureKind(for: error) == .removed) 73 } 74 } 75 76 @Test("Only non-retryable Ping errors become terminal") 77 func terminalPingClassification() { 78 let terminal = [CKError.quotaExceeded, .permissionFailure, .zoneNotFound] 79 let retryable = [ 80 CKError.networkFailure, 81 .serviceUnavailable, 82 .requestRateLimited, 83 .zoneBusy, 84 .accountTemporarilyUnavailable, 85 ] 86 87 for code in terminal { 88 #expect(SyncEngine.isTerminalPingSaveError( 89 NSError(domain: CKErrorDomain, code: code.rawValue) 90 )) 91 } 92 for code in retryable { 93 #expect(!SyncEngine.isTerminalPingSaveError( 94 NSError(domain: CKErrorDomain, code: code.rawValue) 95 )) 96 } 97 98 let retryAfter = NSError( 99 domain: CKErrorDomain, 100 code: CKError.requestRateLimited.rawValue, 101 userInfo: [CKErrorRetryAfterKey: NSNumber(value: 12.5)] 102 ) 103 #expect(SyncEngine.pingSaveRetryDelay(retryAfter) == 12.5) 104 #expect(SyncEngine.pingSaveRetryDelay( 105 NSError(domain: CKErrorDomain, code: CKError.networkFailure.rawValue) 106 ) == 3) 107 #expect(SyncEngine.pingSaveRetryDelay( 108 NSError(domain: CKErrorDomain, code: CKError.quotaExceeded.rawValue) 109 ) == nil) 110 let invitationQuota = SyncEngine.PingOutboxError.deliveryFailed( 111 code: CKError.quotaExceeded.rawValue 112 ) 113 #expect( 114 invitationQuota.errorDescription 115 == String(localized: InviteDeliveryFailure.quotaExceeded.body) 116 ) 117 } 118 }