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")