diff --git a/Apps/Locations/Libraries/Sources/APICore/Classes/MockURLProtocol.swift b/Apps/Locations/Libraries/Sources/APICore/Classes/MockURLProtocol.swift index 53265e6..bfc3c34 100644 --- a/Apps/Locations/Libraries/Sources/APICore/Classes/MockURLProtocol.swift +++ b/Apps/Locations/Libraries/Sources/APICore/Classes/MockURLProtocol.swift @@ -8,6 +8,7 @@ import Foundation +/// This class overrides the `URLProtocol` protocol used by the `URLSession` to handle the loading of protocol-specific URL data so it is possible to mock URL response for testing purposes. public class MockURLProtocol: URLProtocol { // MARK: Properties diff --git a/Apps/Locations/Libraries/Sources/APICore/Models/MockURLResponse.swift b/Apps/Locations/Libraries/Sources/APICore/Models/MockURLResponse.swift index 4404ee3..a368b1a 100644 --- a/Apps/Locations/Libraries/Sources/APICore/Models/MockURLResponse.swift +++ b/Apps/Locations/Libraries/Sources/APICore/Models/MockURLResponse.swift @@ -8,6 +8,7 @@ import Foundation +/// This model includes the data to be injected into an specific URL at the time of mocking its response. public struct MockURLResponse { // MARK: Properties diff --git a/Apps/Locations/Libraries/Sources/Locations/Services/LocationsService.swift b/Apps/Locations/Libraries/Sources/Locations/Services/LocationsService.swift new file mode 100644 index 0000000..2e102ab --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Locations/Services/LocationsService.swift @@ -0,0 +1,39 @@ +// +// LocationsService.swift +// Locations +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import APICore +import Foundation + +public struct LocationsService { + + // MARK: Properties + + private let client: Client + + // MARK: Initialisers + + public init(configuration: URLSessionConfiguration = .default) { + self.client = LocationsClient(configuration: configuration) + } + + // MARK: Functions + + public func getLocations() async throws -> [Location] { + try await client.request( + endpoint: GetLocationsEndpoint(), + for: Locations.self + ).locations + } + +} + +// MARK: - Models + +struct Locations: Decodable, Equatable { + public let locations: [Location] +} diff --git a/Apps/Locations/Libraries/Tests/LocationsTests/Clients/LocationsClientTests.swift b/Apps/Locations/Libraries/Tests/LocationsTests/Clients/LocationsClientTests.swift index 165951f..b3b7b49 100644 --- a/Apps/Locations/Libraries/Tests/LocationsTests/Clients/LocationsClientTests.swift +++ b/Apps/Locations/Libraries/Tests/LocationsTests/Clients/LocationsClientTests.swift @@ -45,7 +45,7 @@ final class LocationsClientTests: XCTestCase { let endpoint = GetLocationsEndpoint() url = try makeURLRequest(endpoint: endpoint).url - data = .locations + data = .Responses.locations MockURLProtocol.mockData[url] = MockURLResponse( status: 200, @@ -85,7 +85,7 @@ final class LocationsClientTests: XCTestCase { let endpoint = GetLocationsEndpoint() url = try makeURLRequest(endpoint: endpoint).url - data = .locations + data = .Responses.locations MockURLProtocol.mockData[url] = MockURLResponse( status: 404, @@ -108,7 +108,7 @@ final class LocationsClientTests: XCTestCase { let endpoint = GetLocationsEndpoint() url = try makeURLRequest(endpoint: endpoint).url - data = .locations + data = .Responses.locations MockURLProtocol.mockData[url] = MockURLResponse( status: 500, @@ -131,7 +131,7 @@ final class LocationsClientTests: XCTestCase { let endpoint = GetLocationsEndpoint() url = try makeURLRequest(endpoint: endpoint).url - data = .locations + data = .Responses.locations MockURLProtocol.mockData[url] = MockURLResponse( status: 302, @@ -150,15 +150,3 @@ final class LocationsClientTests: XCTestCase { } } - -// MARK: - Models - -private struct Locations: Decodable, Equatable { - public let locations: [Location] -} - -// MARK: - String+Constants - -private extension Data { - static let locations = "{\"locations\":[{\"name\":\"Amsterdam\",\"lat\":52.3547498,\"long\":4.8339215},{\"name\":\"Mumbai\",\"lat\":19.0823998,\"long\":72.8111468},{\"name\":\"Copenhagen\",\"lat\":55.6713442,\"long\":12.523785},{\"lat\":40.4380638,\"long\":-3.7495758}]}".data(using: .utf8) -} diff --git a/Apps/Locations/Libraries/Tests/LocationsTests/Helpers/Data+Constants.swift b/Apps/Locations/Libraries/Tests/LocationsTests/Helpers/Data+Constants.swift new file mode 100644 index 0000000..bcca0ba --- /dev/null +++ b/Apps/Locations/Libraries/Tests/LocationsTests/Helpers/Data+Constants.swift @@ -0,0 +1,15 @@ +// +// Data+Constants.swift +// LocationsTests +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Foundation + +extension Data { + enum Responses { + static let locations = "{\"locations\":[{\"name\":\"Amsterdam\",\"lat\":52.3547498,\"long\":4.8339215},{\"name\":\"Mumbai\",\"lat\":19.0823998,\"long\":72.8111468},{\"name\":\"Copenhagen\",\"lat\":55.6713442,\"long\":12.523785},{\"lat\":40.4380638,\"long\":-3.7495758}]}".data(using: .utf8) + } +} diff --git a/Apps/Locations/Libraries/Tests/LocationsTests/Services/LocationsServiceTests.swift b/Apps/Locations/Libraries/Tests/LocationsTests/Services/LocationsServiceTests.swift new file mode 100644 index 0000000..cad2691 --- /dev/null +++ b/Apps/Locations/Libraries/Tests/LocationsTests/Services/LocationsServiceTests.swift @@ -0,0 +1,152 @@ +// +// LocationsServiceTests.swift +// LocationsTests +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import APICore +import XCTest + +@testable import Locations + +final class LocationsServiceTests: XCTestCase { + + // MARK: Properties + + private let makeURLRequest = MakeURLRequestUseCase() + private let sessionConfiguration = { + let configuration = URLSessionConfiguration.default + + configuration.protocolClasses = [MockURLProtocol.self] + + return configuration + }() + + private var service: LocationsService! + private var url: URL! + private var data: Data! + + // MARK: Setup + + override func setUp() async throws { + service = .init(configuration: sessionConfiguration) + } + + override func tearDown() async throws { + service = nil + } + + // MARK: Tests + + func test_getLocations_whenResponseOK() async throws { + // GIVEN + let endpoint = GetLocationsEndpoint() + + url = try makeURLRequest(endpoint: endpoint).url + data = .Responses.locations + + MockURLProtocol.mockData[url] = MockURLResponse( + status: 200, + headers: [:], + data: data + ) + + // WHEN + let result = try await service.getLocations() + + // THEN + XCTAssertEqual(result, [ + .init( + name: "Amsterdam", + latitude: 52.3547498, + longitude: 4.8339215 + ), + .init( + name: "Mumbai", + latitude: 19.0823998, + longitude: 72.8111468 + ), + .init( + name: "Copenhagen", + latitude: 55.6713442, + longitude: 12.523785 + ), + .init( + latitude: 40.4380638, + longitude: -3.7495758 + ) + ]) + } + + func test_getLocations_whenResponseClientError() async throws { + // GIVEN + let endpoint = GetLocationsEndpoint() + + url = try makeURLRequest(endpoint: endpoint).url + data = .Responses.locations + + MockURLProtocol.mockData[url] = MockURLResponse( + status: 404, + headers: [:], + data: data + ) + + // WHEN & THEN + do { + _ = try await service.getLocations() + } catch LocationsClientError.statusErrorClient { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_getLocations_whenResponseServerError() async throws { + // GIVEN + let endpoint = GetLocationsEndpoint() + + url = try makeURLRequest(endpoint: endpoint).url + data = .Responses.locations + + MockURLProtocol.mockData[url] = MockURLResponse( + status: 500, + headers: [:], + data: data + ) + + // WHEN & THEN + do { + _ = try await service.getLocations() + } catch LocationsClientError.statusErrorServer { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_getLocations_whenResponseUnexpectedError() async throws { + // GIVEN + let endpoint = GetLocationsEndpoint() + + url = try makeURLRequest(endpoint: endpoint).url + data = .Responses.locations + + MockURLProtocol.mockData[url] = MockURLResponse( + status: 302, + headers: [:], + data: data + ) + + // WHEN & THEN + do { + _ = try await service.getLocations() + } catch LocationsClientError.statusErrorUnexpected { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + +}