mirror of
https://github.com/ProtonMail/protoncore_ios.git
synced 2026-01-16 23:00:24 +00:00
feature(networking): Pass response dictionary within error for JSON API
Refs: https://gitlab.protontech.ch/apple/shared/protoncore/-/merge_requests/1493
This commit is contained in:
commit
58ca45d3a1
5 changed files with 140 additions and 3 deletions
|
|
@ -56,16 +56,23 @@ extension ErrorResponse {
|
|||
}
|
||||
|
||||
public extension NSError {
|
||||
|
||||
var responseDictionary: JSONDictionary? {
|
||||
userInfo[ResponseError.responseDictionaryUserInfoKey] as? JSONDictionary
|
||||
}
|
||||
|
||||
convenience init(_ serverError: ErrorResponse) {
|
||||
self.init(domain: "ProtonCore-Networking", code: serverError.code,
|
||||
localizedDescription: serverError.error,
|
||||
localizedFailureReason: serverError.errorDescription)
|
||||
}
|
||||
|
||||
convenience init(domain: String, code: Int,
|
||||
convenience init(domain: String,
|
||||
code: Int,
|
||||
responseDictionary: JSONDictionary? = nil,
|
||||
localizedDescription: String,
|
||||
localizedFailureReason: String? = nil, localizedRecoverySuggestion: String? = nil) {
|
||||
var userInfo = [NSLocalizedDescriptionKey: localizedDescription]
|
||||
var userInfo: [String: Any] = [NSLocalizedDescriptionKey: localizedDescription]
|
||||
|
||||
if let localizedFailureReason = localizedFailureReason {
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = localizedFailureReason
|
||||
|
|
@ -74,6 +81,10 @@ public extension NSError {
|
|||
if let localizedRecoverySuggestion = localizedRecoverySuggestion {
|
||||
userInfo[NSLocalizedRecoverySuggestionErrorKey] = localizedRecoverySuggestion
|
||||
}
|
||||
|
||||
if let responseDictionary {
|
||||
userInfo[ResponseError.responseDictionaryUserInfoKey] = responseDictionary
|
||||
}
|
||||
|
||||
self.init(domain: domain, code: code, userInfo: userInfo)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ public enum ResponseErrorDomains: String {
|
|||
}
|
||||
|
||||
public struct ResponseError: Error, Equatable {
|
||||
|
||||
public static let responseDictionaryUserInfoKey = "responseDictionaryUserInfoKey"
|
||||
|
||||
/// This is the http status code, like 200, 404, 500 etc. It will be nil if there was no http response,
|
||||
/// for example in case of timeout
|
||||
|
|
@ -44,20 +46,48 @@ public struct ResponseError: Error, Equatable {
|
|||
|
||||
public let userFacingMessage: String?
|
||||
public let underlyingError: NSError?
|
||||
|
||||
public let responseDictionary: [String: Any]?
|
||||
|
||||
public var bestShotAtReasonableErrorCode: Int {
|
||||
responseCode ?? httpCode ?? underlyingError?.code ?? (self as NSError).code
|
||||
}
|
||||
|
||||
public init(httpCode: Int?, responseCode: Int?, userFacingMessage: String?, underlyingError: NSError?) {
|
||||
if let responseDictionary = underlyingError?.responseDictionary {
|
||||
let strippedUnderlyingError = underlyingError.map {
|
||||
var userInfo = $0.userInfo
|
||||
userInfo.removeValue(forKey: ResponseError.responseDictionaryUserInfoKey)
|
||||
return NSError(domain: $0.domain, code: $0.code, userInfo: userInfo)
|
||||
}
|
||||
self.init(httpCode: httpCode,
|
||||
responseCode: responseCode,
|
||||
userFacingMessage: userFacingMessage,
|
||||
responseDictionary: responseDictionary,
|
||||
underlyingError: strippedUnderlyingError)
|
||||
} else {
|
||||
self.init(httpCode: httpCode,
|
||||
responseCode: responseCode,
|
||||
userFacingMessage: userFacingMessage,
|
||||
responseDictionary: nil,
|
||||
underlyingError: underlyingError)
|
||||
}
|
||||
}
|
||||
|
||||
private init(httpCode: Int?, responseCode: Int?, userFacingMessage: String?, responseDictionary: JSONDictionary?, underlyingError: NSError?) {
|
||||
self.httpCode = httpCode
|
||||
self.responseCode = responseCode
|
||||
self.userFacingMessage = userFacingMessage
|
||||
self.responseDictionary = responseDictionary
|
||||
self.underlyingError = underlyingError
|
||||
}
|
||||
|
||||
public func withUpdated(userFacingMessage: String) -> ResponseError {
|
||||
ResponseError(httpCode: httpCode, responseCode: responseCode, userFacingMessage: userFacingMessage, underlyingError: underlyingError)
|
||||
ResponseError(httpCode: httpCode,
|
||||
responseCode: responseCode,
|
||||
userFacingMessage: userFacingMessage,
|
||||
responseDictionary: responseDictionary,
|
||||
underlyingError: underlyingError)
|
||||
}
|
||||
|
||||
public func withUpdated(underlyingError: LocalizedError) -> ResponseError {
|
||||
|
|
@ -66,6 +96,23 @@ public struct ResponseError: Error, Equatable {
|
|||
userFacingMessage: underlyingError.localizedDescription,
|
||||
underlyingError: underlyingError as NSError)
|
||||
}
|
||||
|
||||
public static func == (lhs: ResponseError, rhs: ResponseError) -> Bool {
|
||||
// the response dictionaries are ignored
|
||||
lhs.httpCode == rhs.httpCode &&
|
||||
lhs.responseCode == rhs.responseCode &&
|
||||
lhs.userFacingMessage == rhs.userFacingMessage &&
|
||||
lhs.underlyingError == rhs.underlyingError
|
||||
}
|
||||
}
|
||||
|
||||
extension ResponseError: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
public var description: String {
|
||||
errorDescription ?? ""
|
||||
}
|
||||
public var debugDescription: String {
|
||||
errorDescription ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
extension ResponseError: LocalizedError {
|
||||
|
|
|
|||
|
|
@ -353,12 +353,14 @@ public class PMAPIService: APIService {
|
|||
error = NSError(
|
||||
domain: ResponseErrorDomains.withResponseCode.rawValue,
|
||||
code: responseCode,
|
||||
responseDictionary: dict,
|
||||
localizedDescription: dict["Error"] as? String ?? ""
|
||||
)
|
||||
} else {
|
||||
error = NSError(
|
||||
domain: ResponseErrorDomains.withStatusCode.rawValue,
|
||||
code: httpResponse.statusCode,
|
||||
responseDictionary: dict,
|
||||
localizedDescription: dict["Error"] as? String ?? ""
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,4 +124,57 @@ final class APIServiceErrorTests: XCTestCase {
|
|||
XCTAssertEqual(error.code, 123)
|
||||
XCTAssertEqual(error.localizedDescription, "test message")
|
||||
}
|
||||
|
||||
func testResponseError_CarriesResponseDict() throws {
|
||||
let originalResponseDict: [String: Any] = ["string": "test string", "number": 24]
|
||||
let underlyingError = NSError(domain: "tests",
|
||||
code: 42,
|
||||
responseDictionary: originalResponseDict,
|
||||
localizedDescription: "test localizedDescription")
|
||||
let testError = ResponseError(httpCode: 200,
|
||||
responseCode: 1000,
|
||||
userFacingMessage: "test facing message",
|
||||
underlyingError: underlyingError)
|
||||
let passedResponseDict = try XCTUnwrap(testError.responseDictionary)
|
||||
XCTAssertTrue((passedResponseDict as NSDictionary).isEqual(to: originalResponseDict))
|
||||
}
|
||||
|
||||
func testResponseError_RemovesResponseDictFromUnderlyingError() throws {
|
||||
let underlyingError = NSError(domain: "tests",
|
||||
code: 42,
|
||||
responseDictionary: ["string": "test string", "number": 24],
|
||||
localizedDescription: "test localizedDescription")
|
||||
let testError = ResponseError(httpCode: 200,
|
||||
responseCode: 1000,
|
||||
userFacingMessage: "test facing message",
|
||||
underlyingError: underlyingError)
|
||||
let passedUnderlyingError = try XCTUnwrap(testError.underlyingError)
|
||||
XCTAssertNotNil(underlyingError.userInfo[ResponseError.responseDictionaryUserInfoKey])
|
||||
XCTAssertNil(passedUnderlyingError.userInfo[ResponseError.responseDictionaryUserInfoKey])
|
||||
}
|
||||
|
||||
func testResponseError_DoesNotIncludeResponseDictWhenPrinted() throws {
|
||||
let testString = "test string"
|
||||
let testNumber = 24
|
||||
let underlyingError = NSError(domain: "tests",
|
||||
code: 42,
|
||||
responseDictionary: ["string": "test string", "number": 24],
|
||||
localizedDescription: "test localizedDescription")
|
||||
let testError = ResponseError(httpCode: 200,
|
||||
responseCode: 1000,
|
||||
userFacingMessage: nil,
|
||||
underlyingError: underlyingError)
|
||||
let interpolatedError = "\(testError)"
|
||||
let errorDescription = testError.localizedDescription
|
||||
let errorUserFacingMessage = testError.userFacingMessage ?? ""
|
||||
let stringDescribingError = String(describing: testError)
|
||||
XCTAssertFalse(interpolatedError.contains(testString))
|
||||
XCTAssertFalse(interpolatedError.contains("\(testNumber)"))
|
||||
XCTAssertFalse(errorDescription.contains(testString))
|
||||
XCTAssertFalse(errorDescription.contains("\(testNumber)"))
|
||||
XCTAssertFalse(errorUserFacingMessage.contains(testString))
|
||||
XCTAssertFalse(errorUserFacingMessage.contains("\(testNumber)"))
|
||||
XCTAssertFalse(stringDescribingError.contains(testString))
|
||||
XCTAssertFalse(stringDescribingError.contains("\(testNumber)"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,30 @@ final class PMAPIServiceRequestTests: XCTestCase {
|
|||
XCTAssertEqual(error.code, 404)
|
||||
}
|
||||
|
||||
func testRequestWithJSONReturnsResponseDictionaryInError() async throws {
|
||||
let originalResponseDict: [String: Any] = ["Code": 4242, "string": "test string", "number": 24]
|
||||
let service = testService
|
||||
sessionMock.generateStub.bodyIs { _, method, url, params, time, retryPolicy in
|
||||
SessionRequest(parameters: params, urlString: url, method: method, timeout: time ?? 30.0, retryPolicy: retryPolicy)
|
||||
}
|
||||
|
||||
sessionMock.requestJSONStub.bodyIs { _, _, _, completion in
|
||||
completion(
|
||||
URLSessionDataTaskMock(response: HTTPURLResponse(statusCode: 404)),
|
||||
.success(originalResponseDict)
|
||||
)
|
||||
}
|
||||
|
||||
let result = await withCheckedContinuation { continuation in
|
||||
service.request(method: .get, path: "/unit/tests", parameters: nil, headers: nil, authenticated: false, authRetry: true,
|
||||
customAuthCredential: nil, nonDefaultTimeout: nil, retryPolicy: .userInitiated, jsonCompletion: optionalContinuation(continuation))
|
||||
}
|
||||
|
||||
let error = try XCTUnwrap(result.error)
|
||||
let responseDictionary = try XCTUnwrap(error.responseDictionary)
|
||||
XCTAssertTrue((responseDictionary as NSDictionary).isEqual(to: originalResponseDict))
|
||||
}
|
||||
|
||||
// MARK: - Deprecated API
|
||||
|
||||
@available(*, deprecated, message: "testing deprecated api")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue