diff --git a/Package.swift b/Package.swift index 236d43f..feaffae 100644 --- a/Package.swift +++ b/Package.swift @@ -28,5 +28,18 @@ let package = Package( exclude: ["Info.plist", "dummy.swift"], resources: [.process("PrivacyInfo.xcprivacy")], publicHeadersPath: "."), + + .testTarget( + name: "mParticle-Google-Analytics-Firebase-Swift-Tests", + dependencies: ["mParticle-Google-Analytics-Firebase"], + path: "mParticle-Google-Analytics-FirebaseTests/Swift" + ), + + .testTarget( + name: "mParticle-Google-Analytics-Firebase-Objc-Tests", + dependencies: ["mParticle-Google-Analytics-Firebase"], + path: "mParticle-Google-Analytics-FirebaseTests/Objc", + resources: [.process("GoogleService-Info.plist")] + ) ] ) diff --git a/mParticle-Google-Analytics-Firebase/MPKitFirebaseAnalytics.h b/mParticle-Google-Analytics-Firebase/MPKitFirebaseAnalytics.h index 9910502..530d4e1 100755 --- a/mParticle-Google-Analytics-Firebase/MPKitFirebaseAnalytics.h +++ b/mParticle-Google-Analytics-Firebase/MPKitFirebaseAnalytics.h @@ -17,6 +17,15 @@ @property (nonatomic, unsafe_unretained, readonly) BOOL started; @property (nonatomic, strong, nullable) MPKitAPI *kitApi; +- (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 kMPFIRGoogleAppIDKey = @"firebaseAppId"; diff --git a/mParticle-Google-Analytics-Firebase/MPKitFirebaseAnalytics.m b/mParticle-Google-Analytics-Firebase/MPKitFirebaseAnalytics.m index 8571605..cedd7ec 100755 --- a/mParticle-Google-Analytics-Firebase/MPKitFirebaseAnalytics.m +++ b/mParticle-Google-Analytics-Firebase/MPKitFirebaseAnalytics.m @@ -13,6 +13,19 @@ #endif #endif +@implementation NSString(PRIVATE) + +- (NSNumber*)isGranted { + if ([self isEqualToString:@"Granted"]) { + return @(YES); + } else if ([self isEqualToString:@"Denied"]) { + return @(NO); + } + return nil; +} + +@end + @interface MPKitFirebaseAnalytics () { BOOL forwardRequestsServerSide; } @@ -77,6 +90,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) { @@ -88,7 +106,7 @@ - (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configu [self updateInstanceIDIntegration]; } - [self updateConsent]; + [self updateConsent: consentState]; _started = YES; @@ -356,21 +374,19 @@ - (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) { mappingsConfig = [self convertToKeyValuePairs: mappings]; } - - MParticleUser *currentUser = [[[MParticle sharedInstance] identity] currentUser]; - NSDictionary *gdprConsents = currentUser.consentState.gdprConsentState; + NSDictionary *gdprConsents = consentState.gdprConsentState; NSNumber *adStorage = [self resolvedConsentForMappingKey:kMPFIRGAAdStorageKey defaultKey:kMPFIRGA4DefaultAdStorageKey @@ -650,16 +666,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-FirebaseTests/Info.plist b/mParticle-Google-Analytics-FirebaseTests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/mParticle-Google-Analytics-FirebaseTests/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-FirebaseTests/GoogleService-Info.plist b/mParticle-Google-Analytics-FirebaseTests/Objc/GoogleService-Info.plist similarity index 100% rename from mParticle-Google-Analytics-FirebaseTests/GoogleService-Info.plist rename to mParticle-Google-Analytics-FirebaseTests/Objc/GoogleService-Info.plist diff --git a/mParticle-Google-Analytics-FirebaseTests/MPKitFirebaseAnalyticsTests.m b/mParticle-Google-Analytics-FirebaseTests/Objc/MPKitFirebaseAnalyticsTests.m similarity index 90% rename from mParticle-Google-Analytics-FirebaseTests/MPKitFirebaseAnalyticsTests.m rename to mParticle-Google-Analytics-FirebaseTests/Objc/MPKitFirebaseAnalyticsTests.m index eebff2e..598d3c1 100644 --- a/mParticle-Google-Analytics-FirebaseTests/MPKitFirebaseAnalyticsTests.m +++ b/mParticle-Google-Analytics-FirebaseTests/Objc/MPKitFirebaseAnalyticsTests.m @@ -20,8 +20,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_mParticle-Google-Analytics-Firebase-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-FirebaseTests/Swift/MPKitFirebaseSwiftTests.swift b/mParticle-Google-Analytics-FirebaseTests/Swift/MPKitFirebaseSwiftTests.swift new file mode 100644 index 0000000..4a82c50 --- /dev/null +++ b/mParticle-Google-Analytics-FirebaseTests/Swift/MPKitFirebaseSwiftTests.swift @@ -0,0 +1,118 @@ +// +// File.swift +// mParticle-Google-Analytics-Firebase +// +// Created by Nick Dimitrakas on 9/16/25. +// + +import XCTest +@testable import mParticle_Google_Analytics_Firebase + +final class MPKitFirebaseSwiftTests: XCTestCase { + + var kit: MPKitFirebaseAnalytics! + + // MARK: - Lifecycle + + override func setUpWithError() throws { + try super.setUpWithError() + kit = MPKitFirebaseAnalytics() + 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) + } +}