From ec0c0df78cd36656aa73acccd423c6024f468a03 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 7 Nov 2022 16:47:23 -0600 Subject: [PATCH] [AltTests] Adds error handling tests Passes all tests [Review] Refactors tests to be more readable Removes unnecessary code --- AltTests/AltTests.swift | 1206 +++++++++++++++++++++++++++++++++++++ AltTests/TestErrors.swift | 232 +++++++ 2 files changed, 1438 insertions(+) diff --git a/AltTests/AltTests.swift b/AltTests/AltTests.swift index 9fc9bcc8..f082ac1f 100644 --- a/AltTests/AltTests.swift +++ b/AltTests/AltTests.swift @@ -7,6 +7,48 @@ // 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 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(filePath: "~/Desktop/TestApp.ipa") +} final class AltTests: XCTestCase { @@ -20,3 +62,1167 @@ final class AltTests: XCTestCase // 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 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 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(filePath: "~/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) + } +} diff --git a/AltTests/TestErrors.swift b/AltTests/TestErrors.swift index 80386c51..c3171c5a 100644 --- a/AltTests/TestErrors.swift +++ b/AltTests/TestErrors.swift @@ -5,3 +5,235 @@ // Created by Riley Testut on 10/17/22. // Copyright © 2022 Riley Testut. All rights reserved. // + +import Foundation +@testable import AltStore +@testable import AltStoreCore + +import AltSign + +typealias TestError = TestErrorCode.Error +enum TestErrorCode: Int, ALTErrorEnum, CaseIterable +{ + static var errorDomain: String { + return "TestErrorDomain" + } + + case computerOnFire + case alienInvasion + + var errorFailureReason: String { + switch self + { + case .computerOnFire: return "Your computer is on fire." + case .alienInvasion: return "There is an ongoing alien invasion." + } + } +} + +extension TestError +{ + static var allErrors: [TestError] { + return Code.allCases.map { TestError($0) } + } + + var recoverySuggestion: String? { + switch self.code + { + case .computerOnFire: return "Try using a fire extinguisher!" + case .alienInvasion: return nil // Nothing you can do to stop the aliens. + } + } +} + +let allTestErrors = TestErrorCode.allCases.map { TestError($0) } + +extension ALTLocalizedError where Self.Code: ALTErrorEnum & CaseIterable +{ + static var testErrors: [DefaultLocalizedError] { + return Code.allCases.map { DefaultLocalizedError($0) } + } +} + +extension AuthenticationError +{ + static var testErrors: [AuthenticationError] { + return AuthenticationError.Code.allCases.map { code -> AuthenticationError in + return AuthenticationError(code) + } + } +} + + +extension VerificationError +{ + static var testErrors: [VerificationError] { + let app = ALTApplication(fileURL: Bundle.main.bundleURL)! + + return VerificationError.Code.allCases.map { code -> VerificationError in + switch code + { + case .privateEntitlements: return VerificationError.privateEntitlements(["dynamic-codesigning": true], app: app) + case .mismatchedBundleIdentifiers: return VerificationError.mismatchedBundleIdentifiers(sourceBundleID: "com.rileytestut.App", app: app) + case .iOSVersionNotSupported: return VerificationError.iOSVersionNotSupported(app: app, requiredOSVersion: OperatingSystemVersion(majorVersion: 21, minorVersion: 1, patchVersion: 0)) + } + } + } +} + +extension PatchAppError +{ + static var testErrors: [PatchAppError] { + PatchAppError.Code.allCases.map { code -> PatchAppError in + switch code + { + case .unsupportedOperatingSystemVersion: return PatchAppError(.unsupportedOperatingSystemVersion(.init(majorVersion: 15, minorVersion: 5, patchVersion: 1))) + } + } + } +} + +extension AltTests +{ + static var allLocalErrors: [any ALTLocalizedError] { + let errors = [ + OperationError.testErrors as [any ALTLocalizedError], + AuthenticationError.testErrors as [any ALTLocalizedError], + VerificationError.testErrors as [any ALTLocalizedError], + PatreonAPIError.testErrors as [any ALTLocalizedError], + RefreshError.testErrors as [any ALTLocalizedError], + PatchAppError.testErrors as [any ALTLocalizedError] + ].flatMap { $0 } + + return errors + } + + static var allRemoteErrors: [any Error] { + let errors: [any Error] = ALTServerError.testErrors + ALTAppleAPIError.testErrors + return errors + } + + static var allRealErrors: [any Error] { + return self.allLocalErrors + self.allRemoteErrors + } +} + +extension ALTServerError +{ + static var testErrors: [ALTServerError] { + [ +// ALTServerError(.underlyingError), // Doesn't occur in practice? But does mess up tests + + ALTServerError(.underlyingError, userInfo: [NSUnderlyingErrorKey: ALTServerError(.pluginNotFound)]), + ALTServerError(ALTServerError(.pluginNotFound)), + ALTServerError(TestError(.computerOnFire)), + ALTServerError(CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: "~/Desktop/TestFile"])), + + ALTServerError(.unknown), + + ALTServerError(.connectionFailed), + ALTServerError(ALTServerConnectionError(.timedOut, userInfo: [ALTUnderlyingErrorDomainErrorKey: "Mobile Image Mounter", + ALTUnderlyingErrorCodeErrorKey: -27, + ALTDeviceNameErrorKey: "Riley's iPhone"])), + + ALTServerError(.lostConnection), + ALTServerError(.deviceNotFound), + ALTServerError(.deviceWriteFailed), + ALTServerError(.invalidRequest), + ALTServerError(.invalidResponse), + ALTServerError(.invalidApp), + ALTServerError(.installationFailed), + ALTServerError(.maximumFreeAppLimitReached), + ALTServerError(.unsupportediOSVersion), + ALTServerError(.unknownRequest), + ALTServerError(.unknownResponse), + ALTServerError(.invalidAnisetteData), + ALTServerError(.pluginNotFound), + ALTServerError(.profileNotFound), + ALTServerError(.appDeletionFailed), + ALTServerError(.requestedAppNotRunning, userInfo: [ALTAppNameErrorKey: "Delta", ALTDeviceNameErrorKey: "Riley's iPhone"]), + ALTServerError(.incompatibleDeveloperDisk, userInfo: [ALTOperatingSystemNameErrorKey: "iOS", + ALTOperatingSystemVersionErrorKey: "13.0", + NSFilePathErrorKey: URL(fileURLWithPath: "~/Library/Application Support/com.rileytestut.AltServer/DeveloperDiskImages/iOS/13.0/DeveloperDiskImage.dmg").path]), + ] + } +} + +extension ALTAppleAPIError.Code: CaseIterable +{ + public static var allCases: [Self] { + return [.unknown, .invalidParameters, + .incorrectCredentials, .appSpecificPasswordRequired, + .noTeams, + .invalidDeviceID, .deviceAlreadyRegistered, + .invalidCertificateRequest, .certificateDoesNotExist, + .invalidAppIDName, .invalidBundleIdentifier, .bundleIdentifierUnavailable, .appIDDoesNotExist, .maximumAppIDLimitReached, + .invalidAppGroup, .appGroupDoesNotExist, + .invalidProvisioningProfileIdentifier, .provisioningProfileDoesNotExist, + .requiresTwoFactorAuthentication, .incorrectVerificationCode, .authenticationHandshakeFailed, + .invalidAnisetteData] + } +} + +extension ALTAppleAPIError +{ + static var testErrors: [Self] { + Code.allCases.map { code -> ALTAppleAPIError in + return ALTAppleAPIError(code) +// +// switch code +// { +// case .unknown: return ALTAppleAPIError(.unknown) +// case .invalidParameters: +// case .incorrectCredentials: +// case .appSpecificPasswordRequired: +// case .noTeams: +// case .invalidDeviceID: +// case .deviceAlreadyRegistered: +// case .invalidCertificateRequest: +// case .certificateDoesNotExist: +// case .invalidAppIDName: +// case .invalidBundleIdentifier: +// case .bundleIdentifierUnavailable: +// case .appIDDoesNotExist: +// case .maximumAppIDLimitReached: +// case .invalidAppGroup: +// case .appGroupDoesNotExist: +// case .invalidProvisioningProfileIdentifier: +// case .provisioningProfileDoesNotExist: +// case .requiresTwoFactorAuthentication: +// case .incorrectVerificationCode: +// case .authenticationHandshakeFailed: +// case .invalidAnisetteData: +// @unknown default: +// } + } + } +} + +extension OperationError +{ + static var testErrors: [OperationError] { + OperationError.Code.allCases.map { code -> OperationError in + switch code + { + case .unknown: return .unknown() + case .unknownResult: return .unknownResult + case .cancelled: return .cancelled + case .timedOut: return .timedOut + case .notAuthenticated: return .notAuthenticated + case .appNotFound: return .appNotFound(name: "Delta") + case .unknownUDID: return .unknownUDID + case .invalidApp: return .invalidApp + case .invalidParameters: return .invalidParameters + case .maximumAppIDLimitReached: return .maximumAppIDLimitReached(appName: "Delta", requiredAppIDs: 2, availableAppIDs: 1, expirationDate: Date()) + case .noSources: return .noSources + case .openAppFailed: return .openAppFailed(name: "Delta") + case .missingAppGroup: return .missingAppGroup + case .serverNotFound: return .serverNotFound + case .connectionFailed: return .connectionFailed + case .connectionDropped: return .connectionDropped + } + } + } +}