// // AltTests.swift // AltTests // // Created by Riley Testut on 10/6/22. // Copyright © 2022 Riley Testut. All rights reserved. // import XCTest @testable import AltStore @testable import AltStoreCore import AltSign extension String { static let testDomain = "TestErrorDomain" static let testLocalizedTitle = "AltTest Failed" static let testLocalizedFailure = "The AltTest failed to pass." static let testOriginalLocalizedFailure = "The AltServer operation could not be completed." static let testUnrecognizedFailureReason = "The alien invasion has begun." static let testUnrecognizedRecoverySuggestion = "Find your loved ones and pray the aliens are merciful." static let testDescription = "The operation could not be completed because an error occured." static let testDebugDescription = "The very specific operation could not be completed because a detailed error occured. Code=101." } extension [String: String] { static let unrecognizedProvider: Self = [ NSLocalizedFailureReasonErrorKey: .testUnrecognizedFailureReason, NSLocalizedRecoverySuggestionErrorKey: .testUnrecognizedRecoverySuggestion ] } extension Error { func serialized(provider: [String: String]?) -> NSError { AltTests.mockUserInfoValueProvider(for: self, values: provider) { return (self as NSError).sanitizedForSerialization() } } } extension URL { static let testFileURL = URL(fileURLWithPath: "~/Desktop/TestApp.ipa") } final class AltTests: XCTestCase { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } } // Helper Methods extension AltTests { func unbridge(_ error: NSError, to errorType: T) throws -> Error { let unbridgedError = try XCTUnwrap(error as? T) return unbridgedError } func send(_ error: Error, serverProvider: [String: String]? = nil, clientProvider: [String: String]? = nil) throws -> NSError { let altserverError = ALTServerError(error) let codableError = CodableError(error: altserverError) let jsonData: Data if let serverProvider { jsonData = try AltTests.mockUserInfoValueProvider(for: error, values: serverProvider) { return try JSONEncoder().encode(codableError) } } else { jsonData = try JSONEncoder().encode(codableError) } let decodedError: CodableError if let clientProvider { decodedError = try AltTests.mockUserInfoValueProvider(for: error, values: clientProvider) { return try Foundation.JSONDecoder().decode(CodableError.self, from: jsonData) } } else { decodedError = try Foundation.JSONDecoder().decode(CodableError.self, from: jsonData) } let receivedError = decodedError.error return receivedError as NSError } static func mockUserInfoValueProvider(for error: Error, values: [String: String]?, closure: () throws -> T) rethrows -> T { let provider = NSError.userInfoValueProvider(forDomain: error._domain) NSError.setUserInfoValueProvider(forDomain: error._domain) { (error, key) -> Any? in let nsError = error as NSError guard nsError.code == error._code else { return provider?(nsError, key) } switch key { case NSLocalizedDescriptionKey: guard nsError.localizedFailure == nil else { // Error has localizedFailure, so return nil to construct localizedDescription from it + localizedFailureReason. return nil } // Otherwise, return failureReason for localizedDescription to avoid system prepending "Operation Failed" message. return values?[NSLocalizedFailureReasonErrorKey] default: return values?[key] } } defer { NSError.setUserInfoValueProvider(forDomain: error._domain) { (error, key) in provider?(error, key) } } let value = try closure() return value } func ALTAssertErrorsEqual(_ error1: Error, _ error2: Error, ignoring ignoredValues: Set = [], ignoreExtraUserInfoValues: Bool = false, file: StaticString = #file, line: UInt = #line) { if !ignoredValues.contains(ALTUnderlyingErrorDomainErrorKey) { XCTAssertEqual(error1._domain, error2._domain, file: file, line: line) } if !ignoredValues.contains(ALTUnderlyingErrorCodeErrorKey) { XCTAssertEqual(error1._code, error2._code, file: file, line: line) } if !ignoredValues.contains(NSLocalizedDescriptionKey) { XCTAssertEqual(error1.localizedDescription, error2.localizedDescription, "Localized Descriptions don't match.", file: file, line: line) } let nsError1 = error1 as NSError let nsError2 = error2 as NSError if !ignoredValues.contains(ALTLocalizedTitleErrorKey) { XCTAssertEqual(nsError1.localizedTitle, nsError2.localizedTitle, "Titles don't match.", file: file, line: line) } if !ignoredValues.contains(NSLocalizedFailureErrorKey) { XCTAssertEqual(nsError1.localizedFailure, nsError2.localizedFailure, "Failures don't match.", file: file, line: line) } if !ignoredValues.contains(NSLocalizedFailureReasonErrorKey) { XCTAssertEqual(nsError1.localizedFailureReason, nsError2.localizedFailureReason, "Failure reasons don't match.", file: file, line: line) } if !ignoredValues.contains(NSLocalizedRecoverySuggestionErrorKey) { XCTAssertEqual(nsError1.localizedRecoverySuggestion, nsError2.localizedRecoverySuggestion, file: file, line: line) } if !ignoredValues.contains(NSDebugDescriptionErrorKey) { XCTAssertEqual(nsError1.localizedDebugDescription, nsError2.localizedDebugDescription, file: file, line: line) } if !ignoreExtraUserInfoValues { // Ensure remaining user info values match. let standardKeys: Set = [NSLocalizedDescriptionKey, ALTLocalizedTitleErrorKey, NSLocalizedFailureErrorKey, NSLocalizedFailureReasonErrorKey, NSLocalizedRecoverySuggestionErrorKey, NSUnderlyingErrorKey, NSDebugDescriptionErrorKey] let filteredUserInfo1 = nsError1.userInfo.filter { !standardKeys.contains($0.key) } let filteredUserInfo2 = nsError2.userInfo.filter { !standardKeys.contains($0.key) } XCTAssertEqual(filteredUserInfo1 as NSDictionary, filteredUserInfo2 as NSDictionary, file: file, line: line) } } @discardableResult func ALTAssertUnderlyingErrorEqualsError(_ receivedError: Error, _ error: Error, ignoring ignoredValues: Set = [], ignoreExtraUserInfoValues: Bool = false, file: StaticString = #file, line: UInt = #line) throws -> NSError { // Test receivedError == ALTServerError.underlyingError XCTAssertEqual(receivedError._domain, ALTServerError.errorDomain, file: file, line: line) if !ignoredValues.contains(ALTUnderlyingErrorCodeErrorKey) { XCTAssertEqual(receivedError._code, ALTServerError.underlyingError.rawValue, file: file, line: line) } // Test underlyingError == error let underlyingError = try XCTUnwrap(receivedError.underlyingError) ALTAssertErrorsEqual(underlyingError, error, ignoring: ignoredValues, ignoreExtraUserInfoValues: ignoreExtraUserInfoValues, file: file, line: line) // Test receivedError forwards all properties to underlyingError. var ignoredValues = ignoredValues ignoredValues.formUnion([ALTUnderlyingErrorDomainErrorKey, ALTUnderlyingErrorCodeErrorKey]) ALTAssertErrorsEqual(receivedError, underlyingError, ignoring: ignoredValues, ignoreExtraUserInfoValues: true, file: file, line: line) // Always ignore extra user info values. return underlyingError as NSError } func ALTAssertErrorFailureAndDescription(_ error: Error, failure: String?, baseDescription: String, file: StaticString = #file, line: UInt = #line) { let localizedDescription: String if let failure { localizedDescription = failure + " " + baseDescription } else { localizedDescription = baseDescription } XCTAssertEqual(error.localizedDescription, localizedDescription, file: file, line: line) XCTAssertEqual((error as NSError).localizedFailure, failure, file: file, line: line) } } // Local Errors extension AltTests { func testToNSErrorBridging() async throws { for error in AltTests.allLocalErrors { let nsError = (error as NSError) XCTAssertEqual(nsError.localizedDescription, error.localizedDescription) XCTAssertEqual(nsError.localizedFailure, error.errorFailure) XCTAssertEqual(nsError.localizedFailureReason, error.errorFailureReason) XCTAssertEqual(nsError.localizedRecoverySuggestion, error.recoverySuggestion) if let provider = NSError.userInfoValueProvider(forDomain: OperationError.errorDomain), let debugDescription = provider(error, NSDebugDescriptionErrorKey) as? String { XCTAssertEqual(debugDescription, nsError.debugDescription) } } } func testToNSErrorAndBackBridging() async throws { for error in AltTests.allLocalErrors { let nsError = (error as NSError) let unbridgedError = try XCTUnwrap(nsError as? any ALTLocalizedError) XCTAssertEqual(unbridgedError.localizedDescription, error.localizedDescription) XCTAssertEqual(unbridgedError.errorFailure, error.errorFailure) XCTAssertEqual(unbridgedError.failureReason, error.errorFailureReason) XCTAssertEqual(unbridgedError.recoverySuggestion, error.recoverySuggestion) if let provider = NSError.userInfoValueProvider(forDomain: OperationError.errorDomain), let debugDescription = provider(error, NSDebugDescriptionErrorKey) as? String { let unbridgedDebugDescription = provider(unbridgedError, NSDebugDescriptionErrorKey) as? String XCTAssertEqual(debugDescription, unbridgedDebugDescription) } } } func testDefaultErrorDomain() { for error in VerificationError.testErrors { let expectedErrorDomain = "AltStore.VerificationError" XCTAssertEqual(error._domain, expectedErrorDomain) let nsError = (error as NSError) XCTAssertEqual(nsError.domain, expectedErrorDomain) } } func testCustomErrorDomain() async throws { for error in allTestErrors { XCTAssertEqual(error._domain, TestError.errorDomain) let nsError = (error as NSError) XCTAssertEqual(nsError.domain, TestError.errorDomain) } } func testLocalizedDescriptionProvider() async throws { for error in AltTests.allRealErrors { let nsError = (error as NSError).withLocalizedFailure(.testLocalizedFailure) let expectedFailureReason = try XCTUnwrap((error as NSError).localizedFailureReason) if let localizedFailure = (error as NSError).localizedFailure { // Test localizedDescription == original localizedFailure + localizedFailureReason let expectedLocalizedDescription = localizedFailure + " " + expectedFailureReason XCTAssertEqual(error.localizedDescription, expectedLocalizedDescription) } else { // Test localizedDescription does not start with "The operation couldn't be completed." XCTAssert(!error.localizedDescription.starts(with: "The operation couldn’t be completed."), error.localizedDescription) } let expectedLocalizedDescription = String.testLocalizedFailure + " " + expectedFailureReason XCTAssertEqual(nsError.localizedDescription, expectedLocalizedDescription) } } func testWithLocalizedFailure() async throws { for error in AltTests.allRealErrors { let nsError = (error as NSError).withLocalizedFailure(.testLocalizedFailure) let expectedFailureReason = try XCTUnwrap((error as NSError).localizedFailureReason) let expectedLocalizedDescription = String.testLocalizedFailure + " " + expectedFailureReason XCTAssertEqual(nsError.localizedDescription, expectedLocalizedDescription) XCTAssertEqual(nsError.localizedFailure, .testLocalizedFailure) ALTAssertErrorsEqual(nsError, error, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) } } func testWithInitialLocalizedFailure() async throws { for error in OperationError.testErrors { var localizedError = OperationError(error, localizedFailure: .testLocalizedFailure) localizedError.sourceFile = error.sourceFile localizedError.sourceLine = error.sourceLine let nsError = localizedError as NSError let expectedLocalizedDescription = String.testLocalizedFailure + " " + error.errorFailureReason XCTAssertEqual(nsError.localizedDescription, expectedLocalizedDescription) XCTAssertEqual(localizedError.errorFailure, .testLocalizedFailure) XCTAssertEqual(nsError.localizedFailure, .testLocalizedFailure) // Test remainder ALTAssertErrorsEqual(nsError, error, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) } } func testWithInitialLocalizedTitle() async throws { for error in OperationError.testErrors { var localizedError = OperationError(error, localizedTitle: .testLocalizedTitle) localizedError.sourceFile = error.sourceFile localizedError.sourceLine = error.sourceLine let nsError = localizedError as NSError XCTAssertEqual(nsError.localizedDescription, error.localizedDescription) XCTAssertEqual(localizedError.errorTitle, .testLocalizedTitle) XCTAssertEqual(nsError.localizedTitle, .testLocalizedTitle) // Test remainder ALTAssertErrorsEqual(nsError, error, ignoring: [NSLocalizedDescriptionKey, ALTLocalizedTitleErrorKey]) } } func testWithLocalizedFailureAndBack() async throws { for error in AltTests.allLocalErrors { let nsError = (error as NSError).withLocalizedFailure(.testLocalizedFailure) func test(_ unbridgedError: Error, against nsError: NSError) { let unbridgedNSError = (unbridgedError as NSError) let expectedLocalizedDescription = String.testLocalizedFailure + " " + error.errorFailureReason XCTAssertEqual(unbridgedError.localizedDescription, expectedLocalizedDescription) XCTAssertEqual(unbridgedNSError.localizedFailure, .testLocalizedFailure) // Test dynamic type matches original error type XCTAssert(type(of: unbridgedError) == type(of: error)) // Test remainder ALTAssertErrorsEqual(unbridgedNSError, error, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) } do { throw nsError as NSError } catch let error as VerificationError { test(error, against: nsError) } catch let error as OperationError { test(error, against: nsError) } catch let error as ALTLocalizedError { // Make sure VerificationError and OperationError were caught by above handlers. XCTAssertNotEqual(error._domain, VerificationError.errorDomain) XCTAssertNotEqual(error._domain, OperationError.errorDomain) let unbridgedError = try self.unbridge(error as NSError, to: error) test(unbridgedError, against: nsError) } } } func testPatternMatchingErrorCode() async throws { do { // ALTLocalizedError throw OperationError.serverNotFound } catch ~OperationError.Code.serverNotFound { // Success } catch { XCTFail("Failed to catch error as OperationError.Code.serverNotFound: \(error)") } do { // ALTErrorEnum throw AuthenticationError(.noTeam) } catch ~AuthenticationErrorCode.noTeam { // Success } catch { XCTFail("Failed to catch error as AuthenticationErrorCode.noTeam: \(error)") } } func testWithLocalizedTitle() async throws { let localizedTitle = "AltTest Failed" for error in AltTests.allLocalErrors { let nsError = (error as NSError).withLocalizedTitle(localizedTitle) XCTAssertEqual(nsError.localizedTitle, localizedTitle) ALTAssertErrorsEqual(nsError, error, ignoring: [ALTLocalizedTitleErrorKey]) } } func testWithLocalizedTitleAndBack() async throws { for error in AltTests.allLocalErrors { let nsError = (error as NSError).withLocalizedTitle(.testLocalizedTitle) let unbridgedError = try self.unbridge(nsError, to: error) let unbridgedNSError = (unbridgedError as NSError) XCTAssertEqual(unbridgedNSError.localizedTitle, .testLocalizedTitle) ALTAssertErrorsEqual(unbridgedNSError, error, ignoring: [ALTLocalizedTitleErrorKey]) } } func testWithLocalizedTitleAndFailure() async throws { for error in AltTests.allRealErrors { var nsError = (error as NSError).withLocalizedTitle(.testLocalizedTitle) nsError = nsError.withLocalizedFailure(.testLocalizedFailure) XCTAssertEqual(nsError.localizedTitle, .testLocalizedTitle) XCTAssertEqual(nsError.localizedFailure, .testLocalizedFailure) let expectedLocalizedDescription = try String.testLocalizedFailure + " " + XCTUnwrap((error as NSError).localizedFailureReason) XCTAssertEqual(nsError.localizedDescription, expectedLocalizedDescription) // Test remainder ALTAssertErrorsEqual(nsError, error, ignoring: [NSLocalizedDescriptionKey, ALTLocalizedTitleErrorKey, NSLocalizedFailureErrorKey]) } } func testSwiftErrorWithLocalizedFailure() async throws { enum MyError: Int, LocalizedError, CaseIterable { case strange case nothing var errorDescription: String? { switch self { case .strange: return "A strange error occured." case .nothing: return nil } } var recoverySuggestion: String? { return "Have you tried turning it off and on again?" } } for error in MyError.allCases { let nsError = (error as NSError).withLocalizedFailure(.testLocalizedFailure) ALTAssertErrorsEqual(nsError, error, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) ALTAssertErrorFailureAndDescription(nsError, failure: .testLocalizedFailure, baseDescription: error.localizedDescription) } } func testNSErrorWithLocalizedFailure() async throws { let error = NSError(domain: .testDomain, code: 14, userInfo: [NSLocalizedDescriptionKey: String.testDescription]) let nsError = (error as NSError).withLocalizedFailure(.testLocalizedFailure) ALTAssertErrorsEqual(nsError, error, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) ALTAssertErrorFailureAndDescription(nsError, failure: .testLocalizedFailure, baseDescription: .testDescription) } func testReceivingAltServerError() async throws { for error in ALTServerError.testErrors { let codableError = CodableError(error: error) let jsonData = try JSONEncoder().encode(codableError) let decodedError = try Foundation.JSONDecoder().decode(CodableError.self, from: jsonData) let receivedError = decodedError.error ALTAssertErrorsEqual(receivedError, error) } } func testReceivingAltServerErrorWithLocalizedFailure() async throws { for error in ALTServerError.testErrors { let nsError = (error as NSError).withLocalizedFailure(.testLocalizedFailure) let altserverError = ALTServerError(nsError) let codableError = CodableError(error: altserverError) let jsonData = try JSONEncoder().encode(codableError) let decodedError = try Foundation.JSONDecoder().decode(CodableError.self, from: jsonData) let receivedError = decodedError.error let receivedNSError = receivedError as NSError let expectedLocalizedDescription = try String.testLocalizedFailure + " " + XCTUnwrap((error as NSError).localizedFailureReason) XCTAssertEqual(nsError.localizedDescription, expectedLocalizedDescription) XCTAssertEqual(receivedNSError.localizedFailure, .testLocalizedFailure) ALTAssertErrorsEqual(receivedError, error, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) } } func testReceivingAltServerErrorWithLocalizedTitle() async throws { for error in ALTServerError.testErrors { let nsError = (error as NSError).withLocalizedTitle(.testLocalizedTitle) let receivedError = try self.send(nsError) XCTAssertEqual(receivedError.localizedTitle, .testLocalizedTitle) ALTAssertErrorsEqual(receivedError, error, ignoring: [ALTLocalizedTitleErrorKey]) } } func testReceivingAltServerErrorThenAddingLocalizedFailure() async throws { for error in ALTServerError.testErrors { let receivedError = try self.send(error) let receivedNSError = (receivedError as NSError).withLocalizedFailure(.testLocalizedFailure) let expectedLocalizedDescription = try String.testLocalizedFailure + " " + XCTUnwrap((error as NSError).localizedFailureReason) XCTAssertEqual(receivedNSError.localizedDescription, expectedLocalizedDescription) XCTAssertEqual(receivedNSError.localizedFailure, .testLocalizedFailure) ALTAssertErrorsEqual(receivedNSError, error, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) } } func testReceivingAltServerErrorThenAddingLocalizedTitle() async throws { for error in ALTServerError.testErrors { let receivedError = try self.send(error) let receivedNSError = (receivedError as NSError).withLocalizedTitle(.testLocalizedTitle) XCTAssertEqual(receivedNSError.localizedTitle, .testLocalizedTitle) ALTAssertErrorsEqual(receivedNSError, error, ignoring: [ALTLocalizedTitleErrorKey]) } } func testReceivingAltServerErrorWithLocalizedFailureThenChangingLocalizedFailure() async throws { for error in ALTServerError.testErrors { let nsError = (error as NSError).withLocalizedFailure(.testOriginalLocalizedFailure) let receivedError = try self.send(nsError) let receivedNSError = (receivedError as NSError).withLocalizedFailure(.testLocalizedFailure) let expectedLocalizedDescription = try String.testLocalizedFailure + " " + XCTUnwrap(nsError.localizedFailureReason) XCTAssertEqual(receivedNSError.localizedDescription, expectedLocalizedDescription) XCTAssertEqual(receivedNSError.localizedFailure, .testLocalizedFailure) // Test that decoded error retains original localized failure. XCTAssertEqual((receivedError as NSError).localizedFailure, .testOriginalLocalizedFailure) ALTAssertErrorsEqual(receivedNSError, error, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) } } func testReceivingAltServerErrorWithLocalizedFailureThenAddingLocalizedTitle() async throws { for error in ALTServerError.testErrors { let nsError = (error as NSError).withLocalizedFailure(.testLocalizedFailure) let receivedError = try self.send(nsError) let receivedNSError = (receivedError as NSError).withLocalizedTitle(.testLocalizedTitle) let expectedLocalizedDescription = try String.testLocalizedFailure + " " + XCTUnwrap(nsError.localizedFailureReason) XCTAssertEqual(receivedNSError.localizedDescription, expectedLocalizedDescription) XCTAssertEqual(receivedNSError.localizedFailure, .testLocalizedFailure) XCTAssertEqual(receivedNSError.localizedTitle, .testLocalizedTitle) ALTAssertErrorsEqual(receivedNSError, error, ignoring: [NSLocalizedDescriptionKey, ALTLocalizedTitleErrorKey, NSLocalizedFailureErrorKey]) } } func testReceivingNonAltServerSwiftError() async throws { for error in allTestErrors { let receivedError = try self.send(error) try ALTAssertUnderlyingErrorEqualsError(receivedError, error) } } func testReceivingNonAltServerSwiftErrorWithSourceLocation() async throws { let file = #fileID let line = #line as UInt let error = OperationError.unknown(file: file, line: line) let receivedError = try self.send(error) XCTAssertEqual(receivedError.userInfo[ALTSourceFileErrorKey] as? String, file) if let uint = receivedError.userInfo[ALTSourceLineErrorKey] as? UInt { XCTAssertEqual(uint, line) } else if let int = receivedError.userInfo[ALTSourceLineErrorKey] as? Int { XCTAssertEqual(int, Int(line)) } try ALTAssertUnderlyingErrorEqualsError(receivedError, error) } func testReceivingNonAltServerSwiftErrorWithLocalizedFailure() async throws { for error in allTestErrors { let nsError = (error as NSError).withLocalizedFailure(.testLocalizedFailure) let receivedError = try self.send(nsError) let expectedLocalizedDescription = try String.testLocalizedFailure + " " + XCTUnwrap(nsError.localizedFailureReason) XCTAssertEqual(receivedError.localizedDescription, expectedLocalizedDescription) XCTAssertEqual(receivedError.localizedFailure, .testLocalizedFailure) let receivedUnderlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedError, error, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) XCTAssertEqual(receivedUnderlyingError.localizedDescription, expectedLocalizedDescription) XCTAssertEqual(receivedUnderlyingError.localizedFailure, .testLocalizedFailure) } } func testReceivingNonAltServerSwiftErrorThenAddingLocalizedFailure() async throws { for error in allTestErrors { let receivedError = try self.send(error) let receivedNSError = (receivedError as NSError).withLocalizedFailure(.testLocalizedFailure) let receivedUnderlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedNSError, error, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) XCTAssertEqual(receivedUnderlyingError.localizedDescription, error.errorFailureReason) XCTAssertNil(receivedUnderlyingError.localizedFailure) let expectedLocalizedDescription = try String.testLocalizedFailure + " " + XCTUnwrap((error as NSError).localizedFailureReason) XCTAssertEqual(receivedNSError.localizedDescription, expectedLocalizedDescription) XCTAssertEqual(receivedNSError.localizedFailure, .testLocalizedFailure) } } func testReceivingNonAltServerSwiftErrorThenAddingLocalizedTitle() async throws { for error in allTestErrors { let receivedError = try self.send(error) let receivedNSError = (receivedError as NSError).withLocalizedTitle(.testLocalizedTitle) XCTAssertNil(error.errorTitle) XCTAssertEqual(receivedNSError.localizedTitle, .testLocalizedTitle) let receivedUnderlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedNSError, error, ignoring: [ALTLocalizedTitleErrorKey]) XCTAssertNil(receivedUnderlyingError.localizedTitle) } } func testReceivingUnrecognizedNonAltServerSwiftError() async throws { enum MyError: Int, LocalizedError, CaseIterable { case strange case nothing var errorDescription: String? { switch self { case .strange: return "A strange error occured." case .nothing: return nil } } var recoverySuggestion: String? { return "Have you tried turning it off and on again?" } } for error in MyError.allCases { let receivedError = try self.send(error, clientProvider: [:]) try ALTAssertUnderlyingErrorEqualsError(receivedError, error) } } func testReceivingUnrecognizedNonAltServerSwiftErrorThenAddingLocalizedFailure() async throws { enum MyError: Int, LocalizedError, CaseIterable { case strange case nothing var errorDescription: String? { switch self { case .strange: return "A strange error occured." case .nothing: return nil } } var recoverySuggestion: String? { return "Have you tried turning it off and on again?" } } for error in MyError.allCases { let receivedError = try self.send(error, clientProvider: [:]) let receivedNSError = receivedError.withLocalizedFailure(.testLocalizedFailure) let receivedUnderlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedNSError, error, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) ALTAssertErrorFailureAndDescription(receivedNSError, failure: .testLocalizedFailure, baseDescription: error.localizedDescription) ALTAssertErrorFailureAndDescription(receivedUnderlyingError, failure: nil, baseDescription: error.localizedDescription) } } func testReceivingNonAltServerCocoaError() async throws { let error = CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: "~/Desktop/TestFile"]) let receivedError = try self.send(error) try ALTAssertUnderlyingErrorEqualsError(receivedError, error) } func testReceivingNonAltServerCocoaErrorWithLocalizedFailure() async throws { let error = CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: "~/Desktop/TestFile"]) let nsError = (error as NSError).withLocalizedFailure(.testLocalizedFailure) let receivedError = try self.send(nsError) try ALTAssertUnderlyingErrorEqualsError(receivedError, nsError) // Description == .testLocalizedFailure + failureReason ?? description ALTAssertErrorFailureAndDescription(receivedError, failure: .testLocalizedFailure, baseDescription: nsError.localizedFailureReason ?? error.localizedDescription) } func testReceivingAltServerConnectionError() async throws { let error = ALTServerConnectionError(.deviceLocked, userInfo: [ALTDeviceNameErrorKey: "Riley's iPhone"]) let nsError = error as NSError let receivedError = try self.send(nsError) let receivedUnderlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedError, nsError, ignoring: [ALTUnderlyingErrorCodeErrorKey, NSLocalizedFailureErrorKey, NSLocalizedDescriptionKey]) // Code == ALTServerError.connectionFailed XCTAssertEqual(receivedError.code, ALTServerError.connectionFailed.rawValue) // Underlying Code == ALTServerConnectionError.deviceLocked XCTAssertEqual(receivedUnderlyingError.code, ALTServerConnectionError.deviceLocked.rawValue) // Description == defaultFailure + error.localizedDescription let defaultFailure = try XCTUnwrap((ALTServerError(.connectionFailed) as NSError).localizedFailureReason) ALTAssertErrorFailureAndDescription(receivedError, failure: defaultFailure, baseDescription: error.localizedDescription) // Underlying Description = error.localizedDescription ALTAssertErrorFailureAndDescription(receivedUnderlyingError, failure: nil, baseDescription: error.localizedDescription) } func testReceivingAppleAPIError() async throws { let error = ALTAppleAPIError(.incorrectCredentials) let nsError = error as NSError let receivedError = try self.send(nsError, serverProvider: [NSDebugDescriptionErrorKey: .testDebugDescription]) // Debug Description == .testDebugDescription XCTAssertEqual(receivedError.localizedDebugDescription, .testDebugDescription) let receivedUnderlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedError, error, ignoring: [NSDebugDescriptionErrorKey]) // Debug Description == .testDebugDescription XCTAssertEqual(receivedUnderlyingError.localizedDebugDescription, .testDebugDescription) } func testReceivingCodableError() async throws { let json = "{'name2': 'riley'}" struct Test: Decodable { var name: String } let rawData = json.data(using: .utf8)! let error: DecodingError do { _ = try Foundation.JSONDecoder().decode(Test.self, from: rawData) return } catch let decodingError as DecodingError { error = decodingError } catch { XCTFail("Only DecodingErrors should be thrown.") return } let nsError = error as NSError let receivedError = try self.send(nsError) // Code == ALTServerError.invalidRequest // Description == CocoaError.coderReadCorrupt.localizedDescription XCTAssertEqual(receivedError.code, ALTServerError.invalidRequest.rawValue) XCTAssertEqual(receivedError.localizedDescription, CocoaError(.coderReadCorrupt).localizedDescription) let receivedUnderlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedError, nsError, ignoring: [ALTUnderlyingErrorCodeErrorKey, NSLocalizedDescriptionKey]) // Underlying Code == CocoaError.coderReadCorrupt // Underlying Description == CocoaError.coderReadCorrupt.localizedDescription XCTAssertEqual(receivedUnderlyingError.code, CocoaError.coderReadCorrupt.rawValue) XCTAssertEqual(receivedUnderlyingError.localizedDescription, CocoaError(.coderReadCorrupt).localizedDescription) } func testReceivingUnrecognizedAppleAPIError() async throws { let error = ALTAppleAPIError(.init(rawValue: -27)!) /* Alien Invasion */ let cachedError = error.serialized(provider: .unrecognizedProvider) let receivedError = try self.send(error, serverProvider: .unrecognizedProvider) // Description == .testUnrecognizedFailureReason // Recovery Suggestion == .testUnrecognizedRecoverySuggestion ALTAssertErrorFailureAndDescription(receivedError, failure: nil, baseDescription: .testUnrecognizedFailureReason) XCTAssertEqual(receivedError.localizedRecoverySuggestion, .testUnrecognizedRecoverySuggestion) let underlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedError, cachedError) // Underlying Description == .testUnrecognizedFailureReason // Underlying Recovery Suggestion == .testUnrecognizedRecoverySuggestion ALTAssertErrorFailureAndDescription(underlyingError, failure: nil, baseDescription: .testUnrecognizedFailureReason) XCTAssertEqual(underlyingError.localizedRecoverySuggestion, .testUnrecognizedRecoverySuggestion) } func testReceivingUnrecognizedAppleAPIErrorWithLocalizedFailure() async throws { let error = ALTAppleAPIError(.init(rawValue: -27)!) /* Alien Invasion */ let nsError = (error as NSError).withLocalizedFailure(.testLocalizedFailure) let receivedError = try self.send(nsError, serverProvider: [ NSLocalizedFailureReasonErrorKey: .testUnrecognizedFailureReason, NSLocalizedRecoverySuggestionErrorKey: .testUnrecognizedRecoverySuggestion ]) // Failure == .testLocalizedFailure // Description == Failure + .testUnrecognizedFailureReason // Recovery Suggestion == .testUnrecognizedRecoverySuggestion ALTAssertErrorFailureAndDescription(receivedError, failure: .testLocalizedFailure, baseDescription: .testUnrecognizedFailureReason) XCTAssertEqual(receivedError.localizedRecoverySuggestion, .testUnrecognizedRecoverySuggestion) let underlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedError, nsError, ignoring: [NSLocalizedFailureReasonErrorKey, NSLocalizedRecoverySuggestionErrorKey, NSLocalizedDescriptionKey]) // Underlying Failure == .testLocalizedFailure // Underlying Description == Failure + .testUnrecognizedFailureReason // Underlying Recovery Suggestion == .testUnrecognizedRecoverySuggestion ALTAssertErrorFailureAndDescription(underlyingError, failure: .testLocalizedFailure, baseDescription: .testUnrecognizedFailureReason) XCTAssertEqual(underlyingError.localizedRecoverySuggestion, .testUnrecognizedRecoverySuggestion) } func testReceivingUnrecognizedAppleAPIErrorThenAddingLocalizedFailure() async throws { let error = ALTAppleAPIError(.init(rawValue: -27)!) /* Alien Invasion */ let receivedError = try self.send(error, serverProvider: [ NSLocalizedFailureReasonErrorKey: .testUnrecognizedFailureReason, NSLocalizedRecoverySuggestionErrorKey: .testUnrecognizedRecoverySuggestion ]) let receivedNSError = (receivedError as NSError).withLocalizedFailure(.testLocalizedFailure) // Failure == .testLocalizedFailure // Description == Failure + .testUnrecognizedFailureReason // Recovery Suggestion == .testUnrecognizedRecoverySuggestion ALTAssertErrorFailureAndDescription(receivedNSError, failure: .testLocalizedFailure, baseDescription: .testUnrecognizedFailureReason) XCTAssertEqual(receivedNSError.localizedRecoverySuggestion, .testUnrecognizedRecoverySuggestion) let underlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedNSError, error, ignoring: [NSLocalizedFailureReasonErrorKey, NSLocalizedRecoverySuggestionErrorKey, NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) // Underlying Failure == nil // Underlying Description == .testUnrecognizedFailureReason // Underlying Recovery Suggestion == .testUnrecognizedRecoverySuggestion ALTAssertErrorFailureAndDescription(underlyingError, failure: nil, baseDescription: .testUnrecognizedFailureReason) XCTAssertEqual(underlyingError.localizedRecoverySuggestion, .testUnrecognizedRecoverySuggestion) } func testReceivingUnrecognizedAppleAPIErrorWithLocalizedFailureThenChangingLocalizedFailure() async throws { let error = ALTAppleAPIError(.init(rawValue: -27)!) /* Alien Invasion */ let nsError = (error as NSError).withLocalizedFailure(.testOriginalLocalizedFailure) let receivedError = try self.send(nsError, serverProvider: [ NSLocalizedFailureReasonErrorKey: .testUnrecognizedFailureReason, NSLocalizedRecoverySuggestionErrorKey: .testUnrecognizedRecoverySuggestion ]) // Failure == .testOriginalLocalizedFailure XCTAssertEqual(receivedError.localizedFailure, .testOriginalLocalizedFailure) let receivedNSError = (receivedError as NSError).withLocalizedFailure(.testLocalizedFailure) // Failure == .testLocalizedFailure // Description == Failure + .testUnrecognizedFailureReason // Recovery Suggestion == .testUnrecognizedRecoverySuggestion ALTAssertErrorFailureAndDescription(receivedNSError, failure: .testLocalizedFailure, baseDescription: .testUnrecognizedFailureReason) XCTAssertEqual(receivedNSError.localizedRecoverySuggestion, .testUnrecognizedRecoverySuggestion) let underlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedNSError, nsError, ignoring: [NSLocalizedFailureReasonErrorKey, NSLocalizedRecoverySuggestionErrorKey, NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) // Underlying Failure == .testOriginalLocalizedFailure // Underlying Description == Underlying Failure + .testUnrecognizedFailureReason // Underlying Recovery Suggestion == .testUnrecognizedRecoverySuggestion ALTAssertErrorFailureAndDescription(underlyingError, failure: .testOriginalLocalizedFailure, baseDescription: .testUnrecognizedFailureReason) XCTAssertEqual(underlyingError.localizedRecoverySuggestion, .testUnrecognizedRecoverySuggestion) } func testReceivingUnrecognizedObjCErrorsWithLocalizedFailureThenChangingLocalizedFailure() async throws { // User Info = nil var nsErrorWithNoUserInfo = NSError(domain: .testDomain, code: 14) nsErrorWithNoUserInfo = nsErrorWithNoUserInfo.withLocalizedFailure(.testOriginalLocalizedFailure) // User Info = Failure Reason var nsErrorWithUserInfoFailureReason = NSError(domain: .testDomain, code: 14, userInfo: [ NSLocalizedFailureReasonErrorKey: String.testUnrecognizedFailureReason ]) nsErrorWithUserInfoFailureReason = nsErrorWithUserInfoFailureReason.withLocalizedFailure(.testOriginalLocalizedFailure) // User Info = Description var nsErrorWithUserInfoDescription = NSError(domain: .testDomain, code: 14, userInfo: [ NSLocalizedDescriptionKey: String.testDebugDescription ]) nsErrorWithUserInfoDescription = nsErrorWithUserInfoDescription.withLocalizedFailure(.testOriginalLocalizedFailure) // User Info = Failure let nsErrorWithUserInfoFailure = NSError(domain: .testDomain, code: 14, userInfo: [ NSLocalizedFailureErrorKey: String.testOriginalLocalizedFailure ]) // User Info = Failure, Failure Reason let nsErrorWithUserInfoFailureAndFailureReason = NSError(domain: .testDomain, code: 14, userInfo: [ NSLocalizedFailureErrorKey: String.testOriginalLocalizedFailure, NSLocalizedFailureReasonErrorKey: String.testUnrecognizedFailureReason ]) // User Info = Failure, Description let nsErrorWithUserInfoFailureAndDescription = NSError(domain: .testDomain, code: 14, userInfo: [ NSLocalizedFailureErrorKey: String.testOriginalLocalizedFailure, NSLocalizedDescriptionKey: String.testDebugDescription ]) let errors = [nsErrorWithNoUserInfo, nsErrorWithUserInfoFailureReason, nsErrorWithUserInfoDescription, nsErrorWithUserInfoFailure, nsErrorWithUserInfoFailureAndFailureReason, nsErrorWithUserInfoFailureAndDescription] for nsError in [errors[0]] { let provider = [NSLocalizedFailureReasonErrorKey: String.testUnrecognizedFailureReason] // Use provider only if user info doesn't contain failure reason or localized description. let serverProvider = (nsError.userInfo.keys.contains(NSLocalizedFailureReasonErrorKey) || nsError.userInfo.keys.contains(NSLocalizedDescriptionKey)) ? nil : provider let baseDescription = serverProvider?[NSLocalizedFailureReasonErrorKey] ?? nsError.localizedFailureReason ?? .testDebugDescription let receivedError = try self.send(nsError, serverProvider: serverProvider) // Failure == .testOriginalLocalizedFailure XCTAssertEqual(receivedError.localizedFailure, .testOriginalLocalizedFailure) let receivedNSError = (receivedError as NSError).withLocalizedFailure(.testLocalizedFailure) // Failure == .testLocalizedFailure // Description == Failure + baseDescription ALTAssertErrorFailureAndDescription(receivedNSError, failure: .testLocalizedFailure, baseDescription: baseDescription) let underlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedNSError, nsError, ignoring: [NSLocalizedFailureReasonErrorKey, NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey]) // Underlying Failure == .testOriginalLocalizedFailure // Underlying Description == Underlying Failure + baseDescription ALTAssertErrorFailureAndDescription(underlyingError, failure: .testOriginalLocalizedFailure, baseDescription: serverProvider?[NSLocalizedFailureReasonErrorKey] ?? nsError.localizedFailureReason ?? .testDebugDescription) } } } extension AltTests { func testReceivingUnrecognizedAltServerError() async throws { let error = ALTServerError(.init(rawValue: -27)!) /* Alien Invasion */ let receivedError = try self.send(error, serverProvider: [ NSLocalizedFailureReasonErrorKey: .testUnrecognizedFailureReason, NSLocalizedRecoverySuggestionErrorKey: .testUnrecognizedRecoverySuggestion ]) // Description == .testUnrecognizedFailureReason // Failure Reason == .testUnrecognizedFailureReason // Recovery Suggestion == .testUnrecognizedRecoverySuggestion XCTAssertEqual(receivedError.localizedFailureReason, .testUnrecognizedFailureReason) XCTAssertEqual(receivedError.localizedRecoverySuggestion, .testUnrecognizedRecoverySuggestion) ALTAssertErrorFailureAndDescription(receivedError, failure: nil, baseDescription: .testUnrecognizedFailureReason) ALTAssertErrorsEqual(receivedError, error, ignoring: [NSLocalizedFailureReasonErrorKey, NSLocalizedRecoverySuggestionErrorKey, NSLocalizedDescriptionKey]) } func testReceivingUnrecognizedAltServerErrorWithLocalizedFailure() async throws { let error = ALTServerError(.init(rawValue: -27)!) /* Alien Invasion */ let nsError = (error as NSError).withLocalizedFailure(.testLocalizedFailure) let receivedError = try self.send(nsError, serverProvider: [ NSLocalizedFailureReasonErrorKey: .testUnrecognizedFailureReason, NSLocalizedRecoverySuggestionErrorKey: .testUnrecognizedRecoverySuggestion ]) // Failure == .testLocalizedFailure // Description == Failure + .testUnrecognizedFailureReason // Failure Reason == .testUnrecognizedFailureReason // Recovery Suggestion == .testUnrecognizedRecoverySuggestion ALTAssertErrorFailureAndDescription(receivedError, failure: .testLocalizedFailure, baseDescription: .testUnrecognizedFailureReason) XCTAssertEqual(receivedError.localizedFailureReason, .testUnrecognizedFailureReason) XCTAssertEqual(receivedError.localizedRecoverySuggestion, .testUnrecognizedRecoverySuggestion) ALTAssertErrorsEqual(receivedError, nsError, ignoring: [NSLocalizedFailureReasonErrorKey, NSLocalizedRecoverySuggestionErrorKey, NSLocalizedDescriptionKey]) } func testReceivingUnrecognizedAltServerErrorWithLocalizedTitle() async throws { let error = ALTServerError(.init(rawValue: -27)!) /* Alien Invasion */ let nsError = (error as NSError).withLocalizedTitle(.testLocalizedTitle) let referenceError = nsError.serialized(provider: .unrecognizedProvider) let receivedError = try self.send(nsError, serverProvider: .unrecognizedProvider) // Title == .testLocalizedTitle XCTAssertEqual(receivedError.localizedTitle, .testLocalizedTitle) ALTAssertErrorsEqual(receivedError, referenceError) } func testReceivingUnrecognizedAltServerErrorThenAddingLocalizedFailure() async throws { let error = ALTServerError(.init(rawValue: -27)!) /* Alien Invasion */ let serializedError = (error as NSError).serialized(provider: .unrecognizedProvider) let receivedError = try self.send(error, serverProvider: .unrecognizedProvider) // Failure == nil XCTAssertEqual(receivedError.localizedFailure, nil) let receivedNSError = receivedError.withLocalizedFailure(.testLocalizedFailure) // Failure == .testLocalizedFailure // Description == Failure + .testUnrecognizedFailureReason ALTAssertErrorFailureAndDescription(receivedNSError, failure: .testLocalizedFailure, baseDescription: .testUnrecognizedFailureReason) ALTAssertErrorsEqual(receivedNSError, serializedError, ignoring: [NSLocalizedFailureErrorKey, NSLocalizedDescriptionKey]) } func testReceivingUnrecognizedAltServerErrorThenAddingLocalizedTitle() async throws { let error = ALTServerError(.init(rawValue: -27)!) /* Alien Invasion */ let serializedError = error.serialized(provider: .unrecognizedProvider) let receivedError = try self.send(error, serverProvider: .unrecognizedProvider) // Title == nil XCTAssertEqual(receivedError.localizedTitle, nil) let receivedNSError = receivedError.withLocalizedTitle(.testLocalizedTitle) // Title == .testLocalizedTitle ALTAssertErrorsEqual(receivedNSError, serializedError, ignoring: [ALTLocalizedTitleErrorKey]) } func testReceivingUnrecognizedAltServerErrorWithLocalizedFailureThenChangingLocalizedFailure() async throws { let error = ALTServerError(.init(rawValue: -27)!) /* Alien Invasion */ let nsError = (error as NSError).withLocalizedFailure(.testOriginalLocalizedFailure) let serializedError = nsError.serialized(provider: .unrecognizedProvider) let receivedError = try self.send(nsError, serverProvider: .unrecognizedProvider) // Failure == .testOriginalLocalizedFailure XCTAssertEqual(receivedError.localizedFailure, .testOriginalLocalizedFailure) let receivedNSError = receivedError.withLocalizedFailure(.testLocalizedFailure) // Failure == .testLocalizedFailure // Description == Failure + .testUnrecognizedFailureReason ALTAssertErrorFailureAndDescription(receivedNSError, failure: .testLocalizedFailure, baseDescription: .testUnrecognizedFailureReason) ALTAssertErrorsEqual(receivedNSError, serializedError, ignoring: [NSLocalizedFailureErrorKey, NSLocalizedDescriptionKey]) } } extension AltTests { func testReceivingAltServerErrorWithDifferentErrorMessages() async throws { let error = ALTServerError(.pluginNotFound) let serializedError = error.serialized(provider: .unrecognizedProvider) let receivedError = try self.send(error, serverProvider: .unrecognizedProvider) // Description == error.localizedDescription (not .testUnrecognizedFailureReason) // Failure Reason == error.localizedFailureReason (not .testUnrecognizedFailureReason) // Recovery Suggestion == error.recoverySuggestion (not .testUnrecognizedRecoverySuggestion) XCTAssertEqual(receivedError.localizedDescription, error.localizedDescription) XCTAssertEqual(receivedError.localizedFailureReason, (error as NSError).localizedFailureReason) XCTAssertEqual(receivedError.localizedRecoverySuggestion, (error as NSError).localizedRecoverySuggestion) ALTAssertErrorsEqual(receivedError, serializedError, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureReasonErrorKey, NSLocalizedRecoverySuggestionErrorKey]) } func testReceivingAltServerErrorWithLocalizedFailureAndDifferentErrorMessages() async throws { let error = ALTServerError(.pluginNotFound) let nsError = (error as NSError).withLocalizedFailure(.testLocalizedFailure) let serializedError = nsError.serialized(provider: .unrecognizedProvider) let receivedError = try self.send(nsError, serverProvider: .unrecognizedProvider) // Failure == .testLocalizedFailure // Description == Failure + error.localizedFailureReason (not .testUnrecognizedFailureReason) // Failure Reason == error.localizedFailureReason (not .testUnrecognizedFailureReason) // Recovery Suggestion == error.recoverySuggestion (not .testUnrecognizedRecoverySuggestion) ALTAssertErrorFailureAndDescription(receivedError, failure: .testLocalizedFailure, baseDescription: try XCTUnwrap(nsError.localizedFailureReason)) XCTAssertEqual(receivedError.localizedFailureReason, (error as NSError).localizedFailureReason) XCTAssertEqual(receivedError.localizedRecoverySuggestion, (error as NSError).localizedRecoverySuggestion) ALTAssertErrorsEqual(receivedError, serializedError, ignoring: [NSLocalizedDescriptionKey, NSLocalizedFailureErrorKey, NSLocalizedFailureReasonErrorKey, NSLocalizedRecoverySuggestionErrorKey]) } } extension AltTests { func testReceivingUnrecognizedAltServerErrorThenAddingLocalizedFailureBeforeSerializing() async throws { let error = ALTServerError(.init(rawValue: -27)!) /* Alien Invasion */ let receivedError = try self.send(error, serverProvider: .unrecognizedProvider) let receivedNSError = receivedError.withLocalizedFailure(.testLocalizedFailure) let serializedError = receivedNSError.sanitizedForSerialization() // Failure == .testLocalizedFailure // Description == Failure + .testUnrecognizedFailureReason ALTAssertErrorFailureAndDescription(serializedError, failure: .testLocalizedFailure, baseDescription: .testUnrecognizedFailureReason) // Failure Reason == .testUnrecognizedFailureReason // Recovery Suggestion == .testUnrecognizedRecoverySuggestion ALTAssertErrorsEqual(serializedError, receivedNSError, ignoring: []) } func testAddingLocalizedFailureThenSerializing() async throws { let error = CocoaError(.fileReadNoSuchFile, userInfo: [NSURLErrorKey: URL(fileURLWithPath: "~/Users/rileytestut/delta")]) let nsError = (error as NSError).withLocalizedFailure(.testOriginalLocalizedFailure) let receivedError = try self.send(nsError) let receivedNSError = receivedError.withLocalizedFailure(.testLocalizedFailure) let serializedError = receivedNSError.sanitizedForSerialization() // Failure == .testLocalizedFailure // Description == Failure + .testUnrecognizedFailureReason ALTAssertErrorFailureAndDescription(serializedError, failure: .testLocalizedFailure, baseDescription: try XCTUnwrap(nsError.localizedFailureReason)) // Failure Reason == .testUnrecognizedFailureReason // Recovery Suggestion == .testUnrecognizedRecoverySuggestion ALTAssertErrorsEqual(serializedError, receivedNSError, ignoring: []) } func testSerializingUserInfoValues() async throws { let userInfo = [ "RSTString": "test", "RSTNumber": -1 as Int, "RSTUnsignedNumber": 2 as UInt, // "RSTURL": URL(string: "https://rileytestut.com")!, // URLs get converted to Strings "RSTArray": [1, "test"], "RSTDictionary": ["key1": 11, "key2": "string"] ] as [String: Any] let error = NSError(domain: .testDomain, code: 17, userInfo: userInfo) let receivedError = try self.send(error) let receivedUnderlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedError, error) let receivedUserInfo = receivedUnderlyingError.userInfo.filter { $0.key != NSLocalizedDescriptionKey } // Remove added NSLocalizedDescription value for unrecognized error. // let receivedURLString = try XCTUnwrap(receivedUserInfo["RSTURL"] as? String) // receivedUserInfo["RSTURL"] = URL(string: receivedURLString) XCTAssertEqual(receivedUserInfo as NSDictionary, userInfo as NSDictionary) } func testSerializingNonCodableUserInfoValues() async throws { struct MyStruct { var property = 1 } let userInfo = [ "MyStruct": MyStruct(), "RSTDictionary": ["key": MyStruct()], "RSTArray": [MyStruct()], ] as [String : Any] let error = NSError(domain: .testDomain, code: 17, userInfo: userInfo) let receivedError = try self.send(error) let receivedUnderlyingError = try ALTAssertUnderlyingErrorEqualsError(receivedError, error, ignoreExtraUserInfoValues: true) XCTAssertNil(receivedUnderlyingError.userInfo["MyStruct"]) XCTAssertFalse(receivedUnderlyingError.userInfo.keys.contains("MyStruct")) let dictionary = try XCTUnwrap(receivedUnderlyingError.userInfo["RSTDictionary"] as? [String: Any]) XCTAssertNil(dictionary["key"]) XCTAssertFalse(dictionary.keys.contains("key")) let array = try XCTUnwrap(receivedUnderlyingError.userInfo["RSTArray"] as? [Any]) XCTAssert(array.isEmpty) } }