├── .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 |
--------------------------------------------------------------------------------