From 6da2e946cee51b146497d3e5dbf53898ca5d1530 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 10 Apr 2023 15:31:22 +0000 Subject: [PATCH] [Libraries] Locations (#4) This PR contains the work that implements the Locations service, which is used to retrieve location data from a remote server. To give further details on what was done: - [x] created the `APICore` and `Locations` libraries into the **Libraries** package; - [x] defined the `Endpoint` and `Client` protocols; - [x] implemented the `MakeURLRequestUseCase` use case; - [x] implemented the `MockURLProtocol` protocol and the `MockURLResponse` models; - [x] implemented the `Location` model; - [x] implemented the `GetLocationsEndpoint` endpoint; - [x] implemented the `LocationsClient` client; - [x] implemented the `LocationsService` service. Co-authored-by: Javier Cicchelli Reviewed-on: https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/pulls/4 --- Apps/Locations/Libraries/Package.swift | 42 +++-- .../APICore/Classes/MockURLProtocol.swift | 63 ++++++++ .../Enumerations/HTTPRequestMethod.swift | 12 ++ .../APICore/Models/MockURLResponse.swift | 32 ++++ .../Sources/APICore/Protocols/Client.swift | 15 ++ .../Sources/APICore/Protocols/Endpoint.swift | 20 +++ .../Use Cases/MakeURLRequestUseCase.swift | 54 +++++++ .../Sources/Libraries/Libraries.swift | 6 - .../Locations/Clients/LocationsClient.swift | 66 ++++++++ .../Endpoints/GetLocationsEndpoint.swift | 20 +++ .../Extensions/String+Constants.swift | 21 +++ .../Sources/Locations/Models/Location.swift | 39 +++++ .../Locations/Services/LocationsService.swift | 39 +++++ .../MakeURLRequestUseCaseTests.swift | 119 ++++++++++++++ .../Tests/LibrariesTests/LibrariesTests.swift | 11 -- .../Clients/LocationsClientTests.swift | 152 ++++++++++++++++++ .../Endpoints/GetLocationsEndpointTests.swift | 32 ++++ .../Helpers/Data+Constants.swift | 15 ++ .../Services/LocationsServiceTests.swift | 152 ++++++++++++++++++ 19 files changed, 880 insertions(+), 30 deletions(-) create mode 100644 Apps/Locations/Libraries/Sources/APICore/Classes/MockURLProtocol.swift create mode 100644 Apps/Locations/Libraries/Sources/APICore/Enumerations/HTTPRequestMethod.swift create mode 100644 Apps/Locations/Libraries/Sources/APICore/Models/MockURLResponse.swift create mode 100644 Apps/Locations/Libraries/Sources/APICore/Protocols/Client.swift create mode 100644 Apps/Locations/Libraries/Sources/APICore/Protocols/Endpoint.swift create mode 100644 Apps/Locations/Libraries/Sources/APICore/Use Cases/MakeURLRequestUseCase.swift delete mode 100644 Apps/Locations/Libraries/Sources/Libraries/Libraries.swift create mode 100644 Apps/Locations/Libraries/Sources/Locations/Clients/LocationsClient.swift create mode 100644 Apps/Locations/Libraries/Sources/Locations/Endpoints/GetLocationsEndpoint.swift create mode 100644 Apps/Locations/Libraries/Sources/Locations/Extensions/String+Constants.swift create mode 100644 Apps/Locations/Libraries/Sources/Locations/Models/Location.swift create mode 100644 Apps/Locations/Libraries/Sources/Locations/Services/LocationsService.swift create mode 100644 Apps/Locations/Libraries/Tests/APICoreTests/Use Cases/MakeURLRequestUseCaseTests.swift delete mode 100644 Apps/Locations/Libraries/Tests/LibrariesTests/LibrariesTests.swift create mode 100644 Apps/Locations/Libraries/Tests/LocationsTests/Clients/LocationsClientTests.swift create mode 100644 Apps/Locations/Libraries/Tests/LocationsTests/Endpoints/GetLocationsEndpointTests.swift create mode 100644 Apps/Locations/Libraries/Tests/LocationsTests/Helpers/Data+Constants.swift create mode 100644 Apps/Locations/Libraries/Tests/LocationsTests/Services/LocationsServiceTests.swift diff --git a/Apps/Locations/Libraries/Package.swift b/Apps/Locations/Libraries/Package.swift index 03df4b5..ef3cbc4 100644 --- a/Apps/Locations/Libraries/Package.swift +++ b/Apps/Locations/Libraries/Package.swift @@ -1,28 +1,44 @@ // swift-tools-version: 5.8 -// The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Libraries", + platforms: [ + .iOS(.v16) + ], products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "Libraries", - targets: ["Libraries"]), - ], - dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), + targets: [ + "Locations" + ] + ), ], + dependencies: [], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( - name: "Libraries", - dependencies: []), + name: "APICore", + dependencies: [] + ), + .target( + name: "Locations", + dependencies: [ + "APICore" + ] + ), .testTarget( - name: "LibrariesTests", - dependencies: ["Libraries"]), + name: "APICoreTests", + dependencies: [ + "APICore" + ] + ), + .testTarget( + name: "LocationsTests", + dependencies: [ + "APICore", + "Locations" + ] + ), ] ) diff --git a/Apps/Locations/Libraries/Sources/APICore/Classes/MockURLProtocol.swift b/Apps/Locations/Libraries/Sources/APICore/Classes/MockURLProtocol.swift new file mode 100644 index 0000000..bfc3c34 --- /dev/null +++ b/Apps/Locations/Libraries/Sources/APICore/Classes/MockURLProtocol.swift @@ -0,0 +1,63 @@ +// +// MockURLProtocol.swift +// APICore +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +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 + + public static var mockData: [URL: MockURLResponse] = [:] + + // MARK: Functions + + public override class func canInit(with task: URLSessionTask) -> Bool { + true + } + + public override class func canInit(with request: URLRequest) -> Bool { + true + } + + public override class func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + public override func startLoading() { + guard + let url = request.url, + let response = Self.mockData[url] + else { + client?.urlProtocolDidFinishLoading(self) + return + } + + if let data = response.data { + client?.urlProtocol(self, didLoad: data) + } + + if let httpResponse = HTTPURLResponse( + url: url, + statusCode: response.status, + httpVersion: nil, + headerFields: response.headers + ) { + client?.urlProtocol( + self, + didReceive: httpResponse, + cacheStoragePolicy: .allowedInMemoryOnly + ) + } + + client?.urlProtocolDidFinishLoading(self) + } + + public override func stopLoading() {} + +} diff --git a/Apps/Locations/Libraries/Sources/APICore/Enumerations/HTTPRequestMethod.swift b/Apps/Locations/Libraries/Sources/APICore/Enumerations/HTTPRequestMethod.swift new file mode 100644 index 0000000..f45d043 --- /dev/null +++ b/Apps/Locations/Libraries/Sources/APICore/Enumerations/HTTPRequestMethod.swift @@ -0,0 +1,12 @@ +// +// HTTPRequestMethod.swift +// APICore +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +/// Enumeration that represents the available HTTP request methods to use in this library. +public enum HTTPRequestMethod: String { + case get = "GET" +} diff --git a/Apps/Locations/Libraries/Sources/APICore/Models/MockURLResponse.swift b/Apps/Locations/Libraries/Sources/APICore/Models/MockURLResponse.swift new file mode 100644 index 0000000..a368b1a --- /dev/null +++ b/Apps/Locations/Libraries/Sources/APICore/Models/MockURLResponse.swift @@ -0,0 +1,32 @@ +// +// MockURLResponse.swift +// APICore +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +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 + + public let status: Int + public let headers: [String: String] + public let data: Data? + + // MARK: Initialisers + + public init( + status: Int, + headers: [String : String], + data: Data? = nil + ) { + self.status = status + self.headers = headers + self.data = data + } + +} diff --git a/Apps/Locations/Libraries/Sources/APICore/Protocols/Client.swift b/Apps/Locations/Libraries/Sources/APICore/Protocols/Client.swift new file mode 100644 index 0000000..fec93a5 --- /dev/null +++ b/Apps/Locations/Libraries/Sources/APICore/Protocols/Client.swift @@ -0,0 +1,15 @@ +// +// Client.swift +// APICore +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +/// This protocol defines a client that will be making the API calls. +public protocol Client { + func request( + endpoint: some Endpoint, + for model: Model.Type + ) async throws -> Model +} diff --git a/Apps/Locations/Libraries/Sources/APICore/Protocols/Endpoint.swift b/Apps/Locations/Libraries/Sources/APICore/Protocols/Endpoint.swift new file mode 100644 index 0000000..4e7df7d --- /dev/null +++ b/Apps/Locations/Libraries/Sources/APICore/Protocols/Endpoint.swift @@ -0,0 +1,20 @@ +// +// Endpoint.swift +// APICore +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Foundation + +/// This protocol defines an endpoint to be used in an API call. +public protocol Endpoint { + var scheme: String { get } + var host: String { get } + var port: Int? { get } + var path: String { get } + var method: HTTPRequestMethod { get } + var headers: [String: String] { get } + var body: Data? { get } +} diff --git a/Apps/Locations/Libraries/Sources/APICore/Use Cases/MakeURLRequestUseCase.swift b/Apps/Locations/Libraries/Sources/APICore/Use Cases/MakeURLRequestUseCase.swift new file mode 100644 index 0000000..fce9754 --- /dev/null +++ b/Apps/Locations/Libraries/Sources/APICore/Use Cases/MakeURLRequestUseCase.swift @@ -0,0 +1,54 @@ +// +// MakeURLRequestUseCase.swift +// APICore +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Foundation + +/// This use case generate a url request out of a given endpoint. +public struct MakeURLRequestUseCase { + + // MARK: Initialisers + + public init() {} + + // MARK: Functions + + /// Generate a `URLRequest` instance out of a given endpoint that conforms to the `Endpoint` protocol. + /// - Parameter endpoint: An endpoint which is used to generate a `URLRequest` instance from. + /// - Returns: A `URLRequest` instance filled with data provided by the given endpoint. + public func callAsFunction(endpoint: some Endpoint) throws -> URLRequest { + var urlComponents = URLComponents() + + urlComponents.scheme = endpoint.scheme + urlComponents.host = endpoint.host + urlComponents.path = endpoint.path + + if let port = endpoint.port { + urlComponents.port = port + } + + guard let url = urlComponents.url else { + throw MakeURLRequestError.urlNotCreated + } + + var urlRequest = URLRequest(url: url) + + urlRequest.httpMethod = endpoint.method.rawValue + urlRequest.httpBody = endpoint.body + urlRequest.allHTTPHeaderFields = endpoint.headers + + return urlRequest + } + +} + +// MARK: - Errors + +enum MakeURLRequestError: Error { + case urlNotCreated +} + diff --git a/Apps/Locations/Libraries/Sources/Libraries/Libraries.swift b/Apps/Locations/Libraries/Sources/Libraries/Libraries.swift deleted file mode 100644 index c1536b6..0000000 --- a/Apps/Locations/Libraries/Sources/Libraries/Libraries.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct Libraries { - public private(set) var text = "Hello, World!" - - public init() { - } -} diff --git a/Apps/Locations/Libraries/Sources/Locations/Clients/LocationsClient.swift b/Apps/Locations/Libraries/Sources/Locations/Clients/LocationsClient.swift new file mode 100644 index 0000000..9ed37f2 --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Locations/Clients/LocationsClient.swift @@ -0,0 +1,66 @@ +// +// LocationsClient.swift +// Locations +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import APICore +import Foundation + +struct LocationsClient { + + // MARK: Properties + + private let session: URLSession + private let decoder: JSONDecoder = .init() + private let makeURLRequest: MakeURLRequestUseCase = .init() + + // MARK: Initialisers + + init(configuration: URLSessionConfiguration = .default) { + self.session = .init(configuration: configuration) + } + +} + +// MARK: - Client + +extension LocationsClient: Client { + + // MARK: Functions + + func request( + endpoint: some Endpoint, + for model: Model.Type + ) async throws -> Model { + let urlRequest = try makeURLRequest(endpoint: endpoint) + let (data, response) = try await session.data(for: urlRequest) + + guard let httpResponse = response as? HTTPURLResponse else { + throw LocationsClientError.responseNotReturned + } + + switch httpResponse.statusCode { + case 200: + return try decoder.decode(model, from: data) + case 400...499: + throw LocationsClientError.statusErrorClient + case 500...599: + throw LocationsClientError.statusErrorServer + default: + throw LocationsClientError.statusErrorUnexpected + } + } + +} + +// MARK: - Errors + +public enum LocationsClientError: Error { + case responseNotReturned + case statusErrorClient + case statusErrorServer + case statusErrorUnexpected +} diff --git a/Apps/Locations/Libraries/Sources/Locations/Endpoints/GetLocationsEndpoint.swift b/Apps/Locations/Libraries/Sources/Locations/Endpoints/GetLocationsEndpoint.swift new file mode 100644 index 0000000..1755734 --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Locations/Endpoints/GetLocationsEndpoint.swift @@ -0,0 +1,20 @@ +// +// GetLocationsEndpoint.swift +// Locations +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import APICore +import Foundation + +struct GetLocationsEndpoint: Endpoint { + let scheme: String = .Scheme.https + let host: String = .Hosts.default + let port: Int? = nil + let path: String = .Paths.getLocations + let method: HTTPRequestMethod = .get + let headers: [String: String] = [:] + let body: Data? = nil +} diff --git a/Apps/Locations/Libraries/Sources/Locations/Extensions/String+Constants.swift b/Apps/Locations/Libraries/Sources/Locations/Extensions/String+Constants.swift new file mode 100644 index 0000000..3eff3f9 --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Locations/Extensions/String+Constants.swift @@ -0,0 +1,21 @@ +// +// String+Constants.swift +// Locations +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +extension String { + enum Scheme { + static let https = "https" + } + + enum Hosts { + static let `default` = "raw.githubusercontent.com" + } + + enum Paths { + static let getLocations = "/abnamrocoesd/assignment-ios/main/locations.json" + } +} diff --git a/Apps/Locations/Libraries/Sources/Locations/Models/Location.swift b/Apps/Locations/Libraries/Sources/Locations/Models/Location.swift new file mode 100644 index 0000000..52567a0 --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Locations/Models/Location.swift @@ -0,0 +1,39 @@ +// +// Location.swift +// Locations (Library) +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +public struct Location: Equatable { + + // MARK: Properties + + public let name: String? + public let latitude: Float + public let longitude: Float + + // MARK: Initialisers + + public init( + name: String? = nil, + latitude: Float, + longitude: Float + ) { + self.name = name + self.latitude = latitude + self.longitude = longitude + } + +} + +// MARK: - Decodable + +extension Location: Decodable { + enum CodingKeys: String, CodingKey { + case name + case latitude = "lat" + case longitude = "long" + } +} 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/APICoreTests/Use Cases/MakeURLRequestUseCaseTests.swift b/Apps/Locations/Libraries/Tests/APICoreTests/Use Cases/MakeURLRequestUseCaseTests.swift new file mode 100644 index 0000000..ae893fb --- /dev/null +++ b/Apps/Locations/Libraries/Tests/APICoreTests/Use Cases/MakeURLRequestUseCaseTests.swift @@ -0,0 +1,119 @@ +// +// MakeURLRequestUseCaseTests.swift +// APICoreTests +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APICore + +final class MakeURLRequestUseCaseTests: XCTestCase { + + // MARK: Properties + + private let makeURLRequest = MakeURLRequestUseCase() + + // MARK: Test cases + + func test_withEndpoint_initialisedByDefault() throws { + // GIVEN + let endpoint = TestEndpoint() + + // WHEN + let result = try makeURLRequest(endpoint: endpoint) + + // THEN + XCTAssertNotNil(result) + XCTAssertEqual(result.url?.absoluteString, "http://www.something.com/path/to/endpoint") + XCTAssertEqual(result.httpMethod, HTTPRequestMethod.get.rawValue) + XCTAssertEqual(result.allHTTPHeaderFields, [:]) + XCTAssertNil(result.httpBody) + } + + func test_withEndpoint_initialisedWithPort() throws { + // GIVEN + let endpoint = TestEndpoint(port: 8080) + + // WHEN + let result = try makeURLRequest(endpoint: endpoint) + + // THEN + XCTAssertNotNil(result) + XCTAssertEqual(result.url?.absoluteString, "http://www.something.com:8080/path/to/endpoint") + XCTAssertEqual(result.httpMethod, HTTPRequestMethod.get.rawValue) + XCTAssertEqual(result.allHTTPHeaderFields, [:]) + XCTAssertNil(result.httpBody) + } + + func test_withEndpoint_initialisedWithHeaders() throws { + // GIVEN + let endpoint = TestEndpoint(headers: [ + "aHeader": "aValueForHead", + "someOtherHeader": "someValueForOtherHeader" + ]) + + // WHEN + let result = try makeURLRequest(endpoint: endpoint) + + // THEN + XCTAssertNotNil(result) + XCTAssertEqual(result.url?.absoluteString, "http://www.something.com/path/to/endpoint") + XCTAssertEqual(result.httpMethod, HTTPRequestMethod.get.rawValue) + XCTAssertEqual(result.allHTTPHeaderFields, [ + "aHeader": "aValueForHead", + "someOtherHeader": "someValueForOtherHeader" + ]) + XCTAssertNil(result.httpBody) + } + + func test_withEndpoint_initialisedWithBody() throws { + // GIVEN + let data = "This is some data for a body of a request".data(using: .utf8) + let endpoint = TestEndpoint(body: data) + + // WHEN + let result = try makeURLRequest(endpoint: endpoint) + + // THEN + XCTAssertNotNil(result) + XCTAssertEqual(result.url?.absoluteString, "http://www.something.com/path/to/endpoint") + XCTAssertEqual(result.httpMethod, HTTPRequestMethod.get.rawValue) + XCTAssertEqual(result.allHTTPHeaderFields, [:]) + XCTAssertEqual(result.httpBody, data) + XCTAssertNotNil(data) + } + +} + +// MARK: - TestEndpoint + +private struct TestEndpoint: Endpoint { + + // MARK: Properties + + let scheme: String = "http" + let host: String = "www.something.com" + let path: String = "/path/to/endpoint" + let method: HTTPRequestMethod = .get + + var port: Int? + var headers: [String : String] + var body: Data? + + // MARK: Initialisers + + init( + port: Int? = nil, + headers: [String : String] = [:], + body: Data? = nil + ) { + self.port = port + self.body = body + self.headers = headers + } + +} diff --git a/Apps/Locations/Libraries/Tests/LibrariesTests/LibrariesTests.swift b/Apps/Locations/Libraries/Tests/LibrariesTests/LibrariesTests.swift deleted file mode 100644 index dd47066..0000000 --- a/Apps/Locations/Libraries/Tests/LibrariesTests/LibrariesTests.swift +++ /dev/null @@ -1,11 +0,0 @@ -import XCTest -@testable import Libraries - -final class LibrariesTests: XCTestCase { - 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. - XCTAssertEqual(Libraries().text, "Hello, World!") - } -} diff --git a/Apps/Locations/Libraries/Tests/LocationsTests/Clients/LocationsClientTests.swift b/Apps/Locations/Libraries/Tests/LocationsTests/Clients/LocationsClientTests.swift new file mode 100644 index 0000000..b3b7b49 --- /dev/null +++ b/Apps/Locations/Libraries/Tests/LocationsTests/Clients/LocationsClientTests.swift @@ -0,0 +1,152 @@ +// +// LocationsClientTests.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 LocationsClientTests: XCTestCase { + + // MARK: Properties + + private let makeURLRequest = MakeURLRequestUseCase() + private let sessionConfiguration = { + let configuration = URLSessionConfiguration.default + + configuration.protocolClasses = [MockURLProtocol.self] + + return configuration + }() + + private var client: LocationsClient! + private var url: URL! + private var data: Data! + + // MARK: Setup + + override func setUp() async throws { + client = .init(configuration: sessionConfiguration) + } + + override func tearDown() async throws { + client = nil + } + + // MARK: Tests + + func test_request_withGetLocationsEndpoint_forLocations_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 client.request(endpoint: endpoint, for: Locations.self) + + // THEN + XCTAssertEqual(result, Locations(locations: [ + .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_request_withGetLocationsEndpoint_forLocations_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 client.request(endpoint: endpoint, for: Locations.self) + } catch LocationsClientError.statusErrorClient { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_request_withGetLocationsEndpoint_forLocations_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 client.request(endpoint: endpoint, for: Locations.self) + } catch LocationsClientError.statusErrorServer { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_request_withGetLocationsEndpoint_forLocations_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 client.request(endpoint: endpoint, for: Locations.self) + } catch LocationsClientError.statusErrorUnexpected { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + +} diff --git a/Apps/Locations/Libraries/Tests/LocationsTests/Endpoints/GetLocationsEndpointTests.swift b/Apps/Locations/Libraries/Tests/LocationsTests/Endpoints/GetLocationsEndpointTests.swift new file mode 100644 index 0000000..b8a05cb --- /dev/null +++ b/Apps/Locations/Libraries/Tests/LocationsTests/Endpoints/GetLocationsEndpointTests.swift @@ -0,0 +1,32 @@ +// +// GetLocationsEndpointTests.swift +// LocationsTests +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import XCTest + +@testable import Locations + +final class GetLocationsEndpointTests: XCTestCase { + + // MARK: Tests + + func test_init() { + // GIVEN + // WHEN + let endpoint = GetLocationsEndpoint() + + // THEN + XCTAssertNotNil(endpoint) + XCTAssertEqual(endpoint.scheme, .Scheme.https) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertNil(endpoint.port) + XCTAssertEqual(endpoint.path, .Paths.getLocations) + XCTAssertTrue(endpoint.headers.isEmpty) + XCTAssertNil(endpoint.body) + } + +} 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) + } + } + +}