/* * 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 FBAEMKit 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 configurationMode = "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 content = "fb_content" 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, acsConfigurationID: nil, businessID: nil, catalogID: nil, isTestMode: false, hasStoreKitAdNetwork: 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) let analyticsAppID = "analytics_123" override func setUp() { super.setUp() AEMReporter.reset() removeReportFile() AEMReporter.configure( networker: networker, appID: "123", reporter: reporter, analyticsAppID: analyticsAppID, store: userDefaultsSpy ) // Actual queue doesn't matter as long as it's not the same as the designated queue name in the class AEMReporter.serialQueue = DispatchQueue(label: name, qos: .background) AEMReporter.isAEMReportEnabled = true AEMReporter.reportFile = reportFilePath } func testEnable() { AEMReporter.enable() XCTAssertFalse(AEMReporter.isAEMReportEnabled, "AEM Report should not 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 testAdvertiserRuleMatchInServerEnabledDefaultConfigure() { XCTAssertFalse( AEMReporter.isAdvertiserRuleMatchInServerEnabled, "AEM Advertiser Rule Match in server should be disabled by default" ) } func testSetAdvertiserRuleMatchInServerEnabled() { AEMReporter.isAdvertiserRuleMatchInServerEnabled = false AEMReporter.setAdvertiserRuleMatchInServerEnabled(true) XCTAssertTrue( AEMReporter.isAdvertiserRuleMatchInServerEnabled, "AEM Advertiser Rule Match in server 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.dataStore as? UserDefaultsSpy, "Should configure with the expected data store" ) XCTAssertEqual( AEMReporter.analyticsAppID, analyticsAppID, "Should configure with the expected analytics app id" ) } 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() 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 testClearCache() { AEMReporter.addConfigurations([SampleAEMData.validConfigurationData1]) AEMReporter.addConfigurations([SampleAEMData.validConfigurationData1, SampleAEMData.validConfigurationData2]) AEMReporter.clearCache() var configurations = AEMReporter.configurations var configList = configurations[Values.defaultMode] XCTAssertEqual(configList?.count, 1, "Should have the expected number of configuration") guard let invocation1 = AEMInvocation( campaignID: "test_campaign_1234", acsToken: "test_token_1234567", acsSharedSecret: "test_shared_secret", acsConfigurationID: "test_config_id_123", businessID: nil, catalogID: nil, isTestMode: false, hasStoreKitAdNetwork: false, isConversionFilteringEligible: true ), let invocation2 = AEMInvocation( campaignID: "test_campaign_1234", acsToken: "test_token_1234567", acsSharedSecret: "test_shared_secret", acsConfigurationID: "test_config_id_123", businessID: nil, catalogID: nil, isTestMode: false, hasStoreKitAdNetwork: false, isConversionFilteringEligible: true ) else { return XCTFail("Unwrapping Error") } invocation1.configurationID = 10000 invocation2.configurationID = 10001 guard let date = Calendar.current.date(byAdding: .day, value: -2, to: Date()) else { return XCTFail("Date Creation Error") } invocation2.conversionTimestamp = date AEMReporter.invocations = [invocation1, invocation2] AEMReporter.addConfigurations( [SampleAEMData.validConfigurationData1, SampleAEMData.validConfigurationData2, SampleAEMData.validConfigData3] ) AEMReporter.clearCache() let invocations = AEMReporter.invocations XCTAssertEqual(invocations.count, 1, "Should clear the expired invocation") XCTAssertEqual(invocations[0].configurationID, 10000, "Should keep the expected invocation") configurations = AEMReporter.configurations configList = configurations[Values.defaultMode] XCTAssertEqual(configList?.count, 2, "Should have the expected number of configuration") XCTAssertEqual(configList?[0].validFrom, 10000, "Should keep the expected ") XCTAssertEqual(configList?[1].validFrom, 20000, "Should keep the expected ") } func testClearConfigurations() { AEMReporter.configurations = [ Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()], Values.brandMode: [SampleAEMConfigurations.createConfigurationWithBusinessIDAndContentRule()], Values.cpasMode: [SampleAEMConfigurations.createCpasConfiguration()], ] AEMReporter.clearConfigurations() let defaultConfigurations = AEMReporter.configurations[Values.defaultMode] let brandConfigurations = AEMReporter.configurations[Values.brandMode] let cpasConfigurations = AEMReporter.configurations[Values.cpasMode] XCTAssertEqual( defaultConfigurations?.count, 1, "Should have default mode " ) XCTAssertEqual( brandConfigurations?.count, 0, "Should not have brand mode " ) XCTAssertEqual( cpasConfigurations?.count, 0, "Should not have cpas mode " ) } 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.configRefreshTimestamp = 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.configRefreshTimestamp = date XCTAssertFalse( AEMReporter.isConfigRefreshTimestampValid(), "Timestamp should not be valid" ) } func testShouldEnforceRefresh() { AEMReporter.invocations = [SampleAEMData.invocationWithoutAdvertiserID] AEMReporter.configRefreshTimestamp = Date() AEMReporter.configurations = [ Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()], ] XCTAssertTrue( AEMReporter.shouldRefresh(withIsForced: true), "Should refresh if it's enforced" ) } func testShouldRefreshWithoutBusinessID1() { AEMReporter.invocations = [SampleAEMData.invocationWithoutAdvertiserID] AEMReporter.configRefreshTimestamp = Date() AEMReporter.configurations = [ Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()], ] XCTAssertFalse( AEMReporter.shouldRefresh(withIsForced: false), "Should not refresh 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.configRefreshTimestamp = date AEMReporter.configurations = [ Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()], ] XCTAssertTrue( AEMReporter.shouldRefresh(withIsForced: false), "Should not refresh 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.configRefreshTimestamp = date AEMReporter.configurations = [:] XCTAssertTrue( AEMReporter.shouldRefresh(withIsForced: false), "Should not refresh if configuration is empty" ) } func testShouldRefreshWithBusinessID() { AEMReporter.invocations = [ SampleAEMData.invocationWithoutAdvertiserID, SampleAEMData.invocationWithAdvertiserID1, ] AEMReporter.configRefreshTimestamp = Date() AEMReporter.configurations = [ Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()], ] XCTAssertTrue( AEMReporter.shouldRefresh(withIsForced: false), "Should not refresh 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 testRuleMatchRequestParameters() { let businessIDs = ["123"] let content = #"[{"id": "123", "quantity": 5}]"# let parameters = AEMReporter.ruleMatchRequestParameters(businessIDs, content: content) let expected = [ "advertiser_ids": #"["123"]"#, "fb_content_data": content, ] XCTAssertEqual( parameters as? [String: String], expected, "Rule match request parameter is not expected" ) } 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.configRefreshTimestamp = Date() guard let invocation = AEMInvocation( campaignID: "test_campaign_1234", acsToken: "test_token_1234567", acsSharedSecret: "test_shared_secret", acsConfigurationID: "test_config_id_123", businessID: nil, catalogID: nil, isTestMode: false, hasStoreKitAdNetwork: false, isConversionFilteringEligible: true ) else { return XCTFail("Unwrapping Error") } guard let configuration = AEMConfiguration(json: SampleAEMData.validConfigData3) else { return XCTFail("Unwrapping Error") } AEMReporter.configurations = [Values.defaultMode: [configuration]] 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 as? [String: [String: Int]], [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.isAEMReportEnabled = false AEMReporter.configRefreshTimestamp = date AEMReporter.recordAndUpdate(event: Values.purchase, currency: Values.USD, value: 100, parameters: nil) XCTAssertNil( networker.capturedGraphPath, "Should not create a request to fetch the if AEM is disabled" ) } func testRecordAndUpdateEventsWithEmptyEvent() { AEMReporter.configRefreshTimestamp = date AEMReporter.recordAndUpdate(event: "", currency: Values.USD, value: 100, parameters: nil) XCTAssertNil( networker.capturedGraphPath, "Should not create a request to fetch the if the event being recorded is empty" ) XCTAssertFalse( FileManager.default.fileExists(atPath: reportFilePath), "Should not save an empty event to disk" ) } func testRecordAndUpdateEventsWithEmptyConfigurations() throws { AEMReporter.configRefreshTimestamp = 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 configuration = AEMConfiguration(json: SampleAEMData.validConfigData3) else { return XCTFail("Unwrapping Error") } AEMReporter.configRefreshTimestamp = Date() AEMReporter.configurations = [Values.defaultMode: [configuration]] 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 configuration = AEMConfiguration(json: SampleAEMData.validConfigData3) else { return XCTFail("Unwrapping Error") } var blockCall = 0 AEMReporter.configRefreshTimestamp = Date() AEMReporter.configurations = [Values.defaultMode: [configuration]] AEMReporter.loadConfiguration(withRefreshForced: false) { _ in blockCall += 1 } XCTAssertEqual( blockCall, 1, "Should call the completion when loading the configuration" ) } func testLoadConfigurationWithoutBlock() { AEMReporter.configRefreshTimestamp = 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 = [SampleAEMData.invocationWithoutAdvertiserID] XCTAssertEqual( AEMReporter.requestParameters() as NSDictionary, ["fields": "", "advertiser_ids": "[]"], "Should not have unexpected advertiserIDs in configuration request params" ) } func testGetConfigRequestParameterWithAdvertiserIDs() { AEMReporter.invocations = [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 configuration request params" ) AEMReporter.invocations = [ SampleAEMData.invocationWithAdvertiserID1, SampleAEMData.invocationWithAdvertiserID2, SampleAEMData.invocationWithoutAdvertiserID, ] XCTAssertEqual( AEMReporter.requestParameters() as NSDictionary, ["fields": "", "advertiser_ids": #"["\#(SampleAEMData.invocationWithAdvertiserID1.businessID!)","\#(SampleAEMData.invocationWithAdvertiserID2.businessID!)"]"#], // swiftlint:disable:this force_unwrapping "Should have expected advertiserIDs in configuration 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 configurations = [ Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()], Values.brandMode: [SampleAEMConfigurations.createConfigurationWithBusinessID()], ] let attributedInvocation = AEMReporter.attributedInvocation( invocations, event: Values.purchase, currency: nil, value: nil, parameters: nil, configurations: configurations ) 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 configurations = [ Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()], Values.brandMode: [SampleAEMConfigurations.createConfigurationWithBusinessID()], ] let attributedInvocation = AEMReporter.attributedInvocation( invocations, event: "test", currency: nil, value: nil, parameters: ["values": "abcdefg"], configurations: configurations ) XCTAssertNil( attributedInvocation, "Should not have invocation attributed" ) } func testAttributedInvocationWithUnmatchedParameters() { let invocations = [ SampleAEMData.invocationWithoutAdvertiserID, SampleAEMData.invocationWithAdvertiserID1, SampleAEMData.invocationWithAdvertiserID2, ] let configurations = [ Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()], Values.brandMode: [SampleAEMConfigurations.createConfigurationWithBusinessID()], ] let attributedInvocation = AEMReporter.attributedInvocation( invocations, event: Values.purchase, currency: nil, value: nil, parameters: ["value": "abcdefg"], configurations: configurations ) 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 configurations = [ Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()], Values.brandMode: [SampleAEMConfigurations.createConfigurationWithBusinessID()], ] let attributedInvocation = AEMReporter.attributedInvocation( invocations, event: Values.purchase, currency: nil, value: nil, parameters: nil, configurations: configurations ) 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 configurations = [ Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()], Values.brandMode: [SampleAEMConfigurations.createConfigurationWithBusinessID()], ] let attributedInvocation = AEMReporter.attributedInvocation( invocations, event: "test", currency: nil, value: nil, parameters: nil, configurations: configurations ) XCTAssertNil( attributedInvocation, "Should not attribute the event with incorrect event" ) } func testAttributedInvocationWithDoubleCounting() { reporter.cutOff = false reporter.reportingEvents = [Values.purchase] let invocation = SampleAEMInvocations.createSKANOverlappedInvocation() let configurations = [ Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()], ] let attributedInvocation = AEMReporter.attributedInvocation( [invocation], event: Values.purchase, currency: Values.USD, value: 10, parameters: ["value": "abcdefg"], configurations: configurations ) XCTAssertNil( attributedInvocation, "Should not have invocation attributed with double counting" ) XCTAssertTrue( invocation.recordedEvents.isEmpty, "Should not expect invocation's recorded events to be changed with double counting" ) XCTAssertTrue( invocation.recordedValues.isEmpty, "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 configurations = [ Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()], ] let attributedInvocation = AEMReporter.attributedInvocation( [invocation], event: Values.purchase, currency: Values.USD, value: 10, parameters: ["value": "abcdefg"], configurations: configurations ) 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: - Rule Match in Server func testLoadRuleMatch() { let content = #"[{"id": "123", "quantity": 5}]"# AEMReporter.loadRuleMatch(["123"], event: "test", currency: nil, value: nil, parameters: [Keys.content: content]) let expectedParameters = [ "advertiser_ids": #"["123"]"#, "fb_content_data": content, ] XCTAssertTrue( (networker.capturedGraphPath?.contains("aem_attribution")) == true, "Should start the rule match request" ) XCTAssertEqual( networker.capturedParameters as? [String: String], expectedParameters, "Should have the expected parameters in the rule match request" ) } // 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 {} } }