crossmate

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

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 }