Files
firebase-ios-sdk/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m
T

847 lines
40 KiB
Objective-C

/*
* 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 <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#import <sqlite3.h>
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h"
#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"
@interface RCNConfigDBManager (Test)
- (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path;
- (void)insertExperimentTableWithKey:(NSString *)key
value:(NSData *)serializedValue
completionHandler:(RCNDBCompletion)handler;
- (void)deleteExperimentTableForKey:(NSString *)key;
- (void)createOrOpenDatabase;
@end
@interface RCNConfigDBManagerTest : XCTestCase {
NSString *_DBPath;
}
@property(nonatomic, strong) RCNConfigDBManager *DBManager;
@property(nonatomic, assign) NSTimeInterval expectionTimeout;
@end
@implementation RCNConfigDBManagerTest
- (void)setUp {
[super setUp];
// always remove the database at the start of testing
_DBPath = [RCNTestUtilities remoteConfigPathForTestDatabase];
_expectionTimeout = 10.0;
id classMock = OCMClassMock([RCNConfigDBManager class]);
OCMStub([classMock remoteConfigPathForDatabase]).andReturn(_DBPath);
_DBManager = [[RCNConfigDBManager alloc] init];
}
- (void)tearDown {
// Causes crash if main thread exits before the RCNConfigDB queue cleans up
// [_DBManager removeDatabaseOnDatabaseQueueAtPath:_DBPath];
}
- (void)testV1NamespaceMigrationToV2Namespace {
// Write v1 namespace.
XCTestExpectation *loadConfigContentExpectation =
[self expectationWithDescription:@"test v1 namespace migration to v2 namespace"];
NSString *namespace_p = @"testNamespace";
NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
__block int count = 0;
for (int i = 0; i <= 100; ++i) {
// Check namespace is updated after database write is completed.
RCNDBCompletion insertCompletion = ^void(BOOL success,
NSDictionary<NSString *, NSString *> *result) {
count++;
XCTAssertTrue(success);
if (count == 100) {
// Migrate to the new namespace.
[self->_DBManager createOrOpenDatabase];
[self->_DBManager
loadMainWithBundleIdentifier:bundleIdentifier
completionHandler:^(
BOOL loadSuccess,
NSDictionary<NSString *, NSDictionary<NSString *, id> *> *fetchedConfig,
NSDictionary<NSString *, NSDictionary<NSString *, id> *> *activeConfig,
NSDictionary<NSString *, NSDictionary<NSString *, id> *> *defaultConfig,
NSDictionary *unusedRolloutMetadata) {
XCTAssertTrue(loadSuccess);
NSString *fullyQualifiedNamespace =
[NSString stringWithFormat:@"%@:%@", namespace_p, kFIRDefaultAppName];
XCTAssertNotNil(fetchedConfig[fullyQualifiedNamespace]);
XCTAssertEqual([fetchedConfig[fullyQualifiedNamespace] count], 101U);
XCTAssertEqual([fetchedConfig[namespace_p] count], 0);
if (loadSuccess) {
[loadConfigContentExpectation fulfill];
}
}];
}
};
NSString *value = [NSString stringWithFormat:@"value%d", i];
NSString *key = [NSString stringWithFormat:@"key%d", i];
NSArray<id> *values =
@[ bundleIdentifier, namespace_p, key, [value dataUsingEncoding:NSUTF8StringEncoding] ];
[_DBManager insertMainTableWithValues:values
fromSource:RCNDBSourceFetched
completionHandler:insertCompletion];
}
[self waitForExpectationsWithTimeout:_expectionTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testWriteAndLoadMainTableResult {
XCTestExpectation *loadConfigContentExpectation =
[self expectationWithDescription:@"Write and read metadata in database serializedly"];
NSString *namespace_p = @"namespace_1";
NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
__block int count = 0;
for (int i = 0; i <= 100; ++i) {
// check DB write correctly
RCNDBCompletion insertCompletion = ^void(BOOL success, NSDictionary *result) {
count++;
XCTAssertTrue(success);
if (count == 100) {
// check DB read correctly
[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];
NSString *key = [NSString stringWithFormat:@"key%d", i];
NSArray *values =
@[ bundleIdentifier, namespace_p, key, [value dataUsingEncoding:NSUTF8StringEncoding] ];
[_DBManager insertMainTableWithValues:values
fromSource:RCNDBSourceFetched
completionHandler:insertCompletion];
}
[self waitForExpectationsWithTimeout:_expectionTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testWriteAndLoadMetadataResult {
XCTestExpectation *writeAndLoadMetadataExpectation =
[self expectationWithDescription:@"Write and load metadata in database successfully"];
NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
NSString *namespace = @"test_namespace";
NSTimeInterval lastFetchTimestamp = [NSDate date].timeIntervalSince1970;
NSDictionary *deviceContext =
@{@"app_version" : @"1.0.1", @"app_build" : @"1.0.1.11", @"os_version" : @"iOS9.1"};
NSDictionary *syncedDBCustomVariables = @{@"user_level" : @15, @"user_experiences" : @"2468"};
NSArray *successFetchTimes = @[];
NSTimeInterval now = [NSDate date].timeIntervalSince1970;
NSArray *failureFetchTimes =
@[ [NSNumber numberWithDouble:now - 200], [NSNumber numberWithDouble:now] ];
// serialize objects
NSError *error;
NSData *serializedAppContext = [NSJSONSerialization dataWithJSONObject:syncedDBCustomVariables
options:NSJSONWritingPrettyPrinted
error:&error];
NSData *serializedDeviceContext =
[NSJSONSerialization dataWithJSONObject:deviceContext
options:NSJSONWritingPrettyPrinted
error:&error];
NSData *serializedDigestPerNamespace =
[NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:&error];
NSData *serializedSuccessTime = [NSJSONSerialization dataWithJSONObject:successFetchTimes
options:NSJSONWritingPrettyPrinted
error:&error];
NSData *serializedFailureTime = [NSJSONSerialization dataWithJSONObject:failureFetchTimes
options:NSJSONWritingPrettyPrinted
error:&error];
NSDictionary *columnNameToValue = @{
RCNKeyBundleIdentifier : bundleIdentifier,
RCNKeyNamespace : namespace,
RCNKeyFetchTime : @(lastFetchTimestamp),
RCNKeyDigestPerNamespace : serializedDigestPerNamespace,
RCNKeyDeviceContext : serializedDeviceContext,
RCNKeyAppContext : serializedAppContext,
RCNKeySuccessFetchTime : serializedSuccessTime,
RCNKeyFailureFetchTime : serializedFailureTime,
RCNKeyLastFetchStatus : @(FIRRemoteConfigFetchStatusSuccess),
RCNKeyLastFetchError : @(FIRRemoteConfigErrorUnknown),
RCNKeyLastApplyTime : @(now - 100),
RCNKeyLastSetDefaultsTime : @(now - 200)
};
RCNDBCompletion completion = ^(BOOL success, NSDictionary *result1) {
NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
namespace:namespace];
XCTAssertNotNil(result);
XCTAssertEqualObjects(result[RCNKeyBundleIdentifier], bundleIdentifier);
XCTAssertEqual([result[RCNKeyFetchTime] doubleValue], lastFetchTimestamp);
XCTAssertEqualObjects([result[RCNKeyDigestPerNamespace] copy], @{});
XCTAssertEqualObjects([result[RCNKeyDeviceContext] copy], deviceContext);
XCTAssertEqualObjects([result[RCNKeyAppContext] copy], syncedDBCustomVariables);
XCTAssertEqualObjects([result[RCNKeySuccessFetchTime] copy], successFetchTimes);
// TODO(chliang): Fix the flakiness caused by the commented out test
// XCTAssertTrue([[result[RCNKeyFailureFetchTime] copy] isEqualToArray:failureFetchTimes]);
XCTAssertEqual([result[RCNKeyLastFetchStatus] intValue],
(int)FIRRemoteConfigFetchStatusSuccess);
XCTAssertEqual([result[RCNKeyLastFetchError] intValue], (int)FIRRemoteConfigErrorUnknown);
XCTAssertEqual([result[RCNKeyLastApplyTime] doubleValue], now - 100);
XCTAssertEqual([result[RCNKeyLastSetDefaultsTime] doubleValue], now - 200);
[writeAndLoadMetadataExpectation fulfill];
};
[_DBManager insertMetadataTableWithValues:columnNameToValue completionHandler:completion];
[self waitForExpectationsWithTimeout:_expectionTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testWriteAndLoadMetadataForMultipleNamespaces {
XCTestExpectation *writeAndLoadMetadataForMultipleNamespacesExpectation =
[self expectationWithDescription:@"Metadata is stored and read based on namespace"];
NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
NSDictionary *deviceContext = @{};
NSDictionary *syncedDBCustomVariables = @{};
NSError *error;
NSData *serializedAppContext = [NSJSONSerialization dataWithJSONObject:syncedDBCustomVariables
options:NSJSONWritingPrettyPrinted
error:&error];
NSData *serializedDeviceContext =
[NSJSONSerialization dataWithJSONObject:deviceContext
options:NSJSONWritingPrettyPrinted
error:&error];
NSData *serializedDigestPerNamespace =
[NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:&error];
NSData *serializedSuccessTime = [NSJSONSerialization dataWithJSONObject:@[]
options:NSJSONWritingPrettyPrinted
error:&error];
NSData *serializedFailureTime = [NSJSONSerialization dataWithJSONObject:@[]
options:NSJSONWritingPrettyPrinted
error:&error];
// Metadata for first namespace
NSString *namespace = @"test_namespace";
double lastApplyTime = 100;
double lastSetDefaultsTime = 200;
NSDictionary *valuesForNamespace = @{
RCNKeyBundleIdentifier : bundleIdentifier,
RCNKeyNamespace : namespace,
RCNKeyFetchTime : @(0),
RCNKeyDigestPerNamespace : serializedDigestPerNamespace,
RCNKeyDeviceContext : serializedDeviceContext,
RCNKeyAppContext : serializedAppContext,
RCNKeySuccessFetchTime : serializedSuccessTime,
RCNKeyFailureFetchTime : serializedFailureTime,
RCNKeyLastFetchStatus : @(FIRRemoteConfigFetchStatusSuccess),
RCNKeyLastFetchError : @(FIRRemoteConfigErrorUnknown),
RCNKeyLastApplyTime : @(lastApplyTime),
RCNKeyLastSetDefaultsTime : @(lastSetDefaultsTime)
};
// Metadata for second namespace
NSString *namespace2 = @"test_namespace_2";
double lastApplyTime2 = 300;
double lastSetDefaultsTime2 = 400;
NSDictionary *valuesForNamespace2 = @{
RCNKeyBundleIdentifier : bundleIdentifier,
RCNKeyNamespace : namespace2,
RCNKeyFetchTime : @(0),
RCNKeyDigestPerNamespace : serializedDigestPerNamespace,
RCNKeyDeviceContext : serializedDeviceContext,
RCNKeyAppContext : serializedAppContext,
RCNKeySuccessFetchTime : serializedSuccessTime,
RCNKeyFailureFetchTime : serializedFailureTime,
RCNKeyLastFetchStatus : @(FIRRemoteConfigFetchStatusSuccess),
RCNKeyLastFetchError : @(FIRRemoteConfigErrorUnknown),
RCNKeyLastApplyTime : @(lastApplyTime2),
RCNKeyLastSetDefaultsTime : @(lastSetDefaultsTime2)
};
RCNDBCompletion insertMetadataCompletion = ^void(BOOL success, NSDictionary *result) {
XCTAssertTrue(success);
// Load metadata for both namespaces and verify they retain their separate values
NSDictionary *resultForNamespace =
[self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier namespace:namespace];
NSDictionary *resultForNamespace2 =
[self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier namespace:namespace2];
XCTAssertNotNil(resultForNamespace);
XCTAssertEqual([resultForNamespace[RCNKeyLastApplyTime] doubleValue], lastApplyTime);
XCTAssertEqual([resultForNamespace[RCNKeyLastSetDefaultsTime] doubleValue],
lastSetDefaultsTime);
XCTAssertNotNil(resultForNamespace2);
XCTAssertEqual([resultForNamespace2[RCNKeyLastApplyTime] doubleValue], lastApplyTime2);
XCTAssertEqual([resultForNamespace2[RCNKeyLastSetDefaultsTime] doubleValue],
lastSetDefaultsTime2);
[writeAndLoadMetadataForMultipleNamespacesExpectation fulfill];
};
// Write metadata for first namespace
[_DBManager insertMetadataTableWithValues:valuesForNamespace
completionHandler:^(BOOL success, NSDictionary *result1) {
XCTAssertTrue(success);
// Write metadata for second namespace
[self->_DBManager
insertMetadataTableWithValues:valuesForNamespace2
completionHandler:insertMetadataCompletion];
}];
[self waitForExpectationsWithTimeout:_expectionTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
// Create a key each for two namespaces, delete it from one namespace, read both namespaces.
- (void)testDeleteParamAndLoadMainTable {
XCTestExpectation *namespaceDeleteExpectation =
[self expectationWithDescription:@"Contents of 'namespace_delete' should be deleted."];
XCTestExpectation *namespaceKeepExpectation =
[self expectationWithDescription:@"Write a key to namespace_keep and read back again."];
NSString *namespaceToDelete = @"namespace_delete";
NSString *namespaceToKeep = @"namespace_keep";
NSString *bundleIdentifier = @"testBundleID";
// Write something to the database for both namespaces.
// Completion handler for the write to namespace_delete namespace.
RCNDBCompletion insertNamespace1Completion = ^void(BOOL success, NSDictionary *result) {
XCTAssertTrue(success);
// Delete the key for given namespace.
[self->_DBManager deleteRecordFromMainTableWithNamespace:namespaceToDelete
bundleIdentifier:bundleIdentifier
fromSource:RCNDBSourceActive];
// Read from the database and verify expected values.
[self->_DBManager
loadMainWithBundleIdentifier:bundleIdentifier
completionHandler:^(BOOL success, NSDictionary *fetchedConfig,
NSDictionary *activeConfig, NSDictionary *defaultConfig,
NSDictionary *unusedRolloutMetadata) {
NSMutableDictionary *res = [activeConfig mutableCopy];
XCTAssertTrue(success);
FIRRemoteConfigValue *value = res[namespaceToDelete][@"keyToDelete"];
XCTAssertNil(value);
FIRRemoteConfigValue *value2 = res[namespaceToKeep][@"keyToRetain"];
XCTAssertTrue([value2.stringValue isEqualToString:@"valueToRetain"]);
[namespaceDeleteExpectation fulfill];
}];
};
// Insert a key into the second namespace.
RCNDBCompletion insertNamespace2Completion = ^void(BOOL success, NSDictionary *result) {
XCTAssertTrue(success);
// Ensure DB read succeeds.
[self->_DBManager
loadMainWithBundleIdentifier:bundleIdentifier
completionHandler:^(BOOL success, NSDictionary *fetchedConfig,
NSDictionary *activeConfig, NSDictionary *defaultConfig,
NSDictionary *unusedRolloutMetadata) {
NSMutableDictionary *res = [activeConfig mutableCopy];
XCTAssertTrue(success);
FIRRemoteConfigValue *value2 = res[namespaceToKeep][@"keyToRetain"];
XCTAssertTrue([value2.stringValue isEqualToString:@"valueToRetain"]);
[namespaceKeepExpectation fulfill];
}];
};
// We will delete this key after storing in the database.
NSString *valueToDelete = @"valueToDelete";
NSString *keyToDelete = @"keyToDelete";
NSArray *items = @[
bundleIdentifier, namespaceToDelete, keyToDelete,
[valueToDelete dataUsingEncoding:NSUTF8StringEncoding]
];
[_DBManager insertMainTableWithValues:items
fromSource:RCNDBSourceActive
completionHandler:insertNamespace1Completion];
// This key value will be retained.
NSString *valueToRetain = @"valueToRetain";
NSString *keyToRetain = @"keyToRetain";
NSArray *items2 = @[
bundleIdentifier, namespaceToKeep, keyToRetain,
[valueToRetain dataUsingEncoding:NSUTF8StringEncoding]
];
[_DBManager insertMainTableWithValues:items2
fromSource:RCNDBSourceActive
completionHandler:insertNamespace2Completion];
[self waitForExpectationsWithTimeout:_expectionTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testWriteAndLoadExperiments {
XCTestExpectation *updateAndLoadExperimentExpectation =
[self expectationWithDescription:@"Update and load experiment in database successfully"];
NSError *error;
NSArray *payload2 = @[ @"ab", @"cd" ];
NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload2
options:NSJSONWritingPrettyPrinted
error:&error];
NSDictionary *payload3 =
@{@"experiment_ID" : @"35667", @"experiment_activate_name" : @"activate_game"};
NSData *payloadData3 = [NSJSONSerialization dataWithJSONObject:payload3
options:NSJSONWritingPrettyPrinted
error:&error];
NSArray *payloads = @[ [[NSData alloc] init], payloadData2, payloadData3 ];
RCNDBCompletion writePayloadCompletion = ^(BOOL success, NSDictionary *result) {
NSDictionary *metadata =
@{@"last_known_start_time" : @(-11), @"experiment_new_metadata" : @"wonderful"};
XCTAssertTrue(success);
RCNDBCompletion writeMetadataCompletion = ^(BOOL success, NSDictionary *result) {
XCTAssertTrue(success);
RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) {
XCTAssertTrue(success);
XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyPayload]);
XCTAssertEqualObjects(payloads, experimentResults[@RCNExperimentTableKeyPayload]);
XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyMetadata]);
XCTAssertEqualWithAccuracy(
-11,
[experimentResults[@RCNExperimentTableKeyMetadata][@"last_known_start_time"]
doubleValue],
1.0);
XCTAssertEqualObjects(
@"wonderful",
experimentResults[@RCNExperimentTableKeyMetadata][@"experiment_new_metadata"]);
[updateAndLoadExperimentExpectation fulfill];
};
[self->_DBManager loadExperimentWithCompletionHandler:readCompletion];
};
NSError *error;
XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]);
NSData *serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata
options:NSJSONWritingPrettyPrinted
error:&error];
[self->_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata
value:serializedMetadata
completionHandler:writeMetadataCompletion];
};
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload
value:[[NSData alloc] init]
completionHandler:nil];
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload
value:payloadData2
completionHandler:nil];
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload
value:payloadData3
completionHandler:writePayloadCompletion];
[self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}
- (void)testWriteAndLoadActivatedExperiments {
XCTestExpectation *updateAndLoadExperimentExpectation =
[self expectationWithDescription:@"Update and load experiment in database successfully"];
NSError *error;
NSArray *payload2 = @[ @"ab", @"cd" ];
NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload2
options:NSJSONWritingPrettyPrinted
error:&error];
NSDictionary *payload3 =
@{@"experiment_ID" : @"35667", @"experiment_activate_name" : @"activate_game"};
NSData *payloadData3 = [NSJSONSerialization dataWithJSONObject:payload3
options:NSJSONWritingPrettyPrinted
error:&error];
NSArray *payloads = @[ [[NSData alloc] init], payloadData2, payloadData3 ];
RCNDBCompletion writePayloadCompletion = ^(BOOL success, NSDictionary *result) {
XCTAssertTrue(success);
RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) {
XCTAssertTrue(success);
XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyActivePayload]);
XCTAssertEqualObjects(payloads, experimentResults[@RCNExperimentTableKeyActivePayload]);
[updateAndLoadExperimentExpectation fulfill];
};
[self->_DBManager loadExperimentWithCompletionHandler:readCompletion];
};
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload
value:[[NSData alloc] init]
completionHandler:nil];
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload
value:payloadData2
completionHandler:nil];
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload
value:payloadData3
completionHandler:writePayloadCompletion];
[self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}
- (void)testWriteAndLoadMetadataMultipleTimes {
XCTestExpectation *updateAndLoadMetadataExpectation = [self
expectationWithDescription:@"Update and load experiment metadata in database successfully"];
RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) {
XCTAssertTrue(success);
XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyPayload]);
XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyMetadata]);
XCTAssertEqualWithAccuracy(
12345678,
[experimentResults[@RCNExperimentTableKeyMetadata][@"last_known_start_time"] doubleValue],
1.0);
XCTAssertEqualObjects(
@"wonderful",
experimentResults[@RCNExperimentTableKeyMetadata][@"experiment_new_metadata"]);
[updateAndLoadMetadataExpectation fulfill];
};
NSDictionary *metadata =
@{@"last_known_start_time" : @(-11), @"experiment_new_metadata" : @"wonderful"};
NSError *error;
XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]);
NSData *serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata
options:NSJSONWritingPrettyPrinted
error:&error];
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata
value:serializedMetadata
completionHandler:nil];
metadata = @{@"last_known_start_time" : @(12345678), @"experiment_new_metadata" : @"wonderful"};
XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]);
serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata
options:NSJSONWritingPrettyPrinted
error:&error];
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata
value:serializedMetadata
completionHandler:nil];
[_DBManager loadExperimentWithCompletionHandler:readCompletion];
[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."];
NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
NSString *namespace = @"test_namespace";
// Metadata row must exist before update
RCNDBCompletion createMetadataCompletion = ^(BOOL success, NSDictionary *createResult) {
NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
namespace:namespace];
XCTAssertTrue(success);
XCTAssertNotNil(result);
XCTAssertEqual([result[RCNKeyLastFetchStatus] intValue],
(int)FIRRemoteConfigFetchStatusSuccess);
XCTAssertEqual([result[RCNKeyLastFetchError] intValue], (int)FIRRemoteConfigErrorUnknown);
RCNDBCompletion updateMetadataCompletion = ^(BOOL success, NSDictionary *updateResult) {
NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
namespace:namespace];
XCTAssertTrue(success);
XCTAssertNotNil(result);
XCTAssertEqual([result[RCNKeyLastFetchStatus] intValue],
(int)FIRRemoteConfigFetchStatusThrottled);
XCTAssertEqual([result[RCNKeyLastFetchError] intValue], (int)FIRRemoteConfigErrorThrottled);
[updateAndLoadMetadataExpectation fulfill];
};
// Update with throttle status.
[self->_DBManager
updateMetadataWithOption:RCNUpdateOptionFetchStatus
namespace:namespace
values:@[
@(FIRRemoteConfigFetchStatusThrottled), @(FIRRemoteConfigErrorThrottled)
]
completionHandler:updateMetadataCompletion];
};
[_DBManager insertMetadataTableWithValues:[self createSampleMetadata]
completionHandler:createMetadataCompletion];
[self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}
/// Tests that we can insert values in the database and can update them.
- (void)testInsertAndUpdateApplyTime {
XCTestExpectation *updateAndLoadMetadataExpectation =
[self expectationWithDescription:@"Update and load apply time in database successfully."];
NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
NSString *namespace = @"test_namespace";
NSTimeInterval lastApplyTimestamp = [NSDate date].timeIntervalSince1970;
// Metadata row must exist before update
RCNDBCompletion createMetadataCompletion = ^(BOOL success, NSDictionary *createResult) {
XCTAssertTrue(success);
// Read newly created metadata.
NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
namespace:namespace];
XCTAssertNotNil(result);
XCTAssertEqual([result[RCNKeyLastApplyTime] doubleValue], (double)100);
RCNDBCompletion updateMetadataCompletion = ^(BOOL success, NSDictionary *updateResult) {
NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
namespace:namespace];
XCTAssertTrue(success);
XCTAssertNotNil(result);
XCTAssertEqual([result[RCNKeyLastApplyTime] doubleValue], lastApplyTimestamp);
[updateAndLoadMetadataExpectation fulfill];
};
// Update apply config timestamp.
[self->_DBManager updateMetadataWithOption:RCNUpdateOptionApplyTime
namespace:namespace
values:@[ @(lastApplyTimestamp) ]
completionHandler:updateMetadataCompletion];
};
[_DBManager insertMetadataTableWithValues:[self createSampleMetadata]
completionHandler:createMetadataCompletion];
[self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}
- (void)testUpdateAndLoadSetDefaultsTime {
XCTestExpectation *updateAndLoadMetadataExpectation = [self
expectationWithDescription:@"Update and load set defaults time in database successfully."];
NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
NSString *namespace = @"test_namespace";
NSTimeInterval lastSetDefaultsTimestamp = [NSDate date].timeIntervalSince1970;
// Metadata row must exist before update
RCNDBCompletion createMetadataCompletion = ^(BOOL success, NSDictionary *createResult) {
NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
namespace:namespace];
XCTAssertTrue(success);
XCTAssertNotNil(result);
XCTAssertEqual([result[RCNKeyLastSetDefaultsTime] doubleValue], (double)200);
RCNDBCompletion updateMetadataCompletion = ^(BOOL success, NSDictionary *updateResult) {
NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
namespace:namespace];
XCTAssertTrue(success);
XCTAssertNotNil(result);
XCTAssertEqual([result[RCNKeyLastSetDefaultsTime] doubleValue], lastSetDefaultsTimestamp);
[updateAndLoadMetadataExpectation fulfill];
};
// Update setting default config timestamp.
[self->_DBManager updateMetadataWithOption:RCNUpdateOptionDefaultTime
namespace:namespace
values:@[ @(lastSetDefaultsTimestamp) ]
completionHandler:updateMetadataCompletion];
};
[_DBManager insertMetadataTableWithValues:[self createSampleMetadata]
completionHandler:createMetadataCompletion];
[self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}
- (NSDictionary *)createSampleMetadata {
NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
NSString *namespace = @"test_namespace";
NSDictionary *deviceContext = @{};
NSDictionary *syncedDBCustomVariables = @{};
NSArray *successFetchTimes = @[];
NSArray *failureFetchTimes = @[];
// serialize objects
NSError *error;
NSData *serializedAppContext = [NSJSONSerialization dataWithJSONObject:syncedDBCustomVariables
options:NSJSONWritingPrettyPrinted
error:&error];
NSData *serializedDeviceContext =
[NSJSONSerialization dataWithJSONObject:deviceContext
options:NSJSONWritingPrettyPrinted
error:&error];
NSData *serializedDigestPerNamespace =
[NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:&error];
NSData *serializedSuccessTime = [NSJSONSerialization dataWithJSONObject:successFetchTimes
options:NSJSONWritingPrettyPrinted
error:&error];
NSData *serializedFailureTime = [NSJSONSerialization dataWithJSONObject:failureFetchTimes
options:NSJSONWritingPrettyPrinted
error:&error];
return @{
RCNKeyBundleIdentifier : bundleIdentifier,
RCNKeyNamespace : namespace,
RCNKeyFetchTime : @(0),
RCNKeyDigestPerNamespace : serializedDigestPerNamespace,
RCNKeyDeviceContext : serializedDeviceContext,
RCNKeyAppContext : serializedAppContext,
RCNKeySuccessFetchTime : serializedSuccessTime,
RCNKeyFailureFetchTime : serializedFailureTime,
RCNKeyLastFetchStatus : @(FIRRemoteConfigFetchStatusSuccess),
RCNKeyLastFetchError : @(FIRRemoteConfigErrorUnknown),
RCNKeyLastApplyTime : @(100),
RCNKeyLastSetDefaultsTime : @(200)
};
}
@end