adding project file
This commit is contained in:
parent
c78b302f99
commit
351517b0c3
|
@ -1,3 +0,0 @@
|
|||
struct AFNetworks {
|
||||
var text = "Hello, World!"
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// DataTransferService.swift
|
||||
// KooberMVVM-AF
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
public enum DataTransferError: Error {
|
||||
case noResponse
|
||||
case paring(Error)
|
||||
case networkFailure(NetworkError)
|
||||
case resolvedNetworkFailure(Error)
|
||||
}
|
||||
|
||||
// MARK: - Error Resolver
|
||||
public protocol DataTransferErrorResolver {
|
||||
func resolve(error: NetworkError) -> Error
|
||||
}
|
||||
|
||||
// MARK: - Error Logger
|
||||
public protocol DataTransferErrorLogger {
|
||||
func log(error: Error)
|
||||
}
|
||||
|
||||
// MARK: - Response Decoder
|
||||
public protocol ResponseDecoder {
|
||||
func decode<T: Decodable>(_ data: Data) throws -> T
|
||||
}
|
||||
|
||||
// MARK: - Data Transfer Service
|
||||
public protocol DataTransferService {
|
||||
typealias CompletionHandler<T> = (Result<T, Error>) -> Void
|
||||
|
||||
@discardableResult
|
||||
func request<T: Decodable, E: ResponseRequestable>(with endpoint: E, completion: @escaping CompletionHandler<T>) -> NetworkCancellable? where E.Response == T
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Response Decoders
|
||||
public class JSONResponseDecoder: ResponseDecoder {
|
||||
private let jsonDecoder = JSONDecoder()
|
||||
public init() { }
|
||||
public func decode<T: Decodable>(_ data: Data) throws -> T {
|
||||
return try jsonDecoder.decode(T.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
public class RawDataResponseDecoder: ResponseDecoder {
|
||||
public init() { }
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case `default` = ""
|
||||
}
|
||||
public func decode<T: Decodable>(_ data: Data) throws -> T {
|
||||
if T.self is Data.Type, let data = data as? T {
|
||||
return data
|
||||
} else {
|
||||
let context = DecodingError.Context(codingPath: [CodingKeys.default], debugDescription: "Expected Data type")
|
||||
throw Swift.DecodingError.typeMismatch(T.self, context)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// DefaultDataTransferErrorLogger.swift
|
||||
// KooberMVVM-AF
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class DefaultDataTransferErrorLogger: DataTransferErrorLogger {
|
||||
public init() { }
|
||||
|
||||
public func log(error: Error) {
|
||||
#if DEBUG
|
||||
print("-------------")
|
||||
print("error: \(error)")
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// DefaultDataTransferErrorResolver.swift
|
||||
// KooberMVVM-AF
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class DefaultDataTransferErrorResolver: DataTransferErrorResolver {
|
||||
public init() { }
|
||||
public func resolve(error: NetworkError) -> Error {
|
||||
return error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// DefaultDataTransferService.swift
|
||||
// KooberMVVM-AF
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class DefaultDataTransferService {
|
||||
|
||||
private let networkService: NetworkService
|
||||
private let errorResolver: DataTransferErrorResolver
|
||||
private let errorLogger: DataTransferErrorLogger
|
||||
|
||||
public init(networkService: NetworkService,
|
||||
errorResolver: DataTransferErrorResolver = DefaultDataTransferErrorResolver(),
|
||||
errorLogger: DataTransferErrorLogger = DefaultDataTransferErrorLogger()) {
|
||||
self.networkService = networkService
|
||||
self.errorResolver = errorResolver
|
||||
self.errorLogger = errorLogger
|
||||
}
|
||||
}
|
||||
|
||||
extension DefaultDataTransferService: DataTransferService {
|
||||
public func request<T: Decodable, E: ResponseRequestable>(with endpoint: E, completion: @escaping CompletionHandler<T>) -> NetworkCancellable? where E.Response == T {
|
||||
|
||||
return self.networkService.request(endpoint: endpoint) { result in
|
||||
switch result {
|
||||
case .success(let data):
|
||||
let result: Result<T, Error> = self.decode(data: data, decoder: endpoint.responseDecoder)
|
||||
DispatchQueue.main.async { return completion(result) }
|
||||
case .failure(let nError):
|
||||
self.errorLogger.log(error: nError)
|
||||
let error = self.resolve(networkError: nError)
|
||||
DispatchQueue.main.async { return completion(.failure(error))}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func decode<T: Decodable> (data: Data?, decoder: ResponseDecoder) -> Result<T, Error> {
|
||||
do {
|
||||
guard let data = data else { return .failure(DataTransferError.noResponse)}
|
||||
let result: T = try decoder.decode(data)
|
||||
return .success(result)
|
||||
} catch {
|
||||
self.errorLogger.log(error: error)
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func resolve(networkError error: NetworkError) -> DataTransferError {
|
||||
let resolvedError = self.errorResolver.resolve(error: error)
|
||||
return resolvedError is NetworkError ? .networkFailure(error) : .resolvedNetworkFailure(resolvedError)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
//
|
||||
// Endpoint.swift
|
||||
// KooberMVVM-AF
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum HTTPMethod: String {
|
||||
case delete = "DELETE"
|
||||
case post = "POST"
|
||||
case get = "GET"
|
||||
case put = "PUT"
|
||||
case head = "HEAD"
|
||||
}
|
||||
|
||||
public enum BodyEncoding {
|
||||
case jsonSerializationData
|
||||
case stringEncodingAscii
|
||||
}
|
||||
|
||||
public enum RequestGenerationError: Error {
|
||||
case components
|
||||
}
|
||||
|
||||
|
||||
public class Endpoint<R>: ResponseRequestable {
|
||||
|
||||
public typealias Response = R
|
||||
|
||||
public var path: String
|
||||
public var isFullPath: Bool
|
||||
public var method: HTTPMethod
|
||||
public var headerParameter: [String : String]
|
||||
public var bodyParameter: [String : Any]
|
||||
public var queryParameters: [String : Any]
|
||||
public var bodyEncoding: BodyEncoding
|
||||
public var responseDecoder: ResponseDecoder
|
||||
|
||||
public init(path: String,
|
||||
isFullPath: Bool = false,
|
||||
method: HTTPMethod = .get,
|
||||
headerParameters: [String: String] = [:],
|
||||
bodyParameters: [String: Any] = [:],
|
||||
queryParameters: [String: Any] = [:],
|
||||
bodyEncoding: BodyEncoding = .jsonSerializationData,
|
||||
responseDecoder: ResponseDecoder = JSONResponseDecoder()) {
|
||||
self.path = path
|
||||
self.isFullPath = isFullPath
|
||||
self.method = method
|
||||
self.headerParameter = headerParameters
|
||||
self.bodyParameter = bodyParameters
|
||||
self.queryParameters = queryParameters
|
||||
self.bodyEncoding = bodyEncoding
|
||||
self.responseDecoder = responseDecoder
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Requestable
|
||||
public protocol Requestable {
|
||||
|
||||
var path: String { get }
|
||||
var isFullPath: Bool { get }
|
||||
var method: HTTPMethod { get }
|
||||
var headerParameter: [String: String] { get }
|
||||
var bodyParameter: [String: Any] { get }
|
||||
var queryParameters: [String: Any] { get }
|
||||
var bodyEncoding: BodyEncoding { get }
|
||||
|
||||
func urlRequest(with networkConfig: NetworkConfigurable) throws -> URLRequest
|
||||
}
|
||||
|
||||
extension Requestable {
|
||||
func url(with config: NetworkConfigurable) throws -> URL {
|
||||
|
||||
let baseURL = config.baseURL.absoluteString.last != "/" ? config.baseURL.absoluteString + "/" : config.baseURL.absoluteString
|
||||
let endpoint = isFullPath ? path : baseURL.appending(path)
|
||||
|
||||
guard var urlComponents = URLComponents(string: endpoint) else { throw RequestGenerationError.components }
|
||||
var urlQueryItems = [URLQueryItem]()
|
||||
|
||||
queryParameters.forEach {
|
||||
urlQueryItems.append(URLQueryItem(name: $0.key, value: "\($0.value)"))
|
||||
}
|
||||
|
||||
config.queryParameters.forEach {
|
||||
urlQueryItems.append(URLQueryItem(name: $0.key, value: $0.value))
|
||||
}
|
||||
|
||||
urlComponents.queryItems = !urlQueryItems.isEmpty ? urlQueryItems : nil
|
||||
|
||||
guard let url = urlComponents.url else { throw RequestGenerationError.components }
|
||||
return url
|
||||
}
|
||||
|
||||
public func urlRequest(with config: NetworkConfigurable) throws -> URLRequest {
|
||||
|
||||
let url = try self.url(with: config)
|
||||
var urlRequest = URLRequest(url: url)
|
||||
var allHeaders: [String: String] = config.headers
|
||||
|
||||
headerParameter.forEach { allHeaders.updateValue($0.key, forKey: $0.value) }
|
||||
|
||||
if !bodyParameter.isEmpty {
|
||||
urlRequest.httpBody = encodeBody(bodyParameters: bodyParameter, bodyEncoding: bodyEncoding)
|
||||
}
|
||||
urlRequest.httpMethod = method.rawValue
|
||||
urlRequest.allHTTPHeaderFields = allHeaders
|
||||
return urlRequest
|
||||
}
|
||||
|
||||
fileprivate func encodeBody(bodyParameters: [String: Any], bodyEncoding: BodyEncoding) -> Data? {
|
||||
switch bodyEncoding {
|
||||
case .jsonSerializationData:
|
||||
return try? JSONSerialization.data(withJSONObject: bodyParameters)
|
||||
case .stringEncodingAscii:
|
||||
return bodyParameters.queryString.data(using: String.Encoding.ascii, allowLossyConversion: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ResponseRequestable
|
||||
public protocol ResponseRequestable: Requestable {
|
||||
associatedtype Response
|
||||
var responseDecoder: ResponseDecoder { get }
|
||||
}
|
||||
|
||||
fileprivate extension Dictionary {
|
||||
var queryString: String {
|
||||
|
||||
return self.map { "\($0.key)=\($0.value)" }
|
||||
.joined(separator: "&")
|
||||
.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed) ?? ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// NetworkConfig.swift
|
||||
// KooberMVVM-AF
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Network Configuration Protocol
|
||||
public protocol NetworkConfigurable {
|
||||
var baseURL: URL { get }
|
||||
var headers: [String: String] { get }
|
||||
var queryParameters: [String: String] { get }
|
||||
}
|
||||
|
||||
// MARK: - Default Configuration
|
||||
public struct ApiDataNetworkConfig: NetworkConfigurable {
|
||||
|
||||
public var baseURL: URL
|
||||
public var headers: [String : String]
|
||||
public var queryParameters: [String : String]
|
||||
|
||||
public init(baseURL: URL, headers: [String: String], queryParameters: [String: String]) {
|
||||
self.baseURL = baseURL
|
||||
self.headers = headers
|
||||
self.queryParameters = queryParameters
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
//
|
||||
// DefaultNetworkService.swift
|
||||
// KooberMVVM-AF
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class DefaultNetworkService {
|
||||
|
||||
private let config: NetworkConfigurable
|
||||
private let sessionManager: NetworkSessionManager
|
||||
private let logger: NetworkErrorLogger
|
||||
|
||||
public init(config: NetworkConfigurable, sessionManager: NetworkSessionManager, logger: NetworkErrorLogger = DefaultNetworkErrorLogger()) {
|
||||
self.config = config
|
||||
self.sessionManager = sessionManager
|
||||
self.logger = logger
|
||||
}
|
||||
|
||||
private func request(request: URLRequest, completion: @escaping CompletionHandler) -> NetworkCancellable {
|
||||
|
||||
let sessionDataTask = sessionManager.request(request) { (data, urlResponse, requestError) in
|
||||
|
||||
if let requestError = requestError {
|
||||
var error: NetworkError
|
||||
if let response = urlResponse as? HTTPURLResponse {
|
||||
error = .error(statusCode: response.statusCode, data: data)
|
||||
} else {
|
||||
error = self.resolve(error: requestError)
|
||||
}
|
||||
|
||||
self.logger.log(error: error)
|
||||
completion(.failure(error))
|
||||
} else {
|
||||
self.logger.log(responseData: data, response: urlResponse)
|
||||
completion(.success(data))
|
||||
}
|
||||
}
|
||||
|
||||
logger.log(request: request)
|
||||
return sessionDataTask
|
||||
}
|
||||
|
||||
private func resolve(error: Error) -> NetworkError {
|
||||
let code = URLError.Code(rawValue: (error as NSError).code)
|
||||
switch code {
|
||||
case .notConnectedToInternet:
|
||||
return .notConnected
|
||||
case .cancelled:
|
||||
return .cancelled
|
||||
default:
|
||||
return .generic(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DefaultNetworkService: NetworkService {
|
||||
public func request(endpoint: Requestable, completion: @escaping CompletionHandler) -> NetworkCancellable? {
|
||||
do {
|
||||
let urlRequest = try endpoint.urlRequest(with: config)
|
||||
return self.request(request: urlRequest, completion: completion)
|
||||
} catch {
|
||||
completion(.failure(.urlGeneration))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// DefaultSessionManager.swift
|
||||
// KooberMVVM-AF
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Default Network Session Manager
|
||||
// Note: If authorization is needed NetworkSessionManager can be implemented by using,
|
||||
// for example, Alamofire SessionManager with its RequestAdapter and RequestRetrier.
|
||||
// And it can be injected into NetworkService instead of default one.
|
||||
public class DefaultNetworkSessionManager: NetworkSessionManager {
|
||||
public init() {}
|
||||
|
||||
public func request(_ request: URLRequest,
|
||||
completion: @escaping CompletionHandler) -> NetworkCancellable {
|
||||
let task = URLSession.shared.dataTask(with: request, completionHandler: completion)
|
||||
task.resume()
|
||||
return task
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// File.swift
|
||||
// KooberMVVM-AF
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class DefaultNetworkErrorLogger: NetworkErrorLogger {
|
||||
|
||||
public init() { }
|
||||
|
||||
public func log(request: URLRequest) {
|
||||
#if DEBUG
|
||||
print("-------------")
|
||||
print("request: \(request.url!)")
|
||||
print("headers: \(request.allHTTPHeaderFields!)")
|
||||
print("method: \(request.httpMethod!)")
|
||||
if let httpBody = request.httpBody, let result = ((try? JSONSerialization.jsonObject(with: httpBody, options: []) as? [String: AnyObject]) as [String: AnyObject]??) {
|
||||
print("body: \(String(describing: result))")
|
||||
}
|
||||
if let httpBody = request.httpBody, let resultString = String(data: httpBody, encoding: .utf8) {
|
||||
print("body: \(String(describing: resultString))")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public func log(responseData data: Data?, response: URLResponse?) {
|
||||
#if DEBUG
|
||||
guard let data = data else { return }
|
||||
if let dataDict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
|
||||
print("responseData: \(String(describing: dataDict))")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public func log(error: Error) {
|
||||
#if DEBUG
|
||||
print("error: \(error)")
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// NetworkService.swift
|
||||
// KooberMVVM-AF
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Network Error Enum
|
||||
public enum NetworkError: Error {
|
||||
case error(statusCode: Int, data: Data?)
|
||||
case notConnected
|
||||
case cancelled
|
||||
case generic(Error)
|
||||
case urlGeneration
|
||||
}
|
||||
|
||||
extension NetworkError {
|
||||
public var isNotFoundError: Bool { return hasStatusCode(404) }
|
||||
|
||||
public func hasStatusCode(_ codeError: Int) -> Bool {
|
||||
switch self {
|
||||
case let .error(code, _):
|
||||
return code == codeError
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cancellable Protocol
|
||||
public protocol NetworkCancellable {
|
||||
func cancel()
|
||||
}
|
||||
|
||||
extension URLSessionTask: NetworkCancellable { }
|
||||
|
||||
// MARK: - Network Service Protocol
|
||||
public protocol NetworkService {
|
||||
typealias CompletionHandler = (Result<Data?, NetworkError>) -> Void
|
||||
|
||||
func request(endpoint: Requestable, completion: @escaping CompletionHandler) -> NetworkCancellable?
|
||||
}
|
||||
|
||||
// MARK: - Network Session Manager Protocol
|
||||
public protocol NetworkSessionManager {
|
||||
typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
|
||||
|
||||
func request(_ urlRequest: URLRequest, completion: @escaping CompletionHandler) -> NetworkCancellable
|
||||
}
|
||||
|
||||
// MARK: - Network Error Logger
|
||||
public protocol NetworkErrorLogger {
|
||||
func log(request: URLRequest)
|
||||
func log(responseData data: Data?, response: URLResponse?)
|
||||
func log(error: Error)
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
//
|
||||
// DataTransferServiceTests.swift
|
||||
// AFNetworkTests
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import AFNetworks
|
||||
|
||||
private struct MockModel: Decodable {
|
||||
let name: String
|
||||
}
|
||||
|
||||
class DataTransferServiceTests: XCTestCase {
|
||||
|
||||
private enum DataTransferErrorMock: Error {
|
||||
case someError
|
||||
}
|
||||
|
||||
func test_whenReceivedValidJsonInResponse_shouldDecodeResponseToDecodableObject() {
|
||||
//given
|
||||
let config = NetworkConfigurableMock()
|
||||
let expectation = self.expectation(description: "Should decode mock object")
|
||||
|
||||
let responseData = #"{"name": "Hello"}"#.data(using: .utf8)
|
||||
let networkService = DefaultNetworkService(config: config, sessionManager: NetworkSessionManagerMock(response: nil,
|
||||
data: responseData,
|
||||
error: nil))
|
||||
|
||||
let sut = DefaultDataTransferService(networkService: networkService)
|
||||
//when
|
||||
_ = sut.request(with: Endpoint<MockModel>(path: "http://mock.endpoint.com", method: .get)) { result in
|
||||
do {
|
||||
let object = try result.get()
|
||||
XCTAssertEqual(object.name, "Hello")
|
||||
expectation.fulfill()
|
||||
} catch {
|
||||
XCTFail("Failed decoding MockObject")
|
||||
}
|
||||
}
|
||||
//then
|
||||
wait(for: [expectation], timeout: 0.1)
|
||||
}
|
||||
|
||||
func test_whenInvalidResponse_shouldNotDecodeObject() {
|
||||
//given
|
||||
let config = NetworkConfigurableMock()
|
||||
let expectation = self.expectation(description: "Should not decode mock object")
|
||||
|
||||
let responseData = #"{"age": 20}"#.data(using: .utf8)
|
||||
let networkService = DefaultNetworkService(config: config, sessionManager: NetworkSessionManagerMock(response: nil,
|
||||
data: responseData,
|
||||
error: nil))
|
||||
|
||||
let sut = DefaultDataTransferService(networkService: networkService)
|
||||
//when
|
||||
_ = sut.request(with: Endpoint<MockModel>(path: "http://mock.endpoint.com", method: .get)) { result in
|
||||
do {
|
||||
_ = try result.get()
|
||||
XCTFail("Should not happen")
|
||||
} catch {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
//then
|
||||
wait(for: [expectation], timeout: 0.1)
|
||||
}
|
||||
|
||||
func test_whenBadRequestReceived_shouldRethrowNetworkError() {
|
||||
//given
|
||||
let config = NetworkConfigurableMock()
|
||||
let expectation = self.expectation(description: "Should throw network error")
|
||||
|
||||
let responseData = #"{"invalidStructure": "Nothing"}"#.data(using: .utf8)!
|
||||
let response = HTTPURLResponse(url: URL(string: "test_url")!,
|
||||
statusCode: 500,
|
||||
httpVersion: "1.1",
|
||||
headerFields: nil)
|
||||
let networkService = DefaultNetworkService(config: config, sessionManager: NetworkSessionManagerMock(response: response,
|
||||
data: responseData,
|
||||
error: DataTransferErrorMock.someError))
|
||||
|
||||
let sut = DefaultDataTransferService(networkService: networkService)
|
||||
//when
|
||||
_ = sut.request(with: Endpoint<MockModel>(path: "http://mock.endpoint.com", method: .get)) { result in
|
||||
do {
|
||||
_ = try result.get()
|
||||
XCTFail("Should not happen")
|
||||
} catch let error {
|
||||
|
||||
if case DataTransferError.networkFailure(NetworkError.error(statusCode: 500, _)) = error {
|
||||
expectation.fulfill()
|
||||
} else {
|
||||
XCTFail("Wrong error")
|
||||
}
|
||||
}
|
||||
}
|
||||
//then
|
||||
wait(for: [expectation], timeout: 0.1)
|
||||
}
|
||||
|
||||
func test_whenNoDataReceived_shouldThrowNoDataError() {
|
||||
//given
|
||||
let config = NetworkConfigurableMock()
|
||||
let expectation = self.expectation(description: "Should throw no data error")
|
||||
|
||||
let response = HTTPURLResponse(url: URL(string: "test_url")!,
|
||||
statusCode: 200,
|
||||
httpVersion: "1.1",
|
||||
headerFields: [:])
|
||||
let networkService = DefaultNetworkService(config: config, sessionManager: NetworkSessionManagerMock(response: response,
|
||||
data: nil,
|
||||
error: nil))
|
||||
|
||||
let sut = DefaultDataTransferService(networkService: networkService)
|
||||
//when
|
||||
_ = sut.request(with: Endpoint<MockModel>(path: "http://mock.endpoint.com", method: .get)) { result in
|
||||
do {
|
||||
_ = try result.get()
|
||||
XCTFail("Should not happen")
|
||||
} catch let error {
|
||||
if case DataTransferError.noResponse = error {
|
||||
expectation.fulfill()
|
||||
} else {
|
||||
XCTFail("Wrong error")
|
||||
}
|
||||
}
|
||||
}
|
||||
//then
|
||||
wait(for: [expectation], timeout: 0.1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// NetworkConfigurableMock.swift
|
||||
// AFNetwork
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@testable import AFNetworks
|
||||
|
||||
class NetworkConfigurableMock: NetworkConfigurable {
|
||||
var baseURL: URL = URL(string: "https://mock.test.com")!
|
||||
var headers: [String: String] = [:]
|
||||
var queryParameters: [String: String] = [:]
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// NetworkSessionManagerMock.swift
|
||||
// AFNetwork
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@testable import AFNetworks
|
||||
|
||||
struct NetworkSessionManagerMock: NetworkSessionManager {
|
||||
let response: HTTPURLResponse?
|
||||
let data: Data?
|
||||
let error: Error?
|
||||
|
||||
func request(_ request: URLRequest,
|
||||
completion: @escaping CompletionHandler) -> NetworkCancellable {
|
||||
completion(data, response, error)
|
||||
if #available(iOS 13, *) {
|
||||
let session = URLSession.shared
|
||||
return session.dataTask(with: request)
|
||||
}else {
|
||||
return URLSessionDataTask()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
//
|
||||
// NetworkServiceTests.swift
|
||||
// AFNetworkTests
|
||||
//
|
||||
// Created by Ali Fakih on 2/24/20.
|
||||
// Copyright © 2020 Ali Fakih. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import AFNetworks
|
||||
|
||||
class NetworkServiceTests: XCTestCase {
|
||||
|
||||
// MARK: - Endpoint Mock
|
||||
private struct EndpointMock: Requestable {
|
||||
var path: String
|
||||
var isFullPath: Bool = false
|
||||
var method: HTTPMethod
|
||||
var headerParameter: [String : String] = [:]
|
||||
var bodyParameter: [String : Any] = [:]
|
||||
var queryParameters: [String : Any] = [:]
|
||||
var bodyEncoding: BodyEncoding = .stringEncodingAscii
|
||||
|
||||
init(path: String, method: HTTPMethod) {
|
||||
self.path = path
|
||||
self.method = method
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Network Error Logger
|
||||
class NetworkErrorLoggerMock: NetworkErrorLogger {
|
||||
var loggedErrors: [Error] = []
|
||||
func log(request: URLRequest) { }
|
||||
func log(responseData data: Data?, response: URLResponse?) { }
|
||||
func log(error: Error) { loggedErrors.append(error) }
|
||||
}
|
||||
|
||||
private enum NetworkErrorMock: Error {
|
||||
case someError
|
||||
}
|
||||
|
||||
func test_whenMockDataPassed_shouldReturnProperResponse() {
|
||||
|
||||
//GIVEN
|
||||
let config = NetworkConfigurableMock()
|
||||
let expectation = self.expectation(description: "Should return Correct Data")
|
||||
|
||||
let expectedResponseData = "Response data".data(using: .utf8)
|
||||
let sut = DefaultNetworkService(config: config, sessionManager: NetworkSessionManagerMock(
|
||||
response: nil,
|
||||
data: expectedResponseData,
|
||||
error: nil))
|
||||
|
||||
//WHEN
|
||||
_ = sut.request(endpoint: EndpointMock(path: "http://mock.test.com", method: .get), completion: { result in
|
||||
guard let responseData = try? result.get() else {
|
||||
XCTFail("Should return proper response")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(responseData, expectedResponseData)
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
//THEN
|
||||
wait(for: [expectation], timeout: 0.1)
|
||||
|
||||
}
|
||||
|
||||
func test_whenErrorWithNSURLErrorCancelledReturned_shouldReturnCancelledError() {
|
||||
//given
|
||||
let config = NetworkConfigurableMock()
|
||||
let expectation = self.expectation(description: "Should return hasStatusCode error")
|
||||
|
||||
let cancelledError = NSError(domain: "network", code: NSURLErrorCancelled, userInfo: nil)
|
||||
let sut = DefaultNetworkService(config: config, sessionManager: NetworkSessionManagerMock(response: nil,
|
||||
data: nil,
|
||||
error: cancelledError as Error))
|
||||
//when
|
||||
_ = sut.request(endpoint: EndpointMock(path: "http://mock.test.com", method: .get)) { result in
|
||||
do {
|
||||
_ = try result.get()
|
||||
XCTFail("Should not happen")
|
||||
} catch let error {
|
||||
guard case NetworkError.cancelled = error else {
|
||||
XCTFail("NetworkError.cancelled not found")
|
||||
return
|
||||
}
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
//then
|
||||
wait(for: [expectation], timeout: 0.1)
|
||||
}
|
||||
|
||||
func test_whenMalformedUrlPassed_shouldReturnUrlGenerationError() {
|
||||
//given
|
||||
let config = NetworkConfigurableMock()
|
||||
let expectation = self.expectation(description: "Should return correct data")
|
||||
|
||||
let expectedResponseData = "Response data".data(using: .utf8)!
|
||||
let sut = DefaultNetworkService(config: config, sessionManager: NetworkSessionManagerMock(response: nil,
|
||||
data: expectedResponseData,
|
||||
error: nil))
|
||||
//when
|
||||
_ = sut.request(endpoint: EndpointMock(path: "-;@,?:ą", method: .get)) { result in
|
||||
do {
|
||||
_ = try result.get()
|
||||
XCTFail("Should throw url generation error")
|
||||
} catch let error {
|
||||
guard case NetworkError.urlGeneration = error else {
|
||||
XCTFail("Should throw url generation error")
|
||||
return
|
||||
}
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
//then
|
||||
wait(for: [expectation], timeout: 0.1)
|
||||
}
|
||||
|
||||
func test_whenStatusCodeEqualOrAbove400_shouldReturnhasStatusCodeError() {
|
||||
//given
|
||||
let config = NetworkConfigurableMock()
|
||||
let expectation = self.expectation(description: "Should return hasStatusCode error")
|
||||
|
||||
let response = HTTPURLResponse(url: URL(string: "test_url")!,
|
||||
statusCode: 500,
|
||||
httpVersion: "1.1",
|
||||
headerFields: [:])
|
||||
let sut = DefaultNetworkService(config: config, sessionManager: NetworkSessionManagerMock(response: response,
|
||||
data: nil,
|
||||
error: NetworkErrorMock.someError))
|
||||
//when
|
||||
_ = sut.request(endpoint: EndpointMock(path: "http://mock.test.com", method: .get)) { result in
|
||||
do {
|
||||
_ = try result.get()
|
||||
XCTFail("Should not happen")
|
||||
} catch let error {
|
||||
if case NetworkError.error(let statusCode, _) = error {
|
||||
XCTAssertEqual(statusCode, 500)
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
//then
|
||||
wait(for: [expectation], timeout: 0.1)
|
||||
}
|
||||
|
||||
func test_whenErrorWithNSURLErrorNotConnectedToInternetReturned_shouldReturnNotConnectedError() {
|
||||
//given
|
||||
let config = NetworkConfigurableMock()
|
||||
let expectation = self.expectation(description: "Should return hasStatusCode error")
|
||||
|
||||
let error = NSError(domain: "network", code: NSURLErrorNotConnectedToInternet, userInfo: nil)
|
||||
let sut = DefaultNetworkService(config: config, sessionManager: NetworkSessionManagerMock(response: nil,
|
||||
data: nil,
|
||||
error: error as Error))
|
||||
|
||||
//when
|
||||
_ = sut.request(endpoint: EndpointMock(path: "http://mock.test.com", method: .get)) { result in
|
||||
do {
|
||||
_ = try result.get()
|
||||
XCTFail("Should not happen")
|
||||
} catch let error {
|
||||
guard case NetworkError.notConnected = error else {
|
||||
XCTFail("NetworkError.notConnected not found")
|
||||
return
|
||||
}
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
//then
|
||||
wait(for: [expectation], timeout: 0.1)
|
||||
}
|
||||
|
||||
func test_whenhasStatusCodeUsedWithWrongError_shouldReturnFalse() {
|
||||
//when
|
||||
let sut = NetworkError.notConnected
|
||||
//then
|
||||
XCTAssertFalse(sut.hasStatusCode(200))
|
||||
}
|
||||
|
||||
func test_whenhasStatusCodeUsed_shouldReturnCorrectStatusCode_() {
|
||||
//when
|
||||
let sut = NetworkError.error(statusCode: 400, data: nil)
|
||||
//then
|
||||
XCTAssertTrue(sut.hasStatusCode(400))
|
||||
XCTAssertFalse(sut.hasStatusCode(399))
|
||||
XCTAssertFalse(sut.hasStatusCode(401))
|
||||
}
|
||||
|
||||
func test_whenErrorWithNSURLErrorNotConnectedToInternetReturned_shouldLogThisError() {
|
||||
//given
|
||||
let config = NetworkConfigurableMock()
|
||||
let expectation = self.expectation(description: "Should return hasStatusCode error")
|
||||
|
||||
let error = NSError(domain: "network", code: NSURLErrorNotConnectedToInternet, userInfo: nil)
|
||||
let networkErrorLogger = NetworkErrorLoggerMock()
|
||||
let sut = DefaultNetworkService(config: config, sessionManager: NetworkSessionManagerMock(response: nil,
|
||||
data: nil,
|
||||
error: error as Error),
|
||||
logger: networkErrorLogger)
|
||||
//when
|
||||
_ = sut.request(endpoint: EndpointMock(path: "http://mock.test.com", method: .get)) { result in
|
||||
do {
|
||||
_ = try result.get()
|
||||
XCTFail("Should not happen")
|
||||
} catch let error {
|
||||
guard case NetworkError.notConnected = error else {
|
||||
XCTFail("NetworkError.notConnected not found")
|
||||
return
|
||||
}
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
//then
|
||||
wait(for: [expectation], timeout: 0.1)
|
||||
XCTAssertTrue(networkErrorLogger.loggedErrors.contains {
|
||||
guard case NetworkError.notConnected = $0 else { return false }
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import XCTest
|
||||
|
||||
import AFNetworksTests
|
||||
|
||||
var tests = [XCTestCaseEntry]()
|
||||
tests += AFNetworksTests.allTests()
|
||||
XCTMain(tests)
|
Loading…
Reference in New Issue