diff --git a/Crashlytics/Crashlytics/FIRCrashlytics.m b/Crashlytics/Crashlytics/FIRCrashlytics.m index f5c606929ac..1e6e93e540f 100644 --- a/Crashlytics/Crashlytics/FIRCrashlytics.m +++ b/Crashlytics/Crashlytics/FIRCrashlytics.m @@ -171,19 +171,6 @@ - (instancetype)initWithApp:(FIRApp *)app [sessions registerWithSubscriber:self]; } - if (remoteConfig) { - FIRCLSDebugLog(@"Registering RemoteConfig SDK subscription for rollouts data"); - - FIRCLSRolloutsPersistenceManager *persistenceManager = - [[FIRCLSRolloutsPersistenceManager alloc] initWithFileManager:_fileManager]; - _remoteConfigManager = - [[FIRCLSRemoteConfigManager alloc] initWithRemoteConfig:remoteConfig - persistenceDelegate:persistenceManager]; - - // TODO(themisw): Import "firebase" from the interop in the future. - [remoteConfig registerRolloutsStateSubscriber:self for:@"firebase"]; - } - _reportUploader = [[FIRCLSReportUploader alloc] initWithManagerData:_managerData]; _existingReportManager = @@ -216,8 +203,19 @@ - (instancetype)initWithApp:(FIRApp *)app }] catch:^void(NSError *error) { FIRCLSErrorLog(@"Crash reporting failed to initialize with error: %@", error); }]; - } + // RemoteConfig subscription should be made after session report directory created. + if (remoteConfig) { + FIRCLSDebugLog(@"Registering RemoteConfig SDK subscription for rollouts data"); + + FIRCLSRolloutsPersistenceManager *persistenceManager = + [[FIRCLSRolloutsPersistenceManager alloc] initWithFileManager:_fileManager]; + _remoteConfigManager = + [[FIRCLSRemoteConfigManager alloc] initWithRemoteConfig:remoteConfig + persistenceDelegate:persistenceManager]; + [remoteConfig registerRolloutsStateSubscriber:self for:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform]; + } + } return self; } diff --git a/FirebaseRemoteConfig/Interop/RemoteConfigConstants.swift b/FirebaseRemoteConfig/Interop/RemoteConfigConstants.swift new file mode 100644 index 00000000000..f9a10e409b7 --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RemoteConfigConstants.swift @@ -0,0 +1,21 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +@objc(FIRRemoteConfigConstants) +public final class RemoteConfigConstants: NSObject { + @objc(FIRNamespaceGoogleMobilePlatform) public static let NamespaceGoogleMobilePlatform = + "firebase" +} diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 098bde00a9c..561ada50693 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -45,6 +45,8 @@ /// Notification when config is successfully activated const NSNotificationName FIRRemoteConfigActivateNotification = @"FIRRemoteConfigActivateNotification"; +static NSNotificationName FIRRolloutsStateDidChangeNotificationName = + @"FIRRolloutsStateDidChangeNotification"; /// Listener for the get methods. typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull); @@ -79,8 +81,9 @@ @implementation FIRRemoteConfig { *RCInstances; + (nonnull FIRRemoteConfig *)remoteConfigWithApp:(FIRApp *_Nonnull)firebaseApp { - return [FIRRemoteConfig remoteConfigWithFIRNamespace:FIRNamespaceGoogleMobilePlatform - app:firebaseApp]; + return [FIRRemoteConfig + remoteConfigWithFIRNamespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform + app:firebaseApp]; } + (nonnull FIRRemoteConfig *)remoteConfigWithFIRNamespace:(NSString *_Nonnull)firebaseNamespace { @@ -116,8 +119,9 @@ + (FIRRemoteConfig *)remoteConfig { @"initializer in SwiftUI."]; } - return [FIRRemoteConfig remoteConfigWithFIRNamespace:FIRNamespaceGoogleMobilePlatform - app:[FIRApp defaultApp]]; + return [FIRRemoteConfig + remoteConfigWithFIRNamespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform + app:[FIRApp defaultApp]]; } /// Singleton instance of serial queue for queuing all incoming RC calls. @@ -329,16 +333,20 @@ - (void)activateWithCompletion:(FIRRemoteConfigActivateChangeCompletion)completi // New config has been activated at this point FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069", @"Config activated."); [strongSelf->_configContent activatePersonalization]; - // Update activeRolloutMetadata - [strongSelf->_configContent activateRolloutMetadata]; // Update last active template version number in setting and userDefaults. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [strongSelf->_settings updateLastActiveTemplateVersion]; - }); + [strongSelf->_settings updateLastActiveTemplateVersion]; + // Update activeRolloutMetadata + [strongSelf->_configContent activateRolloutMetadata:^(BOOL success) { + if (success) { + [self notifyRolloutsStateChange:strongSelf->_configContent.activeRolloutMetadata + versionNumber:strongSelf->_settings.lastActiveTemplateVersion]; + } + }]; + // Update experiments only for 3p namespace NSString *namespace = [strongSelf->_FIRNamespace substringToIndex:[strongSelf->_FIRNamespace rangeOfString:@":"].location]; - if ([namespace isEqualToString:FIRNamespaceGoogleMobilePlatform]) { + if ([namespace isEqualToString:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform]) { dispatch_async(dispatch_get_main_queue(), ^{ [self notifyConfigHasActivated]; }); @@ -383,6 +391,17 @@ - (NSString *)fullyQualifiedNamespace:(NSString *)namespace { return fullyQualifiedNamespace; } +- (FIRRemoteConfigValue *)defaultValueForFullyQualifiedNamespace:(NSString *)namespace + key:(NSString *)key { + FIRRemoteConfigValue *value = self->_configContent.defaultConfig[namespace][key]; + if (!value) { + value = [[FIRRemoteConfigValue alloc] + initWithData:[NSData data] + source:(FIRRemoteConfigSource)FIRRemoteConfigSourceStatic]; + } + return value; +} + #pragma mark - Get Config Result - (FIRRemoteConfigValue *)objectForKeyedSubscript:(NSString *)key { @@ -408,13 +427,7 @@ - (FIRRemoteConfigValue *)configValueForKey:(NSString *)key { config:[self->_configContent getConfigAndMetadataForNamespace:FQNamespace]]; return; } - value = self->_configContent.defaultConfig[FQNamespace][key]; - if (value) { - return; - } - - value = [[FIRRemoteConfigValue alloc] initWithData:[NSData data] - source:FIRRemoteConfigSourceStatic]; + value = [self defaultValueForFullyQualifiedNamespace:FQNamespace key:key]; }); return value; } @@ -619,4 +632,67 @@ - (FIRConfigUpdateListenerRegistration *)addOnConfigUpdateListener: return [self->_configRealtime addConfigUpdateListener:listener]; } +#pragma mark - Rollout + +- (void)addRemoteConfigInteropSubscriber:(id)subscriber { + [[NSNotificationCenter defaultCenter] + addObserverForName:FIRRolloutsStateDidChangeNotificationName + object:self + queue:nil + usingBlock:^(NSNotification *_Nonnull notification) { + FIRRolloutsState *rolloutsState = + notification.userInfo[FIRRolloutsStateDidChangeNotificationName]; + [subscriber rolloutsStateDidChange:rolloutsState]; + }]; + // Send active rollout metadata stored in persistence while app launched if there is activeConfig + NSString *fullyQualifiedNamespace = [self fullyQualifiedNamespace:_FIRNamespace]; + NSDictionary *activeConfig = self->_configContent.activeConfig; + if (activeConfig[fullyQualifiedNamespace] && activeConfig[fullyQualifiedNamespace].count > 0) { + [self notifyRolloutsStateChange:self->_configContent.activeRolloutMetadata + versionNumber:self->_settings.lastActiveTemplateVersion]; + } +} + +- (void)notifyRolloutsStateChange:(NSArray *)rolloutMetadata + versionNumber:(NSString *)versionNumber { + NSArray *rolloutsAssignments = + [self rolloutsAssignmentsWith:rolloutMetadata versionNumber:versionNumber]; + FIRRolloutsState *rolloutsState = + [[FIRRolloutsState alloc] initWithAssignmentList:rolloutsAssignments]; + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069", + @"Send rollouts state notification with name %@ to RemoteConfigInterop.", + FIRRolloutsStateDidChangeNotificationName); + [[NSNotificationCenter defaultCenter] + postNotificationName:FIRRolloutsStateDidChangeNotificationName + object:self + userInfo:@{FIRRolloutsStateDidChangeNotificationName : rolloutsState}]; +} + +- (NSArray *)rolloutsAssignmentsWith: + (NSArray *)rolloutMetadata + versionNumber:(NSString *)versionNumber { + NSMutableArray *rolloutsAssignments = [[NSMutableArray alloc] init]; + NSString *FQNamespace = [self fullyQualifiedNamespace:_FIRNamespace]; + for (NSDictionary *metadata in rolloutMetadata) { + NSString *rolloutId = metadata[RCNFetchResponseKeyRolloutID]; + NSString *variantID = metadata[RCNFetchResponseKeyVariantID]; + NSArray *affectedParameterKeys = metadata[RCNFetchResponseKeyAffectedParameterKeys]; + if (rolloutId && variantID && affectedParameterKeys) { + for (NSString *key in affectedParameterKeys) { + FIRRemoteConfigValue *value = self->_configContent.activeConfig[FQNamespace][key]; + if (!value) { + value = [self defaultValueForFullyQualifiedNamespace:FQNamespace key:key]; + } + FIRRolloutAssignment *assignment = + [[FIRRolloutAssignment alloc] initWithRolloutId:rolloutId + variantId:variantID + templateVersion:[versionNumber longLongValue] + parameterKey:key + parameterValue:value.stringValue]; + [rolloutsAssignments addObject:assignment]; + } + } + } + return rolloutsAssignments; +} @end diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m index 08927453adb..81055451ae4 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m @@ -148,8 +148,8 @@ + (void)load { - (void)registerRolloutsStateSubscriber:(id)subscriber for:(NSString * _Nonnull)namespace { - // TODO(Themisw): Adding the registered subscriber reference to the namespace instance - // [self.instances[namespace] addRemoteConfigInteropSubscriber:subscriber]; + FIRRemoteConfig *instance = [self remoteConfigForNamespace:namespace]; + [instance addRemoteConfigInteropSubscriber:subscriber]; } @end diff --git a/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h b/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h index ef7def6fd9d..4420dcb2679 100644 --- a/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h +++ b/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h @@ -23,6 +23,7 @@ @class RCNConfigFetch; @class RCNConfigRealtime; @protocol FIRAnalyticsInterop; +@protocol FIRRolloutsStateSubscriber; NS_ASSUME_NONNULL_BEGIN @@ -78,6 +79,9 @@ NS_ASSUME_NONNULL_BEGIN configContent:(RCNConfigContent *)configContent analytics:(nullable id)analytics; +/// Register RolloutsStateSubcriber to FIRRemoteConfig instance +- (void)addRemoteConfigInteropSubscriber:(id _Nonnull)subscriber; + @end NS_ASSUME_NONNULL_END diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.h b/FirebaseRemoteConfig/Sources/RCNConfigContent.h index b14c2a8aceb..e8410074b30 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.h @@ -39,6 +39,8 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { @property(nonatomic, readonly, copy) NSDictionary *activeConfig; /// Local default config that is provided by external users; @property(nonatomic, readonly, copy) NSDictionary *defaultConfig; +/// Active Rollout metadata that is currently used. +@property(nonatomic, readonly, copy) NSArray *activeRolloutMetadata; - (instancetype)init NS_UNAVAILABLE; @@ -65,8 +67,8 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { /// Gets the active config and Personalization metadata. - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace; -/// Sets the fetched rollout metadata to active and return the active rollout metadata. -- (NSArray *)activateRolloutMetadata; +/// Sets the fetched rollout metadata to active with a success completion handler. +- (void)activateRolloutMetadata:(void (^)(BOOL success))completionHandler; /// Returns the updated parameters between fetched and active config. - (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index 7746651b8d2..1c266734c40 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -291,12 +291,13 @@ - (void)activatePersonalization { fromSource:RCNDBSourceActive]; } -- (NSArray *)activateRolloutMetadata { +- (void)activateRolloutMetadata:(void (^)(BOOL success))completionHandler { _activeRolloutMetadata = _fetchedRolloutMetadata; [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyActiveMetadata value:_activeRolloutMetadata - completionHandler:nil]; - return _activeRolloutMetadata; + completionHandler:^(BOOL success, NSDictionary *result) { + completionHandler(success); + }]; } #pragma mark State handling @@ -364,7 +365,7 @@ - (void)handleUpdatePersonalization:(NSDictionary *)metadata { - (void)handleUpdateRolloutFetchedMetadata:(NSArray *)metadata { if (!metadata) { - return; + metadata = [[NSArray alloc] init]; } _fetchedRolloutMetadata = metadata; [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata @@ -399,6 +400,11 @@ - (NSDictionary *)activePersonalization { return _activePersonalization; } +- (NSArray *)activeRolloutMetadata { + [self checkAndWaitForInitialDatabaseLoad]; + return _activeRolloutMetadata; +} + - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace { /// If this is the first time reading the active metadata, we might still be reading it from the /// database. diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m index d535738f91d..dbc4b9bec56 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m @@ -25,6 +25,7 @@ #import "FirebaseRemoteConfig/Sources/RCNConfigContent.h" #import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" #import "FirebaseRemoteConfig/Sources/RCNDevice.h" +@import FirebaseRemoteConfigInterop; #ifdef RCN_STAGING_SERVER static NSString *const kServerURLDomain = @@ -572,7 +573,7 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties // Update experiments only for 3p namespace NSString *namespace = [strongSelf->_FIRNamespace substringToIndex:[strongSelf->_FIRNamespace rangeOfString:@":"].location]; - if ([namespace isEqualToString:FIRNamespaceGoogleMobilePlatform]) { + if ([namespace isEqualToString:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform]) { [strongSelf->_experiment updateExperimentsWithResponse: fetchedConfig[RCNFetchResponseKeyExperimentDescriptions]]; } diff --git a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m index 5672351a7ee..e85a63f4873 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m @@ -293,6 +293,7 @@ - (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess [self updateLastFetchTimeInterval:[[NSDate date] timeIntervalSince1970]]; // Note: We expect the googleAppID to always be available. _deviceContext = FIRRemoteConfigDeviceContextWithProjectIdentifier(_googleAppID); + _lastFetchedTemplateVersion = templateVersion; [_userDefaultsManager setLastFetchedTemplateVersion:templateVersion]; } diff --git a/FirebaseRemoteConfig/Sources/RCNConstants3P.m b/FirebaseRemoteConfig/Sources/RCNConstants3P.m deleted file mode 100644 index 6bd5d78d094..00000000000 --- a/FirebaseRemoteConfig/Sources/RCNConstants3P.m +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2019 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h" - -/// Firebase Remote Config service default namespace. -NSString *const FIRNamespaceGoogleMobilePlatform = @"firebase"; diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m index 57c766a9035..e57cc930ea2 100644 --- a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m @@ -21,6 +21,7 @@ #import #import "../../../Sources/Private/FIRRemoteConfig_Private.h" #import "FRCLog.h" +@import FirebaseRemoteConfigInterop; static NSString *const FIRPerfNamespace = @"fireperf"; static NSString *const FIRDefaultFIRAppName = @"__FIRAPP_DEFAULT"; @@ -81,7 +82,8 @@ - (void)viewDidLoad { // TODO(mandard): Add support for deleting and adding namespaces in the app. self.namespacePickerData = - [[NSArray alloc] initWithObjects:FIRNamespaceGoogleMobilePlatform, FIRPerfNamespace, nil]; + [[NSArray alloc] initWithObjects:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform, + FIRPerfNamespace, nil]; self.appPickerData = [[NSArray alloc] initWithObjects:FIRDefaultFIRAppName, FIRSecondFIRAppName, nil]; self.RCInstances = [[NSMutableDictionary alloc] init]; @@ -91,7 +93,8 @@ - (void)viewDidLoad { if (!self.RCInstances[namespaceString]) { self.RCInstances[namespaceString] = [[NSMutableDictionary alloc] init]; } - if ([namespaceString isEqualToString:FIRNamespaceGoogleMobilePlatform] && + if ([namespaceString + isEqualToString:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform] && [appString isEqualToString:FIRDefaultFIRAppName]) { self.RCInstances[namespaceString][appString] = [FIRRemoteConfig remoteConfig]; } else { @@ -120,7 +123,7 @@ - (void)viewDidLoad { [alert addAction:defaultAction]; // Add realtime listener for firebase namespace - [self.RCInstances[FIRNamespaceGoogleMobilePlatform][FIRDefaultFIRAppName] + [self.RCInstances[FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform][FIRDefaultFIRAppName] addOnConfigUpdateListener:^(FIRRemoteConfigUpdate *_Nullable update, NSError *_Nullable error) { if (error != nil) { diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index 43cdda1778c..e02f22f2454 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -24,6 +24,7 @@ #import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" +@import FirebaseRemoteConfigInterop; @interface RCNConfigContent (Testing) - (BOOL)checkAndWaitForInitialDatabaseLoad; @@ -62,6 +63,7 @@ @interface RCNConfigContentTest : XCTestCase { NSTimeInterval _expectationTimeout; RCNConfigContent *_configContent; NSString *namespaceApp1, *namespaceApp2; + NSString *_namespaceGoogleMobilePlatform; } @end @@ -70,11 +72,12 @@ @implementation RCNConfigContentTest - (void)setUp { [super setUp]; _expectationTimeout = 1.0; + _namespaceGoogleMobilePlatform = FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform; namespaceApp1 = [NSString - stringWithFormat:@"%@:%@", FIRNamespaceGoogleMobilePlatform, RCNTestsDefaultFIRAppName]; + stringWithFormat:@"%@:%@", _namespaceGoogleMobilePlatform, RCNTestsDefaultFIRAppName]; namespaceApp2 = [NSString - stringWithFormat:@"%@:%@", FIRNamespaceGoogleMobilePlatform, RCNTestsSecondFIRAppName]; + stringWithFormat:@"%@:%@", _namespaceGoogleMobilePlatform, RCNTestsSecondFIRAppName]; _configContent = [[RCNConfigContent alloc] initWithDBManager:nil]; @@ -129,14 +132,14 @@ - (void)testUpdateConfigContentWithResponse { NSDictionary *entries = @{@"key1" : @"value1", @"key2" : @"value2"}; [configToSet setValue:entries forKey:@"entries"]; [_configContent updateConfigContentWithResponse:configToSet - forNamespace:FIRNamespaceGoogleMobilePlatform]; + forNamespace:_namespaceGoogleMobilePlatform]; NSDictionary *fetchedConfig = _configContent.fetchedConfig; - XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"]); - XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"] stringValue], + XCTAssertNotNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key1"]); + XCTAssertEqualObjects([fetchedConfig[_namespaceGoogleMobilePlatform][@"key1"] stringValue], @"value1"); - XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"]); - XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"] stringValue], + XCTAssertNotNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key2"]); + XCTAssertEqualObjects([fetchedConfig[_namespaceGoogleMobilePlatform][@"key2"] stringValue], @"value2"); } @@ -147,20 +150,20 @@ - (void)testUpdateConfigContentWithStatusUpdateWithDifferentKeys { NSDictionary *entries = @{@"key1" : @"value1"}; [configToSet setValue:entries forKey:@"entries"]; [_configContent updateConfigContentWithResponse:configToSet - forNamespace:FIRNamespaceGoogleMobilePlatform]; + forNamespace:_namespaceGoogleMobilePlatform]; configToSet = [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil]; entries = @{@"key2" : @"value2", @"key3" : @"value3"}; [configToSet setValue:entries forKey:@"entries"]; [_configContent updateConfigContentWithResponse:configToSet - forNamespace:FIRNamespaceGoogleMobilePlatform]; + forNamespace:_namespaceGoogleMobilePlatform]; NSDictionary *fetchedConfig = _configContent.fetchedConfig; - XCTAssertNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"]); - XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"]); - XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"] stringValue], + XCTAssertNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key1"]); + XCTAssertNotNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key2"]); + XCTAssertEqualObjects([fetchedConfig[_namespaceGoogleMobilePlatform][@"key2"] stringValue], @"value2"); - XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key3"]); - XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key3"] stringValue], + XCTAssertNotNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key3"]); + XCTAssertEqualObjects([fetchedConfig[_namespaceGoogleMobilePlatform][@"key3"] stringValue], @"value3"); } @@ -502,8 +505,8 @@ - (void)testConfigUpdate_rolloutMetadataUpdated_returnsKey { rolloutMetadata:rolloutMetadata]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; // populate active config with the same content - NSArray *result = [_configContent activateRolloutMetadata]; - XCTAssertEqualObjects(rolloutMetadata, result); + [_configContent activateRolloutMetadata:nil]; + XCTAssertEqualObjects(rolloutMetadata, _configContent.activeRolloutMetadata); FIRRemoteConfigValue *rcValue = [[FIRRemoteConfigValue alloc] initWithData:[value dataUsingEncoding:NSUTF8StringEncoding] source:FIRRemoteConfigSourceRemote]; @@ -548,8 +551,8 @@ - (void)testConfigUpdate_rolloutMetadataDeleted_returnsKey { rolloutMetadata:rolloutMetadata]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; // populate active config with the same content - NSArray *result = [_configContent activateRolloutMetadata]; - XCTAssertEqualObjects(rolloutMetadata, result); + [_configContent activateRolloutMetadata:nil]; + XCTAssertEqualObjects(rolloutMetadata, _configContent.activeRolloutMetadata); FIRRemoteConfigValue *rcValue = [[FIRRemoteConfigValue alloc] initWithData:[value dataUsingEncoding:NSUTF8StringEncoding] source:FIRRemoteConfigSourceRemote]; @@ -568,6 +571,49 @@ - (void)testConfigUpdate_rolloutMetadataDeleted_returnsKey { XCTAssertTrue([[update updatedKeys] containsObject:key2]); } +- (void)testConfigUpdate_rolloutMetadataDeletedAll_returnsKey { + NSString *namespace = @"test_namespace"; + NSString *key = @"key"; + NSString *value = @"value"; + NSString *rolloutId1 = @"1"; + NSString *variantId1 = @"A"; + NSArray *rolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key ] + } ]; + // Populate fetched config + NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{key : value} + p13nMetadata:nil + rolloutMetadata:rolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + // populate active config with the same content + [_configContent activateRolloutMetadata:nil]; + XCTAssertEqualObjects(rolloutMetadata, _configContent.activeRolloutMetadata); + FIRRemoteConfigValue *rcValue = + [[FIRRemoteConfigValue alloc] initWithData:[value dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote]; + + NSDictionary *namespaceToConfig = @{namespace : @{key : rcValue}}; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceActive + forNamespace:namespace]; + + // New fetch response has updated rollout metadata + NSMutableDictionary *updateFetchResponse = + [self createFetchResponseWithConfigEntries:@{key : value} + p13nMetadata:nil + rolloutMetadata:nil]; + [_configContent updateConfigContentWithResponse:updateFetchResponse forNamespace:namespace]; + + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + [_configContent activateRolloutMetadata:nil]; + + XCTAssertTrue([update updatedKeys].count == 1); + XCTAssertTrue([[update updatedKeys] containsObject:key]); + XCTAssertTrue(_configContent.activeRolloutMetadata.count == 0); +} + - (void)testConfigUpdate_valueSourceChanged_returnsKey { NSString *namespace = @"test_namespace"; NSString *existingParam = @"key1"; diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m index 2a5bd7c67c9..9acb62e0717 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m @@ -23,6 +23,7 @@ #import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" +@import FirebaseRemoteConfigInterop; static NSString *const RCNFakeSenderID = @"855865492447"; static NSString *const RCNFakeToken = @"ctToAh17Exk:" @@ -48,6 +49,7 @@ @interface RCNConfigTest : XCTestCase { RCNConfigExperiment *_experiment; RCNConfigFetch *_configFetch; dispatch_queue_t _queue; + NSString *_namespaceGoogleMobilePlatform; } @end @@ -66,9 +68,10 @@ - (void)setUp { experiment:_experiment queue:_queue]; _configFetch = OCMPartialMock(fetcher); + _namespaceGoogleMobilePlatform = FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform; // Fake a response with a default namespace and a custom namespace. NSDictionary *namespaceToConfig = @{ - FIRNamespaceGoogleMobilePlatform : @{@"key1" : @"value1", @"key2" : @"value2"}, + _namespaceGoogleMobilePlatform : @{@"key1" : @"value1", @"key2" : @"value2"}, FIRNamespaceGooglePlayPlatform : @{@"playerID" : @"36", @"gameLevel" : @"87"}, }; _response = @@ -149,19 +152,19 @@ - (void)testFetchAllConfigsSuccessfully { XCTAssertNotNil(result); [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key1" value:@"value1"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key2" value:@"value2"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"playerID" value:@"36"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"gameLevel" value:@"87"]; XCTAssertEqual(self->_settings.expirationInSeconds, 43200, @@ -200,11 +203,11 @@ - (void)testFetchConfigInCachedResults { NSDictionary *result = self->_configContent.fetchedConfig; XCTAssertNotNil(result); [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key1" value:@"value1"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key2" value:@"value2"]; @@ -246,19 +249,19 @@ - (void)testFetchFailedWithCachedResult { XCTAssertNotNil(result); [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key1" value:@"value1"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key2" value:@"value2"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"playerID" value:@"36"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"gameLevel" value:@"87"]; @@ -340,19 +343,19 @@ - (void)testFetchThrottledWithStaledCachedResult { NSDictionary *result = self->_configContent.fetchedConfig; XCTAssertNotNil(result); [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key1" value:@"value1"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key2" value:@"value2"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"playerID" value:@"36"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"gameLevel" value:@"87"]; XCTAssertEqual( diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m index 5d1b28fb61d..cbbcd0a91bd 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m @@ -29,6 +29,7 @@ #import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h" +@import FirebaseRemoteConfigInterop; @interface RCNConfigFetch (ForTest) - (instancetype)initWithContent:(RCNConfigContent *)content @@ -136,7 +137,8 @@ - (void)setUpConfigMock { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform; + ; break; case RCNTestRCInstanceDefault: default: diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index 6bf23d0777f..e02b8ecaabf 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -32,6 +32,9 @@ #import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" +@import FirebaseRemoteConfigInterop; + +@protocol FIRRolloutsStateSubscriber; @interface RCNConfigFetch (ForTest) - (instancetype)initWithContent:(RCNConfigContent *)content @@ -131,6 +134,7 @@ @interface RCNRemoteConfigTest : XCTestCase { NSTimeInterval _checkCompletionTimeout; NSMutableArray *_configInstances; NSMutableArray *> *_entries; + NSArray *_rolloutMetadata; NSMutableArray *> *_response; NSMutableArray *_responseData; NSMutableArray *_URLResponse; @@ -146,6 +150,7 @@ @interface RCNRemoteConfigTest : XCTestCase { NSString *_fullyQualifiedNamespace; RCNConfigSettings *_settings; dispatch_queue_t _queue; + NSString *_namespaceGoogleMobilePlatform; } @end @@ -181,6 +186,7 @@ - (void)setUp { _URLResponse = [[NSMutableArray alloc] initWithCapacity:3]; _configFetch = [[NSMutableArray alloc] initWithCapacity:3]; _configRealtime = [[NSMutableArray alloc] initWithCapacity:3]; + _namespaceGoogleMobilePlatform = FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform; // Populate the default, second app, second namespace instances. for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { @@ -205,7 +211,7 @@ - (void)setUp { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -260,7 +266,17 @@ __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status, updateCompletionHandler:nil]; }); - _response[i] = @{@"state" : @"UPDATE", @"entries" : _entries[i]}; + _rolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : @"1", + RCNFetchResponseKeyVariantID : @"0", + RCNFetchResponseKeyAffectedParameterKeys : @[ _entries[i].allKeys[0] ] + } ]; + + _response[i] = @{ + @"state" : @"UPDATE", + @"entries" : _entries[i], + RCNFetchResponseKeyRolloutMetadata : _rolloutMetadata + }; _responseData[i] = [NSJSONSerialization dataWithJSONObject:_response[i] options:0 error:nil]; @@ -596,7 +612,7 @@ - (void)testFetchConfigsFailed { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -709,7 +725,7 @@ - (void)testFetchConfigsFailedErrorNoNetwork { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -913,7 +929,7 @@ - (void)testActivateOnFetchNoChangeStatus { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -1784,6 +1800,43 @@ - (void)testRealtimeStreamRequestBody { XCTAssertTrue([strData containsString:@"appInstanceId:'iid'"]); } +- (void)testFetchAndActivateRolloutsNotifyInterop { + id mockNotificationCenter = [OCMockObject mockForClass:[NSNotificationCenter class]]; + [[mockNotificationCenter expect] postNotificationName:@"RolloutsStateDidChangeNotification" + object:[OCMArg any] + userInfo:[OCMArg any]]; + id mockSubscriber = [OCMockObject mockForProtocol:@protocol(FIRRolloutsStateSubscriber)]; + [[mockSubscriber expect] rolloutsStateDidChange:[OCMArg any]]; + + XCTestExpectation *expectation = [self + expectationWithDescription:[NSString + stringWithFormat:@"Test rollout update send notification"]]; + + XCTAssertEqual(_configInstances[RCNTestRCInstanceDefault].lastFetchStatus, + FIRRemoteConfigFetchStatusNoFetchYet); + + FIRRemoteConfigFetchAndActivateCompletion fetchAndActivateCompletion = + ^void(FIRRemoteConfigFetchAndActivateStatus status, NSError *error) { + XCTAssertEqual(status, FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote); + XCTAssertNil(error); + + XCTAssertEqual(self->_configInstances[RCNTestRCInstanceDefault].lastFetchStatus, + FIRRemoteConfigFetchStatusSuccess); + XCTAssertNotNil(self->_configInstances[RCNTestRCInstanceDefault].lastFetchTime); + XCTAssertGreaterThan( + self->_configInstances[RCNTestRCInstanceDefault].lastFetchTime.timeIntervalSince1970, 0, + @"last fetch time interval should be set."); + [expectation fulfill]; + }; + + [_configInstances[RCNTestRCInstanceDefault] + fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion]; + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + #pragma mark - Test Helpers - (FIROptions *)firstAppOptions { diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m b/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m index 8721463feb8..5429c61df1f 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m @@ -25,6 +25,7 @@ #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" #import "FirebaseCore/Extension/FirebaseCoreInternal.h" +@import FirebaseRemoteConfigInterop; @interface RCNThrottlingTests : XCTestCase { RCNConfigContent *_configContentMock; @@ -53,20 +54,22 @@ - (void)setUp { RCNConfigDBManager *DBManager = [[RCNConfigDBManager alloc] init]; _configContentMock = OCMClassMock([RCNConfigContent class]); - _settings = [[RCNConfigSettings alloc] initWithDatabaseManager:DBManager - namespace:FIRNamespaceGoogleMobilePlatform - app:[FIRApp defaultApp]]; + _settings = [[RCNConfigSettings alloc] + initWithDatabaseManager:DBManager + namespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform + app:[FIRApp defaultApp]]; _experimentMock = OCMClassMock([RCNConfigExperiment class]); dispatch_queue_t _queue = dispatch_queue_create( "com.google.GoogleConfigService.FIRRemoteConfigTest", DISPATCH_QUEUE_SERIAL); - _configFetch = [[RCNConfigFetch alloc] initWithContent:_configContentMock - DBManager:DBManager - settings:_settings - experiment:_experimentMock - queue:_queue - namespace:FIRNamespaceGoogleMobilePlatform - app:[FIRApp defaultApp]]; + _configFetch = [[RCNConfigFetch alloc] + initWithContent:_configContentMock + DBManager:DBManager + settings:_settings + experiment:_experimentMock + queue:_queue + namespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform + app:[FIRApp defaultApp]]; } - (void)mockFetchResponseWithStatusCode:(NSInteger)statusCode { diff --git a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift index b695e375887..ccf78ab68c0 100644 --- a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift +++ b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift @@ -16,6 +16,7 @@ import XCTest import FirebaseCore import FirebaseRemoteConfig +import FirebaseRemoteConfigInterop import FirebaseRemoteConfigSwift final class FirebaseRemoteConfigSwift_APIBuildTests: XCTestCase { @@ -23,7 +24,7 @@ final class FirebaseRemoteConfigSwift_APIBuildTests: XCTestCase { // MARK: - FirebaseRemoteConfig // TODO(ncooke3): These global constants should be lowercase. - let _: String = FirebaseRemoteConfig.NamespaceGoogleMobilePlatform + let _: String = RemoteConfigConstants.NamespaceGoogleMobilePlatform let _: String = FirebaseRemoteConfig.RemoteConfigThrottledEndTimeInSecondsKey // TODO(ncooke3): This should probably not be initializable.