// // MockNetworkService.swift // PlantGuideTests // // Mock implementations for network-related services for unit testing. // Provides configurable behavior and call tracking for verification. // import Foundation import Network @testable import PlantGuide // MARK: - MockNetworkMonitor /// Mock implementation of NetworkMonitor for testing /// Note: This creates a testable version that doesn't actually monitor network state @Observable final class MockNetworkMonitor: @unchecked Sendable { // MARK: - Properties /// Current network connectivity status (configurable for tests) var isConnected: Bool = true /// Current connection type (configurable for tests) var connectionType: ConnectionType = .wifi // MARK: - Call Tracking private(set) var startMonitoringCallCount = 0 private(set) var stopMonitoringCallCount = 0 // MARK: - Initialization init(isConnected: Bool = true, connectionType: ConnectionType = .wifi) { self.isConnected = isConnected self.connectionType = connectionType } // MARK: - Public Methods func startMonitoring() { startMonitoringCallCount += 1 } func stopMonitoring() { stopMonitoringCallCount += 1 } // MARK: - Test Helper Methods /// Simulates a connection state change func simulateConnectionChange(isConnected: Bool, connectionType: ConnectionType = .wifi) { self.isConnected = isConnected self.connectionType = connectionType } /// Simulates going offline func simulateDisconnect() { isConnected = false connectionType = .unknown } /// Simulates connecting to WiFi func simulateWiFiConnection() { isConnected = true connectionType = .wifi } /// Simulates connecting to cellular func simulateCellularConnection() { isConnected = true connectionType = .cellular } /// Resets all state for clean test setup func reset() { isConnected = true connectionType = .wifi startMonitoringCallCount = 0 stopMonitoringCallCount = 0 } } // MARK: - MockURLSession /// Mock implementation of URLSession for testing network requests final class MockURLSession: @unchecked Sendable { // MARK: - Call Tracking private(set) var dataCallCount = 0 private(set) var uploadCallCount = 0 // MARK: - Error Configuration var shouldThrowOnData = false var shouldThrowOnUpload = false var errorToThrow: Error = NSError( domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."] ) // MARK: - Return Value Configuration var dataToReturn: Data = Data() var responseToReturn: URLResponse? var statusCodeToReturn: Int = 200 // MARK: - Captured Values private(set) var lastRequestedURL: URL? private(set) var lastUploadData: Data? // MARK: - Mock Methods func data(from url: URL) async throws -> (Data, URLResponse) { dataCallCount += 1 lastRequestedURL = url if shouldThrowOnData { throw errorToThrow } let response = responseToReturn ?? HTTPURLResponse( url: url, statusCode: statusCodeToReturn, httpVersion: "HTTP/1.1", headerFields: nil )! return (dataToReturn, response) } func upload(for request: URLRequest, from bodyData: Data) async throws -> (Data, URLResponse) { uploadCallCount += 1 lastRequestedURL = request.url lastUploadData = bodyData if shouldThrowOnUpload { throw errorToThrow } let response = responseToReturn ?? HTTPURLResponse( url: request.url!, statusCode: statusCodeToReturn, httpVersion: "HTTP/1.1", headerFields: nil )! return (dataToReturn, response) } // MARK: - Helper Methods /// Resets all state for clean test setup func reset() { dataCallCount = 0 uploadCallCount = 0 shouldThrowOnData = false shouldThrowOnUpload = false dataToReturn = Data() responseToReturn = nil statusCodeToReturn = 200 lastRequestedURL = nil lastUploadData = nil } /// Configures the mock to return JSON data func configureJSONResponse(_ value: T, statusCode: Int = 200) throws { let encoder = JSONEncoder() dataToReturn = try encoder.encode(value) statusCodeToReturn = statusCode } /// Configures the mock to return an error response func configureErrorResponse(statusCode: Int, message: String = "Error") { statusCodeToReturn = statusCode dataToReturn = Data(message.utf8) } /// Configures the mock to simulate a network error func configureNetworkError(_ error: URLError.Code = .notConnectedToInternet) { shouldThrowOnData = true shouldThrowOnUpload = true errorToThrow = URLError(error) } /// Configures the mock to simulate a timeout func configureTimeout() { shouldThrowOnData = true shouldThrowOnUpload = true errorToThrow = URLError(.timedOut) } } // MARK: - MockPlantNetAPIService /// Mock implementation of PlantNet API service for testing final class MockPlantNetAPIService: @unchecked Sendable { // MARK: - PlantNet Response Types struct PlantNetResponse: Codable { let results: [PlantNetResult] } struct PlantNetResult: Codable { let score: Double let species: PlantNetSpecies } struct PlantNetSpecies: Codable { let scientificNameWithoutAuthor: String let commonNames: [String] } // MARK: - Call Tracking private(set) var identifyCallCount = 0 // MARK: - Error Configuration var shouldThrow = false var errorToThrow: Error = NSError( domain: "PlantNetError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Mock PlantNet API error"] ) // MARK: - Return Value Configuration var resultsToReturn: [PlantNetResult] = [] // MARK: - Captured Values private(set) var lastImageData: Data? // MARK: - Mock Methods func identify(imageData: Data) async throws -> PlantNetResponse { identifyCallCount += 1 lastImageData = imageData if shouldThrow { throw errorToThrow } return PlantNetResponse(results: resultsToReturn) } // MARK: - Helper Methods /// Resets all state for clean test setup func reset() { identifyCallCount = 0 shouldThrow = false resultsToReturn = [] lastImageData = nil } /// Configures mock to return successful plant identification func configureSuccessfulIdentification() { resultsToReturn = [ PlantNetResult( score: 0.95, species: PlantNetSpecies( scientificNameWithoutAuthor: "Monstera deliciosa", commonNames: ["Swiss Cheese Plant", "Monstera"] ) ), PlantNetResult( score: 0.72, species: PlantNetSpecies( scientificNameWithoutAuthor: "Philodendron bipinnatifidum", commonNames: ["Split Leaf Philodendron"] ) ) ] } /// Configures mock to return no results func configureNoResults() { resultsToReturn = [] } }