Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Modules/CommonsLib/Sources/CommonsLib/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ public struct Constants {
public static let HashType = "SHA256"
public static let Timeout = 120 // Seconds
public static let DefaultTimeout = 5 // Seconds
public static let MaxPollRetries = 3
}

public struct CryptoDefaultValues {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ public enum SmartIdError: Error {
case noResponse
case invalidSslHandshake
case technicalError
case requestInterrupted
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,10 @@ public enum SmartIdSessionStatusResponseCode: String, Sendable, Decodable {
case timeout = "TIMEOUT"
case documentUnusable = "DOCUMENT_UNUSABLE"
case wrongVc = "WRONG_VC"
case unknown = "UNKNOWN"

public init(from decoder: any Decoder) throws {
let rawValue = try decoder.singleValueContainer().decode(String.self)
self = SmartIdSessionStatusResponseCode(rawValue: rawValue) ?? .unknown
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ struct ResponseHandler: ResponseHandlerProtocol {
case .wrongVc: throw SmartIdError.wrongVC
case .documentUnusable: throw SmartIdError.documentUnusable
case .requiredInteractionNotSupportedByApp: throw SmartIdError.oldApi
case .unknown: throw SmartIdError.technicalError
default: break
}
}
Expand All @@ -69,17 +70,21 @@ struct ResponseHandler: ResponseHandlerProtocol {

func handleURLError(_ error: URLError) throws {
switch error.code {
case .notConnectedToInternet, .networkConnectionLost:
case .notConnectedToInternet:
throw SmartIdError.noInternetConnection
case .timedOut:
throw SmartIdError.timeout
case .networkConnectionLost, .timedOut:
throw SmartIdError.requestInterrupted
default:
throw SmartIdError.noInternetConnection
}
}

func handleStatusCodeError(_ statusCode: Int?) throws {
switch statusCode ?? -1 {
guard let statusCode else {
throw SmartIdError.requestInterrupted
}

switch statusCode {
case 400:
throw SmartIdError.incorrectParameters
case 401:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,6 @@ struct RequestPerformer: RequestPerfomerProtocol, Loggable {
}

} catch {
Task { @MainActor in
continuation.resume(throwing: SmartIdError.timeout)
return
}

continuation.resume(with: Result {
try responseHandler.handleCancellationError(error)
guard let smartIdError = error as? SmartIdError else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,42 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol, Loggable {
userAgent: String
) async throws -> SmartIdSessionResponse {
let pollingTimeoutMs = pollingTimeout * 1000
var retriedPolls = 0
var hasPolledSuccessfully = false

while true {
let sessionResponse: SmartIdSessionResponse? = try await requestPerformer.performRequest(
url: "\(url)/\(sessionId)",
method: .get,
parameters: ["timeoutMs": pollingTimeoutMs],
trustedCertificates: trustedCertificates,
proxyInfo: proxyInfo,
userAgent: userAgent
)
try Task.checkCancellation()

let sessionResponse: SmartIdSessionResponse?

do {
sessionResponse = try await requestPerformer.performRequest(
url: "\(url)/\(sessionId)",
method: .get,
parameters: ["timeoutMs": pollingTimeoutMs],
trustedCertificates: trustedCertificates,
proxyInfo: proxyInfo,
userAgent: userAgent
)
} catch let error as SmartIdError where Self.isRetryablePollError(error) {
retriedPolls += 1

guard retriedPolls <= Constants.Signing.MaxPollRetries else {
guard hasPolledSuccessfully, error == .underMaintenance else {
throw error
}

throw SmartIdError.timeout
}

if error == .underMaintenance, !hasPolledSuccessfully {
try await Task.sleep(for: .seconds(Double(pollingTimeout)))
}
continue
}

retriedPolls = 0
hasPolledSuccessfully = true

if let response = sessionResponse,
response.state == .complete {
Expand All @@ -128,6 +154,10 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol, Loggable {
}
}

private static func isRetryablePollError(_ error: SmartIdError) -> Bool {
error == .requestInterrupted || error == .underMaintenance
}

public func getVerificationCode(digest: Data) async -> String {
let code = UInt16(digest[digest.count - 2]) << 8 | UInt16(digest[digest.count - 1])
return String(format: "%04d", (code % 10000))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,25 @@ struct ResponseHandlerTests {
}
}

@Test(
"handleNetworkError_throwsRequestInterruptedWhenNoAnswerReachedTheApp",
arguments: [URLError.Code.timedOut, .networkConnectionLost]
)
func handleNetworkError_throwsRequestInterruptedWhenNoAnswerReachedTheApp(
code: URLError.Code
) {
let afError = AFError.sessionTaskFailed(error: URLError(code))

#expect(throws: SmartIdError.requestInterrupted) {
try handler.handleNetworkError(afError, statusCode: nil)
}
}

@Test
func handleNetworkError_throwsTimeoutWhenSessionTaskFailedErrorWithTimeout() {
let urlError = URLError(.timedOut)
let afError = AFError.sessionTaskFailed(error: urlError)
func handleNetworkError_throwsRequestInterruptedWhenNoUrlErrorAndNoStatusCode() {
let afError = AFError.serverTrustEvaluationFailed(reason: .noRequiredEvaluator(host: "host.test"))

#expect(throws: SmartIdError.timeout) {
#expect(throws: SmartIdError.requestInterrupted) {
try handler.handleNetworkError(afError, statusCode: nil)
}
}
Expand All @@ -197,6 +210,26 @@ struct ResponseHandlerTests {
}
}

@Test
func handleSessionResult_throwsTechnicalErrorWhenEndResultIsUnknown() {
#expect(throws: SmartIdError.technicalError) {
try handler.handleSessionResult(.unknown)
}
}

@Test
func decodingSessionResponse_mapsUnlistedEndResultToUnknown() throws {
let json = Data(
"""
{"state":"COMPLETE","result":{"endResult":"SERVER_ERROR"}}
""".utf8
)

let response = try JSONDecoder().decode(SmartIdSessionResponse.self, from: json)

#expect(response.result?.endResult == .unknown)
}

@Test
func handleNetworkError_throwsTechnicalErrorWhenUnacceptableStatusCode999Returned() {
let afError = AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: 999))
Expand Down
Loading
Loading