diff --git a/script.sh b/script.sh old mode 100644 new mode 100755 diff --git a/swift-sdk/Core/Constants.swift b/swift-sdk/Core/Constants.swift index ab39eef4f..71ce15fd4 100644 --- a/swift-sdk/Core/Constants.swift +++ b/swift-sdk/Core/Constants.swift @@ -82,6 +82,7 @@ enum Const { static let visitorConsentTimestamp = "itbl_visitor_consent_timestamp" static let isNotificationsEnabled = "itbl_isNotificationsEnabled" static let hasStoredNotificationSetting = "itbl_hasStoredNotificationSetting" + static let networkLoggingEnabled = "itbl_network_logging_enabled" static let attributionInfoExpiration = 24 } diff --git a/swift-sdk/Internal/HealthMonitor.swift b/swift-sdk/Internal/HealthMonitor.swift index 909e1000a..0f244f67b 100644 --- a/swift-sdk/Internal/HealthMonitor.swift +++ b/swift-sdk/Internal/HealthMonitor.swift @@ -94,7 +94,12 @@ class HealthMonitor { let currentDate = dateProvider.currentDate let apiCallRequest = apiCallRequest.addingCreatedAt(currentDate) if let urlRequest = apiCallRequest.convertToURLRequest(sentAt: currentDate) { + ITBInfo("Attempting to send failed-to-schedule request directly for path: '\(apiCallRequest.getPath())'") _ = RequestSender.sendRequest(urlRequest, usingSession: networkSession) + } else { + let endpoint = apiCallRequest.endpoint + let path = apiCallRequest.getPath() + ITBError("Failed to convert to URL request in health monitor - endpoint: '\(endpoint)', path: '\(path)'") } onError() } diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index 6469095d4..136c65814 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -964,7 +964,30 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { private static func setApiEndpoint(apiEndPointOverride: String?, config: IterableConfig) -> String { let apiEndPoint = config.dataRegion - return apiEndPointOverride ?? apiEndPoint + let endpoint = apiEndPointOverride ?? apiEndPoint + + // Sanitize and validate endpoint + let sanitized = endpoint.trimmingCharacters(in: .whitespacesAndNewlines) + + // Validate endpoint is a valid URL + if let url = URL(string: sanitized) { + if url.scheme == nil || url.host == nil { + ITBError("Invalid API endpoint - missing scheme or host: '\(sanitized)'") + } + } else { + ITBError("Invalid API endpoint - cannot create URL from: '\(sanitized)'") + } + + // Check for common issues + if sanitized != endpoint { + ITBError("API endpoint contained whitespace, trimmed from '\(endpoint)' to '\(sanitized)'") + } + + if sanitized.isEmpty { + ITBError("API endpoint is empty after sanitization") + } + + return sanitized } init(apiKey: String, @@ -983,6 +1006,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { networkSession = dependencyContainer.networkSession notificationStateProvider = dependencyContainer.notificationStateProvider localStorage = dependencyContainer.localStorage + NetworkHelper.isNetworkLoggingEnabled = localStorage.networkLoggingEnabled inAppDisplayer = dependencyContainer.inAppDisplayer urlOpener = dependencyContainer.urlOpener notificationCenter = dependencyContainer.notificationCenter @@ -1136,6 +1160,12 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { requestHandler.getRemoteConfiguration().onSuccess { remoteConfiguration in self.localStorage.offlineMode = remoteConfiguration.offlineMode self.requestHandler.offlineMode = remoteConfiguration.offlineMode + + if let enableNetworkLogging = remoteConfiguration.enableNetworkLogging { + self.localStorage.networkLoggingEnabled = enableNetworkLogging + NetworkHelper.isNetworkLoggingEnabled = enableNetworkLogging + } + ITBInfo("setting offlineMode: \(self.requestHandler.offlineMode)") }.onError { error in let offlineMode = self.requestHandler.offlineMode diff --git a/swift-sdk/Internal/IterableAPICallTaskProcessor.swift b/swift-sdk/Internal/IterableAPICallTaskProcessor.swift index 019912ead..344d963aa 100644 --- a/swift-sdk/Internal/IterableAPICallTaskProcessor.swift +++ b/swift-sdk/Internal/IterableAPICallTaskProcessor.swift @@ -22,7 +22,11 @@ struct IterableAPICallTaskProcessor: IterableTaskProcessor { let iterableRequest = decodedIterableRequest.addingCreatedAt(task.scheduledAt) guard let urlRequest = iterableRequest.convertToURLRequest(sentAt: dateProvider.currentDate, processorType: .offline) else { - return IterableTaskError.createErroredFuture(reason: "could not convert to url request") + let endpoint = decodedIterableRequest.endpoint + let path = decodedIterableRequest.getPath() + let errorMessage = "Could not convert to URL request - endpoint: '\(endpoint)', path: '\(path)'" + ITBError(errorMessage) + return IterableTaskError.createErroredFuture(reason: errorMessage) } let result = Fulfill() @@ -47,10 +51,38 @@ struct IterableAPICallTaskProcessor: IterableTaskProcessor { private let dateProvider: DateProviderProtocol private static func isNetworkUnavailable(sendRequestError: SendRequestError) -> Bool { + // Check for NSURLError codes that indicate network issues + if let nsError = sendRequestError.originalError as? NSError { + if nsError.domain == NSURLErrorDomain { + let networkErrorCodes: Set = [ + NSURLErrorNotConnectedToInternet, // -1009 + NSURLErrorNetworkConnectionLost, // -1005 + NSURLErrorTimedOut, // -1001 + NSURLErrorCannotConnectToHost, // -1004 + NSURLErrorDNSLookupFailed, // -1006 + NSURLErrorDataNotAllowed, // -1020 (cellular data disabled) + NSURLErrorInternationalRoamingOff // -1018 + ] + if networkErrorCodes.contains(nsError.code) { + ITBInfo("Network error detected: code=\(nsError.code), description=\(nsError.localizedDescription)") + return true + } + } + } + + // Fallback to string check for other network-related errors if let originalError = sendRequestError.originalError { - return originalError.localizedDescription.lowercased().contains("offline") - } else { - return false + let description = originalError.localizedDescription.lowercased() + let isNetworkError = description.contains("offline") || + description.contains("network") || + description.contains("internet") || + description.contains("connection") + if isNetworkError { + ITBInfo("Network error detected via description: \(originalError.localizedDescription)") + } + return isNetworkError } + + return false } } diff --git a/swift-sdk/Internal/IterableRequest.swift b/swift-sdk/Internal/IterableRequest.swift index 9b33019ca..a3897498f 100644 --- a/swift-sdk/Internal/IterableRequest.swift +++ b/swift-sdk/Internal/IterableRequest.swift @@ -53,6 +53,15 @@ extension IterableRequest: Codable { } } + func getPath() -> String { + switch self { + case .get(let request): + return request.path + case .post(let request): + return request.path + } + } + private static let requestTypeGet = "get" private static let requestTypePost = "post" } diff --git a/swift-sdk/Internal/IterableRequestUtil.swift b/swift-sdk/Internal/IterableRequestUtil.swift index 2a6ba98cb..c1033f188 100644 --- a/swift-sdk/Internal/IterableRequestUtil.swift +++ b/swift-sdk/Internal/IterableRequestUtil.swift @@ -13,6 +13,7 @@ struct IterableRequestUtil { headers: [String: String]? = nil, args: [String: String]? = nil) -> URLRequest? { guard let url = getUrlComponents(forApiEndPoint: apiEndPoint, path: path, args: args)?.url else { + ITBError("Failed to create GET request URL") return nil } @@ -53,6 +54,7 @@ struct IterableRequestUtil { args: [String: String]? = nil, body: Data? = nil) -> URLRequest? { guard let url = getUrlComponents(forApiEndPoint: apiEndPoint, path: path, args: args)?.url else { + ITBError("Failed to create POST request URL") return nil } @@ -88,6 +90,7 @@ struct IterableRequestUtil { let endPointCombined = pathCombine(path1: apiEndPoint, path2: path) guard var components = URLComponents(string: "\(endPointCombined)") else { + ITBError("Failed to create URLComponents - apiEndPoint: '\(apiEndPoint)', path: '\(path)', combined: '\(endPointCombined)'") return nil } diff --git a/swift-sdk/Internal/IterableUserDefaults.swift b/swift-sdk/Internal/IterableUserDefaults.swift index f51e16d9a..ffda42aae 100644 --- a/swift-sdk/Internal/IterableUserDefaults.swift +++ b/swift-sdk/Internal/IterableUserDefaults.swift @@ -170,6 +170,14 @@ class IterableUserDefaults { } } + var networkLoggingEnabled: Bool { + get { + bool(withKey: .networkLoggingEnabled) + } set { + save(bool: newValue, withKey: .networkLoggingEnabled) + } + } + func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? { (try? codable(withKey: .attributionInfo, currentDate: currentDate)) ?? nil } @@ -346,6 +354,7 @@ class IterableUserDefaults { static let isNotificationsEnabled = UserDefaultsKey(value: Const.UserDefault.isNotificationsEnabled) static let hasStoredNotificationSetting = UserDefaultsKey(value: Const.UserDefault.hasStoredNotificationSetting) + static let networkLoggingEnabled = UserDefaultsKey(value: Const.UserDefault.networkLoggingEnabled) } private struct Envelope: Codable { let payload: Data diff --git a/swift-sdk/Internal/Models.swift b/swift-sdk/Internal/Models.swift index fbfb1e179..7c54ce8cd 100644 --- a/swift-sdk/Internal/Models.swift +++ b/swift-sdk/Internal/Models.swift @@ -7,6 +7,7 @@ import Foundation struct RemoteConfiguration: Codable, Equatable { let offlineMode: Bool + let enableNetworkLogging: Bool? } struct Criteria: Codable { diff --git a/swift-sdk/Internal/Network/NetworkHelper.swift b/swift-sdk/Internal/Network/NetworkHelper.swift index a3f5003fa..2eab57e77 100644 --- a/swift-sdk/Internal/Network/NetworkHelper.swift +++ b/swift-sdk/Internal/Network/NetworkHelper.swift @@ -30,6 +30,7 @@ extension NetworkError: LocalizedError { struct NetworkHelper { static let maxRetryCount = 5 static let retryDelaySeconds = 2 + static var isNetworkLoggingEnabled = false static func getData(fromUrl url: URL, usingSession networkSession: NetworkSessionProtocol) -> Pending { let fulfill = Fulfill() @@ -57,6 +58,11 @@ struct NetworkHelper { usingSession networkSession: NetworkSessionProtocol) -> Pending { let requestId = IterableUtil.generateUUID() + + if isNetworkLoggingEnabled { + logRequest(request, requestId: requestId) + } + #if NETWORK_DEBUG print() print("====================================================>") @@ -94,6 +100,10 @@ struct NetworkHelper { } func handleSuccess(requestId: String, value: T) { + if isNetworkLoggingEnabled { + logSuccess(requestId: requestId, value: value) + } + #if NETWORK_DEBUG print("request with id: \(requestId) successfully sent, response:") print(value) @@ -105,6 +115,10 @@ struct NetworkHelper { if shouldRetry(error: error, retriesLeft: retriesLeft) { retryRequest(requestId: requestId, request: request, error: error, retriesLeft: retriesLeft) } else { + if isNetworkLoggingEnabled { + logFailure(requestId: requestId, error: error) + } + #if NETWORK_DEBUG print("request with id: \(requestId) errored") print(error) @@ -119,6 +133,10 @@ struct NetworkHelper { } func retryRequest(requestId: String, request: URLRequest, error: NetworkError, retriesLeft: Int) { + if isNetworkLoggingEnabled { + logRetry(requestId: requestId, url: request.url?.absoluteString ?? "", attempt: maxRetryCount - retriesLeft + 1, error: error) + } + #if NETWORK_DEBUG print("retry attempt: \(maxRetryCount-retriesLeft+1) for url: \(request.url?.absoluteString ?? "")") print(error) @@ -203,4 +221,38 @@ struct NetworkHelper { return .success(data) } + + private static func logRequest(_ request: URLRequest, requestId: String) { + var message = "\n====================================================>\n" + message += "sending request: \(request)\n" + message += "requestId: \(requestId)\n" + if let headers = request.allHTTPHeaderFields { + message += "headers:\n\(headers)\n" + } + if let body = request.httpBody { + if let dict = try? JSONSerialization.jsonObject(with: body, options: []) { + message += "request body:\n\(dict)\n" + } + } + message += "====================================================>\n" + ITBInfo(message) + } + + private static func logSuccess(requestId: String, value: T) { + var message = "request with id: \(requestId) successfully sent, response:\n" + message += "\(value)" + ITBInfo(message) + } + + private static func logFailure(requestId: String, error: NetworkError) { + var message = "request with id: \(requestId) errored\n" + message += "\(error)" + ITBError(message) + } + + private static func logRetry(requestId: String, url: String, attempt: Int, error: NetworkError) { + var message = "retry attempt: \(attempt) for url: \(url)\n" + message += "\(error)" + ITBInfo(message) + } } diff --git a/swift-sdk/Internal/Utilities/LocalStorage.swift b/swift-sdk/Internal/Utilities/LocalStorage.swift index c4777b074..49910df60 100644 --- a/swift-sdk/Internal/Utilities/LocalStorage.swift +++ b/swift-sdk/Internal/Utilities/LocalStorage.swift @@ -140,6 +140,14 @@ struct LocalStorage: LocalStorageProtocol { } } + var networkLoggingEnabled: Bool { + get { + iterableUserDefaults.networkLoggingEnabled + } set { + iterableUserDefaults.networkLoggingEnabled = newValue + } + } + func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? { iterableUserDefaults.getAttributionInfo(currentDate: currentDate) } diff --git a/swift-sdk/Internal/Utilities/LocalStorageProtocol.swift b/swift-sdk/Internal/Utilities/LocalStorageProtocol.swift index 197f2e90d..6fda70a4b 100644 --- a/swift-sdk/Internal/Utilities/LocalStorageProtocol.swift +++ b/swift-sdk/Internal/Utilities/LocalStorageProtocol.swift @@ -37,6 +37,8 @@ protocol LocalStorageProtocol { var hasStoredNotificationSetting: Bool { get set } + var networkLoggingEnabled: Bool { get set } + func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? func save(attributionInfo: IterableAttributionInfo?, withExpiration expiration: Date?) diff --git a/swift-sdk/Internal/api-client/ApiClient.swift b/swift-sdk/Internal/api-client/ApiClient.swift index c7dd3e44c..f513ad5af 100644 --- a/swift-sdk/Internal/api-client/ApiClient.swift +++ b/swift-sdk/Internal/api-client/ApiClient.swift @@ -84,7 +84,10 @@ class ApiClient { func send(iterableRequest: IterableRequest) -> Pending { guard let urlRequest = convertToURLRequest(iterableRequest: iterableRequest) else { - return SendRequestError.createErroredFuture() + let path = iterableRequest.getPath() + let errorMessage = "Failed to create URL request for endpoint: '\(endpoint)', path: '\(path)'" + ITBError(errorMessage) + return SendRequestError.createErroredFuture(reason: errorMessage) } return RequestSender.sendRequest(urlRequest, usingSession: networkSession) @@ -92,7 +95,10 @@ class ApiClient { func sendWithoutCreatedAt(iterableRequest: IterableRequest) -> Pending { guard let urlRequest = convertToURLRequestWithoutCreatedAt(iterableRequest: iterableRequest) else { - return SendRequestError.createErroredFuture() + let path = iterableRequest.getPath() + let errorMessage = "Failed to create URL request for endpoint: '\(endpoint)', path: '\(path)'" + ITBError(errorMessage) + return SendRequestError.createErroredFuture(reason: errorMessage) } return RequestSender.sendRequest(urlRequest, usingSession: networkSession) @@ -100,7 +106,10 @@ class ApiClient { func send(iterableRequest: IterableRequest) -> Pending where T: Decodable { guard let urlRequest = convertToURLRequest(iterableRequest: iterableRequest) else { - return SendRequestError.createErroredFuture() + let path = iterableRequest.getPath() + let errorMessage = "Failed to create URL request for endpoint: '\(endpoint)', path: '\(path)'" + ITBError(errorMessage) + return SendRequestError.createErroredFuture(reason: errorMessage) } return RequestSender.sendRequest(urlRequest, usingSession: networkSession) diff --git a/tests/common/MockLocalStorage.swift b/tests/common/MockLocalStorage.swift index 59dac2c3c..39f51c95a 100644 --- a/tests/common/MockLocalStorage.swift +++ b/tests/common/MockLocalStorage.swift @@ -40,6 +40,8 @@ class MockLocalStorage: LocalStorageProtocol { var hasStoredNotificationSetting: Bool = false + var networkLoggingEnabled: Bool = false + func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? { guard !MockLocalStorage.isExpired(expiration: attributionInfoExpiration, currentDate: currentDate) else { return nil diff --git a/tests/offline-events-tests/RequestHandlerTests.swift b/tests/offline-events-tests/RequestHandlerTests.swift index 79749b3d1..6ecbaf619 100644 --- a/tests/offline-events-tests/RequestHandlerTests.swift +++ b/tests/offline-events-tests/RequestHandlerTests.swift @@ -707,7 +707,7 @@ class RequestHandlerTests: XCTestCase { func testGetRemoteConfiguration() throws { let expectation1 = expectation(description: #function) - let expectedRemoteConfiguration = RemoteConfiguration(offlineMode: true) + let expectedRemoteConfiguration = RemoteConfiguration(offlineMode: true, enableNetworkLogging: nil) let data = try JSONEncoder().encode(expectedRemoteConfiguration) let notificationCenter = MockNotificationCenter() let networkSession = MockNetworkSession(statusCode: 200, data: data) @@ -732,6 +732,175 @@ class RequestHandlerTests: XCTestCase { wait(for: [expectation1], timeout: testExpectationTimeout) } + func testGetRemoteConfigurationWithNetworkLogging() throws { + let expectation1 = expectation(description: #function) + let expectedRemoteConfiguration = RemoteConfiguration(offlineMode: false, enableNetworkLogging: true) + let data = try JSONEncoder().encode(expectedRemoteConfiguration) + let notificationCenter = MockNotificationCenter() + let networkSession = MockNetworkSession(statusCode: 200, data: data) + + let requestHandler = createRequestHandler(networkSession: networkSession, + notificationCenter: notificationCenter, + selectOffline: false) + requestHandler.getRemoteConfiguration().onSuccess { remoteConfiguration in + XCTAssertEqual(remoteConfiguration, expectedRemoteConfiguration) + XCTAssertEqual(remoteConfiguration.enableNetworkLogging, true) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: testExpectationTimeout) + } + + func testFeatureFlagTurnOnNetworkLogging() throws { + let expectation1 = expectation(description: "getRemoteConfiguration is called") + let remoteConfigurationData = """ + { + "offlineMode": false, + "enableNetworkLogging": true + } + """.data(using: .utf8)! + var mapper = [String: Data?]() + mapper["getRemoteConfiguration"] = remoteConfigurationData + let networkSession = MockNetworkSession(statusCode: 200, urlPatternDataMapping: mapper) + networkSession.requestCallback = { request in + if request.url!.absoluteString.contains(Const.Path.getRemoteConfiguration) { + expectation1.fulfill() + } + } + let localStorage = MockLocalStorage() + localStorage.email = "user@example.com" + _ = InternalIterableAPI.initializeForTesting(networkSession: networkSession, localStorage: localStorage) + wait(for: [expectation1], timeout: testExpectationTimeout) + + // Check if NetworkHelper flag is set + XCTAssertTrue(NetworkHelper.isNetworkLoggingEnabled) + + // Reset it back + NetworkHelper.isNetworkLoggingEnabled = false + } + + func testNetworkLoggingActualLogs() throws { + // 1. Setup Mock Log Delegate + class MockLogDelegate: NSObject, IterableLogDelegate { + var loggedMessages: [String] = [] + func log(level: LogLevel, message: String) { + loggedMessages.append(message) + } + } + let mockLogDelegate = MockLogDelegate() + IterableLogUtil.sharedInstance = IterableLogUtil(dateProvider: SystemDateProvider(), logDelegate: mockLogDelegate) + + // 2. Enable Network Logging + NetworkHelper.isNetworkLoggingEnabled = true + + // 3. Perform Request (Success) + let expectation1 = expectation(description: "Request success") + // Create a dummy JSON object to be returned as Data + let successData = try! JSONSerialization.data(withJSONObject: ["msg": "success"], options: []) + let networkSession = MockNetworkSession(statusCode: 200, data: successData) + + // Need to set up request callback to fulfill expectation when request completes + networkSession.requestCallback = { _ in + expectation1.fulfill() + } + + let requestHandler = createRequestHandler(networkSession: networkSession, notificationCenter: MockNotificationCenter(), selectOffline: false) + + requestHandler.track(event: "testEvent", dataFields: nil, onSuccess: nil, onFailure: nil) + + wait(for: [expectation1], timeout: testExpectationTimeout) + + // Wait a little for async logging dispatch + let loggingExpectation = expectation(description: "Logging wait") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + loggingExpectation.fulfill() + } + wait(for: [loggingExpectation], timeout: 1.0) + + // 4. Verify Logs for Success + // "sending request" and "successfully sent" + let successLogs = mockLogDelegate.loggedMessages.filter { $0.contains("sending request") || $0.contains("successfully sent") } + XCTAssertTrue(successLogs.count >= 2, "Should have logged request sending and success. Found: \(mockLogDelegate.loggedMessages)") + + // Clear logs for next test + mockLogDelegate.loggedMessages = [] + + // 5. Perform Request (Failure/Retry) + let expectation2 = expectation(description: "Request failure") + // 500 status code triggers retry logic in NetworkHelper + let failureSession = MockNetworkSession(statusCode: 500, data: nil) + + // We expect it to fail eventually after retries + // The MockNetworkSession doesn't automatically retry, the NetworkHelper logic does. + // We need to wait for the final failure callback. + + let requestHandlerFailure = createRequestHandler(networkSession: failureSession, notificationCenter: MockNotificationCenter(), selectOffline: false) + + requestHandlerFailure.track(event: "testEventFail", dataFields: nil, onSuccess: nil, onFailure: { _, _ in + expectation2.fulfill() + }) + + // Wait longer for retries + wait(for: [expectation2], timeout: testExpectationTimeout * 2) + + // 6. Verify Logs for Failure + // Should see "retry attempt" and eventually "errored" + let retryLogs = mockLogDelegate.loggedMessages.filter { $0.contains("retry attempt") } + let errorLogs = mockLogDelegate.loggedMessages.filter { $0.contains("errored") } + + XCTAssertTrue(retryLogs.count > 0, "Should have logged retry attempts") + XCTAssertTrue(errorLogs.count > 0, "Should have logged final error") + + // 7. Cleanup + NetworkHelper.isNetworkLoggingEnabled = false + } + + func testNetworkLoggingGetRequest() throws { + // 1. Setup Mock Log Delegate + class MockLogDelegate: NSObject, IterableLogDelegate { + var loggedMessages: [String] = [] + func log(level: LogLevel, message: String) { + loggedMessages.append(message) + } + } + let mockLogDelegate = MockLogDelegate() + IterableLogUtil.sharedInstance = IterableLogUtil(dateProvider: SystemDateProvider(), logDelegate: mockLogDelegate) + + // 2. Enable Network Logging + NetworkHelper.isNetworkLoggingEnabled = true + + // 3. Perform GET Request + let expectation1 = expectation(description: "GET Request success") + let networkSession = MockNetworkSession(statusCode: 200, data: "{}".data(using: .utf8)!) + + networkSession.requestCallback = { request in + expectation1.fulfill() + } + + let requestHandler = createRequestHandler(networkSession: networkSession, notificationCenter: MockNotificationCenter(), selectOffline: false) + + let _ = requestHandler.getRemoteConfiguration() + + wait(for: [expectation1], timeout: testExpectationTimeout) + + // Wait a little for async logging dispatch + let loggingExpectation = expectation(description: "Logging wait") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + loggingExpectation.fulfill() + } + wait(for: [loggingExpectation], timeout: 1.0) + + // 4. Verify Logs + // Should see "sending request" but NOT "request body" + let requestLogs = mockLogDelegate.loggedMessages.filter { $0.contains("sending request") } + let bodyLogs = mockLogDelegate.loggedMessages.filter { $0.contains("request body") } + + XCTAssertTrue(requestLogs.count > 0, "Should have logged request sending") + XCTAssertTrue(bodyLogs.count == 0, "Should NOT have logged body for GET request") + + // Cleanup + NetworkHelper.isNetworkLoggingEnabled = false + } + func testCreatedAtSentAtForOffline() throws { let expectation1 = expectation(description: #function) let date = Date().addingTimeInterval(-5000) diff --git a/tests/offline-events-tests/TaskProcessorTests.swift b/tests/offline-events-tests/TaskProcessorTests.swift index 7b696e1f9..b76c0bc89 100644 --- a/tests/offline-events-tests/TaskProcessorTests.swift +++ b/tests/offline-events-tests/TaskProcessorTests.swift @@ -116,6 +116,45 @@ class TaskProcessorTests: XCTestCase { wait(for: [expectation1], timeout: 15.0) } + func testNetworkUnavailableWithNSErrors() throws { + let errorCodes = [ + NSURLErrorNotConnectedToInternet, // -1009 + NSURLErrorNetworkConnectionLost, // -1005 + NSURLErrorTimedOut, // -1001 + NSURLErrorCannotConnectToHost, // -1004 + NSURLErrorDNSLookupFailed, // -1006 + NSURLErrorDataNotAllowed, // -1020 + NSURLErrorInternationalRoamingOff // -1018 + ] + + for errorCode in errorCodes { + let expectation = self.expectation(description: "network error \(errorCode)") + let task = try createSampleTask()! + + let nsError = NSError(domain: NSURLErrorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: "Network error"]) + let networkSession = MockNetworkSession(statusCode: 0, data: nil, error: nsError) + + let processor = IterableAPICallTaskProcessor(networkSession: networkSession) + try processor.process(task: task) + .onSuccess { taskResult in + switch taskResult { + case .failureWithRetry(retryAfter: _, detail: _): + expectation.fulfill() + default: + XCTFail("expected failureWithRetry for error code \(errorCode)") + } + } + .onError { _ in + XCTFail("Not expecting onError for task processing logic") + } + + try persistenceProvider.mainQueueContext().delete(task: task) + try persistenceProvider.mainQueueContext().save() + + wait(for: [expectation], timeout: 2.0) + } + } + func testUnrecoverableError() throws { let expectation1 = expectation(description: #function) let task = try createSampleTask()! diff --git a/tests/unit-tests/IterableAPITests.swift b/tests/unit-tests/IterableAPITests.swift index 048c4eb83..e9fde552f 100644 --- a/tests/unit-tests/IterableAPITests.swift +++ b/tests/unit-tests/IterableAPITests.swift @@ -1417,4 +1417,24 @@ class IterableAPITests: XCTestCase { XCTAssertEqual(dateFromMilliseconds, testDate) } + func testInitializeWithNetworkLoggingEnabled() { + let mockLocalStorage = MockLocalStorage() + mockLocalStorage.networkLoggingEnabled = true + + _ = InternalIterableAPI.initializeForTesting(apiKey: IterableAPITests.apiKey, localStorage: mockLocalStorage) + + XCTAssertTrue(NetworkHelper.isNetworkLoggingEnabled) + + // Cleanup + NetworkHelper.isNetworkLoggingEnabled = false + } + + func testInitializeWithNetworkLoggingDisabled() { + let mockLocalStorage = MockLocalStorage() + mockLocalStorage.networkLoggingEnabled = false + + _ = InternalIterableAPI.initializeForTesting(apiKey: IterableAPITests.apiKey, localStorage: mockLocalStorage) + + XCTAssertFalse(NetworkHelper.isNetworkLoggingEnabled) + } } diff --git a/tests/unit-tests/LocalStorageTests.swift b/tests/unit-tests/LocalStorageTests.swift index 0e935d39f..0f7b06633 100644 --- a/tests/unit-tests/LocalStorageTests.swift +++ b/tests/unit-tests/LocalStorageTests.swift @@ -149,6 +149,19 @@ class LocalStorageTests: XCTestCase { testLocalStorage(saver: saver, retriever: retriever, value: false) } + func testNetworkLoggingEnabled() { + let saver = { (storage: LocalStorageProtocol, value: Bool) -> Void in + var localStorage = storage + localStorage.networkLoggingEnabled = value + } + let retriever = { (storage: LocalStorageProtocol) -> Bool? in + storage.networkLoggingEnabled + } + + testLocalStorage(saver: saver, retriever: retriever, value: true) + testLocalStorage(saver: saver, retriever: retriever, value: false) + } + private func testLocalStorage(saver: (LocalStorageProtocol, T) -> Void, retriever: (LocalStorageProtocol) -> T?, value: T) where T: Equatable { let localStorage = LocalStorage(userDefaults: LocalStorageTests.getTestUserDefaults())