diff --git a/Package.swift b/Package.swift index 8d16edc..d5f296e 100644 --- a/Package.swift +++ b/Package.swift @@ -43,5 +43,18 @@ let package = Package( exclude: ["Info.plist", "dummy.swift"], resources: [.process("PrivacyInfo.xcprivacy")], publicHeadersPath: "."), + + .testTarget( + name: "mParticle-Google-Analytics-Firebase-GA4-Swift-Tests", + dependencies: ["mParticle-Google-Analytics-Firebase-GA4"], + path: "mParticle-Google-Analytics-Firebase-GA4Tests/Swift" + ), + + .testTarget( + name: "mParticle-Google-Analytics-Firebase-GA4-Objc-Tests", + dependencies: ["mParticle-Google-Analytics-Firebase-GA4"], + path: "mParticle-Google-Analytics-Firebase-GA4Tests/Objc", + resources: [.process("GoogleService-Info.plist")] + ) ] ) diff --git a/mParticle-Google-Analytics-Firebase-GA4/MPKitFirebaseGA4Analytics.h b/mParticle-Google-Analytics-Firebase-GA4/MPKitFirebaseGA4Analytics.h index b5b9707..4d3cb2f 100755 --- a/mParticle-Google-Analytics-Firebase-GA4/MPKitFirebaseGA4Analytics.h +++ b/mParticle-Google-Analytics-Firebase-GA4/MPKitFirebaseGA4Analytics.h @@ -20,6 +20,15 @@ + (void)setCustomNameStandardization:(NSString * _Nonnull (^_Nullable)(NSString * _Nonnull name))standardization; + (NSString * _Nonnull (^_Nullable)(NSString * _Nonnull name))customNameStandardization; +- (nullable NSNumber *)resolvedConsentForMappingKey:(NSString * _Nonnull)mappingKey + defaultKey:(NSString * _Nonnull)defaultKey + gdprConsents:(NSDictionary * _Nonnull)gdprConsents + mapping:(NSDictionary * _Nullable)mapping; + +- (nullable NSArray*)mappingForKey:(NSString* _Nonnull)key; + +- (nonnull NSDictionary*)convertToKeyValuePairs: (NSArray * _Nonnull)mappings; + @end static NSString * _Nonnull const kMPFIRGA4ExternalUserIdentityType = @"externalUserIdentityType"; diff --git a/mParticle-Google-Analytics-Firebase-GA4/MPKitFirebaseGA4Analytics.m b/mParticle-Google-Analytics-Firebase-GA4/MPKitFirebaseGA4Analytics.m index 1b67c47..192c8ef 100755 --- a/mParticle-Google-Analytics-Firebase-GA4/MPKitFirebaseGA4Analytics.m +++ b/mParticle-Google-Analytics-Firebase-GA4/MPKitFirebaseGA4Analytics.m @@ -15,6 +15,19 @@ static NSString* (^customNameStandardization)(NSString* name) = nil; +@implementation NSString(PRIVATE) + +- (NSNumber*)isGranted { + if ([self isEqualToString:@"Granted"]) { + return @(YES); + } else if ([self isEqualToString:@"Denied"]) { + return @(NO); + } + return nil; +} + +@end + @interface MPKitFirebaseGA4Analytics () { BOOL forwardRequestsServerSide; } @@ -89,6 +102,11 @@ - (MPKitExecStatus *)execStatus:(MPKitReturnCode)returnCode { #pragma mark MPKitInstanceProtocol methods - (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configuration { + MParticleUser *currentUser = [[[MParticle sharedInstance] identity] currentUser]; + return [self didFinishLaunchingWithConfiguration:configuration withConsentState:currentUser.consentState]; +} + +- (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configuration withConsentState: (MPConsentState *)consentState { _configuration = configuration; if ([FIRApp defaultApp] == nil) { @@ -100,8 +118,7 @@ - (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configu } [self updateInstanceIDIntegration]; - - [self updateConsent]; + [self updateConsent: consentState]; _started = YES; @@ -382,12 +399,12 @@ - (void)logUserAttributes:(NSDictionary *)userAttributes { } - (MPKitExecStatus *)setConsentState:(nullable MPConsentState *)state { - [self updateConsent]; + [self updateConsent: state]; return [self execStatus:MPKitReturnCodeSuccess]; } -- (void)updateConsent { +- (void)updateConsent:(MPConsentState *)consentState { NSArray *mappings = [self mappingForKey: @"consentMappingSDK"]; NSDictionary *mappingsConfig; if (mappings != nil) { @@ -395,8 +412,8 @@ - (void)updateConsent { } - MParticleUser *currentUser = [[[MParticle sharedInstance] identity] currentUser]; - NSDictionary *gdprConsents = currentUser.consentState.gdprConsentState; + + NSDictionary *gdprConsents = consentState.gdprConsentState; NSNumber *adStorage = [self resolvedConsentForMappingKey:kMPFIRGA4AdStorageKey defaultKey:kMPFIRGA4DefaultAdStorageKey @@ -684,16 +701,11 @@ - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey // Fallback to configuration defaults NSString *value = self->_configuration[defaultKey]; - if ([value isEqualToString:@"Granted"]) { - return @(YES); - } else if ([value isEqualToString:@"Denied"]) { - return @(NO); - } - return nil; + return [value isGranted]; } - (NSArray*)mappingForKey:(NSString*)key { - NSString *mappingJson = _configuration[@"consentMappingSDK"]; + NSString *mappingJson = _configuration[key]; if (![mappingJson isKindOfClass:[NSString class]]) { return nil; } diff --git a/mParticle-Google-Analytics-Firebase-GA4Tests/Info.plist b/mParticle-Google-Analytics-Firebase-GA4Tests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/mParticle-Google-Analytics-Firebase-GA4Tests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/mParticle-Google-Analytics-Firebase-GA4Tests/GoogleService-Info.plist b/mParticle-Google-Analytics-Firebase-GA4Tests/Objc/GoogleService-Info.plist similarity index 100% rename from mParticle-Google-Analytics-Firebase-GA4Tests/GoogleService-Info.plist rename to mParticle-Google-Analytics-Firebase-GA4Tests/Objc/GoogleService-Info.plist diff --git a/mParticle-Google-Analytics-Firebase-GA4Tests/MPKitFirebaseGA4AnalyticsTests.m b/mParticle-Google-Analytics-Firebase-GA4Tests/Objc/MPKitFirebaseGA4AnalyticsTests.m similarity index 96% rename from mParticle-Google-Analytics-Firebase-GA4Tests/MPKitFirebaseGA4AnalyticsTests.m rename to mParticle-Google-Analytics-Firebase-GA4Tests/Objc/MPKitFirebaseGA4AnalyticsTests.m index ddeedd2..6c50632 100644 --- a/mParticle-Google-Analytics-Firebase-GA4Tests/MPKitFirebaseGA4AnalyticsTests.m +++ b/mParticle-Google-Analytics-Firebase-GA4Tests/Objc/MPKitFirebaseGA4AnalyticsTests.m @@ -22,8 +22,22 @@ @interface mParticle_Firebase_AnalyticsTests : XCTestCase @implementation mParticle_Firebase_AnalyticsTests - (void)setUp { - NSString *bundlePath = [[NSBundle bundleForClass:[self class]] resourcePath]; - NSString *filePath = [bundlePath stringByAppendingPathComponent:@"GoogleService-Info.plist"]; + [super setUp]; + + // 1. Start with the test bundle + NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; + + // 2. Locate the auto-generated resource bundle for this test target + NSURL *resourceBundleURL = [testBundle URLForResource:@"mParticle-Google-Analytics-Firebase-GA4_mParticle-Google-Analytics-Firebase-GA4-Objc-Tests" + withExtension:@"bundle"]; + NSBundle *resourceBundle = [NSBundle bundleWithURL:resourceBundleURL]; + NSAssert(resourceBundle != nil, @"Resource bundle not found"); + + // 3. Fetch the plist inside that resource bundle + NSString *filePath = [resourceBundle pathForResource:@"GoogleService-Info" ofType:@"plist"]; + NSAssert(filePath != nil, @"GoogleService-Info.plist not found in resource bundle"); + + // 4. Configure Firebase FIROptions *options = [[FIROptions alloc] initWithContentsOfFile:filePath]; [FIRApp configureWithOptions:options]; } diff --git a/mParticle-Google-Analytics-Firebase-GA4Tests/Swift/MPKitFirebaseGA4SwiftTests.swift b/mParticle-Google-Analytics-Firebase-GA4Tests/Swift/MPKitFirebaseGA4SwiftTests.swift new file mode 100644 index 0000000..b790c8d --- /dev/null +++ b/mParticle-Google-Analytics-Firebase-GA4Tests/Swift/MPKitFirebaseGA4SwiftTests.swift @@ -0,0 +1,118 @@ +// +// MPKitFirebaseGA4SwiftTests.swift +// mParticle-Google-Analytics-Firebase-GA4 +// +// Created by Nick Dimitrakas on 9/12/25. +// + +import XCTest +@testable import mParticle_Google_Analytics_Firebase_GA4 + +final class MPKitFirebaseGA4AnalyticsTests: XCTestCase { + + var kit: MPKitFirebaseGA4Analytics! + + // MARK: - Lifecycle + + override func setUpWithError() throws { + try super.setUpWithError() + kit = MPKitFirebaseGA4Analytics() + kit.configuration = [:] + } + + override func tearDownWithError() throws { + kit = nil + try super.tearDownWithError() + } + + // MARK: - convertToKeyValuePairs + + func test_convertToKeyValuePairs_createsLowercasedMapping() { + let mappings: [[String: String]] = [ + ["value": "ad_storage", "map": "Advertising"], + ["value": "analytics_storage", "map": "Analytics"] + ] + + let result = kit.convert(toKeyValuePairs: mappings) + XCTAssertEqual(result["ad_storage"] as! String, "advertising") + XCTAssertEqual(result["analytics_storage"] as! String, "analytics") + } + + // MARK: - mappingForKey + + func test_mappingForKey_withValidJSON_returnsArray() { + let jsonString = """ + [ + { "value": "ad_storage", "map": "Advertising" }, + { "value": "analytics_storage", "map": "Analytics" } + ] + """ + kit.configuration["consentMappingSDK"] = jsonString + + let result = kit.mapping(forKey: "consentMappingSDK") + XCTAssertNotNil(result) + XCTAssertEqual(result!.count, 2) + } + + func test_mappingForKey_withInvalidJSON_returnsNil() { + kit.configuration["consentMappingSDK"] = "{ not valid json }" + let result = kit.mapping(forKey: "consentMappingSDK") + XCTAssertNil(result) + } + + // MARK: - resolvedConsentForMappingKey + + func test_resolvedConsentForMappingKey_withGDPRMapping_returnsTrue() { + let consent = MPGDPRConsent() + consent.consented = true + let gdprConsents = ["advertising": consent] + + let mapping = ["ad_storage": "advertising"] + + let result = kit.resolvedConsent( + forMappingKey: "ad_storage", + defaultKey: "defaultAdStorageConsentSDK", + gdprConsents: gdprConsents, + mapping: mapping + ) + XCTAssertEqual(result, true) + } + + func test_resolvedConsentForMappingKey_withGDPRMapping_returnsFalse() { + let consent = MPGDPRConsent() + consent.consented = false + let gdprConsents = ["advertising": consent] + + let mapping = ["ad_storage": "advertising"] + + let result = kit.resolvedConsent( + forMappingKey: "ad_storage", + defaultKey: "defaultAdStorageConsentSDK", + gdprConsents: gdprConsents, + mapping: mapping + ) + XCTAssertEqual(result, false) + } + + func test_resolvedConsentForMappingKey_withDefaultValue_returnsFalse() { + kit.configuration["defaultAdStorageConsentSDK"] = "Denied" + + let result = kit.resolvedConsent( + forMappingKey: "ad_storage", + defaultKey: "defaultAdStorageConsentSDK", + gdprConsents: [:], + mapping: [:] + ) + XCTAssertEqual(result, false) + } + + func test_resolvedConsentForMappingKey_withNoMappingOrDefault_returnsNil() { + let result = kit.resolvedConsent( + forMappingKey: "ad_storage", + defaultKey: "defaultAdStorageConsentSDK", + gdprConsents: [:], + mapping: [:] + ) + XCTAssertNil(result) + } +}