diff --git a/Client/.swiftlint.yml b/Client/.swiftlint.yml index 436e1e1..9a2df31 100644 --- a/Client/.swiftlint.yml +++ b/Client/.swiftlint.yml @@ -54,6 +54,7 @@ opt_in_rules: excluded: - Pods + - ClientTests - Interface - InterfaceTests - The-Norris diff --git a/Client/Base/ClientError.swift b/Client/Base/ClientError.swift index d8519ee..340e4da 100644 --- a/Client/Base/ClientError.swift +++ b/Client/Base/ClientError.swift @@ -28,3 +28,18 @@ public enum ClientErrorReason { } } } + +extension ClientErrorReason: Equatable { + public static func == (lhs: ClientErrorReason, rhs: ClientErrorReason) -> Bool { + switch (lhs, rhs) { + case (.api, .api): + return true + case (.invalidData, .invalidData): + return true + case (.decoding, .decoding): + return true + default: + return false + } + } +} diff --git a/Client/Logger/ClientLogger.swift b/Client/Logger/ClientLogger.swift index 829500e..297d1ff 100644 --- a/Client/Logger/ClientLogger.swift +++ b/Client/Logger/ClientLogger.swift @@ -93,7 +93,6 @@ private extension RequestLogger { printTagged("Headers: [") headers.forEach { printTagged("\t\($0): \($1)") } printTagged("]") - } // MARK: - Response @@ -110,6 +109,5 @@ private extension RequestLogger { printTagged("Headers: [") headers.forEach { printTagged("\t\($0): \($1)") } printTagged("]") - } } diff --git a/Client/ProjectClient/Client.swift b/Client/ProjectClient/Client.swift index e002729..2ab758f 100644 --- a/Client/ProjectClient/Client.swift +++ b/Client/ProjectClient/Client.swift @@ -10,7 +10,7 @@ import Foundation final class Client { private let session: URLSession - private var logger = RequestLogger(level: .debug) + var logger = RequestLogger(level: .debug) public init(session: URLSession = .shared) { self.session = session @@ -43,6 +43,13 @@ extension Client: ClientProtocol { let statusCode = httpResponse.statusCode + if let error = error { + let clientError = ClientError(reason: .api(error.localizedDescription), statusCode: statusCode) + self.logger.log(error: clientError, request: request) + completion(.failure(clientError)) + return + } + guard let data = data else { let clientError = ClientError(reason: .invalidData, statusCode: statusCode) self.logger.log(error: clientError, request: request) diff --git a/ClientTests/ProjectClient/ClientSpec.swift b/ClientTests/ProjectClient/ClientSpec.swift new file mode 100644 index 0000000..f48d3c7 --- /dev/null +++ b/ClientTests/ProjectClient/ClientSpec.swift @@ -0,0 +1,144 @@ +// +// ClientSpec.swift +// ClientTests +// +// Created by erick.lozano.borges on 14/02/21. +// + +import Quick +import Nimble + +@testable import Client + +private struct Object: Decodable { + var boolean: Bool +} + +class ClientSpec: QuickSpec { + override func spec() { + + describe("Client") { + + var sut: Client! + var sessionMock: URLSessionMock! + + context("when initializex") { + beforeEach { + sessionMock = URLSessionMock() + sut = Client(session: sessionMock) + sut.logger = RequestLogger(level: .none) + } + + context("and parameters are passed") { + + context("using get method") { + beforeEach { + sut.request(url: .stub(), method: .get, headers: [:], parameters: ["key": "value"]) { (result: Result) in } + } + + it("shold not have httpBody") { + expect(sessionMock.passedRequest?.httpBody).to(beNil()) + } + } + + context("using a method different from get") { + beforeEach { + sut.request(url: .stub(), method: .post, headers: [:], parameters: ["key": "value"]) { (result: Result) in } + } + + it("shold have a httpBody") { + expect(sessionMock.passedRequest?.httpBody).toNot(beNil()) + } + } + } + + context("and a request is successful") { + beforeEach { + sessionMock.data = "{ \"boolean\": true}".data(using: .utf8) + } + + it("should parsed the Object correctly") { + sut.request(url: .stub(), method: .get) { (result: Result) in + switch result { + case let .success(object): + expect(object.boolean) == true + case .failure: + fail("The request did fail because the Object couldn't be parsed") + } + } + } + } + + context("and the request fails with unknown error") { + beforeEach { + sessionMock.response = nil + } + + it("should return a decoding error") { + sut.request(url: .stub(), method: .get) { (result: Result) in + switch result { + case .success: + fail("The request should not be successfull") + case let .failure(error): + expect(error.reason) == .api(error.localizedDescription) + expect(error.statusCode) == 666 + } + } + } + } + + context("and the request returns an error") { + beforeEach { + sessionMock.error = NSError(domain: "ClientTests", code: 404, userInfo: [:]) + } + + it("should return an api error") { + sut.request(url: .stub(), method: .get) { (result: Result) in + switch result { + case .success: + fail("The request should not be successfull") + case let .failure(error): + expect(error.reason) == .api(error.localizedDescription) + } + } + } + } + + context("and the request returns an invalid data") { + beforeEach { + sessionMock.data = nil + } + + it("should return an api error") { + sut.request(url: .stub(), method: .get) { (result: Result) in + switch result { + case .success: + fail("The request should not be successfull") + case let .failure(error): + expect(error.reason) == .invalidData + } + } + } + } + + context("and a request fails to decode object") { + beforeEach { + sessionMock.data = "{ \"boolean\": 100}".data(using: .utf8) + } + + it("should return a decoding error") { + sut.request(url: .stub(), method: .get) { (result: Result) in + switch result { + case .success: + fail("The request should not be successfull") + case let .failure(error): + expect(error.reason) == .decoding(error) + } + } + } + } + + } + } + } +} diff --git a/The-Norris.xcodeproj/project.pbxproj b/The-Norris.xcodeproj/project.pbxproj index 22b929d..68af440 100644 --- a/The-Norris.xcodeproj/project.pbxproj +++ b/The-Norris.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 371E422125D9947F00C3D8BF /* URLSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371E422025D9947F00C3D8BF /* URLSessionMock.swift */; }; 371E422925D9964D00C3D8BF /* URL+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371E422825D9964D00C3D8BF /* URL+Stub.swift */; }; 371E423525D9966100C3D8BF /* HTTPURLResponse+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371E423425D9966100C3D8BF /* HTTPURLResponse+Stub.swift */; }; + 371E423D25D9975400C3D8BF /* ClientSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371E423C25D9975400C3D8BF /* ClientSpec.swift */; }; 371E424925D999C300C3D8BF /* URLSessionDataTaskMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371E424825D999C300C3D8BF /* URLSessionDataTaskMock.swift */; }; 3726981525D1FE45004FD4F9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3726981425D1FE45004FD4F9 /* AppDelegate.swift */; }; 3726981725D1FE45004FD4F9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3726981625D1FE45004FD4F9 /* SceneDelegate.swift */; }; @@ -156,6 +157,7 @@ 371E422025D9947F00C3D8BF /* URLSessionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionMock.swift; sourceTree = ""; }; 371E422825D9964D00C3D8BF /* URL+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Stub.swift"; sourceTree = ""; }; 371E423425D9966100C3D8BF /* HTTPURLResponse+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPURLResponse+Stub.swift"; sourceTree = ""; }; + 371E423C25D9975400C3D8BF /* ClientSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSpec.swift; sourceTree = ""; }; 371E424825D999C300C3D8BF /* URLSessionDataTaskMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataTaskMock.swift; sourceTree = ""; }; 3726981125D1FE45004FD4F9 /* The-Norris.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "The-Norris.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 3726981425D1FE45004FD4F9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -586,6 +588,7 @@ children = ( 37397E0025D89342008C2EAC /* Info.plist */, 371E421F25D9946300C3D8BF /* Mock */, + 371E423B25D9974900C3D8BF /* ProjectClient */, 371E422725D9962C00C3D8BF /* Stub */, ); path = ClientTests; @@ -1366,6 +1369,7 @@ buildActionMask = 2147483647; files = ( 371E422125D9947F00C3D8BF /* URLSessionMock.swift in Sources */, + 371E423D25D9975400C3D8BF /* ClientSpec.swift in Sources */, 371E422925D9964D00C3D8BF /* URL+Stub.swift in Sources */, 371E423525D9966100C3D8BF /* HTTPURLResponse+Stub.swift in Sources */, 371E424925D999C300C3D8BF /* URLSessionDataTaskMock.swift in Sources */,