From 822fcdfc58c71367d90c501769f21e94951f407c Mon Sep 17 00:00:00 2001 From: Doudou Nan <146472823+ddnan@users.noreply.github.com> Date: Thu, 18 Jan 2024 10:43:50 -0800 Subject: [PATCH] [Rollouts] Implement insert and load logic for rollout metadata in RC (#12262) --- .../Sources/RCNConfigContent.m | 43 +++-- .../Sources/RCNConfigDBManager.h | 14 +- .../Sources/RCNConfigDBManager.m | 122 +++++++++++-- .../Sources/RCNConfigDefines.h | 2 + .../Tests/Unit/RCNConfigContentTest.m | 4 +- .../Tests/Unit/RCNConfigDBManagerTest.m | 165 ++++++++++++++++-- 6 files changed, 303 insertions(+), 47 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index 4f55a2e9274..f28ee662b34 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -38,6 +38,10 @@ @implementation RCNConfigContent { /// Pending Personalization metadata that is latest data from server that might or might not be /// applied. NSDictionary *_fetchedPersonalization; + /// Active Rollout metadata that is currently used. + NSArray *_activeRolloutMetadata; + /// Pending Rollout metadata that is latest data from server that might or might not be applied. + NSArray *_fetchedRolloutMetadata; /// DBManager RCNConfigDBManager *_DBManager; /// Current bundle identifier; @@ -80,6 +84,8 @@ - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager { _defaultConfig = [[NSMutableDictionary alloc] init]; _activePersonalization = [[NSDictionary alloc] init]; _fetchedPersonalization = [[NSDictionary alloc] init]; + _activeRolloutMetadata = [[NSArray alloc] init]; + _fetchedRolloutMetadata = [[NSArray alloc] init]; _bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; if (!_bundleIdentifier) { FIRLogNotice(kFIRLoggerRemoteConfig, @"I-RCN000038", @@ -115,25 +121,30 @@ - (void)loadConfigFromMainTable { _isDatabaseLoadAlreadyInitiated = true; dispatch_group_enter(_dispatch_group); - [_DBManager - loadMainWithBundleIdentifier:_bundleIdentifier - completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, NSDictionary *defaultConfig) { - self->_fetchedConfig = [fetchedConfig mutableCopy]; - self->_activeConfig = [activeConfig mutableCopy]; - self->_defaultConfig = [defaultConfig mutableCopy]; - dispatch_group_leave(self->_dispatch_group); - }]; + [_DBManager loadMainWithBundleIdentifier:_bundleIdentifier + completionHandler:^( + BOOL success, NSDictionary *fetchedConfig, NSDictionary *activeConfig, + NSDictionary *defaultConfig, NSDictionary *rolloutMetadata) { + self->_fetchedConfig = [fetchedConfig mutableCopy]; + self->_activeConfig = [activeConfig mutableCopy]; + self->_defaultConfig = [defaultConfig mutableCopy]; + self->_fetchedRolloutMetadata = + [rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata] copy]; + self->_activeRolloutMetadata = + [rolloutMetadata[@RCNRolloutTableKeyActiveMetadata] copy]; + dispatch_group_leave(self->_dispatch_group); + }]; // TODO(karenzeng): Refactor personalization to be returned in loadMainWithBundleIdentifier above dispatch_group_enter(_dispatch_group); - [_DBManager loadPersonalizationWithCompletionHandler:^( - BOOL success, NSDictionary *fetchedPersonalization, - NSDictionary *activePersonalization, NSDictionary *defaultConfig) { - self->_fetchedPersonalization = [fetchedPersonalization copy]; - self->_activePersonalization = [activePersonalization copy]; - dispatch_group_leave(self->_dispatch_group); - }]; + [_DBManager + loadPersonalizationWithCompletionHandler:^( + BOOL success, NSDictionary *fetchedPersonalization, NSDictionary *activePersonalization, + NSDictionary *defaultConfig, NSDictionary *rolloutMetadata) { + self->_fetchedPersonalization = [fetchedPersonalization copy]; + self->_activePersonalization = [activePersonalization copy]; + dispatch_group_leave(self->_dispatch_group); + }]; } /// Update the current config result to main table. diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h index 39c3e213b73..318c69ab122 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h @@ -53,10 +53,12 @@ typedef void (^RCNDBCompletion)(BOOL success, NSDictionary *result); /// @param fetchedConfig Return fetchedConfig loaded from DB /// @param activeConfig Return activeConfig loaded from DB /// @param defaultConfig Return defaultConfig loaded from DB +/// @param rolloutMetadata Return fetched and active RolloutMetadata loaded from DB typedef void (^RCNDBLoadCompletion)(BOOL success, NSDictionary *fetchedConfig, NSDictionary *activeConfig, - NSDictionary *defaultConfig); + NSDictionary *defaultConfig, + NSDictionary *rolloutMetadata); /// Returns the current version of the Remote Config database. + (NSString *)remoteConfigPathForDatabase; @@ -78,7 +80,6 @@ typedef void (^RCNDBLoadCompletion)(BOOL success, /// Load Personalization from table. /// @param handler The callback when reading from DB is complete. - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler; - /// Insert a record in metadata table. /// @param columnNameToValue The column name and its value to be inserted in metadata table. /// @param handler The callback. @@ -110,6 +111,15 @@ typedef void (^RCNDBLoadCompletion)(BOOL success, /// Insert or update the data in Personalization config. - (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)metadata fromSource:(RCNDBSource)source; +/// Insert rollout metadata in rollout table. +/// @param key Key indicating whether rollout metadata is fetched or active and defined in +/// RCNConfigDefines.h. +/// @param value The value that rollout metadata array. +/// @param handler The callback. +- (void)insertOrUpdateRolloutTableWithKey:(NSString *)key + value:(NSArray *)value + completionHandler:(RCNDBCompletion)handler; + /// Clear the record of given namespace and package name /// before updating the table. - (void)deleteRecordFromMainTableWithNamespace:(NSString *)namespace_p diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m index 6550760c16b..823d1c29895 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m @@ -31,6 +31,7 @@ #define RCNTableNameInternalMetadata "internal_metadata" #define RCNTableNameExperiment "experiment" #define RCNTableNamePersonalization "personalization" +#define RCNTableNameRollout "rollout" static BOOL gIsNewDatabase; /// SQLite file name in versions 0, 1 and 2. @@ -284,11 +285,14 @@ - (BOOL)createTableSchema { "create TABLE IF NOT EXISTS " RCNTableNamePersonalization " (_id INTEGER PRIMARY KEY, key INTEGER, value BLOB)"; + static const char *createTableRollout = "create TABLE IF NOT EXISTS " RCNTableNameRollout + " (_id INTEGER PRIMARY KEY, key TEXT, value BLOB)"; + return [self executeQuery:createTableMain] && [self executeQuery:createTableMainActive] && [self executeQuery:createTableMainDefault] && [self executeQuery:createTableMetadata] && [self executeQuery:createTableInternalMetadata] && [self executeQuery:createTableExperiment] && - [self executeQuery:createTablePersonalization]; + [self executeQuery:createTablePersonalization] && [self executeQuery:createTableRollout]; } - (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path { @@ -618,6 +622,52 @@ - (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)dataValue return YES; } +- (void)insertOrUpdateRolloutTableWithKey:(NSString *)key + value:(NSArray *)value + completionHandler:(RCNDBCompletion)handler { + dispatch_async(_databaseOperationQueue, ^{ + BOOL success = [self insertOrUpdateRolloutTableWithKey:key value:value]; + if (handler) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + handler(success, nil); + }); + } + }); +} + +- (BOOL)insertOrUpdateRolloutTableWithKey:(NSString *)key + value:(NSArray *)arrayValue { + RCN_MUST_NOT_BE_MAIN_THREAD(); + NSError *error; + NSData *dataValue = [NSJSONSerialization dataWithJSONObject:arrayValue + options:NSJSONWritingPrettyPrinted + error:&error]; + const char *SQL = + "INSERT OR REPLACE INTO " RCNTableNameRollout + " (_id, key, value) values ((SELECT _id from " RCNTableNameRollout " WHERE key = ?), ?, ?)"; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return NO; + } + if (![self bindStringToStatement:statement index:1 string:key]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (![self bindStringToStatement:statement index:2 string:key]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (sqlite3_bind_blob(statement, 3, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} + #pragma mark - update - (void)updateMetadataWithOption:(RCNUpdateOption)option @@ -852,7 +902,6 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { - (NSMutableArray *)loadExperimentTableFromKey:(NSString *)key { RCN_MUST_NOT_BE_MAIN_THREAD(); - NSMutableArray *results = [[NSMutableArray alloc] init]; const char *SQL = "SELECT value FROM " RCNTableNameExperiment " WHERE key = ?"; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { @@ -861,12 +910,49 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { NSArray *params = @[ key ]; [self bindStringsToStatement:statement stringArray:params]; - NSData *experimentData; + NSMutableArray *results = [self loadValuesFromStatement:statement]; + return results; +} + +- (NSArray *)loadRolloutTableFromKey:(NSString *)key { + RCN_MUST_NOT_BE_MAIN_THREAD(); + const char *SQL = "SELECT value FROM " RCNTableNameRollout " WHERE key = ?"; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return nil; + } + NSArray *params = @[ key ]; + [self bindStringsToStatement:statement stringArray:params]; + NSMutableArray *results = [self loadValuesFromStatement:statement]; + // There should be only one entry in this table. + if (results.count != 1) { + return nil; + } + NSArray *rollout; + // Convert from NSData to NSArray + if (results[0]) { + NSError *error; + rollout = [NSJSONSerialization JSONObjectWithData:results[0] options:0 error:&error]; + if (!rollout) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011", + @"Failed to convert NSData to NSAarry for Rollout Metadata with error %@.", + error); + } + } + if (!rollout) { + rollout = [[NSArray alloc] init]; + } + return rollout; +} + +- (NSMutableArray *)loadValuesFromStatement:(sqlite3_stmt *)statement { + NSMutableArray *results = [[NSMutableArray alloc] init]; + NSData *value; while (sqlite3_step(statement) == SQLITE_ROW) { - experimentData = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0) - length:sqlite3_column_bytes(statement, 0)]; - if (experimentData) { - [results addObject:experimentData]; + value = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0) + length:sqlite3_column_bytes(statement, 0)]; + if (value) { + [results addObject:value]; } } @@ -880,7 +966,7 @@ - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - handler(NO, [NSMutableDictionary new], [NSMutableDictionary new], nil); + handler(NO, [NSMutableDictionary new], [NSMutableDictionary new], nil, nil); }); return; } @@ -913,7 +999,7 @@ - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { if (handler) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - handler(YES, fetchedPersonalization, activePersonalization, nil); + handler(YES, fetchedPersonalization, activePersonalization, nil, nil); }); } }); @@ -987,7 +1073,7 @@ - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - handler(NO, [NSDictionary new], [NSDictionary new], [NSDictionary new]); + handler(NO, [NSDictionary new], [NSDictionary new], [NSDictionary new], [NSDictionary new]); }); return; } @@ -1000,12 +1086,26 @@ - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier __block NSDictionary *defaultConfig = [strongSelf loadMainTableWithBundleIdentifier:bundleIdentifier fromSource:RCNDBSourceDefault]; + + __block NSArray *fetchedRolloutMetadata = + [strongSelf loadRolloutTableFromKey:@RCNRolloutTableKeyFetchedMetadata]; + __block NSArray *activeRolloutMetadata = + [strongSelf loadRolloutTableFromKey:@RCNRolloutTableKeyActiveMetadata]; + if (handler) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ fetchedConfig = fetchedConfig ? fetchedConfig : [[NSDictionary alloc] init]; activeConfig = activeConfig ? activeConfig : [[NSDictionary alloc] init]; defaultConfig = defaultConfig ? defaultConfig : [[NSDictionary alloc] init]; - handler(YES, fetchedConfig, activeConfig, defaultConfig); + fetchedRolloutMetadata = + fetchedRolloutMetadata ? fetchedRolloutMetadata : [[NSArray alloc] init]; + activeRolloutMetadata = + activeRolloutMetadata ? activeRolloutMetadata : [[NSArray alloc] init]; + NSDictionary *rolloutMetadata = @{ + @RCNRolloutTableKeyActiveMetadata : [activeRolloutMetadata copy], + @RCNRolloutTableKeyFetchedMetadata : [fetchedRolloutMetadata copy] + }; + handler(YES, fetchedConfig, activeConfig, defaultConfig, rolloutMetadata); }); } }); diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDefines.h b/FirebaseRemoteConfig/Sources/RCNConfigDefines.h index cf08f738105..1e95373541b 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDefines.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigDefines.h @@ -31,5 +31,7 @@ #define RCNExperimentTableKeyPayload "experiment_payload" #define RCNExperimentTableKeyMetadata "experiment_metadata" #define RCNExperimentTableKeyActivePayload "experiment_active_payload" +#define RCNRolloutTableKeyActiveMetadata "active_rollout_metadata" +#define RCNRolloutTableKeyFetchedMetadata "fetched_rollout_metadata" #endif diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index d4f33bf0f71..7c7290e7551 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -44,7 +44,7 @@ - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(justSmallDelay * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ self.isLoadMainCompleted = YES; - handler(YES, nil, nil, nil); + handler(YES, nil, nil, nil, nil); }); } - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { @@ -53,7 +53,7 @@ - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(justOtherSmallDelay * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ self.isLoadPersonalizationCompleted = YES; - handler(YES, nil, nil, nil); + handler(YES, nil, nil, nil, nil); }); } @end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m index 23705be1abf..773af690935 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m @@ -83,8 +83,8 @@ - (void)testV1NamespaceMigrationToV2Namespace { BOOL loadSuccess, NSDictionary *> *fetchedConfig, NSDictionary *> *activeConfig, - NSDictionary *> - *defaultConfig) { + NSDictionary *> *defaultConfig, + NSDictionary *unusedRolloutMetadata) { XCTAssertTrue(loadSuccess); NSString *fullyQualifiedNamespace = [NSString stringWithFormat:@"%@:%@", namespace_p, kFIRDefaultAppName]; @@ -125,18 +125,19 @@ - (void)testWriteAndLoadMainTableResult { XCTAssertTrue(success); if (count == 100) { // check DB read correctly - [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier - completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, - NSDictionary *defaultConfig) { - NSMutableDictionary *res = [fetchedConfig mutableCopy]; - XCTAssertTrue(success); - FIRRemoteConfigValue *value = res[namespace_p][@"key100"]; - XCTAssertEqualObjects(value.stringValue, @"value100"); - if (success) { - [loadConfigContentExpectation fulfill]; - } - }]; + [self->_DBManager + loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:^(BOOL success, NSDictionary *fetchedConfig, + NSDictionary *activeConfig, NSDictionary *defaultConfig, + NSDictionary *unusedRolloutMetadata) { + NSMutableDictionary *res = [fetchedConfig mutableCopy]; + XCTAssertTrue(success); + FIRRemoteConfigValue *value = res[namespace_p][@"key100"]; + XCTAssertEqualObjects(value.stringValue, @"value100"); + if (success) { + [loadConfigContentExpectation fulfill]; + } + }]; } }; NSString *value = [NSString stringWithFormat:@"value%d", i]; @@ -382,7 +383,8 @@ - (void)testDeleteParamAndLoadMainTable { [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, NSDictionary *defaultConfig) { + NSDictionary *activeConfig, NSDictionary *defaultConfig, + NSDictionary *unusedRolloutMetadata) { NSMutableDictionary *res = [activeConfig mutableCopy]; XCTAssertTrue(success); FIRRemoteConfigValue *value = res[namespaceToDelete][@"keyToDelete"]; @@ -403,7 +405,8 @@ - (void)testDeleteParamAndLoadMainTable { [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, NSDictionary *defaultConfig) { + NSDictionary *activeConfig, NSDictionary *defaultConfig, + NSDictionary *unusedRolloutMetadata) { NSMutableDictionary *res = [activeConfig mutableCopy]; XCTAssertTrue(success); FIRRemoteConfigValue *value2 = res[namespaceToKeep][@"keyToRetain"]; @@ -587,6 +590,136 @@ - (void)testWriteAndLoadMetadataMultipleTimes { [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; } +- (void)testWriteAndLoadFetchedAndActiveRollout { + XCTestExpectation *writeAndLoadFetchedRolloutExpectation = + [self expectationWithDescription:@"Write and load rollout in database successfully"]; + + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + + NSArray *fetchedRollout = @[ + @{ + @"rollout_id" : @"1", + @"variant_id" : @"B", + @"affected_parameter_keys" : @[ @"key_1", @"key_2" ] + }, + @{ + @"rollout_id" : @"2", + @"variant_id" : @"1", + @"affected_parameter_keys" : @[ @"key_1", @"key_3" ] + } + ]; + + NSArray *activeRollout = @[ + @{ + @"rollout_id" : @"1", + @"variant_id" : @"B", + @"affected_parameter_keys" : @[ @"key_1", @"key_2" ] + }, + @{ + @"rollout_id" : @"3", + @"variant_id" : @"a", + @"affected_parameter_keys" : @[ @"key_1", @"key_3" ] + } + ]; + + RCNDBCompletion writeRolloutCompletion = ^(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + RCNDBLoadCompletion loadCompletion = ^( + BOOL success, NSDictionary *unusedFetchedConfig, NSDictionary *unusedActiveConfig, + NSDictionary *unusedDefaultConfig, NSDictionary *rolloutMetadata) { + XCTAssertTrue(success); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertEqualObjects(fetchedRollout, rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]); + XCTAssertEqualObjects(activeRollout, rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]); + + [writeAndLoadFetchedRolloutExpectation fulfill]; + }; + [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:loadCompletion]; + }; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata + value:fetchedRollout + completionHandler:nil]; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyActiveMetadata + value:activeRollout + completionHandler:writeRolloutCompletion]; + + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + +- (void)testUpdateAndLoadRollout { + XCTestExpectation *updateAndLoadFetchedRolloutExpectation = + [self expectationWithDescription:@"Update and load rollout in database successfully"]; + + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + + NSArray *fetchedRollout = @[ @{ + @"rollout_id" : @"1", + @"variant_id" : @"B", + @"affected_parameter_keys" : @[ @"key_1", @"key_2" ] + } ]; + + NSArray *updatedFetchedRollout = @[ + @{ + @"rollout_id" : @"1", + @"variant_id" : @"B", + @"affected_parameter_keys" : @[ @"key_1", @"key_2" ] + }, + @{ + @"rollout_id" : @"2", + @"variant_id" : @"1", + @"affected_parameter_keys" : @[ @"key_1", @"key_3" ] + } + ]; + + RCNDBCompletion writeRolloutCompletion = ^(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + RCNDBLoadCompletion loadCompletion = + ^(BOOL success, NSDictionary *unusedFetchedConfig, NSDictionary *unusedActiveConfig, + NSDictionary *unusedDefaultConfig, NSDictionary *rolloutMetadata) { + XCTAssertTrue(success); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertEqualObjects(updatedFetchedRollout, + rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + + [updateAndLoadFetchedRolloutExpectation fulfill]; + }; + [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:loadCompletion]; + }; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata + value:fetchedRollout + completionHandler:nil]; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata + value:updatedFetchedRollout + completionHandler:writeRolloutCompletion]; + + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} +- (void)testLoadEmptyRollout { + XCTestExpectation *updateAndLoadFetchedRolloutExpectation = + [self expectationWithDescription:@"Load empty rollout in database successfully"]; + + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + + NSArray *emptyResult = [[NSArray alloc] init]; + + RCNDBLoadCompletion loadCompletion = + ^(BOOL success, NSDictionary *unusedFetchedConfig, NSDictionary *unusedActiveConfig, + NSDictionary *unusedDefaultConfig, NSDictionary *rolloutMetadata) { + XCTAssertTrue(success); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertEqualObjects(emptyResult, rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]); + XCTAssertEqualObjects(emptyResult, rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]); + + [updateAndLoadFetchedRolloutExpectation fulfill]; + }; + [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier completionHandler:loadCompletion]; + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + - (void)testUpdateAndloadLastFetchStatus { XCTestExpectation *updateAndLoadMetadataExpectation = [self expectationWithDescription:@"Update and load last fetch status in database successfully."];