From e0c7ca6cf6a448c725f94b035a440eb944020409 Mon Sep 17 00:00:00 2001 From: Vitor Conceicao Date: Sun, 22 Feb 2026 12:31:50 -0300 Subject: [PATCH 1/5] refactor: use typed error on home service --- .../Protocols/HomeServiceProtocol.swift | 4 +-- .../Home/Sources/Service/HomeService.swift | 26 ++++++++++++++----- .../Sources/ViewModel/HomeViewModel.swift | 4 ++- .../Doubles/HomeServiceProtocolSpy.swift | 8 +++--- .../Doubles/NetworkServiceProtocolSpy.swift | 4 +-- .../Tests/ViewModel/HomeViewModelTests.swift | 10 ++++--- 6 files changed, 37 insertions(+), 19 deletions(-) diff --git a/Projects/Home/Sources/Protocols/HomeServiceProtocol.swift b/Projects/Home/Sources/Protocols/HomeServiceProtocol.swift index d541887..9f147c0 100644 --- a/Projects/Home/Sources/Protocols/HomeServiceProtocol.swift +++ b/Projects/Home/Sources/Protocols/HomeServiceProtocol.swift @@ -10,6 +10,6 @@ import HomeInterfaces import Foundation protocol HomeServiceProtocol: AnyObject, Sendable { - func fetchExchangesList(page: Int, limit: Int) async throws -> [ExchangeSummary] - func fetchDetailsFor(ids: [String]) async throws -> [ExchangeDetail] + func fetchExchangesList(page: Int, limit: Int) async throws(ServiceError) -> [ExchangeSummary] + func fetchDetailsFor(ids: [String]) async throws(ServiceError) -> [ExchangeDetail] } diff --git a/Projects/Home/Sources/Service/HomeService.swift b/Projects/Home/Sources/Service/HomeService.swift index 041d67b..33228da 100644 --- a/Projects/Home/Sources/Service/HomeService.swift +++ b/Projects/Home/Sources/Service/HomeService.swift @@ -12,6 +12,7 @@ import NetworkingInterfaces enum ServiceError: Error, Equatable { case decodeFail(String?) + case requestError(String?) case network(Status) } @@ -22,8 +23,14 @@ final class HomeService: Sendable { self.networkService = networkService } - private func performRequest(endpoint: APIEndpointProtocol) async throws -> T { - let (data, httpUrlResponse) = try await networkService.request(endpoint: endpoint) + private func performRequest(endpoint: APIEndpointProtocol) async throws(ServiceError) -> T { + let (data, httpUrlResponse): (Data, HTTPURLResponse) + + do { + (data, httpUrlResponse) = try await networkService.request(endpoint: endpoint) + } catch { + throw ServiceError.requestError(error.localizedDescription) + } switch httpUrlResponse.statusCode { case 200...299: @@ -34,20 +41,25 @@ final class HomeService: Sendable { } } - func decodeResponse(_ data: Data) throws -> T { - let decoder = JSONDecoder() - return try decoder.decode(T.self, from: data) + func decodeResponse(_ data: Data) throws(ServiceError) -> T { + do { + let decoder = JSONDecoder() + return try decoder.decode(T.self, from: data) + } catch { + throw ServiceError.decodeFail(error.localizedDescription) + } + } } extension HomeService: HomeServiceProtocol { - func fetchExchangesList(page: Int, limit: Int) async throws -> [ExchangeSummary] { + func fetchExchangesList(page: Int, limit: Int) async throws(ServiceError) -> [ExchangeSummary] { let endpoint = HomeEndpoint.fetchItems(page: page, limit: limit) let response: ExchangeResponse = try await performRequest(endpoint: endpoint) return response.data } - func fetchDetailsFor(ids: [String]) async throws -> [ExchangeDetail] { + func fetchDetailsFor(ids: [String]) async throws(ServiceError) -> [ExchangeDetail] { let endpoint = HomeEndpoint.fetchDetail(ids: ids) let response: ExchangeDetailResponse = try await performRequest(endpoint: endpoint) return Array(response.data.values) diff --git a/Projects/Home/Sources/ViewModel/HomeViewModel.swift b/Projects/Home/Sources/ViewModel/HomeViewModel.swift index 79b5190..6fb6560 100644 --- a/Projects/Home/Sources/ViewModel/HomeViewModel.swift +++ b/Projects/Home/Sources/ViewModel/HomeViewModel.swift @@ -93,11 +93,13 @@ final class HomeViewModel: HomeViewModelProtocol { switch serviceError { case .decodeFail, .none: message = defaultMessage - case .network(let status): + case let .network(status): message = status.errorMessage ?? defaultMessage if let statusCode = status.errorCode { code = String(statusCode) } + case let .requestError(error): + message = error ?? defaultMessage } state = .error(message, code) } else { diff --git a/Projects/Home/Tests/Doubles/HomeServiceProtocolSpy.swift b/Projects/Home/Tests/Doubles/HomeServiceProtocolSpy.swift index a98bf23..49b843b 100644 --- a/Projects/Home/Tests/Doubles/HomeServiceProtocolSpy.swift +++ b/Projects/Home/Tests/Doubles/HomeServiceProtocolSpy.swift @@ -19,10 +19,10 @@ final class HomeServiceProtocolSpy: HomeServiceProtocol, Sendable { } var calledMethods: [Method] = [] - var fetchExchangesListResult: Result<[ExchangeSummary], Error>? - var fetchDetailsForResult: Result<[ExchangeDetail], Error>? + var fetchExchangesListResult: Result<[ExchangeSummary], ServiceError>? + var fetchDetailsForResult: Result<[ExchangeDetail], ServiceError>? - func fetchExchangesList(page: Int, limit: Int) async throws -> [ExchangeSummary] { + func fetchExchangesList(page: Int, limit: Int) async throws(ServiceError) -> [ExchangeSummary] { await MainActor.run { calledMethods.append(.fetchExchangesList) } @@ -34,7 +34,7 @@ final class HomeServiceProtocolSpy: HomeServiceProtocol, Sendable { return try result.get() } - func fetchDetailsFor(ids: [String]) async throws -> [ExchangeDetail] { + func fetchDetailsFor(ids: [String]) async throws(ServiceError) -> [ExchangeDetail] { await MainActor.run { calledMethods.append(.fetchDetailsFor) } diff --git a/Projects/Home/Tests/Doubles/NetworkServiceProtocolSpy.swift b/Projects/Home/Tests/Doubles/NetworkServiceProtocolSpy.swift index da4fd58..086cab3 100644 --- a/Projects/Home/Tests/Doubles/NetworkServiceProtocolSpy.swift +++ b/Projects/Home/Tests/Doubles/NetworkServiceProtocolSpy.swift @@ -15,9 +15,9 @@ final class NetworkServiceProtocolSpy: NetworkServiceProtocol, @unchecked Sendab } var calledMethods: [Method] = [] - var requestResult: Result<(Data, HTTPURLResponse), Error>? + var requestResult: Result<(Data, HTTPURLResponse), NetworkError>? - func request(endpoint: APIEndpointProtocol) async throws -> (Data, HTTPURLResponse) { + func request(endpoint: APIEndpointProtocol) async throws(NetworkError) -> (Data, HTTPURLResponse) { calledMethods.append(.request) guard let result = requestResult else { diff --git a/Projects/Home/Tests/ViewModel/HomeViewModelTests.swift b/Projects/Home/Tests/ViewModel/HomeViewModelTests.swift index 680a6a9..58051a8 100644 --- a/Projects/Home/Tests/ViewModel/HomeViewModelTests.swift +++ b/Projects/Home/Tests/ViewModel/HomeViewModelTests.swift @@ -35,8 +35,12 @@ struct HomeViewModelTests { func testLoadDataError() async throws { let (sut, doubles) = makeSut() let messageError = "Failed to Load data. Press try again later or check your connection." - let error = NSError(domain: "test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Network error"]) - doubles.serviceSpy.fetchExchangesListResult = .failure(error) + let serviceError = ServiceError.network(.init(timestamp: "", + errorCode: 1, + errorMessage: messageError, + elapsed: 0, + creditCount: 0)) + doubles.serviceSpy.fetchExchangesListResult = .failure(serviceError) sut.loadData() @@ -45,7 +49,7 @@ struct HomeViewModelTests { #expect(doubles.serviceSpy.calledMethods.contains(.fetchExchangesList)) #expect(sut.numberOfItems == 0) #expect(doubles.delegateSpy.calledMethods == [.didUpdateState(.loading), - .didUpdateState(.error(messageError, nil))]) + .didUpdateState(.error(messageError, "1"))]) } @Test("GIVEN empty state WHEN loadData returns empty array THEN updates state to empty") From 8395d606a4c46ce349e9f91b199a387574068708 Mon Sep 17 00:00:00 2001 From: Vitor Conceicao Date: Sun, 22 Feb 2026 12:38:25 -0300 Subject: [PATCH 2/5] refactor: make home services conform to concurrent --- Projects/Home/Sources/Protocols/HomeServiceProtocol.swift | 2 ++ Projects/Home/Sources/Service/HomeService.swift | 5 ++++- Projects/Home/Tests/Doubles/HomeServiceProtocolSpy.swift | 8 +++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Projects/Home/Sources/Protocols/HomeServiceProtocol.swift b/Projects/Home/Sources/Protocols/HomeServiceProtocol.swift index 9f147c0..fd315ec 100644 --- a/Projects/Home/Sources/Protocols/HomeServiceProtocol.swift +++ b/Projects/Home/Sources/Protocols/HomeServiceProtocol.swift @@ -10,6 +10,8 @@ import HomeInterfaces import Foundation protocol HomeServiceProtocol: AnyObject, Sendable { + @concurrent func fetchExchangesList(page: Int, limit: Int) async throws(ServiceError) -> [ExchangeSummary] + @concurrent func fetchDetailsFor(ids: [String]) async throws(ServiceError) -> [ExchangeDetail] } diff --git a/Projects/Home/Sources/Service/HomeService.swift b/Projects/Home/Sources/Service/HomeService.swift index 33228da..b7c3ce6 100644 --- a/Projects/Home/Sources/Service/HomeService.swift +++ b/Projects/Home/Sources/Service/HomeService.swift @@ -17,12 +17,13 @@ enum ServiceError: Error, Equatable { } final class HomeService: Sendable { - private nonisolated(unsafe) let networkService: NetworkServiceProtocol + private let networkService: NetworkServiceProtocol init(networkService: NetworkServiceProtocol) { self.networkService = networkService } + @concurrent private func performRequest(endpoint: APIEndpointProtocol) async throws(ServiceError) -> T { let (data, httpUrlResponse): (Data, HTTPURLResponse) @@ -53,12 +54,14 @@ final class HomeService: Sendable { } extension HomeService: HomeServiceProtocol { + @concurrent func fetchExchangesList(page: Int, limit: Int) async throws(ServiceError) -> [ExchangeSummary] { let endpoint = HomeEndpoint.fetchItems(page: page, limit: limit) let response: ExchangeResponse = try await performRequest(endpoint: endpoint) return response.data } + @concurrent func fetchDetailsFor(ids: [String]) async throws(ServiceError) -> [ExchangeDetail] { let endpoint = HomeEndpoint.fetchDetail(ids: ids) let response: ExchangeDetailResponse = try await performRequest(endpoint: endpoint) diff --git a/Projects/Home/Tests/Doubles/HomeServiceProtocolSpy.swift b/Projects/Home/Tests/Doubles/HomeServiceProtocolSpy.swift index 49b843b..23b3087 100644 --- a/Projects/Home/Tests/Doubles/HomeServiceProtocolSpy.swift +++ b/Projects/Home/Tests/Doubles/HomeServiceProtocolSpy.swift @@ -13,7 +13,7 @@ import HomeInterfaces @MainActor final class HomeServiceProtocolSpy: HomeServiceProtocol, Sendable { - enum Method: Equatable { + enum Method: Equatable, Sendable { case fetchExchangesList case fetchDetailsFor } @@ -22,24 +22,26 @@ final class HomeServiceProtocolSpy: HomeServiceProtocol, Sendable { var fetchExchangesListResult: Result<[ExchangeSummary], ServiceError>? var fetchDetailsForResult: Result<[ExchangeDetail], ServiceError>? + @concurrent func fetchExchangesList(page: Int, limit: Int) async throws(ServiceError) -> [ExchangeSummary] { await MainActor.run { calledMethods.append(.fetchExchangesList) } - guard let result = fetchExchangesListResult else { + guard let result = await fetchExchangesListResult else { fatalError("fetchExchangesListResult not set") } return try result.get() } + @concurrent func fetchDetailsFor(ids: [String]) async throws(ServiceError) -> [ExchangeDetail] { await MainActor.run { calledMethods.append(.fetchDetailsFor) } - guard let result = fetchDetailsForResult else { + guard let result = await fetchDetailsForResult else { fatalError("fetchDetailsForResult not set") } From 4b7d4bc30e1b4f6fca273df22672cfb12449d01a Mon Sep 17 00:00:00 2001 From: Vitor Conceicao Date: Sun, 22 Feb 2026 12:53:24 -0300 Subject: [PATCH 3/5] test: make network service spy thread safety and adjust home and detail tests --- .../Doubles/NetworkServiceProtocolSpy.swift | 14 +++++++++----- .../Tests/Service/DetailServiceTests.swift | 12 ++++++------ .../Doubles/NetworkServiceProtocolSpy.swift | 12 ++++++++---- .../Home/Tests/Service/HomeServiceTests.swift | 16 ++++++++-------- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/Projects/Detail/Tests/Doubles/NetworkServiceProtocolSpy.swift b/Projects/Detail/Tests/Doubles/NetworkServiceProtocolSpy.swift index da4fd58..57c1249 100644 --- a/Projects/Detail/Tests/Doubles/NetworkServiceProtocolSpy.swift +++ b/Projects/Detail/Tests/Doubles/NetworkServiceProtocolSpy.swift @@ -9,15 +9,15 @@ import Foundation import NetworkingInterfaces -final class NetworkServiceProtocolSpy: NetworkServiceProtocol, @unchecked Sendable { - enum Method: Equatable { +actor NetworkServiceProtocolSpy: NetworkServiceProtocol, Sendable { + enum Method: Equatable, Sendable { case request } - var calledMethods: [Method] = [] - var requestResult: Result<(Data, HTTPURLResponse), Error>? + private(set) var calledMethods: [Method] = [] + private(set) var requestResult: Result<(Data, HTTPURLResponse), NetworkError>? - func request(endpoint: APIEndpointProtocol) async throws -> (Data, HTTPURLResponse) { + func request(endpoint: APIEndpointProtocol) async throws(NetworkError) -> (Data, HTTPURLResponse) { calledMethods.append(.request) guard let result = requestResult else { @@ -26,4 +26,8 @@ final class NetworkServiceProtocolSpy: NetworkServiceProtocol, @unchecked Sendab return try result.get() } + + func setResponse(requestResult: Result<(Data, HTTPURLResponse), NetworkError>) async { + self.requestResult = requestResult + } } diff --git a/Projects/Detail/Tests/Service/DetailServiceTests.swift b/Projects/Detail/Tests/Service/DetailServiceTests.swift index bd1fbfd..fc6ac06 100644 --- a/Projects/Detail/Tests/Service/DetailServiceTests.swift +++ b/Projects/Detail/Tests/Service/DetailServiceTests.swift @@ -58,11 +58,11 @@ struct DetailServiceTests { headerFields: nil )) - networkSpy.requestResult = .success((data, httpResponse)) + await networkSpy.setResponse(requestResult: .success((data, httpResponse))) let result = try await sut.fetchExchangeAssets(id: 1) - #expect(networkSpy.calledMethods == [.request]) + #expect(await networkSpy.calledMethods == [.request]) #expect(result.count == 2) #expect(result[0] == Asset(id: 1, name: "Bitcoin", symbol: "BTC", priceUsd: 50000.0)) #expect(result[1] == Asset(id: 2, name: "Ethereum", symbol: "ETH", priceUsd: 3000.0)) @@ -91,7 +91,7 @@ struct DetailServiceTests { headerFields: nil )) - networkSpy.requestResult = .success((data, httpResponse)) + await networkSpy.setResponse(requestResult: .success((data, httpResponse))) do { _ = try await sut.fetchExchangeAssets(id: 999) @@ -104,7 +104,7 @@ struct DetailServiceTests { elapsed: 10, creditCount: 1 ))) - #expect(networkSpy.calledMethods == [.request]) + #expect(await networkSpy.calledMethods == [.request]) } } @@ -123,13 +123,13 @@ struct DetailServiceTests { headerFields: nil )) - networkSpy.requestResult = .success((data, httpResponse)) + await networkSpy.setResponse(requestResult: .success((data, httpResponse))) do { _ = try await sut.fetchExchangeAssets(id: 1) Issue.record("Expected error to be thrown") } catch { - #expect(networkSpy.calledMethods == [.request]) + #expect(await networkSpy.calledMethods == [.request]) } } } diff --git a/Projects/Home/Tests/Doubles/NetworkServiceProtocolSpy.swift b/Projects/Home/Tests/Doubles/NetworkServiceProtocolSpy.swift index 086cab3..57c1249 100644 --- a/Projects/Home/Tests/Doubles/NetworkServiceProtocolSpy.swift +++ b/Projects/Home/Tests/Doubles/NetworkServiceProtocolSpy.swift @@ -9,13 +9,13 @@ import Foundation import NetworkingInterfaces -final class NetworkServiceProtocolSpy: NetworkServiceProtocol, @unchecked Sendable { - enum Method: Equatable { +actor NetworkServiceProtocolSpy: NetworkServiceProtocol, Sendable { + enum Method: Equatable, Sendable { case request } - var calledMethods: [Method] = [] - var requestResult: Result<(Data, HTTPURLResponse), NetworkError>? + private(set) var calledMethods: [Method] = [] + private(set) var requestResult: Result<(Data, HTTPURLResponse), NetworkError>? func request(endpoint: APIEndpointProtocol) async throws(NetworkError) -> (Data, HTTPURLResponse) { calledMethods.append(.request) @@ -26,4 +26,8 @@ final class NetworkServiceProtocolSpy: NetworkServiceProtocol, @unchecked Sendab return try result.get() } + + func setResponse(requestResult: Result<(Data, HTTPURLResponse), NetworkError>) async { + self.requestResult = requestResult + } } diff --git a/Projects/Home/Tests/Service/HomeServiceTests.swift b/Projects/Home/Tests/Service/HomeServiceTests.swift index 81e6cd8..658f8d9 100644 --- a/Projects/Home/Tests/Service/HomeServiceTests.swift +++ b/Projects/Home/Tests/Service/HomeServiceTests.swift @@ -40,11 +40,11 @@ struct HomeServiceTests { headerFields: nil )! - networkSpy.requestResult = .success((data, httpResponse)) + await networkSpy.setResponse(requestResult: .success((data, httpResponse))) let result = try await sut.fetchExchangesList(page: 1, limit: 20) - #expect(networkSpy.calledMethods == [.request]) + #expect(await networkSpy.calledMethods == [.request]) #expect(result.count == 2) #expect(result[0].id == 1) #expect(result[0].name == "Binance") @@ -81,12 +81,12 @@ struct HomeServiceTests { headerFields: nil )! - networkSpy.requestResult = .success((data, httpResponse)) + await networkSpy.setResponse(requestResult: .success((data, httpResponse))) await #expect(throws: expectedError) { _ = try await sut.fetchDetailsFor(ids: ["1"]) } - #expect(networkSpy.calledMethods == [.request]) + #expect(await networkSpy.calledMethods == [.request]) } @Test("GIVEN successful response WHEN fetchDetailsFor is called THEN returns exchange details") @@ -127,11 +127,11 @@ struct HomeServiceTests { headerFields: nil )! - networkSpy.requestResult = .success((data, httpResponse)) + await networkSpy.setResponse(requestResult: .success((data, httpResponse))) let result = try await sut.fetchDetailsFor(ids: ["1"]) - #expect(networkSpy.calledMethods == [.request]) + #expect(await networkSpy.calledMethods == [.request]) #expect(result.count == 1) #expect(result[0].id == 1) #expect(result[0].name == "Binance") @@ -168,12 +168,12 @@ struct HomeServiceTests { headerFields: nil )! - networkSpy.requestResult = .success((data, httpResponse)) + await networkSpy.setResponse(requestResult: .success((data, httpResponse))) await #expect(throws: expectedError) { _ = try await sut.fetchDetailsFor(ids: ["1"]) } - #expect(networkSpy.calledMethods == [.request]) + #expect(await networkSpy.calledMethods == [.request]) } } From 6fa2ca9430aa66155a1d534a1af4c8eb9e9f0ed6 Mon Sep 17 00:00:00 2001 From: Vitor Conceicao Date: Sun, 22 Feb 2026 14:47:14 -0300 Subject: [PATCH 4/5] refactor: use typed error from swift 6 --- .../Sources/Service/DetailService.swift | 2 +- .../Sources/ViewModel/HomeViewModel.swift | 35 ++++++-------- .../Tests/ViewModel/HomeViewModelTests.swift | 47 ++++++++++++++++++- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/Projects/Detail/Sources/Service/DetailService.swift b/Projects/Detail/Sources/Service/DetailService.swift index f4d5289..37c306c 100644 --- a/Projects/Detail/Sources/Service/DetailService.swift +++ b/Projects/Detail/Sources/Service/DetailService.swift @@ -16,7 +16,7 @@ enum DetailServiceError: Error, Equatable { } final class DetailService: Sendable { - private nonisolated(unsafe) let networkService: NetworkServiceProtocol + private let networkService: NetworkServiceProtocol init(networkService: NetworkServiceProtocol) { self.networkService = networkService diff --git a/Projects/Home/Sources/ViewModel/HomeViewModel.swift b/Projects/Home/Sources/ViewModel/HomeViewModel.swift index 6fb6560..182c93a 100644 --- a/Projects/Home/Sources/ViewModel/HomeViewModel.swift +++ b/Projects/Home/Sources/ViewModel/HomeViewModel.swift @@ -15,6 +15,7 @@ final class HomeViewModel: HomeViewModelProtocol { weak var coordinatorDelegate: HomeViewModelCoordinatorDelegate? private let service: HomeServiceProtocol + private let defaultMessage = "Failed to load data. Press try again or check your connection." private var currentPage = 1 private var isFetching = false @@ -83,29 +84,21 @@ final class HomeViewModel: HomeViewModelProtocol { isFetching = false state = .loaded - } catch { - isFetching = false - if exchanges.isEmpty { - let serviceError = error as? ServiceError - let defaultMessage = "Failed to Load data. Press try again later or check your connection." - let message: String - var code: String? - switch serviceError { - case .decodeFail, .none: - message = defaultMessage - case let .network(status): - message = status.errorMessage ?? defaultMessage - if let statusCode = status.errorCode { - code = String(statusCode) - } - case let .requestError(error): - message = error ?? defaultMessage - } - state = .error(message, code) - } else { - state = .loaded + } catch ServiceError.decodeFail { + state = .error(defaultMessage, nil) + } catch let ServiceError.network(status) { + let message = status.errorMessage ?? defaultMessage + var code: String? + if let statusCode = status.errorCode { + code = String(statusCode) } + state = .error(message, code) + } catch let ServiceError.requestError(error) { + let message = error ?? defaultMessage + state = .error(message, nil) } + + isFetching = false } } diff --git a/Projects/Home/Tests/ViewModel/HomeViewModelTests.swift b/Projects/Home/Tests/ViewModel/HomeViewModelTests.swift index 58051a8..907ae14 100644 --- a/Projects/Home/Tests/ViewModel/HomeViewModelTests.swift +++ b/Projects/Home/Tests/ViewModel/HomeViewModelTests.swift @@ -15,7 +15,6 @@ import Testing @MainActor @Suite struct HomeViewModelTests { - @Test("GIVEN empty state WHEN loadData is called THEN fetches exchanges and updates state to empty") func testLoadDataSuccess() async throws { let (sut, doubles) = makeSut() @@ -52,6 +51,52 @@ struct HomeViewModelTests { .didUpdateState(.error(messageError, "1"))]) } + @Test("GIVEN empty state WHEN loadData fails with decodeFail THEN updates state to error with default message") + func testLoadDataDecodeFailError() async throws { + let (sut, doubles) = makeSut() + doubles.serviceSpy.fetchExchangesListResult = .failure(.decodeFail(nil)) + + sut.loadData() + + try await Task.sleep(nanoseconds: 100_000_000) + + #expect(doubles.serviceSpy.calledMethods.contains(.fetchExchangesList)) + #expect(sut.numberOfItems == 0) + #expect(doubles.delegateSpy.calledMethods == [.didUpdateState(.loading), + .didUpdateState(.error("Failed to load data. Press try again or check your connection.", nil))]) + } + + @Test("GIVEN empty state WHEN loadData fails with requestError THEN updates state to error with request error message") + func testLoadDataRequestError() async throws { + let (sut, doubles) = makeSut() + let errorMessage = "Request timed out." + doubles.serviceSpy.fetchExchangesListResult = .failure(.requestError(errorMessage)) + + sut.loadData() + + try await Task.sleep(nanoseconds: 100_000_000) + + #expect(doubles.serviceSpy.calledMethods.contains(.fetchExchangesList)) + #expect(sut.numberOfItems == 0) + #expect(doubles.delegateSpy.calledMethods == [.didUpdateState(.loading), + .didUpdateState(.error(errorMessage, nil))]) + } + + @Test("GIVEN empty state WHEN loadData fails with requestError nil message THEN updates state to error with default message") + func testLoadDataRequestErrorNilMessage() async throws { + let (sut, doubles) = makeSut() + doubles.serviceSpy.fetchExchangesListResult = .failure(.requestError(nil)) + + sut.loadData() + + try await Task.sleep(nanoseconds: 100_000_000) + + #expect(doubles.serviceSpy.calledMethods.contains(.fetchExchangesList)) + #expect(sut.numberOfItems == 0) + #expect(doubles.delegateSpy.calledMethods == [.didUpdateState(.loading), + .didUpdateState(.error("Failed to load data. Press try again or check your connection.", nil))]) + } + @Test("GIVEN empty state WHEN loadData returns empty array THEN updates state to empty") func testLoadDataEmpty() async throws { let (sut, doubles) = makeSut() From a7b17f2931749e323000bf94f17d09bb99f74b13 Mon Sep 17 00:00:00 2001 From: Vitor Conceicao Date: Sun, 22 Feb 2026 14:48:43 -0300 Subject: [PATCH 5/5] chore: bump networking package to 1.2.0 --- Tuist/Package.resolved | 4 ++-- Tuist/Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved index 3cbbe40..18e7aa1 100644 --- a/Tuist/Package.resolved +++ b/Tuist/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vitor-rc1/networking-package.git", "state" : { - "revision" : "b5498ee2ab408592d4db9a9c95faec3a08de9ea2", - "version" : "1.1.0" + "revision" : "2fe707c620272b40e2c5a4fe258480bebae6e483", + "version" : "1.2.0" } }, { diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 9cc58a7..8a051c8 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -18,6 +18,6 @@ let package = Package( dependencies: [ .package(url: "https://github.com/Swinject/Swinject.git", from: "2.9.1"), .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.18.9"), - .package(url: "https://github.com/vitor-rc1/networking-package.git", from: "1.1.0") + .package(url: "https://github.com/vitor-rc1/networking-package.git", from: "1.2.0") ] )