diff --git a/CryptoWidgetKitApp.xcodeproj/project.pbxproj b/CryptoWidgetKitApp.xcodeproj/project.pbxproj index 1e1ece2..08b1436 100644 --- a/CryptoWidgetKitApp.xcodeproj/project.pbxproj +++ b/CryptoWidgetKitApp.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 4D72664C2C049D800035E348 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D72664B2C049D800035E348 /* ContentView.swift */; }; 4D72664E2C049D810035E348 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4D72664D2C049D810035E348 /* Assets.xcassets */; }; 4D7266512C049D810035E348 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4D7266502C049D810035E348 /* Preview Assets.xcassets */; }; - 4D72665B2C049D820035E348 /* CryptoWidgetKitAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D72665A2C049D820035E348 /* CryptoWidgetKitAppTests.swift */; }; 4D8251B32C05CDDB00DD32A3 /* CryptoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D8251B22C05CDDB00DD32A3 /* CryptoModel.swift */; }; 4D8251B62C05D21D00DD32A3 /* CryptoAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D8251B52C05D21D00DD32A3 /* CryptoAPIService.swift */; }; 4D8251B82C05D2A200DD32A3 /* CryptoAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D8251B72C05D2A100DD32A3 /* CryptoAPIEndpoint.swift */; }; @@ -21,6 +20,8 @@ 4D8251C62C05EAD700DD32A3 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D8251C52C05EAD700DD32A3 /* HTTPMethod.swift */; }; 4D8251C82C05EBBB00DD32A3 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D8251C72C05EBBB00DD32A3 /* MockURLSession.swift */; }; 4D8251CA2C05EC4500DD32A3 /* URLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D8251C92C05EC4500DD32A3 /* URLSessionProtocol.swift */; }; + 4D8251D12C0879E800DD32A3 /* View+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D8251D02C0879E800DD32A3 /* View+.swift */; }; + 4D8251D32C0DAD5000DD32A3 /* CryptoViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D8251D22C0DAD5000DD32A3 /* CryptoViewModelTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,7 +41,6 @@ 4D72664D2C049D810035E348 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 4D7266502C049D810035E348 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 4D7266562C049D820035E348 /* CryptoWidgetKitAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CryptoWidgetKitAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 4D72665A2C049D820035E348 /* CryptoWidgetKitAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoWidgetKitAppTests.swift; sourceTree = ""; }; 4D8251B22C05CDDB00DD32A3 /* CryptoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoModel.swift; sourceTree = ""; }; 4D8251B52C05D21D00DD32A3 /* CryptoAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoAPIService.swift; sourceTree = ""; }; 4D8251B72C05D2A100DD32A3 /* CryptoAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoAPIEndpoint.swift; sourceTree = ""; }; @@ -50,6 +50,8 @@ 4D8251C52C05EAD700DD32A3 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; 4D8251C72C05EBBB00DD32A3 /* MockURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = ""; }; 4D8251C92C05EC4500DD32A3 /* URLSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionProtocol.swift; sourceTree = ""; }; + 4D8251D02C0879E800DD32A3 /* View+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+.swift"; sourceTree = ""; }; + 4D8251D22C0DAD5000DD32A3 /* CryptoViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoViewModelTests.swift; sourceTree = ""; }; 4D8251E42C0F7B0300DD32A3 /* CryptoWidgetKitApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CryptoWidgetKitApp.xctestplan; sourceTree = ""; }; /* End PBXFileReference section */ @@ -96,6 +98,7 @@ 4D72664B2C049D800035E348 /* ContentView.swift */, 4D8251B12C05CDC400DD32A3 /* Models */, 4D8251B92C05DA6000DD32A3 /* ViewModels */, + 4D8251CC2C0878B900DD32A3 /* Views */, 4D8251B42C05D20B00DD32A3 /* Network */, 4D7266732C049D930035E348 /* Resources */, 4D72664F2C049D810035E348 /* Preview Content */, @@ -117,7 +120,7 @@ 4D8251E42C0F7B0300DD32A3 /* CryptoWidgetKitApp.xctestplan */, 4D8251C02C05E9F100DD32A3 /* Mocks */, 4D8251C12C05E9FE00DD32A3 /* Network */, - 4D72665A2C049D820035E348 /* CryptoWidgetKitAppTests.swift */, + 4D8251D22C0DAD5000DD32A3 /* CryptoViewModelTests.swift */, ); path = CryptoWidgetKitAppTests; sourceTree = ""; @@ -182,6 +185,22 @@ path = Utils; sourceTree = ""; }; + 4D8251CC2C0878B900DD32A3 /* Views */ = { + isa = PBXGroup; + children = ( + 4D8251CF2C0879D400DD32A3 /* Extensions */, + ); + path = Views; + sourceTree = ""; + }; + 4D8251CF2C0879D400DD32A3 /* Extensions */ = { + isa = PBXGroup; + children = ( + 4D8251D02C0879E800DD32A3 /* View+.swift */, + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -286,6 +305,7 @@ 4D8251B32C05CDDB00DD32A3 /* CryptoModel.swift in Sources */, 4D8251B62C05D21D00DD32A3 /* CryptoAPIService.swift in Sources */, 4D72664C2C049D800035E348 /* ContentView.swift in Sources */, + 4D8251D12C0879E800DD32A3 /* View+.swift in Sources */, 4D8251B82C05D2A200DD32A3 /* CryptoAPIEndpoint.swift in Sources */, 4D8251C62C05EAD700DD32A3 /* HTTPMethod.swift in Sources */, 4D72664A2C049D800035E348 /* CryptoWidgetKitApp.swift in Sources */, @@ -299,7 +319,7 @@ buildActionMask = 2147483647; files = ( 4D8251C82C05EBBB00DD32A3 /* MockURLSession.swift in Sources */, - 4D72665B2C049D820035E348 /* CryptoWidgetKitAppTests.swift in Sources */, + 4D8251D32C0DAD5000DD32A3 /* CryptoViewModelTests.swift in Sources */, 4D8251BF2C05E9BB00DD32A3 /* CryptoAPIServiceTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/CryptoWidgetKitApp/ContentView.swift b/CryptoWidgetKitApp/ContentView.swift index 4457347..d0242d2 100644 --- a/CryptoWidgetKitApp/ContentView.swift +++ b/CryptoWidgetKitApp/ContentView.swift @@ -14,26 +14,38 @@ struct ContentView: View { var body: some View { @Bindable var cryptoViewModel = cryptoViewModel NavigationStack { - List(cryptoViewModel.cryptos, id: \.coinInfo.id) { crypto in - HStack { - Text(crypto.coinInfo.fullName) - Spacer() - Text(crypto.display?.usd.price ?? "0") + List { + ForEach(cryptoViewModel.cryptos, id: \.coinInfo.id){ crypto in + HStack { + Text(crypto.coinInfo.fullName) + Spacer() + Text(crypto.display?.usd.price ?? "0") + } } - } - .redacted(reason: cryptoViewModel.isLoading ? .placeholder : []) - .task { await cryptoViewModel.fetch() } - .refreshable { await cryptoViewModel.fetch() } - .alert(isPresented: $cryptoViewModel.showError, content: { - Alert(title: Text("An error occurred, try again later"), - dismissButton: .default( - Text("Retry"), - action: { + if !cryptoViewModel.showSkeleton { + Section { + Button(action: { Task { - await cryptoViewModel.fetch() + await cryptoViewModel.loadMoreData() + } + }, label: { + if cryptoViewModel.isLoading { + ProgressView() + } else { + Text("Load more") } - })) - }) + }) + } + } + } + .redacted(reason: cryptoViewModel.showSkeleton ? .placeholder : []) + .task { await cryptoViewModel.fetchInitialData() } + .refreshable { await cryptoViewModel.refreshData() } + .errorAlert(isPresented: $cryptoViewModel.showError) { + Task { + await cryptoViewModel.refreshData() + } + } .toolbar { ToolbarItemGroup(placement: .bottomBar) { Text("API Cache 120 seconds") diff --git a/CryptoWidgetKitApp/CryptoWidgetKitApp.swift b/CryptoWidgetKitApp/CryptoWidgetKitApp.swift index 6358b5f..a5cced4 100644 --- a/CryptoWidgetKitApp/CryptoWidgetKitApp.swift +++ b/CryptoWidgetKitApp/CryptoWidgetKitApp.swift @@ -8,7 +8,23 @@ import SwiftUI @main -struct CryptoWidgetKitApp: App { +struct CryptoWidgetKitApp { + + static func main() { + guard isProduction() else { + TestApp.main() + return + } + ProductionApp.main() + } + + private static func isProduction() -> Bool { + return NSClassFromString("XCTestCase") == nil + } +} + +// MARK: - ProductionApp +struct ProductionApp: App { @State var cryptoViewModel = CryptoViewModel() @@ -19,3 +35,10 @@ struct CryptoWidgetKitApp: App { } } } + +// MARK: - TestApp +struct TestApp: App { + var body: some Scene { + WindowGroup {} + } +} diff --git a/CryptoWidgetKitApp/Network/CryptoAPIService.swift b/CryptoWidgetKitApp/Network/CryptoAPIService.swift index eada098..73ecc45 100644 --- a/CryptoWidgetKitApp/Network/CryptoAPIService.swift +++ b/CryptoWidgetKitApp/Network/CryptoAPIService.swift @@ -6,6 +6,7 @@ // import Foundation +import os // MARK: - CryptoAPIService class CryptoAPIService { @@ -13,6 +14,12 @@ class CryptoAPIService { // MARK: - Dependencies private let session: URLSessionProtocol + // MARK: - Properties + private static let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: CryptoAPIService.self) + ) + // MARK: - Init init(session: URLSessionProtocol) { self.session = session @@ -28,9 +35,11 @@ extension CryptoAPIService { /// Fetches the list of cryptocurrencies /// - Returns: An array of `Crypto` objects. - func fetchRetrieveFullList() async throws -> Cryptos { - let request: URLRequest = .init(endpoint: .fullList, method: .get) + func fetchRetrieveTopList(page: Int? = nil) async throws -> Cryptos { + let request: URLRequest = .init(endpoint: .topList(page: page), method: .get) + Self.logger.debug("Fetching top list with request: \(request)") let (data, _) = try await session.data(for: request) + Self.logger.debug("Received response with data: \(data)") return try JSONDecoder().decode(Cryptos.self, from: data) } } diff --git a/CryptoWidgetKitApp/Network/Utils/CryptoAPIEndpoint.swift b/CryptoWidgetKitApp/Network/Utils/CryptoAPIEndpoint.swift index d2a86ff..a2b31d6 100644 --- a/CryptoWidgetKitApp/Network/Utils/CryptoAPIEndpoint.swift +++ b/CryptoWidgetKitApp/Network/Utils/CryptoAPIEndpoint.swift @@ -9,15 +9,21 @@ import Foundation // MARK: - CryptoAPIEndpoint enum CryptoAPIEndpoint { - case fullList + case topList(page: Int?) } extension CryptoAPIEndpoint { var url: URL { switch self { - case .fullList: - URL(string: "https://min-api.cryptocompare.com/data/top/totalvolfull?tsym=USD")! + case .topList(let page): + var components = URLComponents(string: "https://min-api.cryptocompare.com/data/top/totalvolfull")! + var queryItems = [URLQueryItem(name: "tsym", value: "USD")] + if let page = page { + queryItems.append(URLQueryItem(name: "page", value: String(page))) + } + components.queryItems = queryItems + return components.url! } } } diff --git a/CryptoWidgetKitApp/ViewModels/CryptoViewModel.swift b/CryptoWidgetKitApp/ViewModels/CryptoViewModel.swift index faf940a..73f61f3 100644 --- a/CryptoWidgetKitApp/ViewModels/CryptoViewModel.swift +++ b/CryptoWidgetKitApp/ViewModels/CryptoViewModel.swift @@ -11,11 +11,21 @@ import Foundation @Observable final class CryptoViewModel { - // MARK: - Properties + // MARK: - Published Properties private(set) var cryptos: [Crypto] = [] - private(set) var isLoading: Bool = true + private(set) var showSkeleton: Bool = false + private(set) var isLoading: Bool = false var showError: Bool = false + // MARK: - Private Properties + private var page: Int = 0 { + didSet { + if page < 0 { + page = 0 + } + } + } + // MARK: - Dependencies private let apiService: CryptoAPIService @@ -31,15 +41,63 @@ final class CryptoViewModel { extension CryptoViewModel { /// Fetch the cryptos from API and update the cryptos array - func fetch() async { - isLoading = true + func fetchInitialData() async { + showSkeleton = true + defer { + if !showError { + showSkeleton = false + } + } + await fetchData(page: nil) + } + + /// Load more cryptos from API and append to the cryptos array + func loadMoreData() async { + page += 1 + await fetchData(page: page) + } + + /// Refresh all data from API and update the cryptos array + func refreshData() async { + showSkeleton = true + defer { + if !showError { + showSkeleton = false + } + } + var allData: [Crypto] = [] + for p in 0...page { + do { + let data = try await apiService.fetchRetrieveTopList(page: p).data + allData.append(contentsOf: data) + } catch { + showError = true + return + } + } + cryptos = allData + } +} + +// MARK: - Private Methods +private extension CryptoViewModel { + + /// Method to fetch data from API and update the cryptos array + func fetchData(page: Int?) async { + if page != nil { + isLoading = true + } + defer { isLoading = false } do { - sleep(3) - cryptos = try await apiService.fetchRetrieveFullList().data - isLoading = false + let data = try await apiService.fetchRetrieveTopList(page: page).data + if page != nil && page! > 0 { + cryptos.append(contentsOf: data) + } else { + cryptos = data + } } catch { - print(error) showError = true + self.page = (page ?? 0) - 1 } } } diff --git a/CryptoWidgetKitApp/Views/Extensions/View+.swift b/CryptoWidgetKitApp/Views/Extensions/View+.swift new file mode 100644 index 0000000..8aae045 --- /dev/null +++ b/CryptoWidgetKitApp/Views/Extensions/View+.swift @@ -0,0 +1,20 @@ +// +// View+.swift +// CryptoWidgetKitApp +// +// Created by Jose Jesus Torronteras Hernandez on 30/5/24. +// + +import SwiftUI + +extension View { + + func errorAlert(isPresented: Binding, retryAction: @escaping () -> Void) -> some View { + alert(isPresented: isPresented) { + Alert( + title: Text("An error occurred, try again later"), + dismissButton: .default(Text("Retry"), action: retryAction) + ) + } + } +} diff --git a/CryptoWidgetKitAppTests/CryptoViewModelTests.swift b/CryptoWidgetKitAppTests/CryptoViewModelTests.swift new file mode 100644 index 0000000..80de47a --- /dev/null +++ b/CryptoWidgetKitAppTests/CryptoViewModelTests.swift @@ -0,0 +1,235 @@ +// +// CryptoViewModelTests.swift +// CryptoWidgetKitAppTests +// +// Created by Jose Jesus Torronteras Hernandez on 3/6/24. +// + +import XCTest +@testable import CryptoWidgetKitApp + +// MARK: - CryptoViewModelTests +final class CryptoViewModelTests: XCTestCase { + + private var session: MockURLSession! + private var apiService: CryptoAPIService! + private var sut: CryptoViewModel! + + override func setUp() { + super.setUp() + session = MockURLSession() + apiService = CryptoAPIService(session: session) + sut = CryptoViewModel(apiService: apiService) + } + + override func tearDown() { + session = nil + apiService = nil + sut = nil + super.tearDown() + } +} + +// MARK: - Tests +extension CryptoViewModelTests { + + func test_fetchInitialData_success() async { + // Given + session.data = fullListData + session.response = successFullListResponse + let showSkeletonExpectation = expectation(description: "showSkeleton published property changed") + let cryptosExpectation = expectation(description: "cryptos published property changed") + + withObservationTracking { + _ = sut.showSkeleton + } onChange: { + showSkeletonExpectation.fulfill() + } + + withObservationTracking { + _ = sut.cryptos + } onChange: { + cryptosExpectation.fulfill() + } + + // When + Task { + await sut.fetchInitialData() + } + + // Then + await fulfillment(of: [showSkeletonExpectation, cryptosExpectation], timeout: 5.0) + XCTAssertFalse(sut.showSkeleton) + XCTAssertFalse(sut.isLoading) + XCTAssertFalse(sut.showError) + XCTAssertEqual(sut.cryptos.count, 2) + } + + func test_loadMoreData_success() async { + // Given + session.data = fullListData + session.response = successFullListResponse + await sut.fetchInitialData() + + // Observa los cambios + let isLoadingExpectation = expectation(description: "isLoading published property changed") + let cryptosExpectation = expectation(description: "cryptos published property changed") + + withObservationTracking { + _ = sut.isLoading + } onChange: { + isLoadingExpectation.fulfill() + } + + withObservationTracking { + _ = sut.cryptos + } onChange: { + cryptosExpectation.fulfill() + } + + // When + session.data = loadMoreData + session.response = successLoadMoreDataResponse + Task { + await sut.loadMoreData() + } + + // Then + await fulfillment(of: [isLoadingExpectation, cryptosExpectation], timeout: 5.0) + XCTAssertFalse(sut.isLoading) + XCTAssertFalse(sut.showError) + XCTAssertEqual(sut.cryptos.count, 4) + } + + func test_refreshData_success() async { + // Given + session.data = fullListData + session.response = successFullListResponse + await sut.fetchInitialData() + + // Observa los cambios + let showSkeletonExpectation = expectation(description: "showSkeleton published property changed") + let cryptosExpectation = expectation(description: "cryptos published property changed") + + withObservationTracking { + _ = sut.showSkeleton + } onChange: { + showSkeletonExpectation.fulfill() + } + + withObservationTracking { + _ = sut.cryptos + } onChange: { + cryptosExpectation.fulfill() + } + + // When + Task { + await sut.refreshData() + } + + // Then + await fulfillment(of: [showSkeletonExpectation, cryptosExpectation], timeout: 5.0) + XCTAssertFalse(sut.showSkeleton) + XCTAssertFalse(sut.isLoading) + XCTAssertFalse(sut.showError) + XCTAssertEqual(sut.cryptos.count, 2) + } + + func test_fetchData_failure() async { + // Given + session.error = URLError(.badServerResponse) + + // Observa los cambios + let showSkeletonExpectation = expectation(description: "showSkeleton published property changed") + let showErrorExpectation = expectation(description: "showError published property changed") + + withObservationTracking { + _ = sut.showSkeleton + } onChange: { + showSkeletonExpectation.fulfill() + } + + withObservationTracking { + _ = sut.showError + } onChange: { + showErrorExpectation.fulfill() + } + + // When + Task { + await sut.fetchInitialData() + } + + // Then + await fulfillment(of: [showSkeletonExpectation, showErrorExpectation], timeout: 5.0) + XCTAssertTrue(sut.showSkeleton) + XCTAssertFalse(sut.isLoading) + XCTAssertTrue(sut.showError) + XCTAssertEqual(sut.cryptos.count, [Crypto].mock.count) + } + + func test_refreshData_failure() async { + // Given + session.data = fullListData + session.response = successFullListResponse + await sut.fetchInitialData() + + // Observa los cambios + let showSkeletonExpectation = expectation(description: "showSkeleton published property changed") + let showErrorExpectation = expectation(description: "showError published property changed") + + withObservationTracking { + _ = sut.showSkeleton + } onChange: { + showSkeletonExpectation.fulfill() + } + + withObservationTracking { + _ = sut.showError + } onChange: { + showErrorExpectation.fulfill() + } + + // When + session.error = URLError(.badServerResponse) + Task { + await sut.refreshData() + } + + // Then + await fulfillment(of: [showSkeletonExpectation, showErrorExpectation], timeout: 5.0) + XCTAssertTrue(sut.showSkeleton) + XCTAssertFalse(sut.isLoading) + XCTAssertTrue(sut.showError) + XCTAssertEqual(sut.cryptos.count, 2) + } +} + +// MARK: - Data +fileprivate extension CryptoViewModelTests { + + var fullListData: Data { + Data("{\"Data\":[{\"CoinInfo\":{\"Id\":\"1182\",\"Name\":\"BTC\",\"FullName\":\"Bitcoin\",\"Internal\":\"BTC\",\"ImageUrl\":\"/media/37746251/btc.png\"},\"DISPLAY\":{\"USD\":{\"PRICE\":\"$ 68,512.1\",\"CHANGEPCT24HOUR\":\"-0.04\"}}},{\"CoinInfo\":{\"Id\":\"7605\",\"Name\":\"ETH\",\"FullName\":\"Ethereum\",\"Internal\":\"ETH\",\"ImageUrl\":\"/media/37746238/eth.png\"},\"DISPLAY\":{\"USD\":{\"PRICE\":\"$ 3,901.48\",\"CHANGEPCT24HOUR\":\"-0.07\"}}}]}".utf8) + } + + var loadMoreData: Data { + Data("{\"Data\":[{\"CoinInfo\":{\"Id\":\"1042\",\"Name\":\"LTC\",\"FullName\":\"Litecoin\",\"Internal\":\"LTC\",\"ImageUrl\":\"/media/37746257/ltc.png\"},\"DISPLAY\":{\"USD\":{\"PRICE\":\"$ 181.1\",\"CHANGEPCT24HOUR\":\"-0.04\"}}},{\"CoinInfo\":{\"Id\":\"02cf57e2-9b3c-4e9f-9f0e-2a1f2f7b7c9d\",\"Name\":\"DOGE\",\"FullName\":\"Dogecoin\",\"Internal\":\"DOGE\",\"ImageUrl\":\"/media/37746886/doge.png\"},\"DISPLAY\":{\"USD\":{\"PRICE\":\"$ 0.002\",\"CHANGEPCT24HOUR\":\"-0.07\"}}}]}".utf8) + } + + var successFullListResponse: HTTPURLResponse { + HTTPURLResponse( + url: CryptoAPIEndpoint.topList(page: nil).url, + statusCode: 200, + httpVersion: nil, + headerFields: nil)! + } + + var successLoadMoreDataResponse: HTTPURLResponse { + HTTPURLResponse( + url: CryptoAPIEndpoint.topList(page: 1).url, + statusCode: 200, + httpVersion: nil, + headerFields: nil)! + } +} diff --git a/CryptoWidgetKitAppTests/CryptoWidgetKitAppTests.swift b/CryptoWidgetKitAppTests/CryptoWidgetKitAppTests.swift deleted file mode 100644 index 70ce1e3..0000000 --- a/CryptoWidgetKitAppTests/CryptoWidgetKitAppTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// CryptoWidgetKitAppTests.swift -// CryptoWidgetKitAppTests -// -// Created by Jose Jesus Torronteras Hernandez on 27/1/24. -// - -import XCTest -@testable import CryptoWidgetKitApp - -final class CryptoWidgetKitAppTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/CryptoWidgetKitAppTests/Network/CryptoAPIServiceTests.swift b/CryptoWidgetKitAppTests/Network/CryptoAPIServiceTests.swift index 899aff2..0a95828 100644 --- a/CryptoWidgetKitAppTests/Network/CryptoAPIServiceTests.swift +++ b/CryptoWidgetKitAppTests/Network/CryptoAPIServiceTests.swift @@ -38,7 +38,7 @@ extension CryptoAPIServiceTests { // when do { - let result = try await sut.fetchRetrieveFullList() + let result = try await sut.fetchRetrieveTopList() // then XCTAssertEqual(result.data.count, 2) XCTAssertEqual(result.data[0].coinInfo.fullName, "Bitcoin") @@ -59,7 +59,7 @@ extension CryptoAPIServiceTests { // when do { - _ = try await sut.fetchRetrieveFullList() + _ = try await sut.fetchRetrieveTopList() XCTFail("Expected to throw error, but succeeded") } catch { // then @@ -77,7 +77,7 @@ extension CryptoAPIServiceTests { // when do { - _ = try await sut.fetchRetrieveFullList() + _ = try await sut.fetchRetrieveTopList() XCTFail("Expected to throw error, but succeeded") } catch { // then @@ -98,7 +98,7 @@ fileprivate extension CryptoAPIServiceTests { var successFullListResponse: HTTPURLResponse { HTTPURLResponse( - url: CryptoAPIEndpoint.fullList.url, + url: CryptoAPIEndpoint.topList(page: nil).url, statusCode: 200, httpVersion: nil, headerFields: nil)! @@ -106,7 +106,7 @@ fileprivate extension CryptoAPIServiceTests { var badFullListResponse: HTTPURLResponse { HTTPURLResponse( - url: CryptoAPIEndpoint.fullList.url, + url: CryptoAPIEndpoint.topList(page: nil).url, statusCode: 500, httpVersion: nil, headerFields: nil)!