From d68a2328f567b7f22d8148c0b3d714260ec35b2d Mon Sep 17 00:00:00 2001 From: Vailence Date: Wed, 13 May 2026 16:57:43 +0500 Subject: [PATCH 1/3] MOBILE-164-Sync-send-error-raw --- .../Views/WebView/TransparentView.swift | 16 ++++++-- Mindbox/Network/Abstract/NetworkFetcher.swift | 9 ++++ Mindbox/Network/MBNetworkFetcher.swift | 41 +++++++++++++++++++ .../Event/EventRepository.swift | 8 +++- .../Event/MBEventRepository.swift | 35 ++++++++++++++++ .../Mocks/EventRepositoryMock.swift | 6 ++- .../Mock/MockFailureNetworkFetcher.swift | 14 ++++++- MindboxTests/Mock/MockNetworkFetcher.swift | 10 ++++- 8 files changed, 131 insertions(+), 8 deletions(-) diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift index 1b1e63254..2f1f5d86a 100644 --- a/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift @@ -386,16 +386,24 @@ extension TransparentView { Logger.common(message: "[WebView] syncOperation '\(params.name)' sending", level: .info, category: .webViewInAppMessages) - eventRepository.send(type: OperationResponse.self, event: event) { [weak self] result in + // HTTP 2xx → forward the raw body to JS as a Response so the JS Tracker + // can dispatch onSuccess / onValidationError by the body's `status`. + // 4xx, 5xx and network failures stay on the MindboxError → Error path. + eventRepository.sendRaw(event: event) { [weak self] result in DispatchQueue.main.async { switch result { - case .success(let response): + case .success(let data): + guard let bodyString = String(data: data, encoding: .utf8) else { + Logger.common(message: "[WebView] syncOperation '\(params.name)' response body is not valid UTF-8", + level: .error, category: .webViewInAppMessages) + self?.sendBridgeError("Response body is not valid UTF-8", action: message.action, id: message.id) + return + } Logger.common(message: "[WebView] syncOperation '\(params.name)' success", level: .info, category: .webViewInAppMessages) - let responseJSON = response.createJSON() let successResponse = BridgeMessage( type: .response, action: message.action, - payload: .string(responseJSON), + payload: .string(bodyString), id: message.id ) self?.facade?.sendToJS(successResponse) diff --git a/Mindbox/Network/Abstract/NetworkFetcher.swift b/Mindbox/Network/Abstract/NetworkFetcher.swift index 899ddc368..14d701252 100644 --- a/Mindbox/Network/Abstract/NetworkFetcher.swift +++ b/Mindbox/Network/Abstract/NetworkFetcher.swift @@ -22,6 +22,15 @@ protocol NetworkFetcher { completion: @escaping ((Result) -> Void) ) + /// Returns the raw HTTP 2xx response body without parsing `BaseResponse`, + /// so the caller can decide how to interpret it. 4xx, 5xx and network + /// failures still surface as `MindboxError` through the shared response + /// pipeline. + func requestRaw( + route: Route, + completion: @escaping ((Result) -> Void) + ) + /// Cancels all ongoing network tasks. func cancelAllTasks() } diff --git a/Mindbox/Network/MBNetworkFetcher.swift b/Mindbox/Network/MBNetworkFetcher.swift index 123bee386..1620cc563 100644 --- a/Mindbox/Network/MBNetworkFetcher.swift +++ b/Mindbox/Network/MBNetworkFetcher.swift @@ -133,6 +133,47 @@ class MBNetworkFetcher: NetworkFetcher { } } + func requestRaw( + route: Route, + completion: @escaping (Result) -> Void + ) { + guard let configuration = persistenceStorage.configuration else { + let error = MindboxError(.init( + errorKey: .invalidConfiguration, + reason: "Configuration is not set" + )) + Logger.error(error.asLoggerError()) + completion(.failure(error)) + return + } + + let builder = URLRequestBuilder( + domain: configuration.domain, + operationsDomain: resolvedOperationsDomain(configuration: configuration) + ) + do { + let urlRequest = try builder.asURLRequest(route: route) + Logger.network(request: urlRequest, httpAdditionalHeaders: session.configuration.httpAdditionalHeaders) + let startTime = CFAbsoluteTimeGetCurrent() + session.dataTask(with: urlRequest) { [weak self] data, response, error in + let networkTimeMs = Int((CFAbsoluteTimeGetCurrent() - startTime) * 1000) + self?.handleResponse(data, response, error, needBaseResponse: false, networkTimeMs: networkTimeMs) { result in + switch result { + case let .success(data): + completion(.success(data)) + case let .failure(error): + Logger.error(error.asLoggerError()) + completion(.failure(error)) + } + } + }.resume() + } catch let error { + let errorModel = MindboxError.unknown(error) + Logger.error(errorModel.asLoggerError()) + completion(.failure(errorModel)) + } + } + private func handleResponse( _ data: Data?, _ response: URLResponse?, diff --git a/Mindbox/NetworkRepository/Event/EventRepository.swift b/Mindbox/NetworkRepository/Event/EventRepository.swift index 5d68c7542..bddef28e7 100644 --- a/Mindbox/NetworkRepository/Event/EventRepository.swift +++ b/Mindbox/NetworkRepository/Event/EventRepository.swift @@ -12,7 +12,13 @@ import MindboxLogger protocol EventRepository { func send(event: Event, completion: @escaping (Result) -> Void) func send(type: T.Type, event: Event, completion: @escaping (Result) -> Void) where T: Decodable - + + /// Sends an event and returns the raw HTTP 2xx response body. Skips + /// `BaseResponse` parsing so the caller can dispatch on the body itself + /// (e.g. forward the bytes verbatim to the WebView JS bridge so the JS + /// Tracker can route by the body's `status` field). + func sendRaw(event: Event, completion: @escaping (Result) -> Void) + /// Cancels all ongoing network requests associated with this repository. func cancelAllRequests() } diff --git a/Mindbox/NetworkRepository/Event/MBEventRepository.swift b/Mindbox/NetworkRepository/Event/MBEventRepository.swift index 3beaee1a2..5efe99689 100644 --- a/Mindbox/NetworkRepository/Event/MBEventRepository.swift +++ b/Mindbox/NetworkRepository/Event/MBEventRepository.swift @@ -80,6 +80,41 @@ class MBEventRepository: EventRepository { }) } + func sendRaw(event: Event, completion: @escaping (Result) -> Void) { + guard let configuration = persistenceStorage.configuration else { + let error = MindboxError(.init( + errorKey: .invalidConfiguration, + reason: "Configuration is not set" + )) + completion(.failure(error)) + return + } + guard let deviceUUID = persistenceStorage.deviceUUID else { + let error = MindboxError(.init( + errorKey: .invalidConfiguration, + reason: "DeviceUUID is not set" + )) + completion(.failure(error)) + return + } + let wrapper = EventWrapper( + event: event, + endpoint: configuration.endpoint, + deviceUUID: deviceUUID + ) + let route = makeRoute(wrapper: wrapper) + fetcher.requestRaw(route: route) { result in + DispatchQueue.main.async { + switch result { + case let .failure(error): + completion(.failure(error)) + case let .success(data): + completion(.success(data)) + } + } + } + } + private func makeRoute(wrapper: EventWrapper) -> Route { switch wrapper.event.type { case .installed, diff --git a/MindboxTests/MindboxLogger/Mocks/EventRepositoryMock.swift b/MindboxTests/MindboxLogger/Mocks/EventRepositoryMock.swift index 08f8ee33e..a79f4f768 100644 --- a/MindboxTests/MindboxLogger/Mocks/EventRepositoryMock.swift +++ b/MindboxTests/MindboxLogger/Mocks/EventRepositoryMock.swift @@ -27,6 +27,10 @@ class EventRepositoryMock: EventRepository { func send(type: T.Type, event: Event, completion: @escaping (Result) -> Void) where T: Decodable { return } - + + func sendRaw(event: Event, completion: @escaping (Result) -> Void) { + return + } + func cancelAllRequests() {} } diff --git a/MindboxTests/Mock/MockFailureNetworkFetcher.swift b/MindboxTests/Mock/MockFailureNetworkFetcher.swift index 4de1b3717..66530b469 100644 --- a/MindboxTests/Mock/MockFailureNetworkFetcher.swift +++ b/MindboxTests/Mock/MockFailureNetworkFetcher.swift @@ -50,11 +50,23 @@ class MockFailureNetworkFetcher: NetworkFetcher { } } + func requestRaw(route: any Route, completion: @escaping (Result) -> Void) { + if !hasFailed { + hasFailed = true + completion(.failure(.internalError(.init( + errorKey: .parsing, + rawError: nil + )))) + } else { + completion(.success(MockFailureNetworkFetcher.successData)) + } + } + private static let successData: Data = { let bundle = Bundle(for: MockNetworkFetcher.self) let url = bundle.url(forResource: "SuccessResponse", withExtension: "json")! return try! Data(contentsOf: url) }() - + func cancelAllTasks() {} } diff --git a/MindboxTests/Mock/MockNetworkFetcher.swift b/MindboxTests/Mock/MockNetworkFetcher.swift index 95b7b1968..099b7a029 100644 --- a/MindboxTests/Mock/MockNetworkFetcher.swift +++ b/MindboxTests/Mock/MockNetworkFetcher.swift @@ -44,6 +44,14 @@ class MockNetworkFetcher: NetworkFetcher { } } } - + + func requestRaw(route: Route, completion: @escaping ((Result) -> Void)) { + if let error = error { + completion(.failure(error)) + } else { + completion(.success(data ?? Data())) + } + } + func cancelAllTasks() {} } From 20401c657928628b8bde73e8b984cb917eb99eaf Mon Sep 17 00:00:00 2001 From: Vailence Date: Wed, 13 May 2026 17:50:12 +0500 Subject: [PATCH 2/3] MOBILE-164: Add tests for raw sync-operation pass-through Cover MBNetworkFetcher.requestRaw, MBEventRepository.sendRaw, and the WebView sync-operation response shaping. Extract a pure helper TransparentView.makeSyncOperationResponse so the JS-bridge contract is unit-testable without spinning up WKWebView. --- Mindbox.xcodeproj/project.pbxproj | 126 +++++++---- .../Views/WebView/TransparentView.swift | 64 ++++-- ...parentViewSyncOperationResponseTests.swift | 177 +++++++++++++++ .../MBEventRepositorySendRawTests.swift | 211 ++++++++++++++++++ ...BNetworkFetcherResponseHandlingTests.swift | 160 +++++++++++++ 5 files changed, 671 insertions(+), 67 deletions(-) create mode 100644 MindboxTests/InApp/Tests/TransparentViewSyncOperationResponseTests.swift create mode 100644 MindboxTests/Network/MBEventRepositorySendRawTests.swift diff --git a/Mindbox.xcodeproj/project.pbxproj b/Mindbox.xcodeproj/project.pbxproj index 68b62b075..cf85e01b1 100644 --- a/Mindbox.xcodeproj/project.pbxproj +++ b/Mindbox.xcodeproj/project.pbxproj @@ -11,13 +11,11 @@ 0E7A224A082FA2DA35706CC7 /* MotionServiceResolvePositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8192B8B7043EF74D05B36B /* MotionServiceResolvePositionTests.swift */; }; 0E7A224A082FA2DA35706CC8 /* MotionServiceShakeToEditTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8192B8B7043EF74D05B36C /* MotionServiceShakeToEditTests.swift */; }; 1E3BD63AB3F1521C253CB818 /* MBNetworkFetcherResponseHandlingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97FEDDEB5F71A67F1C4C675F /* MBNetworkFetcherResponseHandlingTests.swift */; }; - F3BA5E000130A000C0000005 /* OperationsURLRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BA5E000130A000C0000006 /* OperationsURLRoutingTests.swift */; }; 302E35788CBDA959283569F4 /* MotionServiceBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DB93A7997961CA7C2BE917 /* MotionServiceBehaviorTests.swift */; }; 313B233A25ADEA0F00A1CB72 /* Mindbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 313B233025ADEA0F00A1CB72 /* Mindbox.framework */; }; 313B233F25ADEA0F00A1CB72 /* MindboxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 313B233E25ADEA0F00A1CB72 /* MindboxTests.swift */; }; 313B234125ADEA0F00A1CB72 /* Mindbox.h in Headers */ = {isa = PBXBuildFile; fileRef = 313B233325ADEA0F00A1CB72 /* Mindbox.h */; settings = {ATTRIBUTES = (Public, ); }; }; 314B38FD25AEE8B200E947B9 /* MBConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314B38FC25AEE8B200E947B9 /* MBConfiguration.swift */; }; - F3BA5E000130A000C0000003 /* BaseAddressesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BA5E000130A000C0000004 /* BaseAddressesModel.swift */; }; 314B390025AEE96F00E947B9 /* CoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314B38FF25AEE96F00E947B9 /* CoreController.swift */; }; 317054CB25AF189800AE624C /* PersistenceStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317054CA25AF189800AE624C /* PersistenceStorage.swift */; }; 317AF8FC25B844DB006348FA /* UtilitiesFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317AF8FB25B844DB006348FA /* UtilitiesFetcher.swift */; }; @@ -28,8 +26,6 @@ 31A20D4E25B6EFB600AAA0A3 /* MindboxDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A20D4D25B6EFB600AAA0A3 /* MindboxDelegate.swift */; }; 31EB907325C402F900368FFB /* TestConfig3.plist in Resources */ = {isa = PBXBuildFile; fileRef = 31EB907125C402F900368FFB /* TestConfig3.plist */; }; 31EB907425C402F900368FFB /* TestConfig2.plist in Resources */ = {isa = PBXBuildFile; fileRef = 31EB907225C402F900368FFB /* TestConfig2.plist */; }; - F3CD20262F600A800065392A /* MBConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CD20272F600A800065392A /* MBConfigurationTests.swift */; }; - F3CD202B2F600A800065392A /* HostNormalizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CD202C2F600A800065392A /* HostNormalizerTests.swift */; }; 31ED2DF225C4456600301FAD /* TestConfig_Invalid_2.plist in Resources */ = {isa = PBXBuildFile; fileRef = 31ED2DEF25C4456600301FAD /* TestConfig_Invalid_2.plist */; }; 31ED2DF325C4456600301FAD /* TestConfig_Invalid_1.plist in Resources */ = {isa = PBXBuildFile; fileRef = 31ED2DF025C4456600301FAD /* TestConfig_Invalid_1.plist */; }; 31ED2DF425C4456600301FAD /* TestConfig_Invalid_3.plist in Resources */ = {isa = PBXBuildFile; fileRef = 31ED2DF125C4456600301FAD /* TestConfig_Invalid_3.plist */; }; @@ -58,7 +54,6 @@ 3333C1B22681D42000B60D84 /* Payload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3333C1B12681D42000B60D84 /* Payload.swift */; }; 3333C1B42681D43C00B60D84 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3333C1B32681D43C00B60D84 /* ImageFormat.swift */; }; 3333C1DE2681E9F300B60D84 /* URLRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3333C1DD2681E9F300B60D84 /* URLRequestBuilder.swift */; }; - F3CD20292F600A800065392A /* HostNormalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CD202A2F600A800065392A /* HostNormalizer.swift */; }; 3333C1E12681EA4D00B60D84 /* NotificationsPayloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3333C1E02681EA4C00B60D84 /* NotificationsPayloads.swift */; }; 3333D7BE265E56F2004279B0 /* OperationResponseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3333D7BD265E56F2004279B0 /* OperationResponseType.swift */; }; 3337E6A3265FAB39006949EB /* BaseResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3337E6A2265FAB39006949EB /* BaseResponse.swift */; }; @@ -313,7 +308,6 @@ 84B625E425C988FA00AB6228 /* URLValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B625E325C988FA00AB6228 /* URLValidator.swift */; }; 84B625E925C989C100AB6228 /* UDIDValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B625E825C989C100AB6228 /* UDIDValidator.swift */; }; 84B625F025C98B1200AB6228 /* ValidatorsTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B625EF25C98B1200AB6228 /* ValidatorsTestCase.swift */; }; - F3CD202D2F600A800065392A /* URLValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CD202E2F600A800065392A /* URLValidatorTests.swift */; }; 84BAEF8225D54919002E8A26 /* BodyDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BAEF8125D54919002E8A26 /* BodyDecoder.swift */; }; 84C65E5E25D4FBA3008996FA /* MobileApplicationInstalled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C65E5D25D4FBA3008996FA /* MobileApplicationInstalled.swift */; }; 84C65E6425D4FBBB008996FA /* MobileApplicationInfoUpdated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C65E6325D4FBBB008996FA /* MobileApplicationInfoUpdated.swift */; }; @@ -440,6 +434,7 @@ A1D017F22976CC9400CD9F99 /* SegmentTargeting.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D017F12976CC9400CD9F99 /* SegmentTargeting.swift */; }; A1D017F52976FC2B00CD9F99 /* InternalTargetingChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D017F42976FC2B00CD9F99 /* InternalTargetingChecker.swift */; }; A1D23AF029DE082E00A75179 /* InAppProductSegmentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D23AEF29DE082E00A75179 /* InAppProductSegmentResponse.swift */; }; + AF174B2121221D323FB95EF0 /* MBEventRepositorySendRawTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BECF3D292B29C1894F80948F /* MBEventRepositorySendRawTests.swift */; }; B36D57852696E59400FEDFD6 /* RetailOrderStatisticsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36D57842696E59400FEDFD6 /* RetailOrderStatisticsResponse.swift */; }; B3A6254C2689F83100B6A3B7 /* PersonalOffersResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A6254B2689F83100B6A3B7 /* PersonalOffersResponse.swift */; }; B3A625502689F8B600B6A3B7 /* BenefitResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A6254F2689F8B600B6A3B7 /* BenefitResponse.swift */; }; @@ -467,6 +462,7 @@ D2F7E24A2BADC2AB00B24BB8 /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F7E2492BADC2AB00B24BB8 /* SessionManager.swift */; }; D2F7E24C2BADC4CA00B24BB8 /* MockSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F7E24B2BADC4CA00B24BB8 /* MockSessionManager.swift */; }; DEC482157E5249DBBFAEFC9A /* FeatureTogglesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9778038796A8426ABDED1E97 /* FeatureTogglesModel.swift */; }; + EA395B77BB16CEFE6DC91D1D /* TransparentViewSyncOperationResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7DAFAB687945FA908DB1AC /* TransparentViewSyncOperationResponseTests.swift */; }; F26DFF81C3FF57C3DE68DEDC /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326423031CA9C6BF0E62BEFD /* Date+Extensions.swift */; }; F30005442CFF3F7D004BE915 /* ABTestStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30005432CFF3F7D004BE915 /* ABTestStubs.swift */; }; F306291A2BD27D7500EF6609 /* InappFrequencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30629192BD27D7500EF6609 /* InappFrequencyTests.swift */; }; @@ -482,7 +478,6 @@ F31470962B96681F00E01E5C /* 27-TargetingRequests.json in Resources */ = {isa = PBXBuildFile; fileRef = F31470952B96681F00E01E5C /* 27-TargetingRequests.json */; }; F31470982B9668F100E01E5C /* 31-TargetingRequests.json in Resources */ = {isa = PBXBuildFile; fileRef = F31470972B9668F100E01E5C /* 31-TargetingRequests.json */; }; F315503F2BBB24E20072A071 /* TTLValidationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F315503E2BBB24E20072A071 /* TTLValidationService.swift */; }; - F3BA5E000130A000C0000007 /* OperationsDomainConfigPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BA5E000130A000C0000008 /* OperationsDomainConfigPolicy.swift */; }; F31909992E979D9E00373E2F /* MindboxAppDelegateProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31909982E979D9E00373E2F /* MindboxAppDelegateProxy.swift */; }; F31A94782BC6995500E6C978 /* InappFrequency.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31A94772BC6995500E6C978 /* InappFrequency.swift */; }; F31A947C2BC69E3900E6C978 /* PeriodicFrequency.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31A947B2BC69E3900E6C978 /* PeriodicFrequency.swift */; }; @@ -564,10 +559,6 @@ F34A10462F455C5B0065392A /* SettingsFeatureTogglesError.json in Resources */ = {isa = PBXBuildFile; fileRef = F34A103E2F455C5B0065392A /* SettingsFeatureTogglesError.json */; }; F34A10472F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorTypeError.json in Resources */ = {isa = PBXBuildFile; fileRef = F34A10412F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorTypeError.json */; }; F34A10482F455C5B0065392A /* SettingsFeatureTogglesTypeError.json in Resources */ = {isa = PBXBuildFile; fileRef = F34A10422F455C5B0065392A /* SettingsFeatureTogglesTypeError.json */; }; - F3BA10552F500A800065392A /* SettingsBaseAddressesError.json in Resources */ = {isa = PBXBuildFile; fileRef = F3BA10512F500A800065392A /* SettingsBaseAddressesError.json */; }; - F3BA10562F500A800065392A /* SettingsBaseAddressesTypeError.json in Resources */ = {isa = PBXBuildFile; fileRef = F3BA10522F500A800065392A /* SettingsBaseAddressesTypeError.json */; }; - F3BA10572F500A800065392A /* SettingsBaseAddressesOperationsError.json in Resources */ = {isa = PBXBuildFile; fileRef = F3BA10532F500A800065392A /* SettingsBaseAddressesOperationsError.json */; }; - F3BA10582F500A800065392A /* SettingsBaseAddressesOperationsTypeError.json in Resources */ = {isa = PBXBuildFile; fileRef = F3BA10542F500A800065392A /* SettingsBaseAddressesOperationsTypeError.json */; }; F34A45AE2B7628B700634C8B /* MBPushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34A45AD2B7628B700634C8B /* MBPushNotification.swift */; }; F34A45B02B762A6100634C8B /* MindboxPushValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34A45AF2B762A6100634C8B /* MindboxPushValidator.swift */; }; F351F1C02CE380A40053423E /* InappMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F351F1BF2CE380A40053423E /* InappMapper.swift */; }; @@ -659,11 +650,22 @@ F3B70A012F250A0100AABB01 /* ForegroundStopwatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B70A002F250A0100AABB01 /* ForegroundStopwatch.swift */; }; F3B70A032F250A0100AABB02 /* ForegroundStopwatchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B70A022F250A0100AABB02 /* ForegroundStopwatchTests.swift */; }; F3B70A052F250A0100AABB03 /* TimeToDisplayBackgroundTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B70A042F250A0100AABB03 /* TimeToDisplayBackgroundTests.swift */; }; + F3BA10552F500A800065392A /* SettingsBaseAddressesError.json in Resources */ = {isa = PBXBuildFile; fileRef = F3BA10512F500A800065392A /* SettingsBaseAddressesError.json */; }; + F3BA10562F500A800065392A /* SettingsBaseAddressesTypeError.json in Resources */ = {isa = PBXBuildFile; fileRef = F3BA10522F500A800065392A /* SettingsBaseAddressesTypeError.json */; }; + F3BA10572F500A800065392A /* SettingsBaseAddressesOperationsError.json in Resources */ = {isa = PBXBuildFile; fileRef = F3BA10532F500A800065392A /* SettingsBaseAddressesOperationsError.json */; }; + F3BA10582F500A800065392A /* SettingsBaseAddressesOperationsTypeError.json in Resources */ = {isa = PBXBuildFile; fileRef = F3BA10542F500A800065392A /* SettingsBaseAddressesOperationsTypeError.json */; }; + F3BA5E000130A000C0000003 /* BaseAddressesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BA5E000130A000C0000004 /* BaseAddressesModel.swift */; }; + F3BA5E000130A000C0000005 /* OperationsURLRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BA5E000130A000C0000006 /* OperationsURLRoutingTests.swift */; }; + F3BA5E000130A000C0000007 /* OperationsDomainConfigPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BA5E000130A000C0000008 /* OperationsDomainConfigPolicy.swift */; }; F3BD9F822F273BCD00647BAF /* BridgeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BD9F812F273BCD00647BAF /* BridgeMessage.swift */; }; F3BDA0382F27444500647BAF /* MindboxWebBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BDA0372F27444500647BAF /* MindboxWebBridge.swift */; }; F3C1A0022F5B100100ABC001 /* InappShowFailureManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C1A0012F5B100100ABC001 /* InappShowFailureManager.swift */; }; F3C1A0042F5B100100ABC001 /* InAppShowFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C1A0032F5B100100ABC001 /* InAppShowFailure.swift */; }; F3C1A0062F5B100100ABC001 /* InappShowFailureManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C1A0052F5B100100ABC001 /* InappShowFailureManagerTests.swift */; }; + F3CD20262F600A800065392A /* MBConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CD20272F600A800065392A /* MBConfigurationTests.swift */; }; + F3CD20292F600A800065392A /* HostNormalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CD202A2F600A800065392A /* HostNormalizer.swift */; }; + F3CD202B2F600A800065392A /* HostNormalizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CD202C2F600A800065392A /* HostNormalizerTests.swift */; }; + F3CD202D2F600A800065392A /* URLValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CD202E2F600A800065392A /* URLValidatorTests.swift */; }; F3D818B02A3885AD0002957C /* ABTestDeviceMixer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3D818AF2A3885AD0002957C /* ABTestDeviceMixer.swift */; }; F3D818B32A3885F50002957C /* ABTestDeviceMixerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3D818B22A3885F50002957C /* ABTestDeviceMixerTests.swift */; }; F3D925AB2A120C0F00135C87 /* InAppImageDownloaderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3D925AA2A120C0F00135C87 /* InAppImageDownloaderMock.swift */; }; @@ -749,7 +751,6 @@ 313B233E25ADEA0F00A1CB72 /* MindboxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MindboxTests.swift; sourceTree = ""; }; 313B234025ADEA0F00A1CB72 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 314B38FC25AEE8B200E947B9 /* MBConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBConfiguration.swift; sourceTree = ""; }; - F3BA5E000130A000C0000004 /* BaseAddressesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseAddressesModel.swift; sourceTree = ""; }; 314B38FF25AEE96F00E947B9 /* CoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreController.swift; sourceTree = ""; }; 317054CA25AF189800AE624C /* PersistenceStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceStorage.swift; sourceTree = ""; }; 317AF8FB25B844DB006348FA /* UtilitiesFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilitiesFetcher.swift; sourceTree = ""; }; @@ -760,8 +761,6 @@ 31A20D4D25B6EFB600AAA0A3 /* MindboxDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MindboxDelegate.swift; sourceTree = ""; }; 31EB907125C402F900368FFB /* TestConfig3.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestConfig3.plist; sourceTree = ""; }; 31EB907225C402F900368FFB /* TestConfig2.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestConfig2.plist; sourceTree = ""; }; - F3CD20272F600A800065392A /* MBConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBConfigurationTests.swift; sourceTree = ""; }; - F3CD202C2F600A800065392A /* HostNormalizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostNormalizerTests.swift; sourceTree = ""; }; 31ED2DEF25C4456600301FAD /* TestConfig_Invalid_2.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestConfig_Invalid_2.plist; sourceTree = ""; }; 31ED2DF025C4456600301FAD /* TestConfig_Invalid_1.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestConfig_Invalid_1.plist; sourceTree = ""; }; 31ED2DF125C4456600301FAD /* TestConfig_Invalid_3.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestConfig_Invalid_3.plist; sourceTree = ""; }; @@ -793,7 +792,6 @@ 3333C1B12681D42000B60D84 /* Payload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Payload.swift; sourceTree = ""; }; 3333C1B32681D43C00B60D84 /* ImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = ""; }; 3333C1DD2681E9F300B60D84 /* URLRequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestBuilder.swift; sourceTree = ""; }; - F3CD202A2F600A800065392A /* HostNormalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostNormalizer.swift; sourceTree = ""; }; 3333C1E02681EA4C00B60D84 /* NotificationsPayloads.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsPayloads.swift; sourceTree = ""; }; 3333D7BD265E56F2004279B0 /* OperationResponseType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationResponseType.swift; sourceTree = ""; }; 3337E6A2265FAB39006949EB /* BaseResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseResponse.swift; sourceTree = ""; }; @@ -987,6 +985,7 @@ 47EFF0FC2E8D85B700E72D0A /* DatabaseMetadataMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseMetadataMigrationTests.swift; sourceTree = ""; }; 47FDF0B92C5BDAB80051F08C /* MigrationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationManagerProtocol.swift; sourceTree = ""; }; 47FDF0BB2C5BE8BB0051F08C /* MigrationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationProtocol.swift; sourceTree = ""; }; + 4B7DAFAB687945FA908DB1AC /* TransparentViewSyncOperationResponseTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransparentViewSyncOperationResponseTests.swift; sourceTree = ""; }; 6F1EAA15266A670E007A335B /* ProductListItemsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListItemsResponse.swift; sourceTree = ""; }; 6FDD143A266F7BD900A50C35 /* ProcessingStatusResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessingStatusResponse.swift; sourceTree = ""; }; 6FDD143C266F7BEB00A50C35 /* ItemResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemResponse.swift; sourceTree = ""; }; @@ -1040,7 +1039,6 @@ 84B625E325C988FA00AB6228 /* URLValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLValidator.swift; sourceTree = ""; }; 84B625E825C989C100AB6228 /* UDIDValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDIDValidator.swift; sourceTree = ""; }; 84B625EF25C98B1200AB6228 /* ValidatorsTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatorsTestCase.swift; sourceTree = ""; }; - F3CD202E2F600A800065392A /* URLValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLValidatorTests.swift; sourceTree = ""; }; 84BAEF8125D54919002E8A26 /* BodyDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyDecoder.swift; sourceTree = ""; }; 84C65E5D25D4FBA3008996FA /* MobileApplicationInstalled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileApplicationInstalled.swift; sourceTree = ""; }; 84C65E6325D4FBBB008996FA /* MobileApplicationInfoUpdated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileApplicationInfoUpdated.swift; sourceTree = ""; }; @@ -1070,7 +1068,6 @@ 84FCD3BC25CA10F600D1E574 /* SuccessResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = SuccessResponse.json; sourceTree = ""; }; 9778038796A8426ABDED1E97 /* FeatureTogglesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureTogglesModel.swift; sourceTree = ""; }; 97FEDDEB5F71A67F1C4C675F /* MBNetworkFetcherResponseHandlingTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MBNetworkFetcherResponseHandlingTests.swift; sourceTree = ""; }; - F3BA5E000130A000C0000006 /* OperationsURLRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsURLRoutingTests.swift; sourceTree = ""; }; 9B24FAAB28C74B8300F10B5D /* InAppConfigurationRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppConfigurationRepository.swift; sourceTree = ""; }; 9B24FAAD28C74BA500F10B5D /* InAppCoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppCoreManager.swift; sourceTree = ""; }; 9B24FAB028C74BD200F10B5D /* InAppConfigurationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppConfigurationManager.swift; sourceTree = ""; }; @@ -1197,6 +1194,7 @@ BD1BE43AA9EAEA03F8ED400B /* HapticRequestValidator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HapticRequestValidator.swift; sourceTree = ""; }; BD1BE43AA9EAEA03F8ED400C /* HapticRequestParserTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HapticRequestParserTests.swift; sourceTree = ""; }; BD1BE43AA9EAEA03F8ED400D /* HapticRequestValidatorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HapticRequestValidatorTests.swift; sourceTree = ""; }; + BECF3D292B29C1894F80948F /* MBEventRepositorySendRawTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MBEventRepositorySendRawTests.swift; sourceTree = ""; }; D216DE502C0716B70020F58A /* StringExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensionsTests.swift; sourceTree = ""; }; D216DE522C0716B80020F58A /* TimeIntervalTimeSpanTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeIntervalTimeSpanTests.swift; sourceTree = ""; }; D2F7E2412BADB89900B24BB8 /* UserVisitManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserVisitManager.swift; sourceTree = ""; }; @@ -1218,7 +1216,6 @@ F31470952B96681F00E01E5C /* 27-TargetingRequests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "27-TargetingRequests.json"; sourceTree = ""; }; F31470972B9668F100E01E5C /* 31-TargetingRequests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "31-TargetingRequests.json"; sourceTree = ""; }; F315503E2BBB24E20072A071 /* TTLValidationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTLValidationService.swift; sourceTree = ""; }; - F3BA5E000130A000C0000008 /* OperationsDomainConfigPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsDomainConfigPolicy.swift; sourceTree = ""; }; F31909982E979D9E00373E2F /* MindboxAppDelegateProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MindboxAppDelegateProxy.swift; sourceTree = ""; }; F31A94772BC6995500E6C978 /* InappFrequency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InappFrequency.swift; sourceTree = ""; }; F31A947B2BC69E3900E6C978 /* PeriodicFrequency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeriodicFrequency.swift; sourceTree = ""; }; @@ -1300,10 +1297,6 @@ F34A10402F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorMissing.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsFeatureTogglesShouldSendInAppShowErrorMissing.json; sourceTree = ""; }; F34A10412F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorTypeError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsFeatureTogglesShouldSendInAppShowErrorTypeError.json; sourceTree = ""; }; F34A10422F455C5B0065392A /* SettingsFeatureTogglesTypeError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsFeatureTogglesTypeError.json; sourceTree = ""; }; - F3BA10512F500A800065392A /* SettingsBaseAddressesError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsBaseAddressesError.json; sourceTree = ""; }; - F3BA10522F500A800065392A /* SettingsBaseAddressesTypeError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsBaseAddressesTypeError.json; sourceTree = ""; }; - F3BA10532F500A800065392A /* SettingsBaseAddressesOperationsError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsBaseAddressesOperationsError.json; sourceTree = ""; }; - F3BA10542F500A800065392A /* SettingsBaseAddressesOperationsTypeError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsBaseAddressesOperationsTypeError.json; sourceTree = ""; }; F34A45AD2B7628B700634C8B /* MBPushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBPushNotification.swift; sourceTree = ""; }; F34A45AF2B762A6100634C8B /* MindboxPushValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MindboxPushValidator.swift; sourceTree = ""; }; F351F1BF2CE380A40053423E /* InappMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InappMapper.swift; sourceTree = ""; }; @@ -1395,11 +1388,22 @@ F3B70A002F250A0100AABB01 /* ForegroundStopwatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForegroundStopwatch.swift; sourceTree = ""; }; F3B70A022F250A0100AABB02 /* ForegroundStopwatchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForegroundStopwatchTests.swift; sourceTree = ""; }; F3B70A042F250A0100AABB03 /* TimeToDisplayBackgroundTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeToDisplayBackgroundTests.swift; sourceTree = ""; }; + F3BA10512F500A800065392A /* SettingsBaseAddressesError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsBaseAddressesError.json; sourceTree = ""; }; + F3BA10522F500A800065392A /* SettingsBaseAddressesTypeError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsBaseAddressesTypeError.json; sourceTree = ""; }; + F3BA10532F500A800065392A /* SettingsBaseAddressesOperationsError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsBaseAddressesOperationsError.json; sourceTree = ""; }; + F3BA10542F500A800065392A /* SettingsBaseAddressesOperationsTypeError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsBaseAddressesOperationsTypeError.json; sourceTree = ""; }; + F3BA5E000130A000C0000004 /* BaseAddressesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseAddressesModel.swift; sourceTree = ""; }; + F3BA5E000130A000C0000006 /* OperationsURLRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsURLRoutingTests.swift; sourceTree = ""; }; + F3BA5E000130A000C0000008 /* OperationsDomainConfigPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsDomainConfigPolicy.swift; sourceTree = ""; }; F3BD9F812F273BCD00647BAF /* BridgeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeMessage.swift; sourceTree = ""; }; F3BDA0372F27444500647BAF /* MindboxWebBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MindboxWebBridge.swift; sourceTree = ""; }; F3C1A0012F5B100100ABC001 /* InappShowFailureManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InappShowFailureManager.swift; sourceTree = ""; }; F3C1A0032F5B100100ABC001 /* InAppShowFailure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppShowFailure.swift; sourceTree = ""; }; F3C1A0052F5B100100ABC001 /* InappShowFailureManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InappShowFailureManagerTests.swift; sourceTree = ""; }; + F3CD20272F600A800065392A /* MBConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBConfigurationTests.swift; sourceTree = ""; }; + F3CD202A2F600A800065392A /* HostNormalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostNormalizer.swift; sourceTree = ""; }; + F3CD202C2F600A800065392A /* HostNormalizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostNormalizerTests.swift; sourceTree = ""; }; + F3CD202E2F600A800065392A /* URLValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLValidatorTests.swift; sourceTree = ""; }; F3D818AF2A3885AD0002957C /* ABTestDeviceMixer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ABTestDeviceMixer.swift; sourceTree = ""; }; F3D818B22A3885F50002957C /* ABTestDeviceMixerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ABTestDeviceMixerTests.swift; sourceTree = ""; }; F3D925AA2A120C0F00135C87 /* InAppImageDownloaderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppImageDownloaderMock.swift; sourceTree = ""; }; @@ -1437,9 +1441,39 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - F385631E2DB6729000D91208 /* InappConfigurationDataFacade */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = InappConfigurationDataFacade; sourceTree = ""; }; - F397DE1C2CFF568800B72DA9 /* JSONs */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = JSONs; sourceTree = ""; }; - F3DEB38C2D47CBA200D0EFA4 /* InappSessionManagerTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = InappSessionManagerTests; sourceTree = ""; }; + F385631E2DB6729000D91208 /* InappConfigurationDataFacade */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = InappConfigurationDataFacade; + sourceTree = ""; + }; + F397DE1C2CFF568800B72DA9 /* JSONs */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = JSONs; + sourceTree = ""; + }; + F3DEB38C2D47CBA200D0EFA4 /* InappSessionManagerTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = InappSessionManagerTests; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2249,6 +2283,7 @@ 97FEDDEB5F71A67F1C4C675F /* MBNetworkFetcherResponseHandlingTests.swift */, F3BA5E000130A000C0000006 /* OperationsURLRoutingTests.swift */, F3CD202C2F600A800065392A /* HostNormalizerTests.swift */, + BECF3D292B29C1894F80948F /* MBEventRepositorySendRawTests.swift */, ); path = Network; sourceTree = ""; @@ -2694,6 +2729,7 @@ F3C1A0052F5B100100ABC001 /* InappShowFailureManagerTests.swift */, 2B4C84F6EDD4B7D977F67A95 /* WebView */, A1B2C3D400000007E1010101 /* Permissions */, + 4B7DAFAB687945FA908DB1AC /* TransparentViewSyncOperationResponseTests.swift */, ); path = Tests; sourceTree = ""; @@ -3501,25 +3537,6 @@ path = FeatureTogglesError; sourceTree = ""; }; - F3CD20282F600A800065392A /* Configuration */ = { - isa = PBXGroup; - children = ( - F3CD20272F600A800065392A /* MBConfigurationTests.swift */, - ); - path = Configuration; - sourceTree = ""; - }; - F3BA10502F500A800065392A /* BaseAddressesError */ = { - isa = PBXGroup; - children = ( - F3BA10512F500A800065392A /* SettingsBaseAddressesError.json */, - F3BA10522F500A800065392A /* SettingsBaseAddressesTypeError.json */, - F3BA10532F500A800065392A /* SettingsBaseAddressesOperationsError.json */, - F3BA10542F500A800065392A /* SettingsBaseAddressesOperationsTypeError.json */, - ); - path = BaseAddressesError; - sourceTree = ""; - }; F34A45AC2B76286D00634C8B /* PublicModels */ = { isa = PBXGroup; children = ( @@ -3732,6 +3749,17 @@ path = Config; sourceTree = ""; }; + F3BA10502F500A800065392A /* BaseAddressesError */ = { + isa = PBXGroup; + children = ( + F3BA10512F500A800065392A /* SettingsBaseAddressesError.json */, + F3BA10522F500A800065392A /* SettingsBaseAddressesTypeError.json */, + F3BA10532F500A800065392A /* SettingsBaseAddressesOperationsError.json */, + F3BA10542F500A800065392A /* SettingsBaseAddressesOperationsTypeError.json */, + ); + path = BaseAddressesError; + sourceTree = ""; + }; F3BD9F802F273BC800647BAF /* Bridge */ = { isa = PBXGroup; children = ( @@ -3742,6 +3770,14 @@ path = Bridge; sourceTree = ""; }; + F3CD20282F600A800065392A /* Configuration */ = { + isa = PBXGroup; + children = ( + F3CD20272F600A800065392A /* MBConfigurationTests.swift */, + ); + path = Configuration; + sourceTree = ""; + }; F3D818AE2A3885A40002957C /* ABTestDeviceMixer */ = { isa = PBXGroup; children = ( @@ -4745,6 +4781,8 @@ 1E3BD63AB3F1521C253CB818 /* MBNetworkFetcherResponseHandlingTests.swift in Sources */, F3BA5E000130A000C0000005 /* OperationsURLRoutingTests.swift in Sources */, F3CD202B2F600A800065392A /* HostNormalizerTests.swift in Sources */, + AF174B2121221D323FB95EF0 /* MBEventRepositorySendRawTests.swift in Sources */, + EA395B77BB16CEFE6DC91D1D /* TransparentViewSyncOperationResponseTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift index 2f1f5d86a..a1a151296 100644 --- a/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift @@ -392,36 +392,54 @@ extension TransparentView { eventRepository.sendRaw(event: event) { [weak self] result in DispatchQueue.main.async { switch result { - case .success(let data): - guard let bodyString = String(data: data, encoding: .utf8) else { - Logger.common(message: "[WebView] syncOperation '\(params.name)' response body is not valid UTF-8", - level: .error, category: .webViewInAppMessages) - self?.sendBridgeError("Response body is not valid UTF-8", action: message.action, id: message.id) - return - } + case .success: Logger.common(message: "[WebView] syncOperation '\(params.name)' success", level: .info, category: .webViewInAppMessages) - let successResponse = BridgeMessage( - type: .response, - action: message.action, - payload: .string(bodyString), - id: message.id - ) - self?.facade?.sendToJS(successResponse) - case .failure(let error): Logger.common(message: "[WebView] syncOperation '\(params.name)' failed: \(error)", level: .error, category: .webViewInAppMessages) - let errorJSON = error.createJSON() - let errorResponse = BridgeMessage( - type: .error, - action: message.action, - payload: .string(errorJSON), - id: message.id - ) - self?.facade?.sendToJS(errorResponse) } + let outgoing = TransparentView.makeSyncOperationResponse( + result: result, + action: message.action, + id: message.id + ) + self?.facade?.sendToJS(outgoing) } } } + + /// Maps the raw `sendRaw` result of a `syncOperation` request to the outgoing + /// `BridgeMessage` sent back to JS. Pure function — no side effects — extracted + /// to keep the JS-bridge contract independently unit-testable. + static func makeSyncOperationResponse( + result: Result, + action: String, + id: UUID + ) -> BridgeMessage { + switch result { + case .success(let data): + guard let bodyString = String(data: data, encoding: .utf8) else { + return BridgeMessage( + type: .error, + action: action, + payload: .object(["error": .string("Response body is not valid UTF-8")]), + id: id + ) + } + return BridgeMessage( + type: .response, + action: action, + payload: .string(bodyString), + id: id + ) + case .failure(let error): + return BridgeMessage( + type: .error, + action: action, + payload: .string(error.createJSON()), + id: id + ) + } + } } // MARK: - LocalState Handlers diff --git a/MindboxTests/InApp/Tests/TransparentViewSyncOperationResponseTests.swift b/MindboxTests/InApp/Tests/TransparentViewSyncOperationResponseTests.swift new file mode 100644 index 000000000..bd245386e --- /dev/null +++ b/MindboxTests/InApp/Tests/TransparentViewSyncOperationResponseTests.swift @@ -0,0 +1,177 @@ +// +// TransparentViewSyncOperationResponseTests.swift +// MindboxTests +// + +import Testing +import Foundation +@_spi(Internal) @testable import Mindbox + +@Suite("TransparentView.makeSyncOperationResponse") +struct TransparentViewSyncOperationResponseTests { + + private let action = "syncOperation" + private let requestId = UUID() + + // MARK: - HTTP 200 + ValidationError body → .response with raw body (regression: MOBILE-164) + + @Test("HTTP 200 ValidationError body becomes .response with raw body string") + func validationErrorBody_becomesResponseWithRawBody() throws { + let rawBody = #"{"status":"ValidationError","validationMessages":[{"message":"Invalid email","location":"/customer/email"}]}"# + let data = try #require(rawBody.data(using: .utf8)) + + let outgoing = TransparentView.makeSyncOperationResponse( + result: .success(data), + action: action, + id: requestId + ) + + #expect(outgoing.type == .response) + #expect(outgoing.action == action) + #expect(outgoing.id == requestId) + if case .string(let value) = outgoing.payload { + #expect(value == rawBody, "Payload must be the raw body, not re-serialized") + } else { + Issue.record("Expected .string payload, got \(String(describing: outgoing.payload))") + } + } + + // MARK: - HTTP 200 + Success body → .response with raw body + + @Test("HTTP 200 Success body becomes .response with raw body string (not re-serialized)") + func successBody_becomesResponseWithRawBody() throws { + let rawBody = #"{"status":"Success","customer":{"email":"a@b.c"}}"# + let data = try #require(rawBody.data(using: .utf8)) + + let outgoing = TransparentView.makeSyncOperationResponse( + result: .success(data), + action: action, + id: requestId + ) + + #expect(outgoing.type == .response) + if case .string(let value) = outgoing.payload { + #expect(value == rawBody) + } else { + Issue.record("Expected .string payload, got \(String(describing: outgoing.payload))") + } + } + + // MARK: - HTTP 200 + non-JSON body → .response with raw body string + + @Test("HTTP 200 with non-JSON body still becomes .response (JS decides)") + func nonJSONBody_becomesResponseWithRawBody() throws { + let rawBody = "plain text body" + let data = try #require(rawBody.data(using: .utf8)) + + let outgoing = TransparentView.makeSyncOperationResponse( + result: .success(data), + action: action, + id: requestId + ) + + #expect(outgoing.type == .response) + if case .string(let value) = outgoing.payload { + #expect(value == rawBody) + } else { + Issue.record("Expected .string payload") + } + } + + // MARK: - HTTP 200 + empty body → .response with empty string + + @Test("HTTP 200 with empty body becomes .response with empty string payload") + func emptyBody_becomesResponseWithEmptyString() { + let outgoing = TransparentView.makeSyncOperationResponse( + result: .success(Data()), + action: action, + id: requestId + ) + + #expect(outgoing.type == .response) + if case .string(let value) = outgoing.payload { + #expect(value == "") + } else { + Issue.record("Expected .string payload") + } + } + + // MARK: - Non-UTF-8 body → .error with explanatory payload + + @Test("Non-UTF-8 body becomes .error with 'Response body is not valid UTF-8'") + func nonUTF8Body_becomesError() { + // Bytes that are not valid UTF-8: lone continuation byte 0xC3 + invalid follow-up + let data = Data([0xC3, 0x28]) + + let outgoing = TransparentView.makeSyncOperationResponse( + result: .success(data), + action: action, + id: requestId + ) + + #expect(outgoing.type == .error) + if case .object(let dict) = outgoing.payload, + case .string(let errorMessage) = dict["error"] { + #expect(errorMessage == "Response body is not valid UTF-8") + } else { + Issue.record("Expected .object payload with 'error' key, got \(String(describing: outgoing.payload))") + } + } + + // MARK: - Failure (.connectionError) → .error with createJSON payload + + @Test("Connection failure becomes .error with MindboxError.createJSON payload") + func connectionError_becomesError() { + let outgoing = TransparentView.makeSyncOperationResponse( + result: .failure(.connectionError), + action: action, + id: requestId + ) + + #expect(outgoing.type == .error) + if case .string(let value) = outgoing.payload { + #expect(value.contains("NetworkError"), "createJSON for connectionError produces a NetworkError envelope") + #expect(value.contains("Connection error")) + } else { + Issue.record("Expected .string payload") + } + } + + // MARK: - Failure (.protocolError) → .error with createJSON payload + + @Test("Protocol error becomes .error with MindboxError.createJSON payload") + func protocolError_becomesError() { + let pe = ProtocolError(status: .protocolError, errorMessage: "Bad", httpStatusCode: 400) + let outgoing = TransparentView.makeSyncOperationResponse( + result: .failure(.protocolError(pe)), + action: action, + id: requestId + ) + + #expect(outgoing.type == .error) + if case .string(let value) = outgoing.payload { + #expect(value.contains("MindboxError")) + #expect(value.contains("ProtocolError")) + } else { + Issue.record("Expected .string payload") + } + } + + // MARK: - id and action propagated + + @Test("Action and id from the request are preserved on the outgoing message") + func actionAndIdPreserved() throws { + let specificAction = "customAction" + let specificId = UUID() + let data = try #require("body".data(using: .utf8)) + + let outgoing = TransparentView.makeSyncOperationResponse( + result: .success(data), + action: specificAction, + id: specificId + ) + + #expect(outgoing.action == specificAction) + #expect(outgoing.id == specificId) + } +} diff --git a/MindboxTests/Network/MBEventRepositorySendRawTests.swift b/MindboxTests/Network/MBEventRepositorySendRawTests.swift new file mode 100644 index 000000000..667050606 --- /dev/null +++ b/MindboxTests/Network/MBEventRepositorySendRawTests.swift @@ -0,0 +1,211 @@ +// +// MBEventRepositorySendRawTests.swift +// MindboxTests +// + +import Testing +import Foundation +@testable import Mindbox + +@Suite("MBEventRepository.sendRaw") +struct MBEventRepositorySendRawTests { + + // MARK: - Test doubles + + private final class FakeNetworkFetcher: NetworkFetcher, @unchecked Sendable { + var requestRawResult: Result = .success(Data()) + private(set) var capturedRoute: Route? + private(set) var requestRawCallCount = 0 + + func request( + type: T.Type, + route: Route, + needBaseResponse: Bool, + completion: @escaping ((Result) -> Void) + ) where T: Decodable { + // not used by sendRaw + } + + func request(route: Route, completion: @escaping ((Result) -> Void)) { + // not used by sendRaw + } + + func requestRaw(route: Route, completion: @escaping ((Result) -> Void)) { + requestRawCallCount += 1 + capturedRoute = route + completion(requestRawResult) + } + + func cancelAllTasks() {} + } + + private func makeStorage(configured: Bool = true, deviceUUID: String? = "test-uuid") throws -> MockPersistenceStorage { + let storage = MockPersistenceStorage() + if configured { + storage.configuration = try MBConfiguration(endpoint: "test-endpoint", domain: "api.mindbox.ru") + } + storage.deviceUUID = deviceUUID + return storage + } + + private func makeSyncEvent(operation: String = "TestOp", body: String = "{}") -> Event { + let customEvent = CustomEvent(name: operation, payload: body) + return Event(type: .syncEvent, body: BodyEncoder(encodable: customEvent).body) + } + + private func awaitResult(_ work: (@escaping (Result) -> Void) -> Void) async -> Result { + await withCheckedContinuation { cont in + work { result in + cont.resume(returning: result) + } + } + } + + // MARK: - Success: forwards raw bytes from fetcher + + @Test("Success path forwards raw bytes from fetcher.requestRaw") + func sendRaw_success_forwardsRawBytes() async throws { + let fetcher = FakeNetworkFetcher() + let rawBody = #"{"status":"ValidationError","validationMessages":[{"message":"x","location":"/y"}]}"#.data(using: .utf8)! + fetcher.requestRawResult = .success(rawBody) + + let repo = MBEventRepository( + fetcher: fetcher, + persistenceStorage: try makeStorage() + ) + + let result = await awaitResult { completion in + repo.sendRaw(event: makeSyncEvent(), completion: completion) + } + + switch result { + case .success(let data): + #expect(data == rawBody) + case .failure(let error): + Issue.record("Expected success, got \(error)") + } + #expect(fetcher.requestRawCallCount == 1) + } + + // MARK: - Route: syncEvent uses EventRoute.syncEvent + + @Test("syncEvent event yields EventRoute.syncEvent") + func sendRaw_syncEvent_routesToSyncEndpoint() async throws { + let fetcher = FakeNetworkFetcher() + let repo = MBEventRepository( + fetcher: fetcher, + persistenceStorage: try makeStorage() + ) + + _ = await awaitResult { completion in + repo.sendRaw(event: makeSyncEvent(), completion: completion) + } + + let route = try #require(fetcher.capturedRoute as? EventRoute) + if case .syncEvent = route { + // expected + } else { + Issue.record("Expected EventRoute.syncEvent, got \(route)") + } + } + + // MARK: - Failure: fetcher error is passed through + + @Test("Fetcher failure is propagated") + func sendRaw_fetcherFailure_isPropagated() async throws { + let fetcher = FakeNetworkFetcher() + fetcher.requestRawResult = .failure(.connectionError) + + let repo = MBEventRepository( + fetcher: fetcher, + persistenceStorage: try makeStorage() + ) + + let result = await awaitResult { completion in + repo.sendRaw(event: makeSyncEvent(), completion: completion) + } + + switch result { + case .success: + Issue.record("Expected failure, got success") + case .failure(let error): + guard case .connectionError = error else { + Issue.record("Expected .connectionError, got \(error)") + return + } + } + } + + // MARK: - Missing configuration → invalidConfiguration error + + @Test("Missing configuration returns invalidConfiguration error") + func sendRaw_missingConfiguration_returnsInvalidConfiguration() async throws { + let fetcher = FakeNetworkFetcher() + let storage = try makeStorage(configured: false) + + let repo = MBEventRepository(fetcher: fetcher, persistenceStorage: storage) + + let result = await awaitResult { completion in + repo.sendRaw(event: makeSyncEvent(), completion: completion) + } + + switch result { + case .success: + Issue.record("Expected failure when configuration is nil") + case .failure(let error): + guard case .internalError(let ie) = error else { + Issue.record("Expected .internalError, got \(error)") + return + } + #expect(ie.errorKey == ErrorKey.invalidConfiguration.rawValue) + } + #expect(fetcher.requestRawCallCount == 0, "Fetcher must not be called when configuration is missing") + } + + // MARK: - Missing deviceUUID → invalidConfiguration error + + @Test("Missing deviceUUID returns invalidConfiguration error") + func sendRaw_missingDeviceUUID_returnsInvalidConfiguration() async throws { + let fetcher = FakeNetworkFetcher() + let storage = try makeStorage(deviceUUID: nil) + + let repo = MBEventRepository(fetcher: fetcher, persistenceStorage: storage) + + let result = await awaitResult { completion in + repo.sendRaw(event: makeSyncEvent(), completion: completion) + } + + switch result { + case .success: + Issue.record("Expected failure when deviceUUID is nil") + case .failure(let error): + guard case .internalError(let ie) = error else { + Issue.record("Expected .internalError, got \(error)") + return + } + #expect(ie.errorKey == ErrorKey.invalidConfiguration.rawValue) + } + #expect(fetcher.requestRawCallCount == 0, "Fetcher must not be called when deviceUUID is missing") + } + + // MARK: - Completion hops to main queue + + @Test("Completion is delivered on the main queue") + func sendRaw_completion_onMainQueue() async throws { + let fetcher = FakeNetworkFetcher() + fetcher.requestRawResult = .success(Data("ok".utf8)) + + let repo = MBEventRepository( + fetcher: fetcher, + persistenceStorage: try makeStorage() + ) + + let isMain: Bool = await withCheckedContinuation { cont in + repo.sendRaw(event: makeSyncEvent()) { _ in + cont.resume(returning: Thread.isMainThread) + } + } + + #expect(isMain) + } +} diff --git a/MindboxTests/Network/MBNetworkFetcherResponseHandlingTests.swift b/MindboxTests/Network/MBNetworkFetcherResponseHandlingTests.swift index d12e39da3..10c750257 100644 --- a/MindboxTests/Network/MBNetworkFetcherResponseHandlingTests.swift +++ b/MindboxTests/Network/MBNetworkFetcherResponseHandlingTests.swift @@ -639,6 +639,166 @@ final class MBNetworkFetcherResponseHandlingTests: XCTestCase { waitForExpectations(timeout: 1) } + // MARK: - requestRaw: HTTP 200 + ValidationError body → raw Data success (regression: MOBILE-164) + + func test_requestRaw_http200_statusValidationError_returnsRawData() throws { + let fetcher = try makeFetcher() + let body = validationErrorData() + stubResponse(statusCode: 200, body: body) + + let expectation = expectation(description: "completion") + fetcher.requestRaw(route: FetchInAppGeoRoute()) { result in + switch result { + case .success(let data): + XCTAssertEqual(data, body, "Raw body must reach caller byte-for-byte") + case .failure(let error): + XCTFail("HTTP 200 + ValidationError must NOT fail in requestRaw; got \(error)") + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + // MARK: - requestRaw: HTTP 200 + Success body → raw Data success + + func test_requestRaw_http200_statusSuccess_returnsRawData() throws { + let fetcher = try makeFetcher() + let body = baseResponseData(status: "Success") + stubResponse(statusCode: 200, body: body) + + let expectation = expectation(description: "completion") + fetcher.requestRaw(route: FetchInAppGeoRoute()) { result in + switch result { + case .success(let data): + XCTAssertEqual(data, body, "Raw body must NOT be re-serialized") + case .failure(let error): + XCTFail("Expected success, got \(error)") + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + // MARK: - requestRaw: HTTP 200 + arbitrary JSON without status → raw Data success + + func test_requestRaw_http200_arbitraryJSON_returnsRawData() throws { + let fetcher = try makeFetcher() + // No `status` field at all — BaseResponse would fail to parse, but requestRaw must not care. + let body = try XCTUnwrap(#"{"customer":{"email":"a@b.c"},"foo":42}"#.data(using: .utf8)) + stubResponse(statusCode: 200, body: body) + + let expectation = expectation(description: "completion") + fetcher.requestRaw(route: FetchInAppGeoRoute()) { result in + switch result { + case .success(let data): + XCTAssertEqual(data, body) + case .failure(let error): + XCTFail("Expected success, got \(error)") + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + // MARK: - requestRaw: HTTP 200 + non-JSON body → still raw Data success + + func test_requestRaw_http200_nonJSONBody_returnsRawData() throws { + let fetcher = try makeFetcher() + let body = try XCTUnwrap("hello world".data(using: .utf8)) + stubResponse(statusCode: 200, body: body) + + let expectation = expectation(description: "completion") + fetcher.requestRaw(route: FetchInAppGeoRoute()) { result in + switch result { + case .success(let data): + XCTAssertEqual(data, body) + case .failure(let error): + XCTFail("Expected success, got \(error)") + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + // MARK: - requestRaw: HTTP 403 → protocolError (unchanged from existing pipeline) + + func test_requestRaw_http403_returnsProtocolError() throws { + let fetcher = try makeFetcher() + let body = "Forbidden".data(using: .utf8) + stubResponse(statusCode: 403, body: body) + + let expectation = expectation(description: "completion") + fetcher.requestRaw(route: FetchInAppGeoRoute()) { result in + switch result { + case .success: + XCTFail("HTTP 403 must NOT be success") + case .failure(let error): + guard case .protocolError(let pe) = error else { + XCTFail("Expected protocolError, got \(error)") + expectation.fulfill() + return + } + XCTAssertEqual(pe.httpStatusCode, 403) + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + // MARK: - requestRaw: HTTP 500 → serverError (unchanged from existing pipeline) + + func test_requestRaw_http500_returnsServerError() throws { + let fetcher = try makeFetcher() + let body = protocolErrorData(status: "InternalServerError", message: "Boom", httpCode: 500) + stubResponse(statusCode: 500, body: body) + + let expectation = expectation(description: "completion") + fetcher.requestRaw(route: FetchInAppGeoRoute()) { result in + switch result { + case .success: + XCTFail("HTTP 500 must NOT be success") + case .failure(let error): + guard case .serverError(let pe) = error else { + XCTFail("Expected serverError, got \(error)") + expectation.fulfill() + return + } + XCTAssertEqual(pe.httpStatusCode, 500) + XCTAssertEqual(pe.errorMessage, "Boom") + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + // MARK: - requestRaw: missing configuration → invalidConfiguration + + func test_requestRaw_noConfiguration_returnsInvalidConfiguration() throws { + let persistenceStorage = MockPersistenceStorage() + // Intentionally leave persistenceStorage.configuration = nil + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [StubURLProtocol.self] + let session = URLSession(configuration: configuration) + let fetcher = MBNetworkFetcher(persistenceStorage: persistenceStorage, session: session) + + let expectation = expectation(description: "completion") + fetcher.requestRaw(route: FetchInAppGeoRoute()) { result in + switch result { + case .success: + XCTFail("Expected failure when configuration is nil") + case .failure(let error): + guard case .internalError(let ie) = error else { + XCTFail("Expected internalError, got \(error)") + expectation.fulfill() + return + } + XCTAssertEqual(ie.errorKey, ErrorKey.invalidConfiguration.rawValue) + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + // MARK: - Typed request + 200 + valid base response but invalid target type → parsing error func test_typedRequest_http200_invalidTargetType_returnsParsingError() throws { From 9a6a8c6cbef76a560c0e98d3470e28c3c9384e75 Mon Sep 17 00:00:00 2001 From: Vailence Date: Thu, 14 May 2026 13:50:00 +0500 Subject: [PATCH 3/3] MOBILE-164: Address review feedback - MBNetworkFetcher.requestRaw: capture self strongly so completion is always invoked even if the fetcher were deallocated mid-request (matches the existing `request` pattern). - TransparentView.handleSyncOperation: build the outgoing BridgeMessage first, then log based on its `type` so the non-UTF-8 fallback path reports "failed" instead of "success". --- .../Views/WebView/TransparentView.swift | 18 ++++++++++++------ Mindbox/Network/MBNetworkFetcher.swift | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift index a1a151296..c82394899 100644 --- a/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift @@ -391,17 +391,23 @@ extension TransparentView { // 4xx, 5xx and network failures stay on the MindboxError → Error path. eventRepository.sendRaw(event: event) { [weak self] result in DispatchQueue.main.async { - switch result { - case .success: - Logger.common(message: "[WebView] syncOperation '\(params.name)' success", level: .info, category: .webViewInAppMessages) - case .failure(let error): - Logger.common(message: "[WebView] syncOperation '\(params.name)' failed: \(error)", level: .error, category: .webViewInAppMessages) - } let outgoing = TransparentView.makeSyncOperationResponse( result: result, action: message.action, id: message.id ) + switch outgoing.type { + case .response: + Logger.common(message: "[WebView] syncOperation '\(params.name)' success", level: .info, category: .webViewInAppMessages) + case .error: + if case .failure(let error) = result { + Logger.common(message: "[WebView] syncOperation '\(params.name)' failed: \(error)", level: .error, category: .webViewInAppMessages) + } else { + Logger.common(message: "[WebView] syncOperation '\(params.name)' failed: non-UTF-8 response body", level: .error, category: .webViewInAppMessages) + } + default: + break + } self?.facade?.sendToJS(outgoing) } } diff --git a/Mindbox/Network/MBNetworkFetcher.swift b/Mindbox/Network/MBNetworkFetcher.swift index 1620cc563..4356118a5 100644 --- a/Mindbox/Network/MBNetworkFetcher.swift +++ b/Mindbox/Network/MBNetworkFetcher.swift @@ -155,9 +155,9 @@ class MBNetworkFetcher: NetworkFetcher { let urlRequest = try builder.asURLRequest(route: route) Logger.network(request: urlRequest, httpAdditionalHeaders: session.configuration.httpAdditionalHeaders) let startTime = CFAbsoluteTimeGetCurrent() - session.dataTask(with: urlRequest) { [weak self] data, response, error in + session.dataTask(with: urlRequest) { data, response, error in let networkTimeMs = Int((CFAbsoluteTimeGetCurrent() - startTime) * 1000) - self?.handleResponse(data, response, error, needBaseResponse: false, networkTimeMs: networkTimeMs) { result in + self.handleResponse(data, response, error, needBaseResponse: false, networkTimeMs: networkTimeMs) { result in switch result { case let .success(data): completion(.success(data))