From 56901df9c6cce13a1ba662bf83177eb8a788e06a Mon Sep 17 00:00:00 2001 From: Elvis Nunez <3lvis@users.noreply.github.com> Date: Wed, 10 Jul 2024 23:45:17 +0200 Subject: [PATCH] Add delay to FakeRequests (#275) * Add test and initial implementation * Add delay to other request types * Clean up * Clean up * Tests --- Sources/Networking/FakeRequest.swift | 11 +++- .../Networking/Networking+HTTPRequests.swift | 44 +++++++-------- Sources/Networking/Networking+Private.swift | 20 +++++-- Tests/NetworkingTests/FakeRequestTests.swift | 56 +++++++++++++++++-- 4 files changed, 98 insertions(+), 33 deletions(-) diff --git a/Sources/Networking/FakeRequest.swift b/Sources/Networking/FakeRequest.swift index 45c1d4d..705ca2c 100644 --- a/Sources/Networking/FakeRequest.swift +++ b/Sources/Networking/FakeRequest.swift @@ -5,6 +5,15 @@ struct FakeRequest { let responseType: Networking.ResponseType let headerFields: [String: String]? let statusCode: Int + let delay: Double + + init(response: Any?, responseType: Networking.ResponseType, headerFields: [String : String]?, statusCode: Int, delay: Double) { + self.response = response + self.responseType = responseType + self.headerFields = headerFields + self.statusCode = statusCode + self.delay = delay + } static func find(ofType type: Networking.RequestType, forPath path: String, in collection: [Networking.RequestType: [String: FakeRequest]]) throws -> FakeRequest? { guard let requests = collection[type] else { return nil } @@ -51,7 +60,7 @@ struct FakeRequest { guard let stringData = responseString.data(using: .utf8) else { continue } let finalJSON = try JSONSerialization.jsonObject(with: stringData, options: []) - return FakeRequest(response: finalJSON, responseType: fakeRequest.responseType, headerFields: fakeRequest.headerFields, statusCode: fakeRequest.statusCode) + return FakeRequest(response: finalJSON, responseType: fakeRequest.responseType, headerFields: fakeRequest.headerFields, statusCode: fakeRequest.statusCode, delay: fakeRequest.delay) } } diff --git a/Sources/Networking/Networking+HTTPRequests.swift b/Sources/Networking/Networking+HTTPRequests.swift index fa9a5b2..387b1b8 100644 --- a/Sources/Networking/Networking+HTTPRequests.swift +++ b/Sources/Networking/Networking+HTTPRequests.swift @@ -19,8 +19,8 @@ public extension Networking { /// - path: The path for the faked GET request. /// - response: An `Any` that will be returned when a GET request is made to the specified path. /// - statusCode: By default it's 200, if you provide any status code that is between 200 and 299 the response object will be returned, otherwise we will return an error containig the provided status code. - func fakeGET(_ path: String, response: Any?, headerFields: [String: String]? = nil, statusCode: Int = 200) { - registerFake(requestType: .get, path: path, headerFields: headerFields, response: response, responseType: .json, statusCode: statusCode) + func fakeGET(_ path: String, response: Any?, headerFields: [String: String]? = nil, statusCode: Int = 200, delay: Double = 0) { + registerFake(requestType: .get, path: path, headerFields: headerFields, response: response, responseType: .json, statusCode: statusCode, delay: delay) } /// Registers a fake GET request for the specified path using the contents of a file. After registering this, every GET request to the path, will return the contents of the registered file. @@ -29,8 +29,8 @@ public extension Networking { /// - path: The path for the faked GET request. /// - fileName: The name of the file, whose contents will be registered as a reponse. /// - bundle: The Bundle where the file is located. - func fakeGET(_ path: String, fileName: String, bundle: Bundle = Bundle.main) { - registerFake(requestType: .get, path: path, fileName: fileName, bundle: bundle) + func fakeGET(_ path: String, fileName: String, bundle: Bundle = Bundle.main, delay: Double = 0) { + registerFake(requestType: .get, path: path, fileName: fileName, bundle: bundle, delay: delay) } /// Cancels the GET request for the specified path. This causes the request to complete with error code URLError.cancelled. @@ -61,8 +61,8 @@ public extension Networking { /// - path: The path for the faked PATCH request. /// - response: An `Any` that will be returned when a PATCH request is made to the specified path. /// - statusCode: By default it's 200, if you provide any status code that is between 200 and 299 the response object will be returned, otherwise we will return an error containig the provided status code. - func fakePATCH(_ path: String, response: Any?, headerFields: [String: String]? = nil, statusCode: Int = 200) { - registerFake(requestType: .patch, path: path, headerFields: headerFields, response: response, responseType: .json, statusCode: statusCode) + func fakePATCH(_ path: String, response: Any?, headerFields: [String: String]? = nil, statusCode: Int = 200, delay: Double = 0) { + registerFake(requestType: .patch, path: path, headerFields: headerFields, response: response, responseType: .json, statusCode: statusCode, delay: delay) } /// Registers a fake PATCH request to the specified path using the contents of a file. After registering this, every PATCH request to the path, will return the contents of the registered file. @@ -71,8 +71,8 @@ public extension Networking { /// - path: The path for the faked PATCH request. /// - fileName: The name of the file, whose contents will be registered as a reponse. /// - bundle: The Bundle where the file is located. - func fakePATCH(_ path: String, fileName: String, bundle: Bundle = Bundle.main) { - registerFake(requestType: .patch, path: path, fileName: fileName, bundle: bundle) + func fakePATCH(_ path: String, fileName: String, bundle: Bundle = Bundle.main, delay: Double = 0) { + registerFake(requestType: .patch, path: path, fileName: fileName, bundle: bundle, delay: delay) } /// Cancels the PATCH request for the specified path. This causes the request to complete with error code URLError.cancelled. @@ -103,8 +103,8 @@ public extension Networking { /// - path: The path for the faked PUT request. /// - response: An `Any` that will be returned when a PUT request is made to the specified path. /// - statusCode: By default it's 200, if you provide any status code that is between 200 and 299 the response object will be returned, otherwise we will return an error containig the provided status code. - func fakePUT(_ path: String, response: Any?, headerFields: [String: String]? = nil, statusCode: Int = 200) { - registerFake(requestType: .put, path: path, headerFields: headerFields, response: response, responseType: .json, statusCode: statusCode) + func fakePUT(_ path: String, response: Any?, headerFields: [String: String]? = nil, statusCode: Int = 200, delay: Double = 0) { + registerFake(requestType: .put, path: path, headerFields: headerFields, response: response, responseType: .json, statusCode: statusCode, delay: delay) } /// Registers a fake PUT request to the specified path using the contents of a file. After registering this, every PUT request to the path, will return the contents of the registered file. @@ -113,8 +113,8 @@ public extension Networking { /// - path: The path for the faked PUT request. /// - fileName: The name of the file, whose contents will be registered as a reponse. /// - bundle: The Bundle where the file is located. - func fakePUT(_ path: String, fileName: String, bundle: Bundle = Bundle.main) { - registerFake(requestType: .put, path: path, fileName: fileName, bundle: bundle) + func fakePUT(_ path: String, fileName: String, bundle: Bundle = Bundle.main, delay: Double = 0) { + registerFake(requestType: .put, path: path, fileName: fileName, bundle: bundle, delay: delay) } /// Cancels the PUT request for the specified path. This causes the request to complete with error code URLError.cancelled. @@ -155,8 +155,8 @@ public extension Networking { /// - path: The path for the faked POST request. /// - response: An `Any` that will be returned when a POST request is made to the specified path. /// - statusCode: By default it's 200, if you provide any status code that is between 200 and 299 the response object will be returned, otherwise we will return an error containig the provided status code. - func fakePOST(_ path: String, response: Any?, headerFields: [String: String]? = nil, statusCode: Int = 200) { - registerFake(requestType: .post, path: path, headerFields: headerFields, response: response, responseType: .json, statusCode: statusCode) + func fakePOST(_ path: String, response: Any?, headerFields: [String: String]? = nil, statusCode: Int = 200, delay: Double = 0) { + registerFake(requestType: .post, path: path, headerFields: headerFields, response: response, responseType: .json, statusCode: statusCode, delay: delay) } /// Registers a fake POST request to the specified path using the contents of a file. After registering this, every POST request to the path, will return the contents of the registered file. @@ -165,8 +165,8 @@ public extension Networking { /// - path: The path for the faked POST request. /// - fileName: The name of the file, whose contents will be registered as a reponse. /// - bundle: The Bundle where the file is located. - func fakePOST(_ path: String, fileName: String, bundle: Bundle = Bundle.main) { - registerFake(requestType: .post, path: path, fileName: fileName, bundle: bundle) + func fakePOST(_ path: String, fileName: String, bundle: Bundle = Bundle.main, delay: Double = 0) { + registerFake(requestType: .post, path: path, fileName: fileName, bundle: bundle, delay: delay) } /// Cancels the POST request for the specified path. This causes the request to complete with error code URLError.cancelled. @@ -197,8 +197,8 @@ public extension Networking { /// - path: The path for the faked DELETE request. /// - response: An `Any` that will be returned when a DELETE request is made to the specified path. /// - statusCode: By default it's 200, if you provide any status code that is between 200 and 299 the response object will be returned, otherwise we will return an error containig the provided status code. - func fakeDELETE(_ path: String, response: Any?, headerFields: [String: String]? = nil, statusCode: Int = 200) { - registerFake(requestType: .delete, path: path, headerFields: headerFields, response: response, responseType: .json, statusCode: statusCode) + func fakeDELETE(_ path: String, response: Any?, headerFields: [String: String]? = nil, statusCode: Int = 200, delay: Double = 0) { + registerFake(requestType: .delete, path: path, headerFields: headerFields, response: response, responseType: .json, statusCode: statusCode, delay: delay) } /// Registers a fake DELETE request to the specified path using the contents of a file. After registering this, every DELETE request to the path, will return the contents of the registered file. @@ -207,8 +207,8 @@ public extension Networking { /// - path: The path for the faked DELETE request. /// - fileName: The name of the file, whose contents will be registered as a reponse. /// - bundle: The Bundle where the file is located. - func fakeDELETE(_ path: String, fileName: String, bundle: Bundle = Bundle.main) { - registerFake(requestType: .delete, path: path, fileName: fileName, bundle: bundle) + func fakeDELETE(_ path: String, fileName: String, bundle: Bundle = Bundle.main, delay: Double = 0) { + registerFake(requestType: .delete, path: path, fileName: fileName, bundle: bundle, delay: delay) } /// Cancels the DELETE request for the specified path. This causes the request to complete with error code URLError.cancelled. @@ -259,8 +259,8 @@ public extension Networking { /// - path: The path for the faked image download request. /// - image: An image that will be returned when there's a request to the registered path. /// - statusCode: The status code to be used when faking the request. - func fakeImageDownload(_ path: String, image: Image, headerFields: [String: String]? = nil, statusCode: Int = 200) { - registerFake(requestType: .get, path: path, headerFields: headerFields, response: image, responseType: .image, statusCode: statusCode) + func fakeImageDownload(_ path: String, image: Image, headerFields: [String: String]? = nil, statusCode: Int = 200, delay: Double = 0) { + registerFake(requestType: .get, path: path, headerFields: headerFields, response: image, responseType: .image, statusCode: statusCode, delay: delay) } /// Downloads data from a URL, caching the result. diff --git a/Sources/Networking/Networking+Private.swift b/Sources/Networking/Networking+Private.swift index 1508e6c..cc61482 100644 --- a/Sources/Networking/Networking+Private.swift +++ b/Sources/Networking/Networking+Private.swift @@ -40,10 +40,10 @@ extension Networking { } } - func registerFake(requestType: RequestType, path: String, fileName: String, bundle: Bundle) { + func registerFake(requestType: RequestType, path: String, fileName: String, bundle: Bundle, delay: Double) { do { if let result = try FileManager.json(from: fileName, bundle: bundle) { - registerFake(requestType: requestType, path: path, headerFields: nil, response: result, responseType: .json, statusCode: 200) + registerFake(requestType: requestType, path: path, headerFields: nil, response: result, responseType: .json, statusCode: 200, delay: delay) } } catch ParsingError.notFound { fatalError("We couldn't find \(fileName), are you sure is there?") @@ -52,9 +52,9 @@ extension Networking { } } - func registerFake(requestType: RequestType, path: String, headerFields: [String: String]?, response: Any?, responseType: ResponseType, statusCode: Int) { + func registerFake(requestType: RequestType, path: String, headerFields: [String: String]?, response: Any?, responseType: ResponseType, statusCode: Int, delay: Double) { var requests = fakeRequests[requestType] ?? [String: FakeRequest]() - requests[path] = FakeRequest(response: response, responseType: responseType, headerFields: headerFields, statusCode: statusCode) + requests[path] = FakeRequest(response: response, responseType: responseType, headerFields: headerFields, statusCode: statusCode, delay: delay) fakeRequests[requestType] = requests } @@ -86,6 +86,10 @@ extension Networking { if let fakeRequest = try FakeRequest.find(ofType: requestType, forPath: path, in: fakeRequests) { let (_, response, error) = try handleFakeRequest(fakeRequest, path: path, cacheName: cacheName, cachingLevel: cachingLevel) + if fakeRequest.delay > 0 { + let nanoseconds = UInt64(fakeRequest.delay * 1_000_000_000) + try? await Task.sleep(nanoseconds: nanoseconds) + } return try JSONResult(body: fakeRequest.response, response: response, error: error) } else { switch cachingLevel { @@ -110,6 +114,10 @@ extension Networking { func handleDataRequest(_ requestType: RequestType, path: String, cacheName: String?, cachingLevel: CachingLevel, responseType: ResponseType) async throws -> DataResult { if let fakeRequests = fakeRequests[requestType], let fakeRequest = fakeRequests[path] { let (_, response, error) = try handleFakeRequest(fakeRequest, path: path, cacheName: cacheName, cachingLevel: cachingLevel) + if fakeRequest.delay > 0 { + let nanoseconds = UInt64(fakeRequest.delay * 1_000_000_000) + try? await Task.sleep(nanoseconds: nanoseconds) + } return DataResult(body: fakeRequest.response, response: response, error: error) } else { let object = try objectFromCache(for: path, cacheName: cacheName, cachingLevel: cachingLevel, responseType: responseType) @@ -132,6 +140,10 @@ extension Networking { func handleImageRequest(_ requestType: RequestType, path: String, cacheName: String?, cachingLevel: CachingLevel, responseType: ResponseType) async throws -> ImageResult { if let fakeRequests = fakeRequests[requestType], let fakeRequest = fakeRequests[path] { let (_, response, error) = try handleFakeRequest(fakeRequest, path: path, cacheName: cacheName, cachingLevel: cachingLevel) + if fakeRequest.delay > 0 { + let nanoseconds = UInt64(fakeRequest.delay * 1_000_000_000) + try? await Task.sleep(nanoseconds: nanoseconds) + } return ImageResult(body: fakeRequest.response, response: response, error: error) } else { let object = try objectFromCache(for: path, cacheName: cacheName, cachingLevel: cachingLevel, responseType: responseType) diff --git a/Tests/NetworkingTests/FakeRequestTests.swift b/Tests/NetworkingTests/FakeRequestTests.swift index 399d2c2..dc2e27f 100644 --- a/Tests/NetworkingTests/FakeRequestTests.swift +++ b/Tests/NetworkingTests/FakeRequestTests.swift @@ -34,7 +34,7 @@ class FakeRequestTests: XCTestCase { } func testFind() throws { - let request = FakeRequest(response: nil, responseType: .json, headerFields: nil, statusCode: 200) + let request = FakeRequest(response: nil, responseType: .json, headerFields: nil, statusCode: 200, delay: 0) let existingRequests = [Networking.RequestType.get: ["/companies": request]] XCTAssertNil(try FakeRequest.find(ofType: .get, forPath: "/users", in: existingRequests)) @@ -45,7 +45,7 @@ class FakeRequestTests: XCTestCase { let json = [ "name": "Name {userID}" ] - let request = FakeRequest(response: json, responseType: .json, headerFields: nil, statusCode: 200) + let request = FakeRequest(response: json, responseType: .json, headerFields: nil, statusCode: 200, delay: 0) let existingRequests = [Networking.RequestType.get: ["/users/{userID}": request]] let result = try FakeRequest.find(ofType: .get, forPath: "/users/10", in: existingRequests) @@ -60,7 +60,7 @@ class FakeRequestTests: XCTestCase { let json = [ "name": "Name {userID}" ] - let request = FakeRequest(response: json, responseType: .json, headerFields: nil, statusCode: 200) + let request = FakeRequest(response: json, responseType: .json, headerFields: nil, statusCode: 200, delay: 0) let existingRequests = [ Networking.RequestType.get: [ "/users/ados": request, @@ -85,7 +85,7 @@ class FakeRequestTests: XCTestCase { "user": "User {userID}", "company": "Company {companyID}" ] - let request = FakeRequest(response: json, responseType: .json, headerFields: nil, statusCode: 200) + let request = FakeRequest(response: json, responseType: .json, headerFields: nil, statusCode: 200, delay: 0) let existingRequests = [Networking.RequestType.get: ["/users/{userID}/companies/{companyID}": request]] let result = try FakeRequest.find(ofType: .get, forPath: "/users/10/companies/20", in: existingRequests) @@ -103,7 +103,7 @@ class FakeRequestTests: XCTestCase { "company": "Company {companyID}", "product": "Product {productID}" ] - let request = FakeRequest(response: json, responseType: .json, headerFields: nil, statusCode: 200) + let request = FakeRequest(response: json, responseType: .json, headerFields: nil, statusCode: 200, delay: 0) let existingRequests = [Networking.RequestType.get: [ "/users/{userID}/companies/{companyID}/products/a": request, "/users/{userID}/companies/{companyID}/products/b": request, @@ -139,7 +139,7 @@ class FakeRequestTests: XCTestCase { "resource10": "Resource {resourceID10}", ] - let request = FakeRequest(response: json, responseType: .json, headerFields: nil, statusCode: 200) + let request = FakeRequest(response: json, responseType: .json, headerFields: nil, statusCode: 200, delay: 0) let existingRequests = [Networking.RequestType.get: ["resource1/{resourceID1}/resource2/{resourceID2}/resource3/{resourceID3}/resource4/{resourceID4}/resource5/{resourceID5}/resource6/{resourceID6}/resource7/{resourceID7}/resource8/{resourceID8}/resource9/{resourceID9}/resource10/{resourceID10}": request]] let result = try FakeRequest.find(ofType: .get, forPath: "resource1/1/resource2/2/resource3/3/resource4/4/resource5/5/resource6/6/resource7/7/resource8/8/resource9/9/resource10/10", in: existingRequests) let expected = [ @@ -176,6 +176,27 @@ extension FakeRequestTests { } } + func testGETWithDelay() async throws { + let networking = Networking(baseURL: baseURL) + let delay: Double = 2.0 + + let startTime1 = Date() + networking.fakeGET("/stories", response: ["name": "Elvis"], delay: delay) + + let firstResult = try await networking.get("/stories") + let endTime1 = Date() + let elapsedTime1 = endTime1.timeIntervalSince(startTime1) + XCTAssertGreaterThanOrEqual(elapsedTime1, delay, "The delay was not correctly applied") + + switch firstResult { + case let .success(response): + let json = response.dictionaryBody + XCTAssertEqual(json["name"] as? String, "Elvis") + case let .failure(response): + XCTFail(response.error.localizedDescription) + } + } + func testFakeGETWithInvalidStatusCode() async throws { let networking = Networking(baseURL: baseURL) @@ -609,6 +630,29 @@ extension FakeRequestTests { } } + func testFakeImageDownloadWithDelay() async throws { + let networking = Networking(baseURL: baseURL) + let pigImage = Image.find(named: "pig.png", inBundle: .module) + let delay: Double = 2.0 + + let startTime = Date() + networking.fakeImageDownload("/image/png", image: pigImage, delay: delay) + + let result = try await networking.downloadImage("/image/png") + let endTime = Date() + let elapsedTime = endTime.timeIntervalSince(startTime) + XCTAssertGreaterThanOrEqual(elapsedTime, delay, "The delay was not correctly applied") + + switch result { + case let .success(response): + let pigImageData = pigImage.pngData() + let imageData = response.image.pngData() + XCTAssertEqual(pigImageData, imageData) + case let .failure(response): + XCTFail(response.error.localizedDescription) + } + } + func testFakeImageDownloadWithInvalidStatusCode() async throws { let networking = Networking(baseURL: baseURL) let pigImage = Image.find(named: "pig.png", inBundle: .module)