diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..e364fe7c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,57 @@
+name: "Networking CI"
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - '*'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ unit-tests:
+ name: ${{ matrix.name }}
+ runs-on: ${{ matrix.runsOn }}
+ env:
+ DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer"
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: 'iOS 17.2'
+ destination: 'OS=17.2,name=iPhone 15 Pro'
+ xcode: 'Xcode_15.2'
+ runsOn: macos-14
+# - name: 'iOS 16.4'
+# destination: 'OS=16.4,name=iPhone 14 Pro'
+# xcode: 'Xcode_14.3.1'
+# runsOn: macos-13
+ - name: 'macOS 13, Xcode 15.2'
+ destination: 'platform=macOS'
+ xcode: 'Xcode_15.2'
+ runsOn: macos-14
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: 'Running unit tests on ${{ matrix.name }}'
+ run: |
+ set -o pipefail && \
+ xcodebuild clean test -resultBundlePath "TestResults-${{ matrix.name }}" -skipPackagePluginValidation -scheme "Networking" -destination "${{ matrix.destination }}" | tee "build-log-${{ matrix.name }}.txt" | xcpretty
+
+ - uses: kishikawakatsumi/xcresulttool@v1
+ with:
+ path: 'TestResults-${{ matrix.name }}.xcresult'
+ title: '${{ matrix.name }} Test Results'
+ if: success() || failure()
+
+ - name: 'Upload Build Log'
+ uses: actions/upload-artifact@v4
+ with:
+ name: 'build-log-${{ matrix.name }}'
+ path: 'build-log-${{ matrix.name }}.txt'
+ if: success() || failure()
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme
index a5adf7f8..5f948cd1 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme
@@ -26,10 +26,22 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
- shouldUseLaunchSchemeArgsEnv = "YES">
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ codeCoverageEnabled = "YES"
+ onlyGenerateCoverageForSpecifiedTargets = "YES">
+
+
+
+
+ skipped = "NO"
+ testExecutionOrdering = "random">
Response {
guard let model = try? await loadModel(for: request) else {
- throw NetworkError.underlying(error: MockResponseProviderError.unableToLoadAssetData)
+ throw NetworkError.underlying(error: StoredResponseProviderError.unableToLoadAssetData)
}
guard
@@ -49,7 +47,7 @@ open class MockResponseProvider: ResponseProviding {
headerFields: model.responseHeaders
)
else {
- throw NetworkError.underlying(error: MockResponseProviderError.unableToConstructResponse)
+ throw NetworkError.underlying(error: StoredResponseProviderError.unableToConstructResponse)
}
return Response(model.responseBody ?? Data(), httpResponse)
@@ -58,7 +56,7 @@ open class MockResponseProvider: ResponseProviding {
// MARK: Private helper functions
-private extension MockResponseProvider {
+private extension StoredResponseProvider {
/// Loads a corresponding file from Assets for a given ``URLRequest`` and decodes the data to `EndpointRequestStorageModel`.
func loadModel(for request: URLRequest) async throws -> EndpointRequestStorageModel? {
// counting from 0, check storage request processing
diff --git a/Sources/Networking/Misc/MockResponseProviderError.swift b/Sources/Networking/Misc/StoredResponseProviderError.swift
similarity index 78%
rename from Sources/Networking/Misc/MockResponseProviderError.swift
rename to Sources/Networking/Misc/StoredResponseProviderError.swift
index 0319461a..378f0440 100644
--- a/Sources/Networking/Misc/MockResponseProviderError.swift
+++ b/Sources/Networking/Misc/StoredResponseProviderError.swift
@@ -1,5 +1,5 @@
//
-// MockResponseProviderError.swift
+// StoredResponseProvider.swift
//
//
// Created by Matej Molnár on 04.01.2023.
@@ -7,8 +7,8 @@
import Foundation
-/// An error that occurs during loading a ``Response`` from assets by `MockResponseProvider`.
-enum MockResponseProviderError: Error {
+/// An error that occurs during loading a ``Response`` from assets by `StoredResponseProvider`.
+enum StoredResponseProviderError: Error {
/// An indication that there was a problem with loading or decoding data from assets.
case unableToLoadAssetData
/// An indication that it was not possible to construct a `Response` from the loaded data.
diff --git a/Sources/Networking/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift b/Sources/Networking/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift
index 91ec57a2..83ee383e 100644
--- a/Sources/Networking/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift
+++ b/Sources/Networking/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift
@@ -23,6 +23,7 @@ open class EndpointRequestStorageProcessor: ResponseProcessing, ErrorProcessing
// MARK: Private variables
private let fileManager: FileManager
private let jsonEncoder: JSONEncoder
+ private let fileDataWriter: FileDataWriting
private let config: Config
private lazy var responsesDirectory = fileManager.temporaryDirectory.appendingPathComponent("responses")
@@ -51,13 +52,15 @@ open class EndpointRequestStorageProcessor: ResponseProcessing, ErrorProcessing
public init(
fileManager: FileManager = .default,
+ fileDataWriter: FileDataWriting = FileDataWriter(),
jsonEncoder: JSONEncoder? = nil,
config: Config = .default
) {
self.fileManager = fileManager
+ self.fileDataWriter = fileDataWriter
self.jsonEncoder = jsonEncoder ?? .default
self.config = config
-
+
deleteStoredSessionsExceedingLimit()
}
@@ -138,7 +141,7 @@ private extension EndpointRequestStorageProcessor {
endpointRequest: EndpointRequest,
urlRequest: URLRequest
) {
- Task(priority: .background) { [weak self] in
+ Task.detached(priority: .utility) { [weak self] in
guard let self else {
return
}
@@ -214,7 +217,7 @@ private extension EndpointRequestStorageProcessor {
func store(_ model: EndpointRequestStorageModel, fileUrl: URL) {
do {
let jsonData = try jsonEncoder.encode(model)
- try jsonData.write(to: fileUrl)
+ try fileDataWriter.write(jsonData, to: fileUrl)
os_log("🎈 Response saved %{public}@ bytes at %{public}@", type: .info, "\(jsonData.count)", fileUrl.path)
} catch {
os_log("❌ Can't store response %{public}@ %{public}@ %{public}@", type: .error, model.method, model.path, error.localizedDescription)
diff --git a/Sources/Networking/Utils/FileDataWriter.swift b/Sources/Networking/Utils/FileDataWriter.swift
new file mode 100644
index 00000000..2f1eb089
--- /dev/null
+++ b/Sources/Networking/Utils/FileDataWriter.swift
@@ -0,0 +1,29 @@
+//
+// FileDataWriter.swift
+//
+//
+// Created by Jan Kodeš on 24.01.2024.
+//
+
+import Foundation
+
+/// A protocol defining an interface for writing data to a file.
+public protocol FileDataWriting {
+ /// Writes the given data to the specified URL.
+ ///
+ /// - Parameters:
+ /// - data: The `Data` object that needs to be written to the file.
+ /// - url: The destination `URL` where the data should be written.
+ /// - Throws: An error if the data cannot be written to the URL.
+ func write(_ data: Data, to url: URL) throws
+}
+
+
+/// A class that implements data writing functionality.
+public class FileDataWriter: FileDataWriting {
+ public init() {}
+
+ public func write(_ data: Data, to url: URL) throws {
+ try data.write(to: url)
+ }
+}
diff --git a/Tests/NetworkingTests/EndpointRequestStorageProcessorTests.swift b/Tests/NetworkingTests/EndpointRequestStorageProcessorTests.swift
index 3980fde9..bbe62d3b 100644
--- a/Tests/NetworkingTests/EndpointRequestStorageProcessorTests.swift
+++ b/Tests/NetworkingTests/EndpointRequestStorageProcessorTests.swift
@@ -14,8 +14,8 @@ import XCTest
final class EndpointRequestStorageProcessorTests: XCTestCase {
private let sessionId = "sessionId_request_storage"
- private let fileManager = FileManager.default
-
+ private let mockFileManager = MockFileManager()
+
struct MockBody: Codable {
let parameter: String
}
@@ -75,6 +75,12 @@ final class EndpointRequestStorageProcessorTests: XCTestCase {
}
}
+ override func tearDown() {
+ mockFileManager.reset()
+
+ super.tearDown()
+ }
+
func testResponseStaysTheSameAfterStoringData() async throws {
let mockEndpointRequest = EndpointRequest(MockRouter.testStoringGet, sessionId: sessionId)
let mockURLRequest = URLRequest(url: MockRouter.testStoringGet.baseURL)
@@ -84,7 +90,38 @@ final class EndpointRequestStorageProcessorTests: XCTestCase {
let response = try await EndpointRequestStorageProcessor().process(mockResponse, with: mockURLRequest, for: mockEndpointRequest)
// test storing data processor doesn't effect response in anyway
- XCTAssert(response.data == mockResponse.0 && response.response == mockResponse.1)
+ XCTAssertEqual(response.data, mockResponse.0)
+ XCTAssertEqual(response.response, mockResponse.1)
+ }
+
+ func testProcessCreatesCorrectFolder() async throws {
+ let mockEndpointRequest = EndpointRequest(MockRouter.testStoringGet, sessionId: sessionId)
+ let mockURLRequest = URLRequest(url: MockRouter.testStoringGet.baseURL)
+ let mockURLResponse: URLResponse = HTTPURLResponse(url: MockRouter.testStoringGet.baseURL, statusCode: 200, httpVersion: nil, headerFields: nil)!
+ let mockResponse = (Data(), mockURLResponse)
+
+ let expectation = expectation(description: "Data was written")
+
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .prettyPrinted
+ let mockFileDataWriter = MockFileDataWriter()
+ mockFileDataWriter.writeClosure = {
+ expectation.fulfill()
+ }
+
+ let processor = EndpointRequestStorageProcessor(
+ fileManager: mockFileManager,
+ fileDataWriter: mockFileDataWriter,
+ jsonEncoder: encoder
+ )
+ _ = try await processor.process(mockResponse, with: mockURLRequest, for: mockEndpointRequest)
+
+ await fulfillment(of: [expectation], timeout: 60)
+
+ mockFileManager.verifyFunctionCall(.fileExists(path: responsesDirectory(for: mockEndpointRequest).path))
+ mockFileManager.verifyFunctionCall(.createDirectory(path: responsesDirectory(for: mockEndpointRequest).path))
+
+ XCTAssertEqual(mockFileDataWriter.receivedURL, fileUrl(for: mockEndpointRequest))
}
func testStoredDataForGetRequestWithJSONResponse() async throws {
@@ -98,37 +135,39 @@ final class EndpointRequestStorageProcessorTests: XCTestCase {
)!
let mockResponseData = "Mock data".data(using: .utf8)!
let mockResponse = (mockResponseData, mockURLResponse)
-
+ let expectation = expectation(description: "Data was written")
+
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
-
- let processor = EndpointRequestStorageProcessor(fileManager: fileManager, jsonEncoder: encoder)
- _ = try await processor.process(mockResponse, with: mockURLRequest, for: mockEndpointRequest)
-
- // The storing runs on background thread so we need to wait before reading the file
- try await Task.sleep(nanoseconds: 1000000000)
-
- let fileUrl = fileUrl(for: mockEndpointRequest)
-
- guard let data = fileManager.contents(atPath: fileUrl.path) else {
- XCTAssert(false, "File doesn't exist")
- return
+ let mockFileDataWriter = MockFileDataWriter()
+ mockFileDataWriter.writeClosure = {
+ expectation.fulfill()
}
-
- let model = try JSONDecoder().decode(EndpointRequestStorageModel.self, from: data)
-
- XCTAssert(
- model.statusCode == 200 &&
- model.method == "GET" &&
- model.path == mockEndpointRequest.endpoint.path &&
- model.parameters == ["query": "mock"] &&
- model.requestBody == nil &&
- model.requestBodyString == nil &&
- model.requestHeaders == mockURLRequest.allHTTPHeaderFields &&
- model.responseBody == mockResponseData &&
- model.responseBodyString == String(data: mockResponseData, encoding: .utf8) &&
- model.responseHeaders == ["mockResponseHeader": "mock"]
+
+ let processor = EndpointRequestStorageProcessor(
+ fileManager: mockFileManager,
+ fileDataWriter: mockFileDataWriter,
+ jsonEncoder: encoder
)
+ _ = try await processor.process(mockResponse, with: mockURLRequest, for: mockEndpointRequest)
+
+ await fulfillment(of: [expectation], timeout: 60)
+
+ let receivedData = try XCTUnwrap(mockFileDataWriter.receivedData)
+ let model = try JSONDecoder().decode(EndpointRequestStorageModel.self, from: receivedData)
+
+ XCTAssertEqual(model.statusCode, 200)
+ XCTAssertEqual(model.method, "GET")
+ XCTAssertEqual(model.path, mockEndpointRequest.endpoint.path)
+ XCTAssertEqual(model.parameters, ["query": "mock"])
+ XCTAssertNil(model.requestBody)
+ XCTAssertNil(model.requestBodyString)
+ XCTAssertEqual(model.requestHeaders, mockURLRequest.allHTTPHeaderFields)
+ XCTAssertEqual(model.responseBody, mockResponseData)
+ XCTAssertEqual(model.responseBodyString, String(data: mockResponseData, encoding: .utf8))
+ XCTAssertEqual(model.responseHeaders, ["mockResponseHeader": "mock"])
+
+ XCTAssertEqual(mockFileDataWriter.receivedURL, fileUrl(for: mockEndpointRequest))
}
func testStoredDataForGetRequestWithImageResponse() async throws {
@@ -151,37 +190,40 @@ final class EndpointRequestStorageProcessorTests: XCTestCase {
#endif
let mockResponse = (mockResponseData, mockURLResponse)
-
+ let expectation = expectation(description: "Data was written")
+
+ let mockFileDataWriter = MockFileDataWriter()
+ mockFileDataWriter.writeClosure = {
+ expectation.fulfill()
+ }
+
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
- let processor = EndpointRequestStorageProcessor(fileManager: fileManager, jsonEncoder: encoder)
+ let processor = EndpointRequestStorageProcessor(
+ fileManager: mockFileManager,
+ fileDataWriter: mockFileDataWriter,
+ jsonEncoder: encoder
+ )
_ = try await processor.process(mockResponse, with: mockURLRequest, for: mockEndpointRequest)
-
- // The storing runs on background thread so we need to wait before reading the file
- try await Task.sleep(nanoseconds: 1000000000)
-
- let fileUrl = fileUrl(for: mockEndpointRequest)
- guard let data = fileManager.contents(atPath: fileUrl.path) else {
- XCTAssert(false, "File doesn't exist")
- return
- }
-
- let model = try JSONDecoder().decode(EndpointRequestStorageModel.self, from: data)
-
- XCTAssert(
- model.statusCode == 200 &&
- model.method == "GET" &&
- model.path == mockEndpointRequest.endpoint.path &&
- model.parameters == ["query": "mock"] &&
- model.requestBody == nil &&
- model.requestBodyString == nil &&
- model.requestHeaders == mockURLRequest.allHTTPHeaderFields &&
- model.responseBody == mockResponseData &&
- model.responseBodyString == String(data: mockResponseData, encoding: .utf8) &&
- model.responseHeaders == ["mockResponseHeader": "mock"]
- )
+ await fulfillment(of: [expectation], timeout: 60)
+
+ let receivedData = try XCTUnwrap(mockFileDataWriter.receivedData)
+ let model = try JSONDecoder().decode(EndpointRequestStorageModel.self, from: receivedData)
+
+ XCTAssertEqual(model.statusCode, 200)
+ XCTAssertEqual(model.method, "GET")
+ XCTAssertEqual(model.path, mockEndpointRequest.endpoint.path)
+ XCTAssertEqual(model.parameters, ["query": "mock"])
+ XCTAssertNil(model.requestBody)
+ XCTAssertNil(model.requestBodyString)
+ XCTAssertEqual(model.requestHeaders, mockURLRequest.allHTTPHeaderFields)
+ XCTAssertEqual(model.responseBody, mockResponseData)
+ XCTAssertEqual(model.responseBodyString, String(data: mockResponseData, encoding: .utf8))
+ XCTAssertEqual(model.responseHeaders, ["mockResponseHeader": "mock"])
+
+ XCTAssertEqual(mockFileDataWriter.receivedURL, fileUrl(for: mockEndpointRequest))
}
func testStoredDataForGetRequestWithErrorResponse() async throws {
@@ -200,37 +242,43 @@ final class EndpointRequestStorageProcessorTests: XCTestCase {
acceptedStatusCodes: HTTPStatusCode.successCodes,
response: mockResponse
)
-
+
+ let expectation = expectation(description: "Data was written")
+
+ let mockFileDataWriter = MockFileDataWriter()
+ mockFileDataWriter.writeClosure = {
+ expectation.fulfill()
+ }
+
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
- let processor = EndpointRequestStorageProcessor(fileManager: fileManager, jsonEncoder: encoder)
+ let processor = EndpointRequestStorageProcessor(
+ fileManager: mockFileManager,
+ fileDataWriter: mockFileDataWriter,
+ jsonEncoder: encoder
+ )
+
_ = await processor.process(mockError, for: mockEndpointRequest)
-
- // The storing runs on background thread so we need to wait before reading the file
- try await Task.sleep(nanoseconds: 1000000000)
-
- let fileUrl = fileUrl(for: mockEndpointRequest)
- guard let data = fileManager.contents(atPath: fileUrl.path) else {
- XCTAssert(false, "File doesn't exist")
- return
- }
-
- let model = try JSONDecoder().decode(EndpointRequestStorageModel.self, from: data)
-
- XCTAssert(
- model.statusCode == 404 &&
- model.method == "GET" &&
- model.path == mockEndpointRequest.endpoint.path &&
- model.parameters == ["query": "mock"] &&
- model.requestBody == nil &&
- model.requestBodyString == nil &&
- model.requestHeaders == mockURLRequest.allHTTPHeaderFields &&
- model.responseBody == mockResponseData &&
- model.responseBodyString == String(data: mockResponseData, encoding: .utf8) &&
- model.responseHeaders == ["mockResponseHeader": "mock"]
- )
+ await fulfillment(of: [expectation], timeout: 60)
+
+ let receivedData = try XCTUnwrap(mockFileDataWriter.receivedData)
+
+ let model = try JSONDecoder().decode(EndpointRequestStorageModel.self, from: receivedData)
+
+ XCTAssertEqual(model.statusCode, 404)
+ XCTAssertEqual(model.method, "GET")
+ XCTAssertEqual(model.path, mockEndpointRequest.endpoint.path)
+ XCTAssertEqual(model.parameters, ["query": "mock"])
+ XCTAssertNil(model.requestBody)
+ XCTAssertNil(model.requestBodyString)
+ XCTAssertEqual(model.requestHeaders, mockURLRequest.allHTTPHeaderFields)
+ XCTAssertEqual(model.responseBody, mockResponseData)
+ XCTAssertEqual(model.responseBodyString, String(data: mockResponseData, encoding: .utf8))
+ XCTAssertEqual(model.responseHeaders, ["mockResponseHeader": "mock"])
+
+ XCTAssertEqual(mockFileDataWriter.receivedURL, fileUrl(for: mockEndpointRequest))
}
func testStoredDataForPostRequest() async throws {
@@ -244,38 +292,43 @@ final class EndpointRequestStorageProcessorTests: XCTestCase {
)!
let mockResponseData = "Mock data".data(using: .utf8)!
let mockResponse = (mockResponseData, mockURLResponse)
-
+
+ let expectation = expectation(description: "Data was written")
+
+ let mockFileDataWriter = MockFileDataWriter()
+ mockFileDataWriter.writeClosure = {
+ expectation.fulfill()
+ }
+
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
- let processor = EndpointRequestStorageProcessor(fileManager: fileManager, jsonEncoder: encoder)
+ let processor = EndpointRequestStorageProcessor(
+ fileManager: mockFileManager,
+ fileDataWriter: mockFileDataWriter,
+ jsonEncoder: encoder
+ )
_ = try await processor.process(mockResponse, with: mockURLRequest, for: mockEndpointRequest)
- // The storing runs on background thread so we need to wait before reading the file
- try await Task.sleep(nanoseconds: 1000000000)
-
- let fileUrl = fileUrl(for: mockEndpointRequest)
+ await fulfillment(of: [expectation], timeout: 60)
- guard let data = fileManager.contents(atPath: fileUrl.path) else {
- XCTAssert(false, "File doesn't exist")
- return
- }
-
- let model = try JSONDecoder().decode(EndpointRequestStorageModel.self, from: data)
- let mockRequestBody = try mockEndpointRequest.endpoint.encodeBody()!
-
- XCTAssert(
- model.statusCode == 200 &&
- model.method == "POST" &&
- model.path == mockEndpointRequest.endpoint.path &&
- model.parameters == ["query": "mock"] &&
- model.requestBody == mockRequestBody &&
- model.requestBodyString == String(data: mockRequestBody, encoding: .utf8) &&
- model.requestHeaders == mockURLRequest.allHTTPHeaderFields &&
- model.responseBody == mockResponseData &&
- model.responseBodyString == String(data: mockResponseData, encoding: .utf8) &&
- model.responseHeaders == ["mockResponseHeader": "mock"]
- )
+ let receivedData = try XCTUnwrap(mockFileDataWriter.receivedData)
+
+ let model = try JSONDecoder().decode(EndpointRequestStorageModel.self, from: receivedData)
+ let mockRequestBody = try XCTUnwrap(try mockEndpointRequest.endpoint.encodeBody())
+
+ XCTAssertEqual(model.statusCode, 200)
+ XCTAssertEqual(model.method, "POST")
+ XCTAssertEqual(model.path, mockEndpointRequest.endpoint.path)
+ XCTAssertEqual(model.parameters, ["query": "mock"])
+ XCTAssertEqual(model.requestBody, mockRequestBody)
+ XCTAssertEqual(model.requestBodyString, String(data: mockRequestBody, encoding: .utf8))
+ XCTAssertEqual(model.requestHeaders, mockURLRequest.allHTTPHeaderFields)
+ XCTAssertEqual(model.responseBody, mockResponseData)
+ XCTAssertEqual(model.responseBodyString, String(data: mockResponseData, encoding: .utf8))
+ XCTAssertEqual(model.responseHeaders, ["mockResponseHeader": "mock"])
+
+ XCTAssertEqual(mockFileDataWriter.receivedURL, fileUrl(for: mockEndpointRequest))
}
// swiftlint:enable force_unwrapping
@@ -290,10 +343,14 @@ final class EndpointRequestStorageProcessorTests: XCTestCase {
private extension EndpointRequestStorageProcessorTests {
func fileUrl(for endpointRequest: EndpointRequest) -> URL {
- let responsesDirectory = fileManager.temporaryDirectory.appendingPathComponent("responses")
let fileName = "\(endpointRequest.sessionId)_\(endpointRequest.endpoint.identifier)_0"
+ return responsesDirectory(for: endpointRequest)
+ .appendingPathComponent("\(fileName).json")
+ }
+
+ func responsesDirectory(for endpointRequest: EndpointRequest) -> URL {
+ let responsesDirectory = mockFileManager.temporaryDirectory.appendingPathComponent("responses")
return responsesDirectory
.appendingPathComponent(endpointRequest.sessionId)
- .appendingPathComponent("\(fileName).json")
}
}
diff --git a/Tests/NetworkingTests/Mocks/MockFileDataWriter.swift b/Tests/NetworkingTests/Mocks/MockFileDataWriter.swift
new file mode 100644
index 00000000..c31085bb
--- /dev/null
+++ b/Tests/NetworkingTests/Mocks/MockFileDataWriter.swift
@@ -0,0 +1,26 @@
+//
+// FileDataWriterSpy.swift
+//
+//
+// Created by Jan Kodeš on 24.01.2024.
+//
+
+import Foundation
+import Networking
+
+/// A test mock class for `FileDataWriting`.
+/// It writes into a file but let's us react when it's finished.
+final class MockFileDataWriter: FileDataWriting {
+ var writeClosure: (() -> Void)?
+ private(set) var writeCalled = false
+ private(set) var receivedData: Data?
+ private(set) var receivedURL: URL?
+
+ func write(_ data: Data, to url: URL) throws {
+ writeCalled = true
+ receivedData = data
+ receivedURL = url
+
+ writeClosure?()
+ }
+}
diff --git a/Tests/NetworkingTests/Mocks/MockFileManager.swift b/Tests/NetworkingTests/Mocks/MockFileManager.swift
new file mode 100644
index 00000000..d51633c5
--- /dev/null
+++ b/Tests/NetworkingTests/Mocks/MockFileManager.swift
@@ -0,0 +1,71 @@
+//
+// MockFileManager.swift
+//
+//
+// Created by Jan Kodeš on 23.01.2024.
+//
+
+import Foundation
+import XCTest
+
+/// A subclass of `FileManager` where the file existence is based on a dictionary whose key is the file path.
+final class MockFileManager: FileManager {
+ enum Function: Equatable {
+ case fileExists(path: String)
+ case createDirectory(path: String)
+ case contentsOfDirectory(path: String)
+ case removeItem(path: String)
+ }
+
+ /// Mocked or received data
+ var dataByFilePath: [String: Data] = [:]
+
+ /// Received functions
+ private var functionCallHistory: [Function] = []
+
+ override func fileExists(atPath path: String) -> Bool {
+ recordCall(.fileExists(path: path))
+ return dataByFilePath[path] != nil
+ }
+
+ override func createDirectory(atPath path: String, withIntermediateDirectories createIntermediates: Bool, attributes: [FileAttributeKey: Any]? = nil) throws {
+ recordCall(.createDirectory(path: path))
+
+ // Simulate directory creation by adding an empty entry
+ dataByFilePath[path] = Data()
+ }
+
+ override func contentsOfDirectory(atPath path: String) throws -> [String] {
+ recordCall(.contentsOfDirectory(path: path))
+
+ // Return file names in the specified directory
+ return dataByFilePath.keys
+ .filter { $0.hasPrefix(path) }
+ .map { $0.replacingOccurrences(of: path, with: "") }
+ }
+
+ override func removeItem(atPath path: String) throws {
+ recordCall(.removeItem(path: path))
+
+ dataByFilePath.removeValue(forKey: path)
+ }
+}
+
+extension MockFileManager {
+ private func recordCall(_ method: Function) {
+ functionCallHistory.append(method)
+ }
+
+ func verifyFunctionCall(_ expectedMethod: Function, file: StaticString = #file, line: UInt = #line) {
+ guard functionCallHistory.contains(where: { $0 == expectedMethod }) else {
+ XCTFail("Expected to have called \(expectedMethod). Received: \(functionCallHistory)", file: file, line: line)
+ print(functionCallHistory)
+ return
+ }
+ }
+
+ func reset() {
+ dataByFilePath = [:]
+ functionCallHistory = []
+ }
+}
diff --git a/Tests/NetworkingTests/MockResponseProviderTests.swift b/Tests/NetworkingTests/StoredResponseProviderTests.swift
similarity index 78%
rename from Tests/NetworkingTests/MockResponseProviderTests.swift
rename to Tests/NetworkingTests/StoredResponseProviderTests.swift
index c3ca6027..2ef1689b 100644
--- a/Tests/NetworkingTests/MockResponseProviderTests.swift
+++ b/Tests/NetworkingTests/StoredResponseProviderTests.swift
@@ -1,5 +1,5 @@
//
-// MockResponseProviderTests.swift
+// StoredResponseProviderTests.swift
//
//
// Created by Matej Molnár on 05.01.2023.
@@ -8,7 +8,7 @@
@testable import Networking
import XCTest
-final class MockResponseProviderTests: XCTestCase {
+final class StoredResponseProviderTests: XCTestCase {
// swiftlint:disable:next force_unwrapping
private lazy var mockUrlRequest = URLRequest(url: URL(string: "https://reqres.in/api/users?page=2")!)
private let mockSessionId = "2023-01-04T16:15:29Z"
@@ -33,12 +33,12 @@ final class MockResponseProviderTests: XCTestCase {
]
func testLoadingData() async throws {
- let mockResponseProvider = MockResponseProvider(with: Bundle.module, sessionId: mockSessionId)
-
+ let storedResponseProvider = StoredResponseProvider(with: Bundle.module, sessionId: mockSessionId)
+
// call request multiple times, 6 testing data files
// test reading correct file
for index in 0...10 {
- let response = try await mockResponseProvider.response(for: mockUrlRequest)
+ let response = try await storedResponseProvider.response(for: mockUrlRequest)
XCTAssert(response.response is HTTPURLResponse)
@@ -69,14 +69,14 @@ final class MockResponseProviderTests: XCTestCase {
}
func testUnableToLoadAssetError() async {
- let mockResponseProvider = MockResponseProvider(with: Bundle.module, sessionId: "NonexistentSessionId")
+ let storedResponseProvider = StoredResponseProvider(with: Bundle.module, sessionId: "NonexistentSessionId")
do {
- _ = try await mockResponseProvider.response(for: mockUrlRequest)
+ _ = try await storedResponseProvider.response(for: mockUrlRequest)
XCTAssert(false, "function didn't throw an error even though it should have")
} catch {
var correctError = false
- if case NetworkError.underlying(error: MockResponseProviderError.unableToLoadAssetData) = error {
+ if case NetworkError.underlying(error: StoredResponseProviderError.unableToLoadAssetData) = error {
correctError = true
}
XCTAssert(correctError, "function threw an incorrect error")
@@ -84,14 +84,14 @@ final class MockResponseProviderTests: XCTestCase {
}
func testUnableToConstructResponseError() async {
- let mockResponseProvider = MockResponseProvider(with: Bundle.module, sessionId: "2023-01-04T16:15:29Z(corrupted)")
+ let storedResponseProvider = StoredResponseProvider(with: Bundle.module, sessionId: "2023-01-04T16:15:29Z(corrupted)")
do {
- _ = try await mockResponseProvider.response(for: mockUrlRequest)
+ _ = try await storedResponseProvider.response(for: mockUrlRequest)
XCTAssert(false, "function didn't throw an error even though it should have")
} catch {
var correctError = false
- if case NetworkError.underlying(error: MockResponseProviderError.unableToConstructResponse) = error {
+ if case NetworkError.underlying(error: StoredResponseProviderError.unableToConstructResponse) = error {
correctError = true
}
XCTAssert(correctError, "function threw an incorrect error")