From 51b1245ec38fdf1696922aa44133bf98913ca67d Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 25 Oct 2026 19:29:23 +0100 Subject: [PATCH] Map wrapped cancellation in the Amiibo live client to the cancelled error. --- Package.swift | 5 +- .../Public/Clients/AmiiboLiveClient.swift | 3 ++ .../Clients/AmiiboLiveClientTests.swift | 48 ++++++++++++++++++- .../Transports/ThrowingTransport.swift | 48 +++++++++++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 Tests/AmiiboService/Helpers/Transports/ThrowingTransport.swift diff --git a/Package.swift b/Package.swift index 11cae30..834dbf6 100644 --- a/Package.swift +++ b/Package.swift @@ -35,6 +35,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-openapi-generator.git", from: "1.11.0"), .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.5.0"), .package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.2"), + .package(url: "https://github.com/apple/swift-http-types", from: "1.0.0"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"), ], targets: [ @@ -52,7 +53,9 @@ let package = Package( .testTarget( name: AmiiboService.test, dependencies: [ - .byName(name: AmiiboService.target) + .byName(name: AmiiboService.target), + .product(name: "HTTPTypes", package: "swift-http-types"), + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime") ], path: "Tests/AmiiboService" ), diff --git a/Sources/AmiiboService/Public/Clients/AmiiboLiveClient.swift b/Sources/AmiiboService/Public/Clients/AmiiboLiveClient.swift index 8f408da..f62a7ff 100644 --- a/Sources/AmiiboService/Public/Clients/AmiiboLiveClient.swift +++ b/Sources/AmiiboService/Public/Clients/AmiiboLiveClient.swift @@ -370,7 +370,10 @@ private extension AmiiboLiveClient { case is CancellationError: throw AmiiboServiceError.cancelled case let clientError as ClientError: + // The underlying client wraps every error, including cancellation, in a `ClientError`. switch clientError.underlyingError { + case is CancellationError: + throw AmiiboServiceError.cancelled case is DecodingError: throw AmiiboServiceError.decoding case let urlError as URLError: diff --git a/Tests/AmiiboService/Cases/Public/Clients/AmiiboLiveClientTests.swift b/Tests/AmiiboService/Cases/Public/Clients/AmiiboLiveClientTests.swift index a577327..c523a8b 100644 --- a/Tests/AmiiboService/Cases/Public/Clients/AmiiboLiveClientTests.swift +++ b/Tests/AmiiboService/Cases/Public/Clients/AmiiboLiveClientTests.swift @@ -42,5 +42,51 @@ struct AmiiboLiveClientTests { // THEN #expect(url == expected) } - + + // MARK: Error mapping tests + + @Test( + "maps an error thrown by the transport to the matching service error", + arguments: zip( + TransportErrors.thrown, + TransportErrors.expected + ) + ) + func mapsTransportError( + thrown: any Error, + expected: AmiiboServiceError + ) async { + // GIVEN + let client = AmiiboLiveClient(transport: ThrowingTransport(error: thrown)) + + // WHEN + // THEN + await #expect(throws: expected) { + try await client.getAmiibos(by: .init()) + } + } + +} + +// MARK: - Arguments + +private enum TransportErrors { + /// Errors thrown by the transport. + static let thrown: [any Error] = [ + CancellationError(), + URLError(.cancelled), + URLError(.cannotParseResponse), + URLError(.notConnectedToInternet), + URLError(.timedOut), + URLError(.badURL) + ] + /// The service error expected for each thrown error. + static let expected: [AmiiboServiceError] = [ + .cancelled, + .cancelled, + .decoding, + .notAvailable, + .notAvailable, + .unknown(String(describing: URLError(.badURL))) + ] } diff --git a/Tests/AmiiboService/Helpers/Transports/ThrowingTransport.swift b/Tests/AmiiboService/Helpers/Transports/ThrowingTransport.swift new file mode 100644 index 0000000..c535080 --- /dev/null +++ b/Tests/AmiiboService/Helpers/Transports/ThrowingTransport.swift @@ -0,0 +1,48 @@ +// ===----------------------------------------------------------------------=== +// +// This source file is part of the Amiibo Service open source project +// +// Copyright (c) 2026 Röck+Cöde VoF. and the Amiibo Service project authors +// Licensed under Apache license v2.0 +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of Amiibo Service project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +// ===----------------------------------------------------------------------=== + +import Foundation +import HTTPTypes +import OpenAPIRuntime + +/// A transport that throws a given error instead of sending the request, for testing purposes. +/// +/// The error reaches the live client wrapped in a `ClientError`, as it would from a real transport. +struct ThrowingTransport: ClientTransport { + + // MARK: Properties + + /// The error to throw. + private let error: any Error + + // MARK: Initializers + + /// Initializes this transport. + /// - Parameter error: An error to throw. + init(error: any Error) { + self.error = error + } + + // MARK: Functions + + func send( + _ request: HTTPRequest, + body: HTTPBody?, + baseURL: URL, + operationID: String + ) async throws -> (HTTPResponse, HTTPBody?) { + throw error + } + +}