Implemented the APIClient client.

This commit is contained in:
Javier Cicchelli 2022-12-04 17:21:11 +01:00
parent 3a87c57371
commit 74a22d5844
5 changed files with 485 additions and 0 deletions

View File

@ -0,0 +1,27 @@
//
// DateFormatter+Formatter.swift
// APIServices
//
// Created by Javier Cicchelli on 04/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import Foundation
extension DateFormatter {
static let iso8601 = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return dateFormatter
}()
static let isoZulu = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
return dateFormatter
}()
}

View File

@ -31,3 +31,7 @@ extension Item: Decodable {
case contentType
}
}
// MARK: - Equatable
extension Item: Equatable {}

View File

@ -15,3 +15,7 @@ public struct Me {
// MARK: - Decodable
extension Me: Decodable {}
// MARK: - Equatable
extension Me: Equatable {}

View File

@ -0,0 +1,113 @@
//
// APIClient.swift
// APIService
//
// Created by Javier Cicchelli on 04/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import Foundation
struct APIClient {
private let session: URLSession
private let decoder: JSONDecoder
private let makeURLRequest: MakeURLRequestUseCase
}
// MARK: - Initialisers
extension APIClient {
init(configuration: URLSessionConfiguration = .default) {
self.session = .init(configuration: configuration)
self.decoder = .init()
self.makeURLRequest = .init()
}
}
// MARK: - Client
extension APIClient: Client {
func request<Model>(
endpoint: some Endpoint,
model: Model.Type
) async throws -> Model where Model : Decodable {
let urlRequest = try makeURLRequest(endpoint: endpoint)
let (data, response) = try await session.data(for: urlRequest)
guard
let httpResponse = response as? HTTPURLResponse,
let responseStatus = ResponseStatus(rawValue: httpResponse.statusCode)
else {
throw APIClientError.responseNotReturned
}
switch responseStatus {
case .ok,
.created:
return try await decode(data, as: model)
case .noContent:
throw APIClientError.notSupported
case .badRequest:
throw APIClientError.itemAlreadyExist
case .notFound:
throw APIClientError.itemDoesNotExist
}
}
@discardableResult
func request(
endpoint: some Endpoint
) async throws -> Data {
let urlRequest = try makeURLRequest(endpoint: endpoint)
let (data, response) = try await session.data(for: urlRequest)
guard
let httpResponse = response as? HTTPURLResponse,
let responseStatus = ResponseStatus(rawValue: httpResponse.statusCode)
else {
throw APIClientError.responseNotReturned
}
switch responseStatus {
case .ok,
.noContent:
return data
case .created:
throw APIClientError.notSupported
case .badRequest:
throw APIClientError.itemIsNotFile
case .notFound:
throw APIClientError.itemDoesNotExist
}
}
}
// MARK: - Helpers
private extension APIClient {
func decode<Model: Decodable>(
_ data: Data,
as model: Model.Type
) async throws -> Model {
do {
decoder.dateDecodingStrategy = .formatted(.isoZulu)
return try decoder.decode(model, from: data)
} catch {
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(model, from: data)
}
}
}
// MARK: - Errors
public enum APIClientError: Error {
case responseNotReturned
case itemIsNotFile
case itemAlreadyExist
case itemDoesNotExist
case notSupported
}

View File

@ -0,0 +1,337 @@
//
// APIClient+RequestTests.swift
// APIServiceTests
//
// Created by Javier Cicchelli on 04/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import Foundation
import XCTest
@testable import APIService
final class APIClientRequestTests: XCTestCase {
// MARK: Properties
private let makeURLRequest = MakeURLRequestUseCase()
private let dateFormatter = DateFormatter.iso8601
private let sessionConfiguration = {
let configuration = URLSessionConfiguration.default
configuration.protocolClasses = [MockURLProtocol.self]
return configuration
}()
private var client: APIClient!
private var url: URL!
private var data: Data!
// MARK: Setup
override func setUp() async throws {
client = .init(configuration: sessionConfiguration)
}
// MARK: Request cases
func test_withSomeEndpoint_andSomeModel_whenResponseStatusOk() async throws {
// GIVEN
let endpoint = GetMeEndpoint(
username: "username",
password: "password"
)
url = try makeURLRequest(endpoint: endpoint).url
data = "{\"firstName\":\"Noel\",\"lastName\":\"Flantier\",\"rootItem\":{\"id\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\",\"parentId\":\"\",\"name\":\"dossierTest\",\"isDir\":true,\"modificationDate\":\"2021-11-29T10:57:13Z\"}}\n".data(using: .utf8)
MockURLProtocol.mockData[url] = MockURLResponse(
status: .ok,
headers: [:],
data: data
)
// WHEN
let result = try await client.request(endpoint: endpoint, model: Me.self)
// THEN
XCTAssertEqual(result, Me(
firstName: "Noel",
lastName: "Flantier",
rootItem: .init(
idParent: "",
id: "4b8e41fd4a6a89712f15bbf102421b9338cfab11",
name: "dossierTest",
isDirectory: true,
lastModifiedAt: dateFormatter.date(from: "2021-11-29T10:57:13Z")!,
size: nil,
contentType: nil
)
))
}
func test_withSomeEndpoint_andSomeModel_whenResponseStatusCreated() async throws {
// GIVEN
let endpoint = GetMeEndpoint(
username: "username",
password: "password"
)
url = try makeURLRequest(endpoint: endpoint).url
data = "{\"firstName\":\"Noel\",\"lastName\":\"Flantier\",\"rootItem\":{\"id\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\",\"parentId\":\"\",\"name\":\"dossierTest\",\"isDir\":true,\"modificationDate\":\"2021-11-29T10:57:13Z\"}}\n".data(using: .utf8)
MockURLProtocol.mockData[url] = MockURLResponse(
status: .created,
headers: [:],
data: data
)
// WHEN
let result = try await client.request(endpoint: endpoint, model: Me.self)
// THEN
XCTAssertEqual(result, Me(
firstName: "Noel",
lastName: "Flantier",
rootItem: .init(
idParent: "",
id: "4b8e41fd4a6a89712f15bbf102421b9338cfab11",
name: "dossierTest",
isDirectory: true,
lastModifiedAt: dateFormatter.date(from: "2021-11-29T10:57:13Z")!,
size: nil,
contentType: nil
)
))
}
func test_withSomeEndpoint_andSomeModel_whenResponseStatusNoContent() async throws {
// GIVEN
let endpoint = GetMeEndpoint(
username: "username",
password: "password"
)
url = try makeURLRequest(endpoint: endpoint).url
MockURLProtocol.mockData[url] = MockURLResponse(
status: .noContent,
headers: [:],
data: nil
)
// WHEN & THEN
do {
_ = try await client.request(endpoint: endpoint, model: Me.self)
} catch APIClientError.notSupported {
XCTAssertTrue(true)
} catch {
XCTAssertTrue(false)
}
}
func test_withSomeEndpoint_andSomeModel_whenResponseStatusBadRequest() async throws {
// GIVEN
let endpoint = GetMeEndpoint(
username: "username",
password: "password"
)
url = try makeURLRequest(endpoint: endpoint).url
MockURLProtocol.mockData[url] = MockURLResponse(
status: .badRequest,
headers: [:],
data: nil
)
// WHEN & THEN
do {
_ = try await client.request(endpoint: endpoint, model: Me.self)
} catch APIClientError.itemAlreadyExist {
XCTAssertTrue(true)
} catch {
XCTAssertTrue(false)
}
}
func test_withSomeEndpoint_andSomeModel_whenResponseStatusNotFound() async throws {
// GIVEN
let endpoint = GetMeEndpoint(
username: "username",
password: "password"
)
url = try makeURLRequest(endpoint: endpoint).url
MockURLProtocol.mockData[url] = MockURLResponse(
status: .notFound,
headers: [:],
data: nil
)
// WHEN & THEN
do {
_ = try await client.request(endpoint: endpoint, model: Me.self)
} catch APIClientError.itemDoesNotExist {
XCTAssertTrue(true)
} catch {
XCTAssertTrue(false)
}
}
func test_withSomeEndpoint_andSomeModel_whenResponseDataIncorrect() async throws {
// GIVEN
let endpoint = GetMeEndpoint(
username: "username",
password: "password"
)
url = try makeURLRequest(endpoint: endpoint).url
data = "{\"firstName\":\"Noel\",\"lastName\":\"Flantier\",\"rootItem\":{\"id\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\"\"isDir\":true}}\n".data(using: .utf8)
MockURLProtocol.mockData[url] = MockURLResponse(
status: .ok,
headers: [:],
data: data
)
// WHEN & THEN
do {
_ = try await client.request(endpoint: endpoint, model: Me.self)
} catch is DecodingError {
XCTAssertTrue(true)
} catch {
XCTAssertTrue(false)
}
}
func test_withSomeEndpoint_whenResponseStatusOk() async throws {
// GIVEN
let endpoint = GetDataEndpoint(
itemId: UUID().uuidString,
username: "username",
password: "password"
)
url = try makeURLRequest(endpoint: endpoint).url
data = "This is just some dummy data for testing purposes".data(using: .utf8)
MockURLProtocol.mockData[url] = MockURLResponse(
status: .ok,
headers: [:],
data: data
)
// WHEN
let result = try await client.request(endpoint: endpoint)
// THEN
XCTAssertEqual(result, data)
}
func test_withSomeEndpoint_whenResponseStatusCreated() async throws {
// GIVEN
let endpoint = GetDataEndpoint(
itemId: UUID().uuidString,
username: "username",
password: "password"
)
url = try makeURLRequest(endpoint: endpoint).url
MockURLProtocol.mockData[url] = MockURLResponse(
status: .created,
headers: [:],
data: nil
)
// WHEN & THEN
do {
try await client.request(endpoint: endpoint)
} catch APIClientError.notSupported {
XCTAssertTrue(true)
} catch {
XCTAssertTrue(false)
}
}
func test_withSomeEndpoint_whenResponseStatusNoContent() async throws {
// GIVEN
let endpoint = GetDataEndpoint(
itemId: UUID().uuidString,
username: "username",
password: "password"
)
url = try makeURLRequest(endpoint: endpoint).url
data = .init()
MockURLProtocol.mockData[url] = MockURLResponse(
status: .noContent,
headers: [:],
data: nil
)
// WHEN
let result = try await client.request(endpoint: endpoint)
// THEN
XCTAssertEqual(result, data)
}
func test_withSomeEndpoint_whenResponseStatusBadRequest() async throws {
// GIVEN
let endpoint = GetDataEndpoint(
itemId: UUID().uuidString,
username: "username",
password: "password"
)
url = try makeURLRequest(endpoint: endpoint).url
MockURLProtocol.mockData[url] = MockURLResponse(
status: .badRequest,
headers: [:],
data: nil
)
// WHEN & THEN
do {
try await client.request(endpoint: endpoint)
} catch APIClientError.itemIsNotFile {
XCTAssertTrue(true)
} catch {
XCTAssertTrue(false)
}
}
func test_withSomeEndpoint_whenResponseStatusNotFound() async throws {
// GIVEN
let endpoint = GetDataEndpoint(
itemId: UUID().uuidString,
username: "username",
password: "password"
)
url = try makeURLRequest(endpoint: endpoint).url
MockURLProtocol.mockData[url] = MockURLResponse(
status: .notFound,
headers: [:],
data: nil
)
// WHEN & THEN
do {
try await client.request(endpoint: endpoint)
} catch APIClientError.itemDoesNotExist {
XCTAssertTrue(true)
} catch {
XCTAssertTrue(false)
}
}
}