├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift ├── README.md ├── Sources └── NetworkLibrary │ ├── AnyNetworkManager.swift │ ├── Dictionary+paramsString.swift │ ├── NetworkManager.swift │ ├── NetworkManagerError.swift │ └── Protocol │ ├── NetworkManagerProtocol.swift │ ├── ProtocolExtension │ └── URLSession+URLSessionProtocol.swift │ └── URLSessionProtocol.swift └── Tests ├── LinuxMain.swift └── NetworkLibraryTests ├── AnyNetworkManagerTests.swift ├── DictionaryExtensionTests.swift ├── Mocks ├── MockURLSession.swift └── MockURLSessionDataTask.swift ├── NetworkManagerTests.swift ├── TestResources ├── EndPointModel.swift ├── Error.swift ├── TestHelpers.swift └── TestStrings.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "NetworkLibrary", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 13 | .library( 14 | name: "NetworkLibrary", 15 | targets: ["NetworkLibrary"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 24 | .target( 25 | name: "NetworkLibrary", 26 | dependencies: []), 27 | .testTarget( 28 | name: "NetworkLibraryTests", 29 | dependencies: ["NetworkLibrary"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NetworkLibrary 2 | 3 | A simple tested network library. 4 | 5 | ## Installation 6 | This library supports Swift Package Manager ([installation guide](https://stevenpcurtis.medium.com/use-swift-package-manager-to-add-dependencies-b605f91a4990b605f91a4990?sk=adfd10c7d96557b37ba6ea0443145eb4)). 7 | 8 | ## Functionality 9 | - Get 10 | - Post 11 | - Patch 12 | - Put 13 | - Delete 14 | 15 | To use the network manager you must `import NetworkLibrary` at the top of the relevant class. 16 | 17 | This provides an `AnyNetworkManager` that can be stored in a property 18 | 19 | ```swift 20 | private var anyNetworkManager: AnyNetworkManager 21 | ``` 22 | 23 | which may be passed through an initializer 24 | 25 | ```swift 26 | init(networkManager: T) { 27 | self.anyNetworkManager = AnyNetworkManager(manager: networkManager) 28 | } 29 | ``` 30 | 31 | The network manager can then be called with something like the following (if you have previously declared some body data and a URL) 32 | 33 | ```swift 34 | anyNetworkManager.fetch(url: url, method: .post(body: data), completionBlock: {[weak self] res in 35 | // process Result type 36 | } 37 | ) 38 | ``` 39 | 40 | The network manager itself can be instantiated by using NetworkManager itself with something like: `NetworkManager(session: URLSession.shared)`. 41 | 42 | Even better we can use mutliple initializers to instantiate `AnyNetworkManager?`. This can be implemented with a class like the following: 43 | 44 | ```swift 45 | final class ApiService { 46 | private var anyNetworkManager: AnyNetworkManager? 47 | 48 | init() { 49 | self.anyNetworkManager = AnyNetworkManager() 50 | } 51 | 52 | init( 53 | networkManager: T 54 | ) { 55 | self.anyNetworkManager = AnyNetworkManager(manager: networkManager) 56 | } 57 | } 58 | ``` 59 | 60 | ## Guide 61 | [There is an accompanying guide on Medium](https://stevenpcurtis.medium.com/write-a-network-layer-in-swift-388fbb5d9497) 62 | -------------------------------------------------------------------------------- /Sources/NetworkLibrary/AnyNetworkManager.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import Foundation 4 | 5 | public final class AnyNetworkManager: NetworkManagerProtocol { 6 | public let session: U 7 | private let fetchClosure: (URL, HTTPMethod, @escaping (Result) -> Void) -> () 8 | private let cancelClosure: () -> () 9 | private let fetchAsyncClosure: (URL, HTTPMethod) async throws -> Data 10 | 11 | public func cancel() { 12 | cancelClosure() 13 | } 14 | 15 | public init(manager: T) throws { 16 | fetchClosure = manager.fetch 17 | fetchAsyncClosure = manager.fetch(url:method:) 18 | if let sessionAsU = manager.session as? U { 19 | session = sessionAsU 20 | } else { 21 | throw NetworkManagerError.sessionTypeMismatch 22 | } 23 | cancelClosure = manager.cancel 24 | } 25 | 26 | public convenience init() throws { 27 | let manager = NetworkManager(session: URLSession.shared) 28 | try self.init(manager: manager) 29 | } 30 | 31 | public func fetch( 32 | url: URL, 33 | method: HTTPMethod, 34 | completionBlock: @escaping (Result) -> Void 35 | ) { 36 | fetchClosure(url, method, completionBlock) 37 | } 38 | 39 | public func fetch(url: URL, method: HTTPMethod) async throws -> Data { 40 | try await fetchAsyncClosure(url, method) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/NetworkLibrary/Dictionary+paramsString.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import Foundation 4 | 5 | extension Dictionary { 6 | func paramsString() -> String { 7 | var paramsString = [String]() 8 | for (key, value) in self { 9 | guard let stringValue = value as? String, 10 | let stringKey = key as? String else { 11 | return "" 12 | } 13 | paramsString += [stringKey + "=" + "\(stringValue)"] 14 | 15 | } 16 | return (paramsString.isEmpty ? "" : paramsString.joined(separator: "&")) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/NetworkLibrary/NetworkManager.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import Foundation 4 | 5 | public enum HTTPMethod { 6 | case get(headers: [String : String] = [:], token: String? = nil) 7 | case post(headers: [String : String] = [:], token: String? = nil, body: [String: Any]) 8 | case put(headers: [String : String] = [:], token: String? = nil) 9 | case delete(headers: [String : String] = [:], token: String? = nil) 10 | case patch(headers: [String : String] = [:], token: String? = nil) 11 | } 12 | 13 | extension HTTPMethod: CustomStringConvertible { 14 | 15 | func getHeaders() -> [String: String]? { 16 | switch self { 17 | case .get(headers: let headers, _): 18 | return headers 19 | case .post(headers: let headers, _, body: _): 20 | return headers 21 | case .put(headers: let headers, _): 22 | return headers 23 | case .delete(headers: let headers, _): 24 | return headers 25 | case .patch(headers: let headers, _): 26 | return headers 27 | } 28 | } 29 | 30 | func getToken() -> String? { 31 | switch self { 32 | case .get(_, token: let token): 33 | return token 34 | case .post(_, token: let token, body: _): 35 | return token 36 | case .put(_, token: let token): 37 | return token 38 | case .delete(_, token: let token): 39 | return token 40 | case .patch(_, token: let token): 41 | return token 42 | } 43 | } 44 | 45 | func getData() -> [String: Any]? { 46 | switch self { 47 | case .get: 48 | return nil 49 | case .post( _, _, body: let body): 50 | return body 51 | case .put: 52 | return nil 53 | case .delete: 54 | return nil 55 | case .patch: 56 | return nil 57 | } 58 | } 59 | 60 | public var method: String { 61 | self.description 62 | } 63 | 64 | public var description: String { 65 | switch self { 66 | case .get: 67 | return "GET" 68 | case .post: 69 | return "POST" 70 | case .put: 71 | return "PUT" 72 | case .delete: 73 | return "DELETE" 74 | case .patch: 75 | return "PATCH" 76 | } 77 | } 78 | } 79 | 80 | public final class NetworkManager: NetworkManagerProtocol { 81 | public let session: T 82 | private var task: URLSessionDataTask? 83 | private var dataTask: Task? 84 | 85 | public required init(session: T) { 86 | self.session = session 87 | } 88 | 89 | public convenience init() throws { 90 | guard let session = URLSession.shared as? T else { 91 | throw NetworkManagerError.sessionTypeMismatch 92 | } 93 | self.init(session: session) 94 | } 95 | 96 | public func cancel() { 97 | task?.cancel() 98 | dataTask?.cancel() 99 | } 100 | 101 | public func fetch(url: URL, method: HTTPMethod, completionBlock: @escaping (Result) -> Void) { 102 | let request = makeRequest(url: url, method: method) 103 | 104 | task = session.dataTask(with: request) { data, httpResponse, error in 105 | guard error == nil else { 106 | if let error = error { 107 | completionBlock(.failure(error)) 108 | } else { 109 | completionBlock(.failure(NetworkManagerError.httpError(.unknown))) 110 | } 111 | return 112 | } 113 | 114 | guard let httpResponse = httpResponse as? HTTPURLResponse else { 115 | completionBlock(.failure(NetworkManagerError.invalidResponse(data, httpResponse))) 116 | return 117 | } 118 | 119 | switch self.handleStatusCode(statusCode: httpResponse.statusCode) { 120 | case .success: 121 | if let data = data { 122 | completionBlock(.success(data)) 123 | } else { 124 | completionBlock(.failure(NetworkManagerError.dataNotReceived)) 125 | } 126 | case .failure(let error): 127 | completionBlock(.failure(error)) 128 | } 129 | } 130 | task?.resume() 131 | } 132 | 133 | public func fetch(url: URL, method: HTTPMethod) async throws -> Data { 134 | dataTask = Task { 135 | let request = makeRequest(url: url, method: method) 136 | let (data, _) = try await session.data(for: request) 137 | return data 138 | } 139 | guard let taskData = try await dataTask?.value else { 140 | throw NetworkManagerError.dataNotReceived 141 | } 142 | return taskData 143 | } 144 | 145 | private func handleStatusCode(statusCode: Int) -> Result { 146 | switch statusCode { 147 | case 200 ..< 300: 148 | return .success(()) 149 | case 400: 150 | return .failure(NetworkManagerError.httpError(.badRequest)) 151 | case 401: 152 | return .failure(NetworkManagerError.httpError(.unauthorized)) 153 | case 403: 154 | return .failure(NetworkManagerError.httpError(.forbidden)) 155 | case 404: 156 | return .failure(NetworkManagerError.httpError(.notFound)) 157 | case 500: 158 | return .failure(NetworkManagerError.httpError(.serverError)) 159 | default: 160 | return .failure(NetworkManagerError.httpError(.unknown)) 161 | } 162 | } 163 | 164 | private func makeRequest(url: URL, method: HTTPMethod) -> URLRequest { 165 | var request = URLRequest( 166 | url: url, 167 | cachePolicy: .useProtocolCachePolicy, 168 | timeoutInterval: 30.0 169 | ) 170 | request.httpMethod = method.method 171 | request.allHTTPHeaderFields = method.getHeaders() 172 | 173 | if let bearerToken = method.getToken() { 174 | request.setValue("Bearer \(bearerToken)", forHTTPHeaderField: "Authorization") 175 | } 176 | 177 | if let data = method.getData() { 178 | let stringParams = data.paramsString() 179 | let bodyData = stringParams.data(using: .utf8, allowLossyConversion: false) 180 | request.httpBody = bodyData 181 | } 182 | return request 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Sources/NetworkLibrary/NetworkManagerError.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import Foundation 4 | 5 | enum NetworkManagerError: Error { 6 | case sessionTypeMismatch 7 | case dataNotReceived 8 | case bodyInGet 9 | case invalidURL 10 | case noInternet 11 | case invalidResponse(Data?, URLResponse?) 12 | case accessForbidden 13 | case httpError(HTTPError) 14 | } 15 | 16 | enum HTTPError: Error { 17 | case badRequest // 400 18 | case unauthorized // 401 19 | case forbidden // 403 20 | case notFound // 404 21 | case serverError // 500 22 | case unknown // for other status codes 23 | 24 | var description: String { 25 | switch self { 26 | case .badRequest: 27 | return "Bad Request" 28 | case .unauthorized: 29 | return "Unauthorized" 30 | case .forbidden: 31 | return "Forbidden" 32 | case .notFound: 33 | return "Not Found" 34 | case .serverError: 35 | return "Internal Server Error" 36 | case .unknown: 37 | return "Unknown HTTP Error" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/NetworkLibrary/Protocol/NetworkManagerProtocol.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import Foundation 4 | 5 | public protocol NetworkManagerProtocol { 6 | associatedtype aType 7 | var session: aType { get } 8 | func cancel() 9 | func fetch(url: URL, method: HTTPMethod, completionBlock: @escaping (Result) -> Void) 10 | func fetch(url: URL, method: HTTPMethod) async throws -> Data 11 | } 12 | 13 | public extension NetworkManagerProtocol { 14 | func fetch(url: URL, method: HTTPMethod, completionBlock: @escaping (Result) -> Void) { 15 | fetch(url: url, method: method, completionBlock: completionBlock) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/NetworkLibrary/Protocol/ProtocolExtension/URLSession+URLSessionProtocol.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import Foundation 4 | 5 | extension URLSession: URLSessionProtocol {} 6 | -------------------------------------------------------------------------------- /Sources/NetworkLibrary/Protocol/URLSessionProtocol.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import Foundation 4 | 5 | public protocol URLSessionProtocol { 6 | func dataTask( 7 | with url: URL, 8 | completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void 9 | ) -> URLSessionDataTask 10 | 11 | func dataTask( 12 | with request: URLRequest, 13 | completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void 14 | ) -> URLSessionDataTask 15 | 16 | func data(for request: URLRequest) async throws -> (Data, URLResponse) 17 | } 18 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import NetworkLibraryTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += NetworkLibraryTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/NetworkLibraryTests/AnyNetworkManagerTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import XCTest 4 | @testable import NetworkLibrary 5 | 6 | final class AnyNetworkManagerTests: XCTestCase { 7 | var urlSession: MockURLSession? 8 | var networkManager: AnyNetworkManager? 9 | 10 | func testGetMethodNoBody() { 11 | urlSession = MockURLSession() 12 | let testString = "Test String" 13 | let data = Data(testString.utf8) 14 | urlSession?.data = data 15 | 16 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 17 | let url = URL(fileURLWithPath: "http://www.google.com") 18 | networkManager?.fetch(url: url, method: .get(), completionBlock: { result in 19 | XCTAssertNotNil(result) 20 | switch result { 21 | case .success(let data): 22 | let decodedString = String(decoding: data, as: UTF8.self) 23 | XCTAssertEqual(decodedString, testString) 24 | case .failure: 25 | XCTFail() 26 | } 27 | }) 28 | } 29 | 30 | func testPostMethodNoBody() { 31 | urlSession = MockURLSession() 32 | let testString = "Test String" 33 | let data = Data(testString.utf8) 34 | urlSession?.data = data 35 | 36 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 37 | let url = URL(fileURLWithPath: "http://www.google.com") 38 | networkManager?.fetch(url: url, method: .post(body: [:]), completionBlock: { result in 39 | XCTAssertNotNil(result) 40 | switch result { 41 | case .success(let data): 42 | let decodedString = String(decoding: data, as: UTF8.self) 43 | XCTAssertEqual(decodedString, testString) 44 | case .failure: 45 | XCTFail() 46 | } 47 | }) 48 | } 49 | 50 | func testPostMethodBody() { 51 | urlSession = MockURLSession() 52 | let testString = "Test String" 53 | let data = Data(testString.utf8) 54 | urlSession?.data = data 55 | 56 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 57 | let url = URL(fileURLWithPath: "http://www.google.com") 58 | networkManager?.fetch(url: url, method: .post(body: ["email": "eve.holt@reqres.in", "password": "cityslicka"]), completionBlock: { result in 59 | XCTAssertNotNil(result) 60 | switch result { 61 | case .success(let data): 62 | let decodedString = String(decoding: data, as: UTF8.self) 63 | XCTAssertEqual(decodedString, testString) 64 | case .failure: 65 | XCTFail() 66 | } 67 | }) 68 | } 69 | 70 | func testSuccessfulGetURLResponse() { 71 | urlSession = MockURLSession() 72 | let testString = "Test String" 73 | let data = Data(testString.utf8) 74 | urlSession?.data = data 75 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 76 | let url = URL(fileURLWithPath: "http://www.google.com") 77 | 78 | networkManager?.fetch(url: url, method: .get(), completionBlock: { result in 79 | XCTAssertNotNil(result) 80 | switch result { 81 | case .success(let data): 82 | let decodedString = String(decoding: data, as: UTF8.self) 83 | XCTAssertEqual(decodedString, testString) 84 | case .failure: 85 | XCTFail() 86 | } 87 | }) 88 | } 89 | 90 | func testSuccessfulPatchURLResponse() { 91 | urlSession = MockURLSession() 92 | let testString = "Test String" 93 | let data = Data(testString.utf8) 94 | urlSession?.data = data 95 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 96 | let url = URL(fileURLWithPath: "http://www.google.com") 97 | 98 | networkManager?.fetch(url: url, method: .patch(), completionBlock: { result in 99 | XCTAssertNotNil(result) 100 | switch result { 101 | case .success(let data): 102 | let decodedString = String(decoding: data, as: UTF8.self) 103 | XCTAssertEqual(decodedString, testString) 104 | case .failure: 105 | XCTFail() 106 | } 107 | }) 108 | } 109 | 110 | func testSuccessfulPutURLResponse() { 111 | urlSession = MockURLSession() 112 | let testString = "Test String" 113 | let data = Data(testString.utf8) 114 | urlSession?.data = data 115 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 116 | let url = URL(fileURLWithPath: "http://www.google.com") 117 | 118 | networkManager?.fetch(url: url, method: .put(), completionBlock: { result in 119 | XCTAssertNotNil(result) 120 | switch result { 121 | case .success(let data): 122 | let decodedString = String(decoding: data, as: UTF8.self) 123 | XCTAssertEqual(decodedString, testString) 124 | case .failure: 125 | XCTFail() 126 | } 127 | }) 128 | } 129 | 130 | func testSuccessfulDeleteURLResponse() { 131 | urlSession = MockURLSession() 132 | let testString = "Test Data" 133 | let data = Data(testString.utf8) 134 | urlSession?.data = data 135 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 136 | let url = URL(fileURLWithPath: "http://www.google.com") 137 | 138 | networkManager?.fetch(url: url, method: .delete(), completionBlock: { result in 139 | XCTAssertNotNil(result) 140 | switch result { 141 | case .success(let data): 142 | let decodedString = String(decoding: data, as: UTF8.self) 143 | XCTAssertEqual(decodedString, testString) 144 | case .failure: 145 | XCTFail() 146 | } 147 | }) 148 | } 149 | 150 | func testFailureGetURLResponse() { 151 | // One way of testing failure is for the URLSession to simply provide no data to return 152 | urlSession = MockURLSession() 153 | urlSession?.error = NSError(domain: "error", code: 101, userInfo: nil) 154 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 155 | let url = URL(fileURLWithPath: "http://www.google.com") 156 | networkManager?.fetch(url: url, method: .get(), completionBlock: {result in 157 | XCTAssertNotNil(result) 158 | switch result { 159 | case .success: 160 | XCTFail() 161 | case .failure(let error): 162 | XCTAssertEqual((error as NSError).code, 101) 163 | } 164 | }) 165 | } 166 | 167 | func testFailurePatchURLResponse() { 168 | // One way of testing failure is for the URLSession to simply provide no data to return 169 | urlSession = MockURLSession() 170 | urlSession?.error = NSError(domain: "error", code: 101, userInfo: nil) 171 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 172 | let url = URL(fileURLWithPath: "http://www.google.com") 173 | networkManager?.fetch(url: url, method: .patch(), completionBlock: {result in 174 | XCTAssertNotNil(result) 175 | switch result { 176 | case .success: 177 | XCTFail() 178 | case .failure(let error): 179 | XCTAssertEqual((error as NSError).code, 101) 180 | } 181 | }) 182 | } 183 | 184 | func testFailurePutURLResponse() { 185 | // One way of testing failure is for the URLSession to simply provide no data to return 186 | urlSession = MockURLSession() 187 | urlSession?.error = NSError(domain: "error", code: 101, userInfo: nil) 188 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 189 | let url = URL(fileURLWithPath: "http://www.google.com") 190 | networkManager?.fetch(url: url, method: .put(), completionBlock: {result in 191 | XCTAssertNotNil(result) 192 | switch result { 193 | case .success: 194 | XCTFail() 195 | case .failure(let error): 196 | XCTAssertEqual((error as NSError).code, 101) 197 | } 198 | }) 199 | } 200 | 201 | func testFailureDeleteURLResponse() { 202 | // One way of testing failure is for the URLSession to simply provide no data to return 203 | urlSession = MockURLSession() 204 | urlSession?.error = NSError(domain: "error", code: 101, userInfo: nil) 205 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 206 | let url = URL(fileURLWithPath: "http://www.google.com") 207 | networkManager?.fetch(url: url, method: .delete(), completionBlock: {result in 208 | XCTAssertNotNil(result) 209 | switch result { 210 | case .success: 211 | XCTFail() 212 | case .failure(let error): 213 | XCTAssertEqual((error as NSError).code, 101) 214 | } 215 | }) 216 | } 217 | 218 | func testBadlyFormattedgetURLResponse() { 219 | urlSession = MockURLSession() 220 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 221 | let url = URL(fileURLWithPath: "http://www.google.com") 222 | networkManager?.fetch(url: url, method: .get(), completionBlock: { result in 223 | switch result { 224 | case .success: 225 | XCTFail() 226 | case .failure(let error): 227 | XCTAssertNotNil(error) 228 | } 229 | }) 230 | } 231 | 232 | func testBadlyFormattedputURLResponse() { 233 | urlSession = MockURLSession() 234 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 235 | let url = URL(fileURLWithPath: "http://www.google.com") 236 | networkManager?.fetch(url: url, method: .put(), completionBlock: { result in 237 | switch result { 238 | case .success: 239 | XCTFail() 240 | case .failure(let error): 241 | XCTAssertNotNil(error) 242 | } 243 | }) 244 | } 245 | 246 | func testBadlyFormattedDeleteURLResponse() { 247 | urlSession = MockURLSession() 248 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 249 | let url = URL(fileURLWithPath: "http://www.google.com") 250 | networkManager?.fetch(url: url, method: .delete(), completionBlock: { result in 251 | switch result { 252 | case .success: 253 | XCTFail() 254 | case .failure(let error): 255 | XCTAssertNotNil(error) 256 | } 257 | }) 258 | } 259 | 260 | func testAnyNetworkConv() { 261 | var networkManager: AnyNetworkManager? 262 | networkManager = try? AnyNetworkManager() 263 | let url = URL(fileURLWithPath: "http://www.google.com") 264 | networkManager?.fetch(url: url, method: .delete(), completionBlock: { result in 265 | switch result { 266 | case .success: 267 | XCTFail() 268 | case .failure(let error): 269 | XCTAssertNotNil(error) 270 | } 271 | }) 272 | } 273 | 274 | func testCancellation() throws { 275 | var networkManager: AnyNetworkManager? 276 | 277 | urlSession = MockURLSession() 278 | let mgr = NetworkManager(session: try XCTUnwrap(urlSession)) 279 | networkManager = try? AnyNetworkManager(manager: mgr) 280 | 281 | let url = try XCTUnwrap(URL(string: "https://example.com")) 282 | networkManager?.fetch( 283 | url: url, 284 | method: .get(headers: [:], token: ""), 285 | completionBlock: { _ in } 286 | ) 287 | 288 | networkManager?.cancel() 289 | let mockTask = try XCTUnwrap(urlSession?.task as? MockURLSessionDataTask) 290 | XCTAssertTrue(mockTask.cancelTaskCalled) 291 | } 292 | 293 | func testAsyncFetch() async throws { 294 | urlSession = MockURLSession() 295 | let testString = "Test Data" 296 | let sessionData = Data(testString.utf8) 297 | urlSession?.data = sessionData 298 | 299 | networkManager = try AnyNetworkManager( 300 | manager: NetworkManager(session: XCTUnwrap(urlSession)) 301 | ) 302 | let url = URL(fileURLWithPath: "http://www.google.com") 303 | let data = try await networkManager?.fetch(url: url, method: .get()) 304 | let dataUnwrapped = try XCTUnwrap(data) 305 | let decodedString = String(decoding: dataUnwrapped, as: UTF8.self) 306 | XCTAssertEqual(decodedString, testString) 307 | } 308 | 309 | func testAsyncFetchCancel() async throws { 310 | urlSession = MockURLSession() 311 | let testString = "Test Data" 312 | let sessionData = Data(testString.utf8) 313 | urlSession?.data = sessionData 314 | 315 | networkManager = try? AnyNetworkManager(manager: NetworkManager(session: XCTUnwrap(urlSession))) 316 | networkManager?.cancel() 317 | let url = URL(fileURLWithPath: "http://www.google.com") 318 | let data = try await networkManager?.fetch(url: url, method: .get()) 319 | let dataUnwrapped = try XCTUnwrap(data) 320 | let decodedString = String(decoding: dataUnwrapped, as: UTF8.self) 321 | XCTAssertEqual(decodedString, testString) 322 | } 323 | 324 | @MainActor 325 | func testAsyncFetchCancellation() async throws { 326 | let taskCompletedExpectation = expectation(description: "Task completed") 327 | 328 | urlSession = MockURLSession() 329 | let testString = "Test Data" 330 | let sessionData = Data(testString.utf8) 331 | urlSession?.data = sessionData 332 | let underlyingNetworkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 333 | networkManager = try? AnyNetworkManager(manager: underlyingNetworkManager) 334 | let url = URL(fileURLWithPath: "http://www.google.com") 335 | 336 | let _ = Task.detached { 337 | do { 338 | let _ = try? await self.networkManager?.fetch(url: url, method: .get()) 339 | self.networkManager?.cancel() 340 | 341 | if Task.isCancelled { 342 | throw CancellationError() 343 | } 344 | 345 | taskCompletedExpectation.fulfill() 346 | } 347 | } 348 | 349 | wait(for: [taskCompletedExpectation], timeout: 3) 350 | XCTAssertTrue(testDataTaskIsCancelled(in: underlyingNetworkManager)) 351 | } 352 | } 353 | 354 | extension AnyNetworkManagerTests { 355 | private func testDataTaskIsCancelled(in manager: NetworkManager) -> Bool { 356 | let mirror = Mirror(reflecting: manager) 357 | if let dataTask = mirror.descendant("dataTask") as? Task { 358 | return dataTask.isCancelled 359 | } 360 | return false 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /Tests/NetworkLibraryTests/DictionaryExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Steven Curtis on 09/11/2020. 6 | // 7 | 8 | import XCTest 9 | @testable import NetworkLibrary 10 | 11 | final class DictionaryExtensionTests: XCTestCase { 12 | func testExtension() throws { 13 | let data : [String : Any]? = ["email": "eve.holt@reqres.in", "password": "cityslicka"] 14 | let stringParams = data!.paramsString() 15 | let straightData = 16 | try XCTUnwrap("email=eve.holt@reqres.in&password=cityslicka".data(using: .utf8)) 17 | let dataParams = stringParams.data(using: String.Encoding.utf8, allowLossyConversion: false) 18 | XCTAssertEqual(dataParams!.count, straightData.count) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/NetworkLibraryTests/Mocks/MockURLSession.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import Foundation 4 | @testable import NetworkLibrary 5 | 6 | final class MockURLSession: URLSessionProtocol { 7 | typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void 8 | var data: Data? 9 | var error: Error? 10 | var task: URLSessionDataTask? 11 | func dataTask( 12 | with request: URLRequest, 13 | completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void 14 | ) -> URLSessionDataTask { 15 | let data = self.data 16 | let error = self.error 17 | let response = HTTPURLResponse( 18 | url: URL(fileURLWithPath: ""), 19 | statusCode: 200, 20 | httpVersion: nil, 21 | headerFields: nil 22 | ) 23 | return makeTask(data: data, response: response, error: error, completionHandler: completionHandler) 24 | } 25 | 26 | func dataTask( 27 | with url: URL, 28 | completionHandler: @escaping CompletionHandler 29 | ) -> URLSessionDataTask { 30 | let data = self.data 31 | let error = self.error 32 | return makeTask(data: data, error: error, completionHandler: completionHandler) 33 | } 34 | 35 | private func makeTask( 36 | data: Data?, 37 | response: HTTPURLResponse? = nil, 38 | error: Error?, 39 | completionHandler: @escaping CompletionHandler 40 | ) -> URLSessionDataTask { 41 | let task = MockURLSessionDataTask { 42 | completionHandler(data, response, error) 43 | } 44 | self.task = task 45 | return task 46 | } 47 | 48 | func data(for request: URLRequest) async throws -> (Data, URLResponse) { 49 | let response = HTTPURLResponse( 50 | url: URL(fileURLWithPath: ""), 51 | statusCode: 200, 52 | httpVersion: nil, 53 | headerFields: nil 54 | )! 55 | return (data ?? Data(), response) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/NetworkLibraryTests/Mocks/MockURLSessionDataTask.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import Foundation 4 | @testable import NetworkLibrary 5 | 6 | final class MockURLSessionDataTask: URLSessionDataTask { 7 | private let closure: () -> Void 8 | 9 | init(closure: @escaping () -> Void) { 10 | self.closure = closure 11 | } 12 | 13 | override func resume() { 14 | closure() 15 | } 16 | 17 | var cancelTaskCalled = false 18 | override func cancel() { 19 | cancelTaskCalled = true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/NetworkLibraryTests/NetworkManagerTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import XCTest 4 | @testable import NetworkLibrary 5 | 6 | final class NetworkManagerTests: XCTestCase { 7 | var urlSession: MockURLSession? 8 | var networkManager: NetworkManager? 9 | 10 | func testGetMethodNoBody() throws { 11 | urlSession = MockURLSession() 12 | let data = Data("TEsts12".utf8) 13 | urlSession?.data = data 14 | networkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 15 | let expect = expectation(description: #function) 16 | let url = URL(fileURLWithPath: "http://www.google.com") 17 | networkManager?.fetch(url: url, method: .get(), completionBlock: { result in 18 | XCTAssertNotNil(result) 19 | switch result { 20 | case .success(let data): 21 | let decodedString = String(decoding: data, as: UTF8.self) 22 | XCTAssertEqual(decodedString, "TEsts12") 23 | expect.fulfill() 24 | case .failure: 25 | XCTFail() 26 | } 27 | }) 28 | waitForExpectations(timeout: 3.0) 29 | } 30 | 31 | func testSuccessfulGetURLResponse() throws { 32 | urlSession = MockURLSession() 33 | let data = Data("TEsts12".utf8) 34 | urlSession?.data = data 35 | networkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 36 | let expect = expectation(description: #function) 37 | let url = URL(fileURLWithPath: "http://www.google.com") 38 | 39 | networkManager?.fetch(url: url, method: .get(), completionBlock: { result in 40 | XCTAssertNotNil(result) 41 | switch result { 42 | case .success(let data): 43 | let decodedString = String(decoding: data, as: UTF8.self) 44 | XCTAssertEqual(decodedString, "TEsts12") 45 | expect.fulfill() 46 | case .failure: 47 | XCTFail() 48 | } 49 | }) 50 | waitForExpectations(timeout: 3.0) 51 | } 52 | 53 | func testSuccessfulPatchURLResponse() throws { 54 | urlSession = MockURLSession() 55 | let data = Data("TEsts12".utf8) 56 | urlSession?.data = data 57 | networkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 58 | let expect = expectation(description: #function) 59 | let url = URL(fileURLWithPath: "http://www.google.com") 60 | 61 | networkManager?.fetch(url: url, method: .patch(), completionBlock: { result in 62 | XCTAssertNotNil(result) 63 | switch result { 64 | case .success(let data): 65 | let decodedString = String(decoding: data, as: UTF8.self) 66 | XCTAssertEqual(decodedString, "TEsts12") 67 | expect.fulfill() 68 | case .failure: 69 | XCTFail() 70 | } 71 | }) 72 | waitForExpectations(timeout: 3.0) 73 | } 74 | 75 | func testSuccessfulPutURLResponse() throws { 76 | urlSession = MockURLSession() 77 | let data = Data("TEsts12".utf8) 78 | urlSession?.data = data 79 | networkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 80 | let expect = expectation(description: #function) 81 | let url = URL(fileURLWithPath: "http://www.google.com") 82 | 83 | networkManager?.fetch(url: url, method: .put(), completionBlock: { result in 84 | XCTAssertNotNil(result) 85 | switch result { 86 | case .success(let data): 87 | let decodedString = String(decoding: data, as: UTF8.self) 88 | XCTAssertEqual(decodedString, "TEsts12") 89 | expect.fulfill() 90 | case .failure: 91 | XCTFail() 92 | } 93 | }) 94 | waitForExpectations(timeout: 3.0) 95 | } 96 | 97 | func testSuccessfulDeleteURLResponse() throws { 98 | urlSession = MockURLSession() 99 | let data = Data("TEsts12".utf8) 100 | urlSession?.data = data 101 | networkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 102 | let expect = expectation(description: #function) 103 | let url = URL(fileURLWithPath: "http://www.google.com") 104 | 105 | networkManager?.fetch(url: url, method: .delete(), completionBlock: { result in 106 | XCTAssertNotNil(result) 107 | switch result { 108 | case .success(let data): 109 | let decodedString = String(decoding: data, as: UTF8.self) 110 | XCTAssertEqual(decodedString, "TEsts12") 111 | expect.fulfill() 112 | case .failure: 113 | XCTFail() 114 | } 115 | }) 116 | waitForExpectations(timeout: 3.0) 117 | } 118 | 119 | func testFailureGetURLResponse() throws { 120 | urlSession = MockURLSession() 121 | urlSession?.error = NSError(domain: "error", code: 101, userInfo: nil) 122 | networkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 123 | let expect = expectation(description: #function) 124 | let url = URL(fileURLWithPath: "http://www.google.com") 125 | networkManager?.fetch(url: url, method: .get(), completionBlock: {result in 126 | XCTAssertNotNil(result) 127 | switch result { 128 | case .success: 129 | XCTFail() 130 | case .failure(let error): 131 | XCTAssertEqual((error as NSError).code, 101) 132 | expect.fulfill() 133 | } 134 | }) 135 | waitForExpectations(timeout: 3.0) 136 | } 137 | 138 | func testFailurePatchURLResponse() throws { 139 | urlSession = MockURLSession() 140 | urlSession?.error = NSError(domain: "error", code: 101, userInfo: nil) 141 | networkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 142 | let expect = expectation(description: #function) 143 | let url = URL(fileURLWithPath: "http://www.google.com") 144 | networkManager?.fetch(url: url, method: .patch(), completionBlock: {result in 145 | XCTAssertNotNil(result) 146 | switch result { 147 | case .success: 148 | XCTFail() 149 | case .failure(let error): 150 | XCTAssertEqual((error as NSError).code, 101) 151 | expect.fulfill() 152 | } 153 | }) 154 | waitForExpectations(timeout: 3.0) 155 | } 156 | 157 | func testFailurePutURLResponse() throws { 158 | urlSession = MockURLSession() 159 | urlSession?.error = NSError(domain: "error", code: 101, userInfo: nil) 160 | networkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 161 | let expect = expectation(description: #function) 162 | let url = URL(fileURLWithPath: "http://www.google.com") 163 | networkManager?.fetch(url: url, method: .put(), completionBlock: {result in 164 | XCTAssertNotNil(result) 165 | switch result { 166 | case .success: 167 | XCTFail() 168 | case .failure(let error): 169 | XCTAssertEqual((error as NSError).code, 101) 170 | expect.fulfill() 171 | } 172 | }) 173 | waitForExpectations(timeout: 3.0) 174 | } 175 | 176 | func testFailureDeleteURLResponse() throws { 177 | urlSession = MockURLSession() 178 | urlSession?.error = NSError(domain: "error", code: 101, userInfo: nil) 179 | networkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 180 | let expect = expectation(description: #function) 181 | let url = URL(fileURLWithPath: "http://www.google.com") 182 | networkManager?.fetch(url: url, method: .delete(), completionBlock: {result in 183 | XCTAssertNotNil(result) 184 | switch result { 185 | case .success: 186 | XCTFail() 187 | case .failure(let error): 188 | XCTAssertEqual((error as NSError).code, 101) 189 | expect.fulfill() 190 | } 191 | }) 192 | waitForExpectations(timeout: 3.0) 193 | } 194 | 195 | func testBadlyFormattedgetURLResponse() throws { 196 | urlSession = MockURLSession() 197 | networkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 198 | let expectation = XCTestExpectation(description: #function) 199 | let url = URL(fileURLWithPath: "http://www.google.com") 200 | networkManager?.fetch(url: url, method: .get(), completionBlock: { result in 201 | switch result { 202 | case .success: 203 | XCTFail() 204 | case .failure(let error): 205 | XCTAssertNotNil(error) 206 | expectation.fulfill() 207 | } 208 | }) 209 | wait(for: [expectation], timeout: 3.0) 210 | } 211 | 212 | func testBadlyFormattedputURLResponse() throws { 213 | urlSession = MockURLSession() 214 | networkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 215 | let expectation = XCTestExpectation(description: #function) 216 | let url = URL(fileURLWithPath: "http://www.google.com") 217 | networkManager?.fetch(url: url, method: .put(), completionBlock: { result in 218 | switch result { 219 | case .success: 220 | XCTFail() 221 | case .failure(let error): 222 | XCTAssertNotNil(error) 223 | expectation.fulfill() 224 | } 225 | }) 226 | wait(for: [expectation], timeout: 3.0) 227 | } 228 | 229 | func testBadlyFormattedDeleteURLResponse() throws { 230 | urlSession = MockURLSession() 231 | networkManager = NetworkManager(session: try XCTUnwrap(urlSession)) 232 | let expectation = XCTestExpectation(description: #function) 233 | let url = URL(fileURLWithPath: "http://www.google.com") 234 | 235 | networkManager?.fetch(url: url, method: .delete(), completionBlock: { result in 236 | switch result { 237 | case .success: 238 | XCTFail() 239 | case .failure(let error): 240 | XCTAssertNotNil(error) 241 | expectation.fulfill() 242 | } 243 | }) 244 | wait(for: [expectation], timeout: 3.0) 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /Tests/NetworkLibraryTests/TestResources/EndPointModel.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | struct EndPointModel: Codable { 4 | let products: [Products] 5 | let title: String 6 | let product_count: Int 7 | } 8 | 9 | struct Products: Codable { 10 | let id: String 11 | let name: String 12 | let price: String 13 | let image: String 14 | } 15 | -------------------------------------------------------------------------------- /Tests/NetworkLibraryTests/TestResources/Error.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | struct ErrorModel: Error { 4 | var errorDescription: String 5 | } 6 | -------------------------------------------------------------------------------- /Tests/NetworkLibraryTests/TestResources/TestHelpers.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import Foundation 4 | @testable import NetworkLibrary 5 | 6 | let testProduct = Products(id: "1", name: "Test Shirt", price: "£199", image: "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg") 7 | 8 | func createTestProducts() -> [Products] { 9 | var products: [Products] = [] 10 | for _ in 1...50 { 11 | let product = Products(id: "1", name: "Test Shirt", price: "£199", image: "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg") 12 | products.append(product) 13 | } 14 | return products 15 | } 16 | 17 | extension Products: Equatable { 18 | public static func == (lhs: Products, rhs: Products) -> Bool { 19 | return lhs.id == rhs.id 20 | } 21 | } 22 | 23 | struct UnexpectedNilError: Error {} 24 | -------------------------------------------------------------------------------- /Tests/NetworkLibraryTests/TestResources/TestStrings.swift: -------------------------------------------------------------------------------- 1 | // Created by Steven Curtis 2 | 3 | import Foundation 4 | 5 | let emptyString = """ 6 | """ 7 | 8 | let invalidProductsString = """ 9 | { 10 | "products": [ 11 | { 12 | { 13 | "garbled": "1", 14 | "name": "Test Shirt", 15 | "price": "£199", 16 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 17 | } 18 | ], 19 | "title": "Exercise Listing", 20 | "product_count": 50 21 | } 22 | """ 23 | 24 | let endPointString = """ 25 | { 26 | "products": [ 27 | { 28 | "id": "1", 29 | "name": "Test Shirt", 30 | "price": "£199", 31 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 32 | }, 33 | { 34 | "id": "1", 35 | "name": "Test Shirt", 36 | "price": "£199", 37 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 38 | }, 39 | { 40 | "id": "1", 41 | "name": "Test Shirt", 42 | "price": "£199", 43 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 44 | }, 45 | { 46 | "id": "1", 47 | "name": "Test Shirt", 48 | "price": "£199", 49 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 50 | }, 51 | { 52 | "id": "1", 53 | "name": "Test Shirt", 54 | "price": "£199", 55 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 56 | }, 57 | { 58 | "id": "1", 59 | "name": "Test Shirt", 60 | "price": "£199", 61 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 62 | }, 63 | { 64 | "id": "1", 65 | "name": "Test Shirt", 66 | "price": "£199", 67 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 68 | }, 69 | { 70 | "id": "1", 71 | "name": "Test Shirt", 72 | "price": "£199", 73 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 74 | }, 75 | { 76 | "id": "1", 77 | "name": "Test Shirt", 78 | "price": "£199", 79 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 80 | }, 81 | { 82 | "id": "1", 83 | "name": "Test Shirt", 84 | "price": "£199", 85 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 86 | }, 87 | { 88 | "id": "1", 89 | "name": "Test Shirt", 90 | "price": "£199", 91 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 92 | }, 93 | { 94 | "id": "1", 95 | "name": "Test Shirt", 96 | "price": "£199", 97 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 98 | }, 99 | { 100 | "id": "1", 101 | "name": "Test Shirt", 102 | "price": "£199", 103 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 104 | }, 105 | { 106 | "id": "1", 107 | "name": "Test Shirt", 108 | "price": "£199", 109 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 110 | }, 111 | { 112 | "id": "1", 113 | "name": "Test Shirt", 114 | "price": "£199", 115 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 116 | }, 117 | { 118 | "id": "1", 119 | "name": "Test Shirt", 120 | "price": "£199", 121 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 122 | }, 123 | { 124 | "id": "1", 125 | "name": "Test Shirt", 126 | "price": "£199", 127 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 128 | }, 129 | { 130 | "id": "1", 131 | "name": "Test Shirt", 132 | "price": "£199", 133 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 134 | }, 135 | { 136 | "id": "1", 137 | "name": "Test Shirt", 138 | "price": "£199", 139 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 140 | }, 141 | { 142 | "id": "1", 143 | "name": "Test Shirt", 144 | "price": "£199", 145 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 146 | }, 147 | { 148 | "id": "1", 149 | "name": "Test Shirt", 150 | "price": "£199", 151 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 152 | }, 153 | { 154 | "id": "1", 155 | "name": "Test Shirt", 156 | "price": "£199", 157 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 158 | }, 159 | { 160 | "id": "1", 161 | "name": "Test Shirt", 162 | "price": "£199", 163 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 164 | }, 165 | { 166 | "id": "1", 167 | "name": "Test Shirt", 168 | "price": "£199", 169 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 170 | }, 171 | { 172 | "id": "1", 173 | "name": "Test Shirt", 174 | "price": "£199", 175 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 176 | }, 177 | { 178 | "id": "1", 179 | "name": "Test Shirt", 180 | "price": "£199", 181 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 182 | }, 183 | { 184 | "id": "1", 185 | "name": "Test Shirt", 186 | "price": "£199", 187 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 188 | }, 189 | { 190 | "id": "1", 191 | "name": "Test Shirt", 192 | "price": "£199", 193 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 194 | }, 195 | { 196 | "id": "1", 197 | "name": "Test Shirt", 198 | "price": "£199", 199 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 200 | }, 201 | { 202 | "id": "1", 203 | "name": "Test Shirt", 204 | "price": "£199", 205 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 206 | }, 207 | { 208 | "id": "1", 209 | "name": "Test Shirt", 210 | "price": "£199", 211 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 212 | }, 213 | { 214 | "id": "1", 215 | "name": "Test Shirt", 216 | "price": "£199", 217 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 218 | }, 219 | { 220 | "id": "1", 221 | "name": "Test Shirt", 222 | "price": "£199", 223 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 224 | }, 225 | { 226 | "id": "1", 227 | "name": "Test Shirt", 228 | "price": "£199", 229 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 230 | }, 231 | { 232 | "id": "1", 233 | "name": "Test Shirt", 234 | "price": "£199", 235 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 236 | }, 237 | { 238 | "id": "1", 239 | "name": "Test Shirt", 240 | "price": "£199", 241 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 242 | }, 243 | { 244 | "id": "1", 245 | "name": "Test Shirt", 246 | "price": "£199", 247 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 248 | }, 249 | { 250 | "id": "1", 251 | "name": "Test Shirt", 252 | "price": "£199", 253 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 254 | }, 255 | { 256 | "id": "1", 257 | "name": "Test Shirt", 258 | "price": "£199", 259 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 260 | }, 261 | { 262 | "id": "1", 263 | "name": "Test Shirt", 264 | "price": "£199", 265 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 266 | }, 267 | { 268 | "id": "1", 269 | "name": "Test Shirt", 270 | "price": "£199", 271 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 272 | }, 273 | { 274 | "id": "1", 275 | "name": "Test Shirt", 276 | "price": "£199", 277 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 278 | }, 279 | { 280 | "id": "1", 281 | "name": "Test Shirt", 282 | "price": "£199", 283 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 284 | }, 285 | { 286 | "id": "1", 287 | "name": "Test Shirt", 288 | "price": "£199", 289 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 290 | }, 291 | { 292 | "id": "1", 293 | "name": "Test Shirt", 294 | "price": "£199", 295 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 296 | }, 297 | { 298 | "id": "1", 299 | "name": "Test Shirt", 300 | "price": "£199", 301 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 302 | }, 303 | { 304 | "id": "1", 305 | "name": "Test Shirt", 306 | "price": "£199", 307 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 308 | }, 309 | { 310 | "id": "1", 311 | "name": "Test Shirt", 312 | "price": "£199", 313 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 314 | }, 315 | { 316 | "id": "1", 317 | "name": "Test Shirt", 318 | "price": "£199", 319 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 320 | }, 321 | { 322 | "id": "1", 323 | "name": "Test Shirt", 324 | "price": "£199", 325 | "image": "https://media.endclothing.com/media/f_auto,q_auto,w_760,h_760/prodmedia/media/catalog/product/2/6/26-03-2018_gosha_rubchinskiyxadidas_copaprimeknitboostmidsneaker_yellow_g012sh12-220_ka_1.jpg" 326 | } 327 | ], 328 | "title": "Exercise Listing", 329 | "product_count": 50 330 | } 331 | """ 332 | -------------------------------------------------------------------------------- /Tests/NetworkLibraryTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(NetworkLibraryTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------