FBAEMKit/FBAEMKitTests/AEMReporterTests.swift (977 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ @testable import FBSDKCoreKit_Basics import TestTools import XCTest final class AEMReporterTests: XCTestCase { enum Keys { static let defaultCurrency = "default_currency" static let cutoffTime = "cutoff_time" static let validFrom = "valid_from" static let configMode = "config_mode" static let conversionValueRules = "conversion_value_rules" static let conversionValue = "conversion_value" static let priority = "priority" static let events = "events" static let eventName = "event_name" static let advertiserID = "advertiser_id" static let businessID = "advertiser_id" static let campaignID = "campaign_id" static let catalogID = "catalog_id" static let contentID = "fb_content_ids" static let token = "token" } enum Values { static let purchase = "fb_mobile_purchase" static let donate = "Donate" static let defaultMode = "DEFAULT" static let brandMode = "BRAND" static let cpasMode = "CPAS" static let USD = "USD" } let networker = TestAEMNetworker() let reporter = TestSKAdNetworkReporter() let userDefaultsSpy = UserDefaultsSpy() let date = Calendar.current.date( byAdding: .day, value: -2, to: Date() )! // swiftlint:disable:this force_unwrapping lazy var testInvocation = TestInvocation( campaignID: name, acsToken: name, acsSharedSecret: nil, acsConfigID: nil, businessID: nil, catalogID: nil, isTestMode: false, hasSKAN: false, isConversionFilteringEligible: true )! // swiftlint:disable:this force_unwrapping lazy var reportFilePath = BasicUtility.persistenceFilePath(name) let urlWithInvocation = URL(string: "fb123://test.com?al_applink_data=%7B%22acs_token%22%3A+%22test_token_1234567%22%2C+%22campaign_ids%22%3A+%22test_campaign_1234%22%2C+%22advertiser_id%22%3A+%22test_advertiserid_12345%22%7D")! // swiftlint:disable:this force_unwrapping let sampleCatalogOptimizationDictionary = ["data": [["content_id_belongs_to_catalog_id": true]]] let aggregationRequestTimestampToNotDelay = Date().addingTimeInterval(-100) override class func setUp() { super.setUp() } override func setUp() { super.setUp() AEMReporter.reset() removeReportFile() AEMReporter.configure(withNetworker: networker, appID: "123", reporter: reporter, store: userDefaultsSpy) // Actual queue doesn't matter as long as it's not the same as the designated queue name in the class AEMReporter.queue = DispatchQueue(label: name, qos: .background) AEMReporter.isEnabled = true AEMReporter.reportFilePath = reportFilePath } func testEnable() { AEMReporter.isEnabled = false AEMReporter.enable() XCTAssertTrue(AEMReporter.isEnabled, "AEM Report should be enabled") } func testConversionFilteringDefaultConfigure() { XCTAssertFalse(AEMReporter.isConversionFilteringEnabled, "AEM Conversion Filtering should be disabled by default") } func testSetConversionFilteringEnabled() { AEMReporter.isConversionFilteringEnabled = false AEMReporter.setConversionFilteringEnabled(true) XCTAssertTrue(AEMReporter.isConversionFilteringEnabled, "AEM Conversion Filtering should be enabled") } func testCatalogMatchingDefaultConfigure() { XCTAssertFalse(AEMReporter.isCatalogMatchingEnabled, "AEM Catalog Matching should be disabled by default") } func testSetCatalogMatchingEnabled() { AEMReporter.isCatalogMatchingEnabled = false AEMReporter.setCatalogMatchingEnabled(true) XCTAssertTrue(AEMReporter.isCatalogMatchingEnabled, "AEM Catalog Matching should be enabled") } func testConfigure() { XCTAssertEqual( networker, AEMReporter.networker as? TestAEMNetworker, "Should configure with the expected AEM networker" ) XCTAssertEqual( reporter, AEMReporter.reporter as? TestSKAdNetworkReporter, "Should configure with the expected SKAdNetwork reporter" ) XCTAssertEqual( userDefaultsSpy, AEMReporter.store as? UserDefaultsSpy, "Should configure with the expected data store" ) } func testParseURL() { var url: URL? XCTAssertNil(AEMReporter.parseURL(url)) url = URL(string: "fb123://test.com") XCTAssertNil(AEMReporter.parseURL(url)) url = URL(string: "fb123://test.com?al_applink_data=%7B%22acs_token%22%3A+%22test_token_1234567%22%2C+%22campaign_ids%22%3A+%22test_campaign_1234%22%7D") var invocation = AEMReporter.parseURL(url) XCTAssertEqual(invocation?.acsToken, "test_token_1234567") XCTAssertEqual(invocation?.campaignID, "test_campaign_1234") XCTAssertNil(invocation?.businessID) invocation = AEMReporter.parseURL(urlWithInvocation) XCTAssertEqual(invocation?.acsToken, "test_token_1234567") XCTAssertEqual(invocation?.campaignID, "test_campaign_1234") XCTAssertEqual(invocation?.businessID, "test_advertiserid_12345") } func testLoadReportData() { guard let invocation = AEMReporter.parseURL(urlWithInvocation) else { return XCTFail("Parsing Error") } AEMReporter.invocations = [invocation] AEMReporter._saveReportData() let data = AEMReporter._loadReportData() as? [_AEMInvocation] XCTAssertEqual(data?.count, 1) XCTAssertEqual(data?[0].acsToken, "test_token_1234567") XCTAssertEqual(data?[0].campaignID, "test_campaign_1234") XCTAssertEqual(data?[0].businessID, "test_advertiserid_12345") } func testLoadConfigs() { AEMReporter._addConfigs([SampleAEMData.validConfigData1]) AEMReporter._addConfigs([SampleAEMData.validConfigData1, SampleAEMData.validConfigData2]) let loadedConfigs: NSMutableDictionary? = AEMReporter._loadConfigs() XCTAssertEqual(loadedConfigs?.count, 1, "Should load the expected number of configs") let defaultConfigs: [_AEMConfiguration]? = loadedConfigs?[Values.defaultMode] as? [_AEMConfiguration] XCTAssertEqual( defaultConfigs?.count, 2, "Should load the expected number of default configs" ) XCTAssertEqual( defaultConfigs?[0].defaultCurrency, Values.USD, "Should save the expected default_currency of the config" ) XCTAssertEqual( defaultConfigs?[0].cutoffTime, 1, "Should save the expected cutoff_time of the config" ) XCTAssertEqual( defaultConfigs?[0].validFrom, 10000, "Should save the expected valid_from of the config" ) XCTAssertEqual( defaultConfigs?[0].configMode, Values.defaultMode, "Should save the expected config_mode of the config" ) XCTAssertEqual( defaultConfigs?[0].conversionValueRules.count, 1, "Should save the expected conversion_value_rules of the config" ) XCTAssertEqual( defaultConfigs?[1].defaultCurrency, Values.USD, "Should save the expected default_currency of the config" ) XCTAssertEqual( defaultConfigs?[1].cutoffTime, 1, "Should save the expected cutoff_time of the config" ) XCTAssertEqual( defaultConfigs?[1].validFrom, 10001, "Should save the expected valid_from of the config" ) XCTAssertEqual( defaultConfigs?[1].configMode, Values.defaultMode, "Should save the expected config_mode of the config" ) XCTAssertEqual( defaultConfigs?[1].conversionValueRules.count, 2, "Should save the expected conversion_value_rules of the config" ) } func testClearCache() { AEMReporter._addConfigs([SampleAEMData.validConfigData1]) AEMReporter._addConfigs([SampleAEMData.validConfigData1, SampleAEMData.validConfigData2]) AEMReporter._clearCache() var configs = AEMReporter.configs var configList: [_AEMConfiguration]? = configs[Values.defaultMode] as? [_AEMConfiguration] XCTAssertEqual(configList?.count, 1, "Should have the expected number of configs") guard let invocation1 = _AEMInvocation( campaignID: "test_campaign_1234", acsToken: "test_token_1234567", acsSharedSecret: "test_shared_secret", acsConfigID: "test_config_id_123", businessID: nil, catalogID: nil, isTestMode: false, hasSKAN: false, isConversionFilteringEligible: true ), let invocation2 = _AEMInvocation( campaignID: "test_campaign_1234", acsToken: "test_token_1234567", acsSharedSecret: "test_shared_secret", acsConfigID: "test_config_id_123", businessID: nil, catalogID: nil, isTestMode: false, hasSKAN: false, isConversionFilteringEligible: true ) else { return XCTFail("Unwrapping Error") } invocation1.setConfigID(10000) invocation2.setConfigID(10001) guard let date = Calendar.current.date(byAdding: .day, value: -2, to: Date()) else { return XCTFail("Date Creation Error") } invocation2.setConversionTimestamp(date) AEMReporter.invocations = [invocation1, invocation2] AEMReporter._addConfigs( [SampleAEMData.validConfigData1, SampleAEMData.validConfigData2, SampleAEMData.validConfigData3] ) AEMReporter._clearCache() guard let invocations = AEMReporter.invocations as? [_AEMInvocation] else { return XCTFail("Should have invocations") } XCTAssertEqual(invocations.count, 1, "Should clear the expired invocation") XCTAssertEqual(invocations[0].configID, 10000, "Should keep the expected invocation") configs = AEMReporter.configs configList = configs[Values.defaultMode] as? [_AEMConfiguration] XCTAssertEqual(configList?.count, 2, "Should have the expected number of configs") XCTAssertEqual(configList?[0].validFrom, 10000, "Should keep the expected config") XCTAssertEqual(configList?[1].validFrom, 20000, "Should keep the expected config") } func testClearConfigs() { AEMReporter.configs = [ Values.defaultMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithoutBusinessID()]), Values.brandMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithBusinessIDAndContentRule()]), Values.cpasMode: NSMutableArray(array: [SampleAEMConfigurations.createCpasConfig()]), ] AEMReporter._clearConfigs() let defaultConfigs = AEMReporter.configs[Values.defaultMode] as? [_AEMConfiguration] let brandConfigs = AEMReporter.configs[Values.brandMode] as? [_AEMConfiguration] let cpasConfigs = AEMReporter.configs[Values.cpasMode] as? [_AEMConfiguration] XCTAssertEqual( defaultConfigs?.count, 1, "Should have default mode config" ) XCTAssertEqual( brandConfigs?.count, 0, "Should not have brand mode config" ) XCTAssertEqual( cpasConfigs?.count, 0, "Should not have cpas mode config" ) } func testHandleURL() throws { let url = try XCTUnwrap( URL(string: "fb123://test.com?al_applink_data=%7B%22acs_token%22%3A+%22test_token_1234567%22%2C+%22campaign_ids%22%3A+%22test_campaign_1234%22%7D"), "Should be able to create URL with valid deeplink" ) AEMReporter.handle(url) let invocations = AEMReporter.invocations XCTAssertGreaterThan( invocations.count, 0, "Handling a url that contains invocations should set the invocations on the reporter" ) } func testHandleDebuggingURL() { guard let url = URL(string: "fb123://test.com?al_applink_data=%7B%22acs_token%22%3A+%22debugging_token%22%2C+%22campaign_ids%22%3A+%2210%22%2C+%22test_deeplink%22%3A+1%7D") else { return XCTFail("Unwrapping Error") } AEMReporter.invocations = [] AEMReporter.handle(url) XCTAssertEqual( AEMReporter.invocations.count, 0, "Handling a debugging url should not affect production traffic" ) } func testIsConfigRefreshTimestampValid() { AEMReporter.timestamp = Date() XCTAssertTrue( AEMReporter._isConfigRefreshTimestampValid(), "Timestamp should be valid" ) guard let date = Calendar.current.date(byAdding: .day, value: -2, to: Date()) else { return XCTFail("Date Creation Error") } AEMReporter.timestamp = date XCTAssertFalse( AEMReporter._isConfigRefreshTimestampValid(), "Timestamp should not be valid" ) } func testShouldEnforceRefresh() { AEMReporter.invocations = [SampleAEMData.invocationWithoutAdvertiserID] AEMReporter.timestamp = Date() AEMReporter.configs = [ Values.defaultMode: [SampleAEMConfigurations.createConfigWithoutBusinessID()], ] XCTAssertTrue( AEMReporter._shouldRefresh(withIsForced: true), "Should refresh config if it's enforced" ) } func testShouldRefreshWithoutBusinessID1() { AEMReporter.invocations = [SampleAEMData.invocationWithoutAdvertiserID] AEMReporter.timestamp = Date() AEMReporter.configs = [ Values.defaultMode: [SampleAEMConfigurations.createConfigWithoutBusinessID()], ] XCTAssertFalse( AEMReporter._shouldRefresh(withIsForced: false), "Should not refresh config if timestamp is not expired and there is no business ID" ) } func testShouldRefreshWithoutBusinessID2() { AEMReporter.invocations = [SampleAEMData.invocationWithoutAdvertiserID] guard let date = Calendar.current.date(byAdding: .day, value: -2, to: Date()) else { return XCTFail("Date Creation Error") } AEMReporter.timestamp = date AEMReporter.configs = [ Values.defaultMode: [SampleAEMConfigurations.createConfigWithoutBusinessID()], ] XCTAssertTrue( AEMReporter._shouldRefresh(withIsForced: false), "Should not refresh config if timestamp is expired" ) } func testShouldRefreshWithoutBusinessID3() { AEMReporter.invocations = [SampleAEMData.invocationWithoutAdvertiserID] guard let date = Calendar.current.date(byAdding: .day, value: -2, to: Date()) else { return XCTFail("Date Creation Error") } AEMReporter.timestamp = date AEMReporter.configs = [:] XCTAssertTrue( AEMReporter._shouldRefresh(withIsForced: false), "Should not refresh config if configs is empty" ) } func testShouldRefreshWithBusinessID() { AEMReporter.invocations = [ SampleAEMData.invocationWithoutAdvertiserID, SampleAEMData.invocationWithAdvertiserID1, ] AEMReporter.timestamp = Date() AEMReporter.configs = [ Values.defaultMode: [SampleAEMConfigurations.createConfigWithoutBusinessID()], ] XCTAssertTrue( AEMReporter._shouldRefresh(withIsForced: false), "Should not refresh config if there exists an invocation with business ID" ) } func testSendDebuggingRequest() { AEMReporter._sendDebuggingRequest(SampleAEMInvocations.createDebuggingInvocation()) XCTAssertTrue( networker.capturedGraphPath?.hasSuffix("aem_conversions") == true, "GraphRequst should be created because of there is a debugging invocation" ) XCTAssertEqual( networker.startCallCount, 1, "Should start the graph request to update the test mode" ) } func testDebuggingRequestParameters() { XCTAssertEqual( AEMReporter._debuggingRequestParameters(SampleAEMInvocations.createDebuggingInvocation()) as NSDictionary, [ "campaign_id": "debugging_campaign", "conversion_data": 0, "consumption_hour": 0, "token": "debugging_token", "delay_flow": "server", ], "Should have expected request parameters for debugging invocation" ) } func testSendAggregationRequest() { AEMReporter.invocations = [] AEMReporter._sendAggregationRequest() XCTAssertNil( networker.capturedGraphPath, "GraphRequest should not be created because of there is no invocation" ) XCTAssertNil( userDefaultsSpy.capturedSetObjectKey, "Min aggregation request timestamp should not be updated because of there is no request" ) guard let invocation = AEMReporter.parseURL(urlWithInvocation) else { return XCTFail("Parsing Error") } invocation.isAggregated = false AEMReporter.invocations = [invocation] AEMReporter._sendAggregationRequest() XCTAssertTrue( networker.capturedGraphPath?.hasSuffix("aem_conversions") == true, "GraphRequst should be created because of there is non-aggregated invocation" ) XCTAssertEqual( userDefaultsSpy.capturedSetObjectKey, "com.facebook.sdk:FBAEMMinAggregationRequestTimestamp", "Min aggregation request timestamp should not be updated because of there is non-aggregated invocation" ) } func testSendAggregationRequestWithDelay() { AEMReporter.minAggregationRequestTimestamp = Date().addingTimeInterval(100) guard let invocation = AEMReporter.parseURL(urlWithInvocation) else { return XCTFail("Parsing Error") } invocation.isAggregated = false AEMReporter.invocations = [invocation] AEMReporter._sendAggregationRequest() XCTAssertNil( networker.capturedGraphPath, "GraphRequst should not be created immediately because of there is delay" ) XCTAssertEqual( userDefaultsSpy.capturedSetObjectKey, "com.facebook.sdk:FBAEMMinAggregationRequestTimestamp", "Min aggregation request timestamp should not be updated because of there is non-aggregated invocation" ) } func testCompletingAggregationRequestWithError() { guard let invocation = AEMReporter.parseURL(urlWithInvocation) else { return XCTFail("Parsing Error") } invocation.isAggregated = false AEMReporter.invocations = [invocation] AEMReporter._sendAggregationRequest() networker.capturedCompletionHandler?(nil, SampleAEMError()) XCTAssertFalse( invocation.isAggregated, "Completing with an error should not mark the invocation as aggregated" ) XCTAssertFalse( FileManager.default.fileExists(atPath: reportFilePath), "Completing with an error should not write the report to the expected file path" ) } func testCompletingAggregationRequestWithoutError() { guard let invocation = AEMReporter.parseURL(urlWithInvocation) else { return XCTFail("Parsing Error") } invocation.isAggregated = false AEMReporter.invocations = [invocation] AEMReporter._sendAggregationRequest() networker.capturedCompletionHandler?(nil, nil) XCTAssertTrue( invocation.isAggregated, "Completing with no error should mark the invocation as aggregated" ) XCTAssertTrue( FileManager.default.fileExists(atPath: reportFilePath), "Completing with no error should write the report to the expected file path" ) } func testRecordAndUpdateEvents() { AEMReporter.timestamp = Date() guard let invocation = _AEMInvocation( campaignID: "test_campaign_1234", acsToken: "test_token_1234567", acsSharedSecret: "test_shared_secret", acsConfigID: "test_config_id_123", businessID: nil, catalogID: nil, isTestMode: false, hasSKAN: false, isConversionFilteringEligible: true ) else { return XCTFail("Unwrapping Error") } guard let config = _AEMConfiguration(json: SampleAEMData.validConfigData3) else { return XCTFail("Unwrapping Error") } AEMReporter.configs = [Values.defaultMode: [config]] AEMReporter.invocations = [invocation] AEMReporter.recordAndUpdate(event: Values.purchase, currency: Values.USD, value: 100, parameters: nil) // Invocation should be attributed and updated while request should be sent XCTAssertEqual( invocation.recordedEvents, [Values.purchase], "Invocation's cached events should be updated" ) XCTAssertEqual( invocation.recordedValues, [Values.purchase: [Values.USD: 100]], "Invocation's cached values should be updated" ) XCTAssertTrue( networker.capturedGraphPath?.hasSuffix("aem_conversions") == true, "Should create a request to update the conversions for a valid event" ) XCTAssertFalse( invocation.isAggregated, "Should not mark the invocation as aggregated if it is recorded and sent" ) XCTAssertTrue( FileManager.default.fileExists(atPath: reportFilePath), "Should save uploaded events to disk" ) XCTAssertEqual( networker.startCallCount, 1, "Should start the graph request to update the conversions" ) } func testRecordAndUpdateEventsWithAEMDisabled() { AEMReporter.isEnabled = false AEMReporter.timestamp = date AEMReporter.recordAndUpdate(event: Values.purchase, currency: Values.USD, value: 100, parameters: nil) XCTAssertNil( networker.capturedGraphPath, "Should not create a request to fetch the config if AEM is disabled" ) } func testRecordAndUpdateEventsWithEmptyEvent() { AEMReporter.timestamp = date AEMReporter.recordAndUpdate(event: "", currency: Values.USD, value: 100, parameters: nil) XCTAssertNil( networker.capturedGraphPath, "Should not create a request to fetch the config if the event being recorded is empty" ) XCTAssertFalse( FileManager.default.fileExists(atPath: reportFilePath), "Should not save an empty event to disk" ) } func testRecordAndUpdateEventsWithEmptyConfigs() throws { AEMReporter.timestamp = date AEMReporter.invocations = [testInvocation] AEMReporter.recordAndUpdate(event: Values.purchase, currency: Values.USD, value: 100, parameters: nil) XCTAssertEqual( testInvocation.attributionCallCount, 0, "Should not attribute events with empty configurations" ) XCTAssertEqual( testInvocation.updateConversionCallCount, 0, "Should not update conversions with empty configurations" ) } func testLoadConfigurationWithRefreshEnforced() { guard let config = _AEMConfiguration(json: SampleAEMData.validConfigData3) else { return XCTFail("Unwrapping Error") } AEMReporter.timestamp = Date() AEMReporter.configs = [Values.defaultMode: [config]] AEMReporter.isLoadingConfiguration = false AEMReporter._loadConfiguration(withRefreshForced: true, block: nil) guard let path = networker.capturedGraphPath, path.hasSuffix("aem_conversion_configs") else { return XCTFail("Should load configuration when refresh is enforced") } } func testLoadConfigurationWithBlock() { guard let config = _AEMConfiguration(json: SampleAEMData.validConfigData3) else { return XCTFail("Unwrapping Error") } var blockCall = 0 AEMReporter.timestamp = Date() AEMReporter.configs = [Values.defaultMode: [config]] AEMReporter._loadConfiguration(withRefreshForced: false) { _ in blockCall += 1 } XCTAssertEqual( blockCall, 1, "Should call the completion when loading the configuration" ) } func testLoadConfigurationWithoutBlock() { AEMReporter.timestamp = date AEMReporter.isLoadingConfiguration = false AEMReporter._loadConfiguration(withRefreshForced: false, block: nil) guard let path = networker.capturedGraphPath, path.hasSuffix("aem_conversion_configs") else { return XCTFail("Should not require a completion block to load a configuration") } } func testGetConfigRequestParameterWithoutAdvertiserIDs() { AEMReporter.invocations = NSMutableArray(array: [SampleAEMData.invocationWithoutAdvertiserID]) XCTAssertEqual( AEMReporter._requestParameters() as NSDictionary, ["fields": "", "advertiser_ids": "[]"], "Should not have unexpected advertiserIDs in config request params" ) } func testGetConfigRequestParameterWithAdvertiserIDs() { AEMReporter.invocations = NSMutableArray(array: [SampleAEMData.invocationWithAdvertiserID1, SampleAEMData.invocationWithoutAdvertiserID]) XCTAssertEqual( AEMReporter._requestParameters() as NSDictionary, ["fields": "", "advertiser_ids": #"["\#(SampleAEMData.invocationWithAdvertiserID1.businessID!)"]"#], // swiftlint:disable:this force_unwrapping "Should have expected advertiserIDs in config request params" ) AEMReporter.invocations = NSMutableArray(array: [SampleAEMData.invocationWithAdvertiserID1, SampleAEMData.invocationWithAdvertiserID2, SampleAEMData.invocationWithoutAdvertiserID]) // swiftlint:disable:this line_length XCTAssertEqual( AEMReporter._requestParameters() as NSDictionary, ["fields": "", "advertiser_ids": #"["\#(SampleAEMData.invocationWithAdvertiserID1.businessID!)","\#(SampleAEMData.invocationWithAdvertiserID2.businessID!)"]"#], // swiftlint:disable:this force_unwrapping "Should have expected advertiserIDs in config request params" ) } func testGetAggregationRequestParameterWithoutAdvertiserID() { let params: [String: Any] = AEMReporter._aggregationRequestParameters(SampleAEMData.invocationWithoutAdvertiserID) XCTAssertEqual( params[Keys.campaignID] as? String, SampleAEMData.invocationWithoutAdvertiserID.campaignID, "Should have expected campaign_id in aggregation request params" ) XCTAssertEqual( params[Keys.token] as? String, SampleAEMData.invocationWithoutAdvertiserID.acsToken, "Should have expected ACS token in aggregation request params" ) XCTAssertNil( params[Keys.businessID], "Should not have unexpected advertiser_id in aggregation request params" ) } func testGetAggregationRequestParameterWithAdvertiserID() { let params: [String: Any] = AEMReporter._aggregationRequestParameters(SampleAEMData.invocationWithAdvertiserID1) XCTAssertEqual( params[Keys.campaignID] as? String, SampleAEMData.invocationWithAdvertiserID1.campaignID, "Should have expected campaign_id in aggregation request params" ) XCTAssertEqual( params[Keys.token] as? String, SampleAEMData.invocationWithAdvertiserID1.acsToken, "Should have expected ACS token in aggregation request params" ) XCTAssertNotNil( params[Keys.businessID], "Should have expected advertiser_id in aggregation request params" ) } func testAttributedInvocationWithoutParameters() { let invocations = [ SampleAEMData.invocationWithoutAdvertiserID, SampleAEMData.invocationWithAdvertiserID1, SampleAEMData.invocationWithAdvertiserID2, ] let configs = [ Values.defaultMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithoutBusinessID()]), Values.brandMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithBusinessID()]), ] let attributedInvocation = AEMReporter._attributedInvocation( invocations, event: Values.purchase, currency: nil, value: nil, parameters: nil, configs: configs ) XCTAssertNotNil( attributedInvocation, "Should have invocation attributed" ) XCTAssertNil( attributedInvocation?.businessID, "The attributed invocation should not have advertiser ID" ) } func testAttributedInvocationWithParameters() { let invocations = [ SampleAEMData.invocationWithoutAdvertiserID, SampleAEMData.invocationWithAdvertiserID1, SampleAEMData.invocationWithAdvertiserID2, ] let configs = [ Values.defaultMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithoutBusinessID()]), Values.brandMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithBusinessID()]), ] let attributedInvocation = AEMReporter._attributedInvocation( invocations, event: "test", currency: nil, value: nil, parameters: ["values": "abcdefg"], configs: configs ) XCTAssertNil( attributedInvocation, "Should not have invocation attributed" ) } func testAttributedInvocationWithUnmatchedParameters() { let invocations = [ SampleAEMData.invocationWithoutAdvertiserID, SampleAEMData.invocationWithAdvertiserID1, SampleAEMData.invocationWithAdvertiserID2, ] let configs = [ Values.defaultMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithoutBusinessID()]), Values.brandMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithBusinessID()]), ] let attributedInvocation = AEMReporter._attributedInvocation( invocations, event: Values.purchase, currency: nil, value: nil, parameters: ["value": "abcdefg"], configs: configs ) XCTAssertNotNil( attributedInvocation, "Should have invocation attributed" ) XCTAssertEqual( attributedInvocation?.businessID, SampleAEMData.invocationWithAdvertiserID1.businessID, "The attributed invocation should have advertiser ID" ) } func testAttributedInvocationWithMultipleGeneralInvocations() { let invocation1 = SampleAEMInvocations.createGeneralInvocation1() let invocation2 = SampleAEMInvocations.createGeneralInvocation2() let invocations = [invocation1, invocation2] let configs = [ Values.defaultMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithoutBusinessID()]), Values.brandMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithBusinessID()]), ] let attributedInvocation = AEMReporter._attributedInvocation( invocations, event: Values.purchase, currency: nil, value: nil, parameters: nil, configs: configs ) XCTAssertEqual( attributedInvocation?.campaignID, invocation2.campaignID, "Should attribute the event to the latest general invocation" ) } func testAttributedInvocationWithUnmatchedEvent() { let invocation1 = SampleAEMInvocations.createGeneralInvocation1() let invocation2 = SampleAEMInvocations.createGeneralInvocation2() let invocations = [invocation1, invocation2] let configs = [ Values.defaultMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithoutBusinessID()]), Values.brandMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithBusinessID()]), ] let attributedInvocation = AEMReporter._attributedInvocation( invocations, event: "test", currency: nil, value: nil, parameters: nil, configs: configs ) XCTAssertNil( attributedInvocation, "Should not attribute the event with incorrect event" ) } func testAttributedInvocationWithDoubleCounting() { reporter.cutOff = false reporter.reportingEvents = [Values.purchase] let invocation = SampleAEMInvocations.createSKANOverlappedInvocation() let configs = [ Values.defaultMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithoutBusinessID()]), ] let attributedInvocation = AEMReporter._attributedInvocation( [invocation], event: Values.purchase, currency: Values.USD, value: 10, parameters: ["value": "abcdefg"], configs: configs ) XCTAssertNil( attributedInvocation, "Should not have invocation attributed with double counting" ) XCTAssertEqual( invocation.recordedEvents, [], "Should not expect invocation's recorded events to be changed with double counting" ) XCTAssertEqual( invocation.recordedValues, [:], "Should not expect invocation's recorded values to be changed with double counting" ) } func testAttributedInvocationWithoutDoubleCounting() { reporter.cutOff = false reporter.reportingEvents = [Values.purchase] let invocation = SampleAEMInvocations.createGeneralInvocation1() let configs = [ Values.defaultMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithoutBusinessID()]), ] let attributedInvocation = AEMReporter._attributedInvocation( [invocation], event: Values.purchase, currency: Values.USD, value: 10, parameters: ["value": "abcdefg"], configs: configs ) XCTAssertNotNil( attributedInvocation, "Should have invocation attributed without double counting" ) } func testIsDoubleCounting() { reporter.cutOff = false reporter.reportingEvents = ["fb_test"] let invocation = SampleAEMInvocations.createSKANOverlappedInvocation() XCTAssertTrue( AEMReporter._isDoubleCounting(invocation, event: "fb_test"), "Should expect double counting" ) XCTAssertFalse( AEMReporter._isDoubleCounting(invocation, event: "test"), "Should not expect double counting" ) } func testIsDoubleCountingWithCutOff() { reporter.cutOff = true reporter.reportingEvents = ["fb_test"] let invocation = SampleAEMInvocations.createSKANOverlappedInvocation() XCTAssertFalse( AEMReporter._isDoubleCounting(invocation, event: "fb_test"), "Should not expect double counting with SKAN cutoff" ) } func testIsDoubleCountingWithoutSKANClick() { reporter.cutOff = false reporter.reportingEvents = ["fb_test"] let invocation = SampleAEMInvocations.createGeneralInvocation1() XCTAssertFalse( AEMReporter._isDoubleCounting(invocation, event: "fb_test"), "Should not expect double counting without SKAN click" ) } // MARK: - Catalog Reporting func testLoadCatalogOptimizationWithoutContentID() { let invocation = SampleAEMInvocations.createCatalogOptimizedInvocation() var blockCall = 0 AEMReporter._loadCatalogOptimization(with: invocation, contentID: nil) { blockCall += 1 } XCTAssertTrue( (networker.capturedGraphPath?.contains("aem_conversion_filter")) == true, "Should start the catalog request" ) XCTAssertEqual(blockCall, 0, "Should not execute the block when contentID is nil") } func testLoadCatalogOptimizationWithOptimizedContent() { let invocation = SampleAEMInvocations.createCatalogOptimizedInvocation() var blockCall = 0 AEMReporter._loadCatalogOptimization(with: invocation, contentID: "test_content_id") { blockCall += 1 } XCTAssertTrue( (networker.capturedGraphPath?.contains("aem_conversion_filter")) == true, "Should start the catalog request" ) networker.capturedCompletionHandler?(nil, SampleAEMError()) XCTAssertEqual(blockCall, 0, "Should not execute the block when there is a network error") networker.capturedCompletionHandler?(["data": [["content_id_belongs_to_catalog_id": false]]], nil) XCTAssertEqual(blockCall, 0, "Should not execute the block when content is not optmized") networker.capturedCompletionHandler?(["data": [["content_id_belongs_to_catalog_id": true]]], nil) XCTAssertEqual(blockCall, 1, "Should execute the block when content is optmized") } func testLoadCatalogOptimizationWithFuzzyInput() { let invocation = SampleAEMInvocations.createCatalogOptimizedInvocation() AEMReporter._loadCatalogOptimization(with: invocation, contentID: "test_content_id") {} for _ in 0 ..< 100 { networker.capturedCompletionHandler?( Fuzzer.randomize(json: sampleCatalogOptimizationDictionary), nil ) } } func testIsContentOptimized() { var data = [ "data": [["content_id_belongs_to_catalog_id": true]], ] XCTAssertTrue(AEMReporter._isContentOptimized(data), "Should expect content is optimized") data = ["data": [["content_id_belongs_to_catalog_id": false]]] XCTAssertFalse(AEMReporter._isContentOptimized(data), "Should expect content is optimized") } func testCatalogRequestParameters() { let params = AEMReporter._catalogRequestParameters("test_catalog", contentID: "test_content_id") XCTAssertEqual( params as NSDictionary, [ Keys.catalogID: "test_catalog", Keys.contentID: "test_content_id", ], "Catalog request parameters are not expected" ) } func testCatalogRequestParametersWithMalformedInput() { let malformedInput = [nil, ""] for catalogID in malformedInput { for contentID in malformedInput { AEMReporter._catalogRequestParameters(catalogID, contentID: contentID) } } } func testShouldReportConversionInCatalogLevel() { for conversionFilteringEnabled in [true, false] { for catalogMatchingEnabled in [true, false] { for isOptimizedEvent in [true, false] { for catalogID in ["test_catalog", nil] { AEMReporter.setConversionFilteringEnabled(conversionFilteringEnabled) AEMReporter.setCatalogMatchingEnabled(catalogMatchingEnabled) testInvocation.isOptimizedEvent = isOptimizedEvent testInvocation.catalogID = catalogID if conversionFilteringEnabled, catalogMatchingEnabled, isOptimizedEvent, catalogID != nil { XCTAssertTrue( AEMReporter._shouldReportConversion(inCatalogLevel: testInvocation, event: Values.purchase), "Should expect to report conversion in catalog level" ) } else { XCTAssertFalse( AEMReporter._shouldReportConversion(inCatalogLevel: testInvocation, event: Values.purchase), "Should expect not to report conversion in catalog level" ) } } } } } } // MARK: - Aggregation Request func testShouldDelayAggregationRequestWithNilTimestamp() { AEMReporter.minAggregationRequestTimestamp = nil XCTAssertFalse( AEMReporter._shouldDelayAggregationRequest(), "Should not expect to delay aggregation request when timestamp is nil" ) } func testShouldDelayAggregationRequestWithExpiredTimestamp() { AEMReporter.minAggregationRequestTimestamp = aggregationRequestTimestampToNotDelay XCTAssertFalse( AEMReporter._shouldDelayAggregationRequest(), "Should not expect to delay aggregation request when timestamp is expired" ) } func testShouldDelayAggregationRequestWithValidTimestamp() { AEMReporter.minAggregationRequestTimestamp = Date().addingTimeInterval(5) XCTAssertTrue( AEMReporter._shouldDelayAggregationRequest(), "Should not expect to delay aggregation request when timestamp is within the range" ) } func testLoadMinAggregationRequestTimestamp() { let timestamp = Date() userDefaultsSpy.set( timestamp, forKey: "com.facebook.sdk:FBAEMMinAggregationRequestTimestamp" ) let data = AEMReporter._loadMinAggregationRequestTimestamp() XCTAssertEqual( timestamp, data, "Should return the timestamp from the userDefaultsSpy" ) XCTAssertEqual( userDefaultsSpy.capturedObjectRetrievalKey, "com.facebook.sdk:FBAEMMinAggregationRequestTimestamp", "Should retrieve the min aggregation request timestamp from the userDefaultsSpy" ) } func testUpdateAggregationRequestTimestamp() { let timestamp = Date().timeIntervalSince1970 AEMReporter._updateAggregationRequestTimestamp(timestamp) XCTAssertEqual( timestamp, AEMReporter.minAggregationRequestTimestamp?.timeIntervalSince1970, "Should set the expected tiemstamp" ) XCTAssertEqual( userDefaultsSpy.capturedSetObjectKey, "com.facebook.sdk:FBAEMMinAggregationRequestTimestamp", "Should persist the min aggregation request timestamp when setting a new one" ) } // MARK: - Helpers func removeReportFile() { do { try FileManager.default.removeItem(at: URL(fileURLWithPath: reportFilePath)) } catch _ as NSError {} } }