Files
firebase-ios-sdk/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m

2393 lines
117 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/OCMStubRecorder.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#import "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h"
#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
#import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigRealtime.h"
#import "FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h"
#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"
#import <GoogleUtilities/GULNSData+zlib.h>
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
@import FirebaseRemoteConfigInterop;
@protocol FIRRolloutsStateSubscriber;
@interface RCNConfigFetch (ForTest)
- (instancetype)initWithContent:(RCNConfigContent *)content
DBManager:(RCNConfigDBManager *)DBManager
settings:(RCNConfigSettings *)settings
experiment:(RCNConfigExperiment *)experiment
queue:(dispatch_queue_t)queue
namespace:firebaseNamespace
app:firebaseApp;
/// Skip fetching user properties from analytics because we cannot mock the action here. Instead
/// overriding the method to skip.
- (void)fetchWithUserPropertiesCompletionHandler:(NSString *)fetchTypeHeader
completionHandler:(FIRAInteropUserPropertiesCallback)block;
- (NSURLSessionDataTask *)URLSessionDataTaskWithContent:(NSData *)content
fetchTypeHeader:(NSString *)fetchTypeHeader
completionHandler:
(RCNConfigFetcherCompletion)fetcherCompletion;
- (void)fetchConfigWithExpirationDuration:(NSTimeInterval)expirationDuration
completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler;
- (void)realtimeFetchConfigWithNoExpirationDuration:(NSInteger)fetchAttemptNumber
completionHandler:(RCNConfigFetchCompletion)completionHandler;
- (void)fetchWithUserProperties:(NSDictionary *)userProperties
fetchTypeHeader:(NSString *)fetchTypeHeader
completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler
updateCompletionHandler:(RCNConfigFetchCompletion)updateCompletionHandler;
- (NSString *)constructServerURL;
- (NSURLSession *)currentNetworkSession;
@end
@interface RCNConfigRealtime (ForTest)
- (instancetype _Nonnull)init:(RCNConfigFetch *_Nonnull)configFetch
settings:(RCNConfigSettings *_Nonnull)settings
namespace:(NSString *_Nonnull)namespace
options:(FIROptions *_Nonnull)options;
- (void)fetchLatestConfig:(NSInteger)remainingAttempts targetVersion:(NSInteger)targetVersion;
- (void)scheduleFetch:(NSInteger)remainingAttempts targetVersion:(NSInteger)targetVersion;
- (void)autoFetch:(NSInteger)remainingAttempts targetVersion:(NSInteger)targetVersion;
- (void)beginRealtimeStream;
- (void)pauseRealtimeStream;
- (void)createRequestBodyWithCompletion:(void (^)(NSData *_Nonnull requestBody))completion;
- (FIRConfigUpdateListenerRegistration *_Nonnull)addConfigUpdateListener:
(RCNConfigUpdateCompletion _Nonnull)listener;
- (void)removeConfigUpdateListener:(RCNConfigUpdateCompletion _Nonnull)listener;
- (void)evaluateStreamResponse:(NSDictionary *)response error:(NSError *)dataError;
- (void)propagateErrors:(NSError *)error;
@end
@interface FIRRemoteConfig (ForTest)
- (void)updateWithNewInstancesForConfigFetch:(RCNConfigFetch *)configFetch
configContent:(RCNConfigContent *)configContent
configSettings:(RCNConfigSettings *)configSettings
configExperiment:(RCNConfigExperiment *)configExperiment;
- (void)updateWithNewInstancesForConfigRealtime:(RCNConfigRealtime *)configRealtime;
@end
@implementation FIRRemoteConfig (ForTest)
- (void)updateWithNewInstancesForConfigFetch:(RCNConfigFetch *)configFetch
configContent:(RCNConfigContent *)configContent
configSettings:(RCNConfigSettings *)configSettings
configExperiment:(RCNConfigExperiment *)configExperiment {
[self setValue:configFetch forKey:@"_configFetch"];
[self setValue:configContent forKey:@"_configContent"];
[self setValue:configSettings forKey:@"_settings"];
[self setValue:configExperiment forKey:@"_configExperiment"];
}
- (void)updateWithNewInstancesForConfigRealtime:(RCNConfigRealtime *)configRealtime {
[self setValue:configRealtime forKey:@"_configRealtime"];
}
@end
@interface RCNConfigDBManager (Test)
- (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path;
@end
@interface RCNUserDefaultsManager (Test)
+ (NSUserDefaults *)sharedUserDefaultsForBundleIdentifier:(NSString *)bundleIdentifier;
@end
@interface RCNConfigSettings (Test)
- (NSString *)nextRequestWithUserProperties:(NSDictionary *)userProperties;
@end
typedef NS_ENUM(NSInteger, RCNTestRCInstance) {
RCNTestRCInstanceDefault,
RCNTestRCInstanceSecondNamespace,
RCNTestRCInstanceSecondApp,
RCNTestRCNumTotalInstances
};
@interface RCNRemoteConfigTest : XCTestCase {
NSTimeInterval _expectationTimeout;
NSTimeInterval _checkCompletionTimeout;
NSMutableArray<FIRRemoteConfig *> *_configInstances;
NSMutableArray<NSDictionary<NSString *, NSString *> *> *_entries;
NSArray<NSDictionary *> *_rolloutMetadata;
NSMutableArray<NSDictionary<NSString *, id> *> *_response;
NSMutableArray<NSData *> *_responseData;
NSMutableArray<NSURLResponse *> *_URLResponse;
NSMutableArray<id> *_configFetch;
NSMutableArray<id> *_configRealtime;
RCNConfigDBManager *_DBManager;
NSUserDefaults *_userDefaults;
NSString *_userDefaultsSuiteName;
NSString *_DBPath;
id _DBManagerMock;
id _experimentMock;
id _userDefaultsMock;
NSString *_fullyQualifiedNamespace;
RCNConfigSettings *_settings;
dispatch_queue_t _queue;
NSString *_namespaceGoogleMobilePlatform;
}
@end
@implementation RCNRemoteConfigTest
- (void)setUp {
[super setUp];
FIRSetLoggerLevel(FIRLoggerLevelMax);
_expectationTimeout = 5;
_checkCompletionTimeout = 1.0;
// Always remove the database at the start of testing.
_DBPath = [RCNTestUtilities remoteConfigPathForTestDatabase];
_DBManagerMock = OCMClassMock([RCNConfigDBManager class]);
OCMStub([_DBManagerMock remoteConfigPathForDatabase]).andReturn(_DBPath);
_DBManager = [[RCNConfigDBManager alloc] init];
_userDefaultsSuiteName = [RCNTestUtilities userDefaultsSuiteNameForTestSuite];
_userDefaults = [[NSUserDefaults alloc] initWithSuiteName:_userDefaultsSuiteName];
_userDefaultsMock = OCMClassMock([RCNUserDefaultsManager class]);
OCMStub([_userDefaultsMock sharedUserDefaultsForBundleIdentifier:[OCMArg any]])
.andReturn(_userDefaults);
_experimentMock = OCMClassMock([RCNConfigExperiment class]);
OCMStub([_experimentMock
updateExperimentsWithHandler:([OCMArg invokeBlockWithArgs:[NSNull null], nil])]);
RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:_DBManager];
_configInstances = [[NSMutableArray alloc] initWithCapacity:3];
_entries = [[NSMutableArray alloc] initWithCapacity:3];
_response = [[NSMutableArray alloc] initWithCapacity:3];
_responseData = [[NSMutableArray alloc] initWithCapacity:3];
_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++) {
// Fake a response for default instance.
NSMutableDictionary<NSString *, NSString *> *valuesDict = [[NSMutableDictionary alloc] init];
for (int count = 1; count <= 100; count++) {
NSString *key = [NSString stringWithFormat:@"key%d-%d", count, i];
NSString *value = [NSString stringWithFormat:@"value%d-%d", count, i];
valuesDict[key] = value;
}
_entries[i] = valuesDict;
NSString *currentAppName = nil;
FIROptions *currentOptions = nil;
NSString *currentNamespace = nil;
switch (i) {
case RCNTestRCInstanceSecondNamespace:
currentAppName = RCNTestsDefaultFIRAppName;
currentOptions = [self firstAppOptions];
currentNamespace = RCNTestsPerfNamespace;
break;
case RCNTestRCInstanceSecondApp:
currentAppName = RCNTestsSecondFIRAppName;
currentOptions = [self secondAppOptions];
currentNamespace = _namespaceGoogleMobilePlatform;
break;
case RCNTestRCInstanceDefault:
default:
currentAppName = RCNTestsDefaultFIRAppName;
currentOptions = [self firstAppOptions];
currentNamespace = RCNTestsFIRNamespace;
break;
}
_fullyQualifiedNamespace =
[NSString stringWithFormat:@"%@:%@", currentNamespace, currentAppName];
FIRRemoteConfig *config =
OCMPartialMock([[FIRRemoteConfig alloc] initWithAppName:currentAppName
FIROptions:currentOptions
namespace:currentNamespace
DBManager:_DBManager
configContent:configContent
analytics:nil]);
_configInstances[i] = config;
_settings = [[RCNConfigSettings alloc] initWithDatabaseManager:_DBManager
namespace:_fullyQualifiedNamespace
firebaseAppName:currentAppName
googleAppID:currentOptions.googleAppID];
_queue = dispatch_queue_create(
[[NSString stringWithFormat:@"testqueue: %d", i] cStringUsingEncoding:NSUTF8StringEncoding],
DISPATCH_QUEUE_SERIAL);
_configFetch[i] =
OCMPartialMock([[RCNConfigFetch alloc] initWithContent:configContent
DBManager:_DBManager
settings:_settings
analytics:nil
experiment:_experimentMock
queue:_queue
namespace:_fullyQualifiedNamespace
options:currentOptions]);
_configRealtime[i] = OCMPartialMock([[RCNConfigRealtime alloc] init:_configFetch[i]
settings:_settings
namespace:_fullyQualifiedNamespace
options:currentOptions]);
_settings.configInstallationsIdentifier = @"iid";
OCMStubRecorder *mock = OCMStub([_configFetch[i] fetchConfigWithExpirationDuration:0
completionHandler:OCMOCK_ANY]);
mock = [mock ignoringNonObjectArgs];
mock.andDo(^(NSInvocation *invocation) {
__unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status,
NSError *_Nullable error) = nil;
[invocation getArgument:&handler atIndex:3];
[self->_configFetch[i] fetchWithUserProperties:[[NSDictionary alloc] init]
fetchTypeHeader:@"Base/1"
completionHandler:handler
updateCompletionHandler:nil];
});
_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];
_URLResponse[i] = [[NSHTTPURLResponse alloc]
initWithURL:[NSURL URLWithString:@"https://firebase.com"]
statusCode:200
HTTPVersion:nil
headerFields:@{@"etag" : [NSString stringWithFormat:@"etag1-%d", i]}];
id completionBlock =
[OCMArg invokeBlockWithArgs:_responseData[i], _URLResponse[i], [NSNull null], nil];
OCMStub([_configFetch[i] URLSessionDataTaskWithContent:[OCMArg any]
fetchTypeHeader:[OCMArg any]
completionHandler:completionBlock])
.andReturn(nil);
[_configInstances[i] updateWithNewInstancesForConfigFetch:_configFetch[i]
configContent:configContent
configSettings:_settings
configExperiment:_experimentMock];
[_configInstances[i] updateWithNewInstancesForConfigRealtime:_configRealtime[i]];
}
}
- (void)tearDown {
[_DBManager removeDatabaseOnDatabaseQueueAtPath:_DBPath];
[FIRRemoteConfigComponent clearAllComponentInstances];
[[NSUserDefaults standardUserDefaults] removePersistentDomainForName:_userDefaultsSuiteName];
[_DBManagerMock stopMocking];
_DBManagerMock = nil;
[_userDefaultsMock stopMocking];
_userDefaultsMock = nil;
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
[(id)_configInstances[i] stopMocking];
}
[_configInstances removeAllObjects];
[_configFetch removeAllObjects];
[_configRealtime removeAllObjects];
_configInstances = nil;
_configFetch = nil;
[super tearDown];
}
- (void)testFetchConfigWithNilCallback {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Set defaults no callback expectation - instance %d", i]];
XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet);
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:nil];
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
XCTAssertEqual(self->_configInstances[i].lastFetchStatus,
FIRRemoteConfigFetchStatusSuccess);
[expectations[i] fulfill];
});
}
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
- (void)testFetchConfigsSuccessfully {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] =
[self expectationWithDescription:
[NSString stringWithFormat:@"Test fetch configs successfully - instance %d", i]];
XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet);
FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(self->_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess);
XCTAssertNil(error);
[self->_configInstances[i] activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertTrue(changed);
NSString *key1 = [NSString stringWithFormat:@"key1-%d", i];
NSString *key2 = [NSString stringWithFormat:@"key2-%d", i];
NSString *value1 = [NSString stringWithFormat:@"value1-%d", i];
NSString *value2 = [NSString stringWithFormat:@"value2-%d", i];
XCTAssertEqualObjects(self->_configInstances[i][key1].stringValue, value1);
XCTAssertEqualObjects(self->_configInstances[i][key2].stringValue, value2);
OCMVerify([self->_configInstances[i] objectForKeyedSubscript:key1]);
XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess,
@"Callback of first successful config "
@"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccessFresh.");
XCTAssertNotNil(self->_configInstances[i].lastFetchTime);
XCTAssertGreaterThan(self->_configInstances[i].lastFetchTime.timeIntervalSince1970, 0,
@"last fetch time interval should be set.");
[expectations[i] fulfill];
}];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testFetchAndActivate {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] =
[self expectationWithDescription:
[NSString stringWithFormat:@"Test fetch configs successfully - instance %d", i]];
XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet);
FIRRemoteConfigFetchAndActivateCompletion fetchAndActivateCompletion = ^void(
FIRRemoteConfigFetchAndActivateStatus status, NSError *error) {
XCTAssertEqual(self->_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess);
XCTAssertNil(error);
NSString *key1 = [NSString stringWithFormat:@"key1-%d", i];
NSString *key2 = [NSString stringWithFormat:@"key2-%d", i];
NSString *value1 = [NSString stringWithFormat:@"value1-%d", i];
NSString *value2 = [NSString stringWithFormat:@"value2-%d", i];
XCTAssertEqualObjects(self->_configInstances[i][key1].stringValue, value1);
XCTAssertEqualObjects(self->_configInstances[i][key2].stringValue, value2);
OCMVerify([self->_configInstances[i] objectForKeyedSubscript:key1]);
XCTAssertEqual(
status, FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote,
@"Callback of first successful config "
@"fetchAndActivate status must equal to FIRRemoteConfigFetchAndStatusSuccessFromRemote.");
XCTAssertNotNil(self->_configInstances[i].lastFetchTime);
XCTAssertGreaterThan(self->_configInstances[i].lastFetchTime.timeIntervalSince1970, 0,
@"last fetch time interval should be set.");
[expectations[i] fulfill];
};
// Update the minimum fetch interval to 0. This disables the cache and forces a remote fetch
// request.
FIRRemoteConfigSettings *settings = [[FIRRemoteConfigSettings alloc] init];
settings.minimumFetchInterval = 0;
[_configInstances[i] setConfigSettings:settings];
[_configInstances[i] fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
// TODO: Try splitting into smaller tests.
- (void)testFetchConfigsSuccessfullyWithNewActivateMethodSignature {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] =
[self expectationWithDescription:
[NSString stringWithFormat:@"Test fetch configs successfully - instance %d", i]];
XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet);
FIRRemoteConfigFetchCompletion fetchCompletion = ^(FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(self->_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess);
XCTAssertNil(error);
[self->_configInstances[i] activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertTrue(changed);
XCTAssertNil(error);
NSString *key1 = [NSString stringWithFormat:@"key1-%d", i];
NSString *key2 = [NSString stringWithFormat:@"key2-%d", i];
NSString *value1 = [NSString stringWithFormat:@"value1-%d", i];
NSString *value2 = [NSString stringWithFormat:@"value2-%d", i];
XCTAssertEqualObjects(self->_configInstances[i][key1].stringValue, value1);
XCTAssertEqualObjects(self->_configInstances[i][key2].stringValue, value2);
OCMVerify([self->_configInstances[i] objectForKeyedSubscript:key1]);
XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess,
@"Callback of first successful config "
@"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccessFresh.");
XCTAssertNotNil(self->_configInstances[i].lastFetchTime);
XCTAssertGreaterThan(self->_configInstances[i].lastFetchTime.timeIntervalSince1970, 0,
@"last fetch time interval should be set.");
// A second activate should have no effect.
[self->_configInstances[i]
activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertFalse(changed);
XCTAssertNil(error);
}];
[expectations[i] fulfill];
}];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testEnumeratingConfigResults {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Test enumerating configs successfully - instance %d", i]];
XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet);
FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(self->_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess);
XCTAssertNil(error);
[self->_configInstances[i] activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertTrue(changed);
NSString *key5 = [NSString stringWithFormat:@"key5-%d", i];
NSString *key19 = [NSString stringWithFormat:@"key19-%d", i];
NSString *value5 = [NSString stringWithFormat:@"value5-%d", i];
NSString *value19 = [NSString stringWithFormat:@"value19-%d", i];
XCTAssertEqualObjects(self->_configInstances[i][key5].stringValue, value5);
XCTAssertEqualObjects(self->_configInstances[i][key19].stringValue, value19);
// Test enumerating config values.
for (NSString *key in self->_configInstances[i]) {
if ([key isEqualToString:key5]) {
XCTAssertEqualObjects(self->_configInstances[i][key5].stringValue, value5);
}
if ([key isEqualToString:key19]) {
XCTAssertEqualObjects(self->_configInstances[i][key19].stringValue, value19);
}
}
XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess,
@"Callback of first successful config "
@"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccessFresh.");
XCTAssertNotNil(self->_configInstances[i].lastFetchTime);
XCTAssertGreaterThan(self->_configInstances[i].lastFetchTime.timeIntervalSince1970, 0,
@"last fetch time interval should be set.");
[expectations[i] fulfill];
}];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testFetchAndActivate3pNamespaceUpdatesExperiments {
[[_experimentMock expect] updateExperimentsWithResponse:[OCMArg any]];
XCTestExpectation *expectation = [self
expectationWithDescription:[NSString stringWithFormat:@"FetchAndActivate call for 'firebase' "
@"namespace updates experiment data"]];
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);
}];
}
- (void)testFetchAndActivateOtherNamespaceDoesntUpdateExperiments {
[[_experimentMock reject] updateExperimentsWithResponse:[OCMArg any]];
XCTestExpectation *expectation = [self
expectationWithDescription:
[NSString stringWithFormat:@"FetchAndActivate call for namespace other than 'firebase' "
@"doesn't update experiment data"]];
XCTAssertEqual(_configInstances[RCNTestRCInstanceSecondNamespace].lastFetchStatus,
FIRRemoteConfigFetchStatusNoFetchYet);
FIRRemoteConfigFetchAndActivateCompletion fetchAndActivateCompletion =
^void(FIRRemoteConfigFetchAndActivateStatus status, NSError *error) {
XCTAssertEqual(status, FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote);
XCTAssertNil(error);
XCTAssertEqual(self->_configInstances[RCNTestRCInstanceSecondNamespace].lastFetchStatus,
FIRRemoteConfigFetchStatusSuccess);
XCTAssertNotNil(self->_configInstances[RCNTestRCInstanceSecondNamespace].lastFetchTime);
XCTAssertGreaterThan(self->_configInstances[RCNTestRCInstanceSecondNamespace]
.lastFetchTime.timeIntervalSince1970,
0, @"last fetch time interval should be set.");
[expectation fulfill];
};
[_configInstances[RCNTestRCInstanceSecondNamespace]
fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion];
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testFetchConfigsFailed {
// Override the setup values to return back an error status.
RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:_DBManager];
// Populate the default, second app, second namespace instances.
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
NSString *currentAppName = nil;
FIROptions *currentOptions = nil;
NSString *currentNamespace = nil;
switch (i) {
case RCNTestRCInstanceSecondNamespace:
currentAppName = RCNTestsDefaultFIRAppName;
currentOptions = [self firstAppOptions];
currentNamespace = RCNTestsPerfNamespace;
break;
case RCNTestRCInstanceSecondApp:
currentAppName = RCNTestsSecondFIRAppName;
currentOptions = [self secondAppOptions];
currentNamespace = _namespaceGoogleMobilePlatform;
break;
case RCNTestRCInstanceDefault:
default:
currentAppName = RCNTestsDefaultFIRAppName;
currentOptions = [self firstAppOptions];
currentNamespace = RCNTestsFIRNamespace;
break;
}
RCNUserDefaultsManager *userDefaultsManager =
[[RCNUserDefaultsManager alloc] initWithAppName:currentAppName
bundleID:[NSBundle mainBundle].bundleIdentifier
namespace:_fullyQualifiedNamespace];
userDefaultsManager.lastFetchTime = 0;
FIRRemoteConfig *config =
OCMPartialMock([[FIRRemoteConfig alloc] initWithAppName:currentAppName
FIROptions:currentOptions
namespace:currentNamespace
DBManager:_DBManager
configContent:configContent
analytics:nil]);
_configInstances[i] = config;
_configFetch[i] =
OCMPartialMock([[RCNConfigFetch alloc] initWithContent:configContent
DBManager:_DBManager
settings:_settings
analytics:nil
experiment:nil
queue:_queue
namespace:_fullyQualifiedNamespace
options:currentOptions]);
_configRealtime[i] = OCMPartialMock([[RCNConfigRealtime alloc] init:_configFetch[i]
settings:_settings
namespace:_fullyQualifiedNamespace
options:currentOptions]);
OCMStub([_configFetch[i] fetchConfigWithExpirationDuration:43200 completionHandler:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
__unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status,
NSError *_Nullable error) = nil;
[invocation getArgument:&handler atIndex:3];
[self->_configFetch[i] fetchWithUserProperties:[[NSDictionary alloc] init]
fetchTypeHeader:@"Base/1"
completionHandler:handler
updateCompletionHandler:nil];
});
_response[i] = @{};
_responseData[i] = [NSJSONSerialization dataWithJSONObject:_response[i] options:0 error:nil];
_URLResponse[i] =
[[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"https://firebase.com"]
statusCode:500
HTTPVersion:nil
headerFields:@{@"etag" : @"etag1"}];
[_configInstances[i] updateWithNewInstancesForConfigFetch:_configFetch[i]
configContent:configContent
configSettings:_settings
configExperiment:nil];
}
// Make the fetch calls for all instances.
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Test enumerating configs successfully - instance %d", i]];
XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet);
FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(self->_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusFailure);
[self->_configInstances[i] activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertFalse(changed);
XCTAssertNil(error);
FIRRemoteConfigValue *value = self->_configInstances[RCNTestRCInstanceDefault][@"key1"];
XCTAssertEqual((int)value.source, (int)FIRRemoteConfigSourceStatic);
XCTAssertEqualObjects(value.stringValue, @"");
XCTAssertEqual(value.boolValue, NO);
[expectations[i] fulfill];
}];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
// TODO(mandard): Break up test with helper methods.
- (void)testFetchConfigsFailedErrorNoNetwork {
// Override the setup values to return back an error status.
RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:_DBManager];
// Populate the default, second app, second namespace instances.
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
NSString *currentAppName = nil;
FIROptions *currentOptions = nil;
NSString *currentNamespace = nil;
switch (i) {
case RCNTestRCInstanceSecondNamespace:
currentAppName = RCNTestsDefaultFIRAppName;
currentOptions = [self firstAppOptions];
currentNamespace = RCNTestsPerfNamespace;
break;
case RCNTestRCInstanceSecondApp:
currentAppName = RCNTestsSecondFIRAppName;
currentOptions = [self secondAppOptions];
currentNamespace = _namespaceGoogleMobilePlatform;
break;
case RCNTestRCInstanceDefault:
default:
currentAppName = RCNTestsDefaultFIRAppName;
currentOptions = [self firstAppOptions];
currentNamespace = RCNTestsFIRNamespace;
break;
}
NSString *fullyQualifiedNamespace =
[NSString stringWithFormat:@"%@:%@", currentNamespace, currentAppName];
RCNUserDefaultsManager *userDefaultsManager =
[[RCNUserDefaultsManager alloc] initWithAppName:currentAppName
bundleID:[NSBundle mainBundle].bundleIdentifier
namespace:fullyQualifiedNamespace];
userDefaultsManager.lastFetchTime = 0;
FIRRemoteConfig *config =
OCMPartialMock([[FIRRemoteConfig alloc] initWithAppName:currentAppName
FIROptions:currentOptions
namespace:currentNamespace
DBManager:_DBManager
configContent:configContent
analytics:nil]);
_configInstances[i] = config;
RCNConfigSettings *settings =
[[RCNConfigSettings alloc] initWithDatabaseManager:_DBManager
namespace:fullyQualifiedNamespace
firebaseAppName:currentAppName
googleAppID:currentOptions.googleAppID];
dispatch_queue_t queue = dispatch_queue_create(
[[NSString stringWithFormat:@"testqueue: %d", i] cStringUsingEncoding:NSUTF8StringEncoding],
DISPATCH_QUEUE_SERIAL);
_configFetch[i] = OCMPartialMock([[RCNConfigFetch alloc] initWithContent:configContent
DBManager:_DBManager
settings:settings
analytics:nil
experiment:nil
queue:queue
namespace:fullyQualifiedNamespace
options:currentOptions]);
_configRealtime[i] = OCMPartialMock([[RCNConfigRealtime alloc] init:_configFetch[i]
settings:settings
namespace:fullyQualifiedNamespace
options:currentOptions]);
OCMStub([_configFetch[i] fetchConfigWithExpirationDuration:43200 completionHandler:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
__unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status,
NSError *_Nullable error) = nil;
[invocation getArgument:&handler atIndex:3];
[self->_configFetch[i] fetchWithUserProperties:[[NSDictionary alloc] init]
fetchTypeHeader:@"Base/1"
completionHandler:handler
updateCompletionHandler:nil];
});
_response[i] = @{};
_responseData[i] = [NSJSONSerialization dataWithJSONObject:_response[i] options:0 error:nil];
// A no network error is accompanied with an HTTP status code of 0.
_URLResponse[i] =
[[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"https://firebase.com"]
statusCode:0
HTTPVersion:nil
headerFields:@{@"etag" : @"etag1"}];
[_configInstances[i] updateWithNewInstancesForConfigFetch:_configFetch[i]
configContent:configContent
configSettings:settings
configExperiment:nil];
}
// Make the fetch calls for all instances.
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Test enumerating configs successfully - instance %d", i]];
XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet);
FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(self->_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusFailure);
[self->_configInstances[i] activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertFalse(changed);
XCTAssertNil(error);
// No such key, still return a static value.
FIRRemoteConfigValue *value = self->_configInstances[RCNTestRCInstanceDefault][@"key1"];
XCTAssertEqual((int)value.source, (int)FIRRemoteConfigSourceStatic);
XCTAssertEqualObjects(value.stringValue, @"");
XCTAssertEqual(value.boolValue, NO);
[expectations[i] fulfill];
}];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testFetchFailedNoNetworkErrorDoesNotThrottle {
RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:_DBManager];
NSString *currentAppName = RCNTestsDefaultFIRAppName;
FIROptions *currentOptions = [self firstAppOptions];
NSString *currentNamespace = RCNTestsFIRNamespace;
NSString *fullyQualifiedNamespace =
[NSString stringWithFormat:@"%@:%@", currentNamespace, currentAppName];
RCNUserDefaultsManager *userDefaultsManager =
[[RCNUserDefaultsManager alloc] initWithAppName:currentAppName
bundleID:[NSBundle mainBundle].bundleIdentifier
namespace:fullyQualifiedNamespace];
userDefaultsManager.lastFetchTime = 0;
FIRRemoteConfig *config = OCMPartialMock([[FIRRemoteConfig alloc] initWithAppName:currentAppName
FIROptions:currentOptions
namespace:currentNamespace
DBManager:_DBManager
configContent:configContent
analytics:nil]);
RCNConfigSettings *settings =
[[RCNConfigSettings alloc] initWithDatabaseManager:_DBManager
namespace:fullyQualifiedNamespace
firebaseAppName:currentAppName
googleAppID:currentOptions.googleAppID];
dispatch_queue_t queue = dispatch_queue_create(
[[NSString stringWithFormat:@"testqueue"] cStringUsingEncoding:NSUTF8StringEncoding],
DISPATCH_QUEUE_SERIAL);
RCNConfigFetch *configFetch =
OCMPartialMock([[RCNConfigFetch alloc] initWithContent:configContent
DBManager:_DBManager
settings:settings
analytics:nil
experiment:nil
queue:queue
namespace:fullyQualifiedNamespace
options:currentOptions]);
OCMStub([configFetch fetchConfigWithExpirationDuration:43200 completionHandler:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
__unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status,
NSError *_Nullable error) = nil;
[invocation getArgument:&handler atIndex:3];
[configFetch fetchWithUserProperties:[[NSDictionary alloc] init]
fetchTypeHeader:@"Base/1"
completionHandler:handler
updateCompletionHandler:nil];
});
_responseData[0] = [NSJSONSerialization dataWithJSONObject:@{} options:0 error:nil];
// A no network error is accompanied with an HTTP status code of 0.
_URLResponse[0] =
[[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"https://firebase.com"]
statusCode:0
HTTPVersion:nil
headerFields:@{@"etag" : @"etag1"}];
[config updateWithNewInstancesForConfigFetch:configFetch
configContent:configContent
configSettings:settings
configExperiment:nil];
XCTestExpectation *expectation =
[self expectationWithDescription:@"Network error doesn't increase throttle interval"];
XCTAssertEqual(config.lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet);
FIRRemoteConfigFetchCompletion fetchCompletion =
^void(FIRRemoteConfigFetchStatus status, NSError *error) {
XCTAssertEqual(config.lastFetchStatus, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(settings.exponentialBackoffRetryInterval, 0);
[expectation fulfill];
};
[config fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
// Activate should return false if a fetch response returns 200 with NO_CHANGE as the response body.
- (void)testActivateOnFetchNoChangeStatus {
// Override the setup values to return back an error status.
RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:_DBManager];
// Populate the default, second app, second namespace instances.
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
NSString *currentAppName = nil;
FIROptions *currentOptions = nil;
NSString *currentNamespace = nil;
switch (i) {
case RCNTestRCInstanceSecondNamespace:
currentAppName = RCNTestsDefaultFIRAppName;
currentOptions = [self firstAppOptions];
currentNamespace = RCNTestsPerfNamespace;
break;
case RCNTestRCInstanceSecondApp:
currentAppName = RCNTestsSecondFIRAppName;
currentOptions = [self secondAppOptions];
currentNamespace = _namespaceGoogleMobilePlatform;
break;
case RCNTestRCInstanceDefault:
default:
currentAppName = RCNTestsDefaultFIRAppName;
currentOptions = [self firstAppOptions];
currentNamespace = RCNTestsFIRNamespace;
break;
}
NSString *fullyQualifiedNamespace =
[NSString stringWithFormat:@"%@:%@", currentNamespace, currentAppName];
RCNUserDefaultsManager *userDefaultsManager =
[[RCNUserDefaultsManager alloc] initWithAppName:currentAppName
bundleID:[NSBundle mainBundle].bundleIdentifier
namespace:fullyQualifiedNamespace];
userDefaultsManager.lastFetchTime = 10;
FIRRemoteConfig *config =
OCMPartialMock([[FIRRemoteConfig alloc] initWithAppName:currentAppName
FIROptions:currentOptions
namespace:currentNamespace
DBManager:_DBManager
configContent:configContent
analytics:nil]);
_configInstances[i] = config;
RCNConfigSettings *settings =
[[RCNConfigSettings alloc] initWithDatabaseManager:_DBManager
namespace:fullyQualifiedNamespace
firebaseAppName:currentAppName
googleAppID:currentOptions.googleAppID];
// Start the test with the assumption that we have some data that was fetched and activated.
settings.lastETag = @"etag1";
settings.lastETagUpdateTime = 100;
settings.lastApplyTimeInterval = 101;
dispatch_queue_t queue =
dispatch_queue_create([[NSString stringWithFormat:@"testNoStatusFetchQueue: %d", i]
cStringUsingEncoding:NSUTF8StringEncoding],
DISPATCH_QUEUE_SERIAL);
_configFetch[i] = OCMPartialMock([[RCNConfigFetch alloc] initWithContent:configContent
DBManager:_DBManager
settings:settings
analytics:nil
experiment:nil
queue:queue
namespace:fullyQualifiedNamespace
options:currentOptions]);
_configRealtime[i] = OCMPartialMock([[RCNConfigRealtime alloc] init:_configFetch[i]
settings:settings
namespace:fullyQualifiedNamespace
options:currentOptions]);
OCMStub([_configFetch[i] fetchConfigWithExpirationDuration:43200 completionHandler:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
__unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status,
NSError *_Nullable error) = nil;
[invocation getArgument:&handler atIndex:3];
[self->_configFetch[i] fetchWithUserProperties:[[NSDictionary alloc] init]
fetchTypeHeader:@"Base/1"
completionHandler:handler
updateCompletionHandler:nil];
});
_response[i] = @{@"state" : @"NO_CHANGE"};
_responseData[i] = [NSJSONSerialization dataWithJSONObject:_response[i] options:0 error:nil];
_URLResponse[i] =
[[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"https://firebase.com"]
statusCode:200
HTTPVersion:nil
headerFields:@{@"etag" : @"etag1"}];
id completionBlock =
[OCMArg invokeBlockWithArgs:_responseData[i], _URLResponse[i], [NSNull null], nil];
OCMStub([_configFetch[i] URLSessionDataTaskWithContent:[OCMArg any]
fetchTypeHeader:@"Base/1"
completionHandler:completionBlock])
.andReturn(nil);
[_configInstances[i] updateWithNewInstancesForConfigFetch:_configFetch[i]
configContent:configContent
configSettings:settings
configExperiment:nil];
}
// Make the fetch calls for all instances.
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Test enumerating configs successfully - instance %d", i]];
XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet);
// Make sure activate returns false in fetch completion.
FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(self->_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess);
[self->_configInstances[i] activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertFalse(changed);
XCTAssertNil(error);
[expectations[i] fulfill];
}];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testConfigValueForKey {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] =
[self expectationWithDescription:
[NSString stringWithFormat:@"Test configValueForKey: method - instance %d", i]];
XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet);
FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess);
XCTAssertNil(error);
[self->_configInstances[i] activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertTrue(changed);
NSString *key1 = [NSString stringWithFormat:@"key1-%d", i];
NSString *key2 = [NSString stringWithFormat:@"key2-%d", i];
NSString *key3 = [NSString stringWithFormat:@"key3-%d", i];
NSString *key7 = [NSString stringWithFormat:@"key7-%d", i];
NSString *value1 = [NSString stringWithFormat:@"value1-%d", i];
NSString *value2 = [NSString stringWithFormat:@"value2-%d", i];
NSString *value3 = [NSString stringWithFormat:@"value3-%d", i];
NSString *value7 = [NSString stringWithFormat:@"value7-%d", i];
XCTAssertEqualObjects(self->_configInstances[i][key1].stringValue, value1);
XCTAssertEqualObjects(self->_configInstances[i][key2].stringValue, value2);
OCMVerify([self->_configInstances[i] objectForKeyedSubscript:key1]);
XCTAssertEqualObjects([self->_configInstances[i] configValueForKey:key3].stringValue,
value3);
if (i == RCNTestRCInstanceDefault) {
XCTAssertEqualObjects([self->_configInstances[i] configValueForKey:key7].stringValue,
value7);
}
XCTAssertEqualObjects([self->_configInstances[i] configValueForKey:key7].stringValue,
value7);
XCTAssertNotNil([self->_configInstances[i] configValueForKey:nil]);
XCTAssertEqual([self->_configInstances[i] configValueForKey:nil].source,
FIRRemoteConfigSourceStatic);
XCTAssertEqual([self->_configInstances[i] configValueForKey:nil].source,
FIRRemoteConfigSourceStatic);
XCTAssertEqual([self->_configInstances[i] configValueForKey:nil source:-1].source,
FIRRemoteConfigSourceStatic);
[expectations[i] fulfill];
}];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testFetchConfigWithDefaultSets {
NSMutableArray<XCTestExpectation *> *fetchConfigsExpectation =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
fetchConfigsExpectation[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Test fetch configs with defaults set - instance %d", i]];
NSString *key1 = [NSString stringWithFormat:@"key1-%d", i];
NSString *key2 = [NSString stringWithFormat:@"key2-%d", i];
NSString *key0 = [NSString stringWithFormat:@"key0-%d", i];
NSString *value1 = [NSString stringWithFormat:@"value1-%d", i];
NSString *value2 = [NSString stringWithFormat:@"value2-%d", i];
NSDictionary<NSString *, NSString *> *defaults = @{key1 : @"default key1", key0 : @"value0-0"};
[_configInstances[i] setDefaults:defaults];
FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(self->_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess);
XCTAssertNil(error);
XCTAssertEqualObjects(self->_configInstances[i][key1].stringValue, @"default key1");
XCTAssertEqual(self->_configInstances[i][key1].source, FIRRemoteConfigSourceDefault);
[self->_configInstances[i] activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertTrue(changed);
XCTAssertEqualObjects(self->_configInstances[i][key1].stringValue, value1);
XCTAssertEqual(self->_configInstances[i][key1].source, FIRRemoteConfigSourceRemote);
XCTAssertEqualObjects([self->_configInstances[i] defaultValueForKey:key1].stringValue,
@"default key1");
XCTAssertEqualObjects(self->_configInstances[i][key2].stringValue, value2);
XCTAssertEqualObjects(self->_configInstances[i][key0].stringValue, @"value0-0");
XCTAssertNil([self->_configInstances[i] defaultValueForKey:nil]);
OCMVerify([self->_configInstances[i] objectForKeyedSubscript:key1]);
XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess,
@"Callback of first successful config "
@"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccess.");
[fetchConfigsExpectation[i] fulfill];
}];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testDefaultsSetsOnly {
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
NSString *key1 = [NSString stringWithFormat:@"key1-%d", i];
NSString *key2 = [NSString stringWithFormat:@"key2-%d", i];
NSString *key3 = [NSString stringWithFormat:@"key3-%d", i];
NSString *key4 = [NSString stringWithFormat:@"key4-%d", i];
NSString *key5 = [NSString stringWithFormat:@"key5-%d", i];
NSString *defaultValue1 = @"default value1";
NSData *defaultValue2 = [defaultValue1 dataUsingEncoding:NSUTF8StringEncoding];
NSNumber *defaultValue3 = [NSNumber numberWithFloat:3.1415926];
NSDate *defaultValue4 = [NSDate date];
BOOL defaultValue5 = NO;
NSMutableDictionary<NSString *, id> *defaults = [NSMutableDictionary dictionaryWithDictionary:@{
key1 : defaultValue1,
key2 : defaultValue2,
key3 : defaultValue3,
key4 : defaultValue4,
key5 : @(defaultValue5),
}];
[_configInstances[i] setDefaults:defaults];
if (i == RCNTestRCInstanceSecondNamespace) {
[defaults setObject:@"2860" forKey:@"experience"];
[_configInstances[i] setDefaults:defaults];
}
// Remove objects right away to make sure dispatch_async gets the copy.
[defaults removeAllObjects];
FIRRemoteConfig *config = _configInstances[i];
XCTAssertEqualObjects(config[key1].stringValue, defaultValue1, @"Should support string format");
XCTAssertEqualObjects(config[key2].dataValue, defaultValue2, @"Should support data format");
XCTAssertEqual(config[key3].numberValue.longValue, defaultValue3.longValue,
@"Should support number format");
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSString *strValueOfDate = [dateFormatter stringFromDate:(NSDate *)defaultValue4];
XCTAssertEqualObjects(
config[key4].stringValue, strValueOfDate,
@"Date format can be set as an input from plist, but output coming out of "
@"defaultConfig as string.");
XCTAssertEqual(config[key5].boolValue, defaultValue5, @"Should support bool format");
if (i == RCNTestRCInstanceSecondNamespace) {
XCTAssertEqualObjects(
[_configInstances[i] configValueForKey:@"experience"].stringValue, @"2860",
@"Only default config has the key, must equal to default config value.");
}
// Reset default sets
[_configInstances[i] setDefaults:nil];
XCTAssertEqual([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceDefault].count, 0);
}
}
- (void)testSetDefaultsWithNilParams {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Set defaults no callback expectation - instance %d", i]];
// Should work when passing nil.
[_configInstances[i] setDefaults:nil];
[_configInstances[i] setDefaults:nil];
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
XCTAssertEqual(
[self->_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceDefault].count, 0);
[expectations[i] fulfill];
});
}
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
- (void)testFetchConfigOverwriteDefaultSet {
NSMutableArray<XCTestExpectation *> *fetchConfigsExpectation =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
fetchConfigsExpectation[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Test fetch configs with defaults set - instance %d", i]];
NSString *key1 = [NSString stringWithFormat:@"key1-%d", i];
NSString *value1 = [NSString stringWithFormat:@"value1-%d", i];
[_configInstances[i] setDefaults:@{key1 : @"default key1"}];
FIRRemoteConfigValue *value = _configInstances[i][key1];
XCTAssertEqualObjects(value.stringValue, @"default key1");
XCTAssertEqual(value.source, FIRRemoteConfigSourceDefault);
value = _configInstances[i][@"A key doesn't exist"];
XCTAssertEqual(value.source, FIRRemoteConfigSourceStatic);
FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(self->_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess);
XCTAssertNil(error);
[self->_configInstances[i] activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertTrue(changed);
XCTAssertEqualObjects(self->_configInstances[i][key1].stringValue, value1);
XCTAssertEqual(self->_configInstances[i][key1].source, FIRRemoteConfigSourceRemote);
XCTAssertEqualObjects([self->_configInstances[i] defaultValueForKey:key1].stringValue,
@"default key1");
OCMVerify([self->_configInstances[i] objectForKeyedSubscript:key1]);
XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess,
@"Callback of first successful config "
@"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccess.");
[fetchConfigsExpectation[i] fulfill];
}];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testGetConfigValueBySource {
NSMutableArray<XCTestExpectation *> *fetchConfigsExpectation =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
fetchConfigsExpectation[i] =
[self expectationWithDescription:
[NSString stringWithFormat:@"Test get config value by source - instance %d", i]];
NSString *key1 = [NSString stringWithFormat:@"key1-%d", i];
NSString *value1 = [NSString stringWithFormat:@"value1-%d", i];
NSDictionary<NSString *, NSString *> *defaults = @{key1 : @"default value1"};
[_configInstances[i] setDefaults:defaults];
FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(self->_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess);
XCTAssertNil(error);
XCTAssertEqualObjects(self->_configInstances[i][key1].stringValue, @"default value1");
XCTAssertEqual(self->_configInstances[i][key1].source, FIRRemoteConfigSourceDefault);
[self->_configInstances[i] activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertTrue(changed);
XCTAssertEqualObjects(self->_configInstances[i][key1].stringValue, value1);
XCTAssertEqual(self->_configInstances[i][key1].source, FIRRemoteConfigSourceRemote);
FIRRemoteConfigValue *value;
if (i == RCNTestRCInstanceDefault) {
value = [self->_configInstances[i] configValueForKey:key1
source:FIRRemoteConfigSourceRemote];
XCTAssertEqualObjects(value.stringValue, value1);
value = [self->_configInstances[i] configValueForKey:key1
source:FIRRemoteConfigSourceDefault];
XCTAssertEqualObjects(value.stringValue, @"default value1");
value = [self->_configInstances[i] configValueForKey:key1
source:FIRRemoteConfigSourceStatic];
} else {
value = [self->_configInstances[i] configValueForKey:key1
source:FIRRemoteConfigSourceRemote];
XCTAssertEqualObjects(value.stringValue, value1);
value = [self->_configInstances[i] configValueForKey:key1
source:FIRRemoteConfigSourceDefault];
XCTAssertEqualObjects(value.stringValue, @"default value1");
value = [self->_configInstances[i] configValueForKey:key1
source:FIRRemoteConfigSourceStatic];
}
XCTAssertEqualObjects(value.stringValue, @"");
XCTAssertEqualObjects(value.numberValue, @(0));
XCTAssertEqual(value.boolValue, NO);
XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess,
@"Callback of first successful config "
@"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccess.");
[fetchConfigsExpectation[i] fulfill];
}];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
- (void)testInvalidKeyOrNamespace {
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
FIRRemoteConfigValue *value = [_configInstances[i] configValueForKey:nil];
XCTAssertNotNil(value);
XCTAssertEqual(value.source, FIRRemoteConfigSourceStatic);
value = [_configInstances[i] configValueForKey:nil];
XCTAssertNotNil(value);
XCTAssertEqual(value.source, FIRRemoteConfigSourceStatic);
value = [_configInstances[i] configValueForKey:nil source:5];
XCTAssertNotNil(value);
XCTAssertEqual(value.source, FIRRemoteConfigSourceStatic);
}
}
// Remote Config converts UTC times in the plists to local times. This utility function makes it
// possible to check the times when running the tests in any timezone.
static NSString *UTCToLocal(NSString *utcTime) {
// Create a UTC dateFormatter.
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
NSDate *date = [dateFormatter dateFromString:utcTime];
[dateFormatter setTimeZone:[NSTimeZone localTimeZone]];
return [dateFormatter stringFromDate:date];
}
// Manage different bundle locations for Swift Package Manager, CocoaPods static, CocoaPods dynamic.
- (void)setDefaultsFor:(FIRRemoteConfig *)config {
#if SWIFT_PACKAGE
NSBundle *bundle = SWIFTPM_MODULE_BUNDLE;
NSString *plistFile = [bundle pathForResource:@"Defaults-testInfo" ofType:@"plist"];
#else
NSBundle *bundle = [NSBundle mainBundle];
NSString *plistFile = [bundle pathForResource:@"Defaults-testInfo" ofType:@"plist"];
if (plistFile != nil) {
[config setDefaultsFromPlistFileName:@"Defaults-testInfo"];
return;
}
// We've linked dynamically and the plist file is in the test's bundle.
for (bundle in [NSBundle allBundles]) {
plistFile = [bundle pathForResource:@"Defaults-testInfo" ofType:@"plist"];
if (plistFile != nil) {
break;
}
}
#endif
NSDictionary *defaults = [[NSDictionary alloc] initWithContentsOfFile:plistFile];
[config setDefaults:defaults];
}
- (void)testSetDefaultsFromPlist {
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
FIRRemoteConfig *config = _configInstances[i];
[self setDefaultsFor:config];
XCTAssertEqualObjects(_configInstances[i][@"lastCheckTime"].stringValue,
UTCToLocal(@"2016-02-28 18:33:31"));
XCTAssertEqual(_configInstances[i][@"isPaidUser"].boolValue, YES);
XCTAssertEqualObjects(_configInstances[i][@"dataValue"].stringValue, @"2.4");
XCTAssertEqualObjects(_configInstances[i][@"New item"].numberValue, @(2.4));
XCTAssertEqualObjects(_configInstances[i][@"Languages"].stringValue, @"English");
XCTAssertEqualObjects(_configInstances[i][@"FileInfo"].stringValue,
@"To setup default config.");
XCTAssertEqualObjects(_configInstances[i][@"format"].stringValue, @"key to value.");
XCTAssertEqualObjects(_configInstances[i][@"arrayValue"].JSONValue,
((id) @[ @"foo", @"bar", @"baz" ]));
XCTAssertEqualObjects(_configInstances[i][@"dictValue"].JSONValue,
((id) @{@"foo" : @"foo", @"bar" : @"bar", @"baz" : @"baz"}));
// If given a wrong file name, the default will not be set and kept as previous results.
[_configInstances[i] setDefaultsFromPlistFileName:@""];
XCTAssertEqualObjects(_configInstances[i][@"lastCheckTime"].stringValue,
UTCToLocal(@"2016-02-28 18:33:31"));
[_configInstances[i] setDefaultsFromPlistFileName:@"non-existed_file"];
XCTAssertEqualObjects(_configInstances[i][@"dataValue"].stringValue, @"2.4");
[_configInstances[i] setDefaultsFromPlistFileName:nil];
XCTAssertEqualObjects(_configInstances[i][@"New item"].numberValue, @(2.4));
[_configInstances[i] setDefaultsFromPlistFileName:@""];
XCTAssertEqualObjects(_configInstances[i][@"Languages"].stringValue, @"English");
}
}
- (void)testAllKeysFromSource {
NSMutableArray<XCTestExpectation *> *fetchConfigsExpectation =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
fetchConfigsExpectation[i] = [self
expectationWithDescription:[NSString
stringWithFormat:@"Test allKeys methods - instance %d", i]];
NSString *key1 = [NSString stringWithFormat:@"key1-%d", i];
NSString *key0 = [NSString stringWithFormat:@"key0-%d", i];
NSDictionary<NSString *, NSString *> *defaults = @{key1 : @"default key1", key0 : @"value0-0"};
[_configInstances[i] setDefaults:defaults];
FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess);
XCTAssertNil(error);
[self->_configInstances[i] activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertTrue(changed);
XCTAssertEqual(
[self->_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceRemote].count, 100);
XCTAssertEqual(
[self->_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceDefault].count, 2);
XCTAssertEqual(
[self->_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceStatic].count, 0);
[fetchConfigsExpectation[i] fulfill];
}];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testAllKeysWithPrefix {
NSMutableArray<XCTestExpectation *> *fetchConfigsExpectation =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
fetchConfigsExpectation[i] = [self
expectationWithDescription:[NSString
stringWithFormat:@"Test allKeys methods - instance %d", i]];
FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess);
XCTAssertNil(error);
NSLog(@"Testing _configInstances %d", i);
[self->_configInstances[i] activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
XCTAssertTrue(changed);
// Test keysWithPrefix: method.
XCTAssertEqual([self->_configInstances[i] keysWithPrefix:@"key1"].count, 12);
XCTAssertEqual([self->_configInstances[i] keysWithPrefix:@"key"].count, 100);
XCTAssertEqual([self->_configInstances[i] keysWithPrefix:@"invalid key"].count, 0);
XCTAssertEqual([self->_configInstances[i] keysWithPrefix:nil].count, 100);
XCTAssertEqual([self->_configInstances[i] keysWithPrefix:@""].count, 100);
[fetchConfigsExpectation[i] fulfill];
}];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
/// Test the minimum fetch interval is applied and read back correctly.
- (void)testSetMinimumFetchIntervalConfigSetting {
NSMutableArray<XCTestExpectation *> *fetchConfigsExpectation =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
fetchConfigsExpectation[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Test minimumFetchInterval expectation - instance %d", i]];
FIRRemoteConfigSettings *settings = [[FIRRemoteConfigSettings alloc] init];
settings.minimumFetchInterval = 123;
[_configInstances[i] setConfigSettings:settings];
XCTAssertEqual([_configInstances[i] configSettings].minimumFetchInterval, 123);
FIRRemoteConfigFetchCompletion fetchCompletion =
^void(FIRRemoteConfigFetchStatus status, NSError *error) {
XCTAssertFalse([self->_configInstances[i].settings hasMinimumFetchIntervalElapsed:43200]);
// Update minimum fetch interval.
FIRRemoteConfigSettings *settings = [[FIRRemoteConfigSettings alloc] init];
settings.minimumFetchInterval = 0;
[self->_configInstances[i] setConfigSettings:settings];
XCTAssertEqual([self->_configInstances[i] configSettings].minimumFetchInterval, 0);
XCTAssertTrue([self->_configInstances[i].settings hasMinimumFetchIntervalElapsed:0]);
[fetchConfigsExpectation[i] fulfill];
};
[_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
}
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
/// Test the fetch timeout is properly set and read back.
- (void)testSetFetchTimeoutConfigSetting {
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
FIRRemoteConfigSettings *settings = [[FIRRemoteConfigSettings alloc] init];
settings.fetchTimeout = 1;
[_configInstances[i] setConfigSettings:settings];
XCTAssertEqual([_configInstances[i] configSettings].fetchTimeout, 1);
NSURLSession *networkSession = [_configFetch[i] currentNetworkSession];
XCTAssertNotNil(networkSession);
XCTAssertEqual(networkSession.configuration.timeoutIntervalForResource, 1);
XCTAssertEqual(networkSession.configuration.timeoutIntervalForRequest, 1);
}
}
- (void)testFetchRequestWithUserPropertiesOnly {
NSDictionary *userProperties = @{@"user_key" : @"user_value"};
NSString *req = [_settings nextRequestWithUserProperties:userProperties];
XCTAssertTrue([req containsString:@"analytics_user_properties:{\"user_key\":\"user_value\"}"]);
XCTAssertFalse([req containsString:@"first_open_time"]);
}
- (void)testFetchRequestWithFirstOpenTimeAndUserProperties {
NSDictionary *userProperties = @{@"_fot" : @1649116800000, @"user_key" : @"user_value"};
NSString *req = [_settings nextRequestWithUserProperties:userProperties];
XCTAssertTrue([req containsString:@"first_open_time:'2022-04-05T00:00:00Z'"]);
XCTAssertTrue([req containsString:@"analytics_user_properties:{\"user_key\":\"user_value\"}"]);
}
- (void)testFetchRequestFirstOpenTimeOnly {
NSDictionary *userProperties = @{@"_fot" : @1650315600000};
NSString *req = [_settings nextRequestWithUserProperties:userProperties];
XCTAssertTrue([req containsString:@"first_open_time:'2022-04-18T21:00:00Z'"]);
XCTAssertFalse([req containsString:@"analytics_user_properties"]);
}
#pragma mark - Public Factory Methods
- (void)testConfigureConfigWithValidInput {
// Configure the default app with our options and ensure the Remote Config instance is set up
// properly.
if (![FIRApp isDefaultAppConfigured]) {
XCTAssertNoThrow([FIRApp configureWithOptions:[self firstAppOptions]]);
}
XCTAssertNoThrow([FIRRemoteConfig remoteConfig]);
FIRRemoteConfig *config = [FIRRemoteConfig remoteConfig];
XCTAssertNotNil(config);
// Ensure the same instance is returned from the singleton.
FIRRemoteConfig *sameConfig = [FIRRemoteConfig remoteConfig];
XCTAssertNotNil(sameConfig);
XCTAssertEqual(config, sameConfig);
// Ensure the app name is stored properly.
XCTAssertEqual([config valueForKey:@"_appName"], kFIRDefaultAppName);
}
#pragma mark - Realtime tests
- (void)testRealtimeAddConfigUpdateListenerWithValidListener {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString
stringWithFormat:@"Test Realtime add listener successfully - instance %d", i]];
OCMStub([_configRealtime[i] beginRealtimeStream]).andDo(nil);
id completion = ^void(FIRRemoteConfigUpdate *_Nullable configUpdate, NSError *_Nullable error) {
if (error != nil) {
NSLog(@"Callback");
}
};
[_configRealtime[i] addConfigUpdateListener:completion];
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
OCMVerify([self->_configRealtime[i] beginRealtimeStream]);
OCMVerify([self->_configRealtime[i] addConfigUpdateListener:completion]);
[expectations[i] fulfill];
});
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
}
- (void)testRealtimeAddConfigUpdateListenerWithInvalidListener {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString
stringWithFormat:@"Test Realtime add listener unsuccessfully - instance %d", i]];
id completion = nil;
[_configRealtime[i] addConfigUpdateListener:completion];
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
OCMVerify(never(), [self->_configRealtime[i] beginRealtimeStream]);
[expectations[i] fulfill];
});
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
}
- (void)testRemoveRealtimeListener {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString
stringWithFormat:@"Test Realtime remove listeners successfully - instance %d", i]];
id completion = ^void(FIRRemoteConfigUpdate *_Nullable configUpdate, NSError *_Nullable error) {
if (error != nil) {
NSLog(@"Callback");
}
};
OCMStub([_configRealtime[i] beginRealtimeStream]).andDo(nil);
FIRConfigUpdateListenerRegistration *registration =
[_configRealtime[i] addConfigUpdateListener:completion];
[registration remove];
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
OCMVerify([self->_configRealtime[i] addConfigUpdateListener:completion]);
OCMVerify([self->_configRealtime[i] removeConfigUpdateListener:completion]);
OCMVerify([self->_configRealtime[i] pauseRealtimeStream]);
[expectations[i] fulfill];
});
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
}
- (void)testRealtimeFetch {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Test Realtime Autofetch successfully - instance %d", i]];
OCMStub([_configFetch[i] realtimeFetchConfigWithNoExpirationDuration:1
completionHandler:OCMOCK_ANY])
.andDo(nil);
OCMStub([_configRealtime[i] scheduleFetch:1 targetVersion:1]).andDo(nil);
[_configRealtime[i] fetchLatestConfig:3 targetVersion:1];
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
OCMVerify([self->_configFetch[i] realtimeFetchConfigWithNoExpirationDuration:1
completionHandler:OCMOCK_ANY]);
[expectations[i] fulfill];
});
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
}
- (void)testAutofetch {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Test Realtime Autofetch successfully - instance %d", i]];
OCMStub([_configRealtime[i] scheduleFetch:1 targetVersion:1]).andDo(nil);
[_configRealtime[i] autoFetch:1 targetVersion:1];
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
OCMVerify([self->_configRealtime[i] scheduleFetch:1 targetVersion:1]);
[expectations[i] fulfill];
});
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
}
- (void)testAddOnConfigUpdateMethodSuccess {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString
stringWithFormat:@"Test public realtime method successfully - instance %d", i]];
OCMStub([_configRealtime[i] beginRealtimeStream]).andDo(nil);
id completion = ^void(FIRRemoteConfigUpdate *_Nullable configUpdate, NSError *_Nullable error) {
if (error != nil) {
NSLog(@"Callback");
}
};
[_configInstances[i] addOnConfigUpdateListener:completion];
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
OCMVerify([self->_configRealtime[i] addConfigUpdateListener:completion]);
[expectations[i] fulfill];
});
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
}
- (void)testAddOnConfigUpdateMethodFail {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Test public realtime method and fails - instance %d", i]];
id completion = nil;
[_configInstances[i] addOnConfigUpdateListener:completion];
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
OCMVerify(never(), [self->_configRealtime[i] beginRealtimeStream]);
[expectations[i] fulfill];
});
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
}
- (void)testRealtimeDisabled {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString
stringWithFormat:@"Test isRealtimeDisabled flag and makes it true - instance %d",
i]];
OCMStub([_configRealtime[i] pauseRealtimeStream]).andDo(nil);
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
[dictionary setValue:@"true" forKey:@"featureDisabled"];
[dictionary setValue:@"1" forKey:@"latestTemplateVersionNumber"];
[_configRealtime[i] evaluateStreamResponse:dictionary error:nil];
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
OCMVerify([self->_configRealtime[i] pauseRealtimeStream]);
OCMVerify(never(), [self->_configRealtime[i] autoFetch:5 targetVersion:1]);
[expectations[i] fulfill];
});
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
}
- (void)testRealtimeUpdatesBackoffMetadataWhenRetryIntervalIsProvided {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] =
[self expectationWithDescription:
[NSString stringWithFormat:@"Test backoff metadata updates with a provided retry "
@"interval in the stream response - instance %d",
i]];
NSTimeInterval realtimeRetryInterval = 240;
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
[dictionary setValue:@"1" forKey:@"latestTemplateVersionNumber"];
[dictionary setValue:@(realtimeRetryInterval) forKey:@"retryIntervalSeconds"];
NSTimeInterval expectedThrottleEndTime =
[[NSDate date] timeIntervalSince1970] + realtimeRetryInterval;
[_configRealtime[i] evaluateStreamResponse:dictionary error:nil];
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
NSTimeInterval retrievedThrottleEndTime =
self->_configInstances[i].settings.realtimeExponentialBackoffThrottleEndTime;
XCTAssertEqualWithAccuracy(retrievedThrottleEndTime, expectedThrottleEndTime, 1.0);
[expectations[i] fulfill];
});
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
}
- (void)testRealtimeStreamRequestBody {
XCTestExpectation *requestBodyExpectation = [self expectationWithDescription:@"requestBody"];
__block NSData *requestBody;
[_configRealtime[0] createRequestBodyWithCompletion:^(NSData *_Nonnull data) {
requestBody = data;
[requestBodyExpectation fulfill];
}];
[self waitForExpectations:@[ requestBodyExpectation ] timeout:5.0];
NSError *error;
NSData *uncompressedRequest = [NSData gul_dataByInflatingGzippedData:requestBody error:&error];
NSString *strData = [[NSString alloc] initWithData:uncompressedRequest
encoding:NSUTF8StringEncoding];
XCTAssertTrue([strData containsString:@"project:'correct_gcm_sender_id'"]);
XCTAssertTrue([strData containsString:@"namespace:'firebase'"]);
XCTAssertTrue([strData containsString:@"lastKnownVersionNumber:'0'"]);
XCTAssertTrue([strData containsString:@"appId:'1:123:ios:123abc'"]);
XCTAssertTrue([strData containsString:@"sdkVersion:"]);
XCTAssertTrue([strData containsString:@"appInstanceId:'iid'"]);
}
- (void)testFetchAndActivateRolloutsNotifyInterop {
XCTestExpectation *notificationExpectation =
[self expectationForNotification:@"FIRRolloutsStateDidChangeNotification"
object:nil
handler:nil];
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.");
[notificationExpectation fulfill];
};
[_configInstances[RCNTestRCInstanceDefault]
fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion];
[self waitForExpectations:@[ notificationExpectation ] timeout:_expectationTimeout];
}
- (void)testURLSessionDelegateHandlesChunkedJSON {
NSString *testString = @"} {\"testKey\":\"testValue\"}";
NSData *testData = [testString dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString
stringWithFormat:@"Test delegate method handling chunked JSON - instance %d", i]];
NSURLSession *networkSession = [_configFetch[i] currentNetworkSession];
NSURLSessionDataTask *dataTask = [_configFetch[i] URLSessionDataTaskWithContent:[OCMArg any]
fetchTypeHeader:[OCMArg any]
completionHandler:nil];
XCTAssertNoThrow([_configRealtime[i] URLSession:networkSession
dataTask:dataTask
didReceiveData:testData]);
[expectations[i] fulfill];
}
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
- (void)testURLSessionDelegateIgnoresInvalidUTF8Data {
const unsigned char bytes[] = {0xFF, 0xFE, '{', '}'};
NSData *testData = [NSData dataWithBytes:bytes length:sizeof(bytes)];
NSURLSession *networkSession = [_configFetch[0] currentNetworkSession];
NSURLSessionDataTask *dataTask = [_configFetch[0] URLSessionDataTaskWithContent:[OCMArg any]
fetchTypeHeader:[OCMArg any]
completionHandler:nil];
XCTAssertNoThrow([_configRealtime[0] URLSession:networkSession
dataTask:dataTask
didReceiveData:testData]);
}
- (void)testRealtimeStreamResponseWithMalformedFieldsDoesNotCrash {
OCMStub([_configRealtime[0] pauseRealtimeStream]).andDo(nil);
OCMStubRecorder *autoFetchStub = OCMStub([_configRealtime[0] autoFetch:0 targetVersion:0]);
autoFetchStub = [autoFetchStub ignoringNonObjectArgs];
autoFetchStub.andDo(nil);
NSArray *malformedValues = @[ [NSNull null], @[ @1 ], @{@"key" : @"value"} ];
for (id value in malformedValues) {
NSDictionary *response = @{
@"latestTemplateVersionNumber" : value,
@"featureDisabled" : value,
@"retryIntervalSeconds" : value
};
XCTAssertNoThrow([_configRealtime[0] evaluateStreamResponse:response error:nil]);
}
XCTAssertNoThrow([_configRealtime[0] evaluateStreamResponse:(NSDictionary *)@[] error:nil]);
// Malformed `featureDisabled` values must not disable realtime.
OCMVerify(never(), [_configRealtime[0] pauseRealtimeStream]);
}
- (void)testRealtimeFeatureDisabledFalseKeepsRealtimeEnabled {
OCMStub([_configRealtime[0] pauseRealtimeStream]).andDo(nil);
for (id value in @[ @NO, @"false" ]) {
NSDictionary *response = @{@"featureDisabled" : value, @"latestTemplateVersionNumber" : @"0"};
[_configRealtime[0] evaluateStreamResponse:response error:nil];
}
OCMVerify(never(), [_configRealtime[0] pauseRealtimeStream]);
}
- (void)testRealtimeRetryIntervalIsClamped {
RCNConfigSettings *settings = _configInstances[0].settings;
// Includes values outside the NSInteger range, which must not wrap negative.
for (id retryInterval in @[ @(1000000000000), @(1e300), @"1e300" ]) {
[settings updateRealtimeBackoffTimeWithInterval:0];
NSDictionary *response =
@{@"latestTemplateVersionNumber" : @"0", @"retryIntervalSeconds" : retryInterval};
NSTimeInterval maxThrottleEndTime = [[NSDate date] timeIntervalSince1970] + 24 * 60 * 60;
[_configRealtime[0] evaluateStreamResponse:response error:nil];
XCTAssertEqualWithAccuracy(settings.realtimeExponentialBackoffThrottleEndTime,
maxThrottleEndTime, 5.0, @"retryIntervalSeconds: %@", retryInterval);
}
}
- (void)testRealtimeNonPositiveRetryIntervalIsIgnored {
RCNConfigSettings *settings = _configInstances[0].settings;
[settings updateRealtimeBackoffTimeWithInterval:0];
NSTimeInterval throttleEndTime = settings.realtimeExponentialBackoffThrottleEndTime;
for (id retryInterval in @[ @0, @(-100), @(-1e300), @"-1e300", @(NAN) ]) {
NSDictionary *response =
@{@"latestTemplateVersionNumber" : @"0", @"retryIntervalSeconds" : retryInterval};
[_configRealtime[0] evaluateStreamResponse:response error:nil];
XCTAssertEqual(settings.realtimeExponentialBackoffThrottleEndTime, throttleEndTime,
@"retryIntervalSeconds: %@", retryInterval);
}
}
/// Returns the error `evaluateStreamResponse:error:` propagates to listeners.
- (NSError *)propagatedErrorForStreamResponse:(id)response error:(NSError *)dataError {
__block NSError *propagatedError;
OCMStub([_configRealtime[0] propagateErrors:[OCMArg checkWithBlock:^BOOL(NSError *error) {
propagatedError = error;
return YES;
}]])
.andDo(nil);
[_configRealtime[0] evaluateStreamResponse:response error:dataError];
return propagatedError;
}
- (void)testRealtimeStreamResponseParseErrorPreservesUnderlyingError {
NSError *jsonError = [NSError errorWithDomain:NSCocoaErrorDomain
code:NSPropertyListReadCorruptError
userInfo:nil];
NSError *error = [self propagatedErrorForStreamResponse:nil error:jsonError];
XCTAssertEqualObjects(error.domain, FIRRemoteConfigUpdateErrorDomain);
XCTAssertEqual(error.code, FIRRemoteConfigUpdateErrorMessageInvalid);
XCTAssertEqualObjects(error.localizedDescription, @"Unable to parse ConfigUpdate.");
XCTAssertEqualObjects(error.userInfo[NSUnderlyingErrorKey], jsonError);
}
- (void)testRealtimeStreamResponseNotDictionaryPreservesUnderlyingError {
NSError *error = [self propagatedErrorForStreamResponse:@[] error:nil];
XCTAssertEqualObjects(error.domain, FIRRemoteConfigUpdateErrorDomain);
XCTAssertEqual(error.code, FIRRemoteConfigUpdateErrorMessageInvalid);
XCTAssertEqualObjects(error.localizedDescription, @"Unable to parse ConfigUpdate.");
NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];
XCTAssertEqualObjects(underlyingError.domain, FIRRemoteConfigUpdateErrorDomain);
XCTAssertEqual(underlyingError.code, FIRRemoteConfigUpdateErrorMessageInvalid);
XCTAssertEqualObjects(underlyingError.localizedDescription,
@"ConfigUpdate message is not a JSON object.");
}
/// Fetches with a fresh `RCNConfigFetch` whose network response is `responseData`.
- (void)fetchWithMalformedResponseData:(NSData *)responseData
expectedStatus:(FIRRemoteConfigFetchStatus)expectedStatus {
FIRRemoteConfig *config = _configInstances[RCNTestRCInstanceDefault];
RCNConfigContent *configContent = [config valueForKey:@"_configContent"];
NSString *fullyQualifiedNamespace =
[NSString stringWithFormat:@"%@:%@", RCNTestsFIRNamespace, RCNTestsDefaultFIRAppName];
dispatch_queue_t queue = dispatch_queue_create("testMalformedFetchQueue", DISPATCH_QUEUE_SERIAL);
id configFetch = OCMPartialMock([[RCNConfigFetch alloc] initWithContent:configContent
DBManager:_DBManager
settings:config.settings
analytics:nil
experiment:_experimentMock
queue:queue
namespace:fullyQualifiedNamespace
options:[self firstAppOptions]]);
id completionBlock =
[OCMArg invokeBlockWithArgs:responseData, _URLResponse[0], [NSNull null], nil];
OCMStub([configFetch URLSessionDataTaskWithContent:[OCMArg any]
fetchTypeHeader:[OCMArg any]
completionHandler:completionBlock])
.andReturn(nil);
XCTestExpectation *expectation = [self expectationWithDescription:@"Malformed fetch response"];
[configFetch
fetchWithUserProperties:@{}
fetchTypeHeader:@"Base/1"
completionHandler:^(FIRRemoteConfigFetchStatus status, NSError *_Nullable error) {
XCTAssertEqual(status, expectedStatus);
[expectation fulfill];
}
updateCompletionHandler:nil];
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
[configFetch stopMocking];
}
- (void)testFetchWithMalformedServerErrorDoesNotCrash {
NSData *data = [@"{\"error\":\"internal\"}" dataUsingEncoding:NSUTF8StringEncoding];
[self fetchWithMalformedResponseData:data expectedStatus:FIRRemoteConfigFetchStatusFailure];
}
- (void)testSetCustomSignals {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:[NSString
stringWithFormat:@"Set custom signals - instance %d", i]];
NSDictionary<NSString *, NSObject *> *testSignals = @{
@"signal1" : @"stringValue",
@"signal2" : @"stringValue2",
};
[_configInstances[i] setCustomSignals:testSignals
withCompletion:^(NSError *_Nullable error) {
XCTAssertNil(error);
NSDictionary<NSString *, NSString *> *retrievedSignals =
self->_configInstances[i].settings.customSignals;
XCTAssertEqualObjects(retrievedSignals, testSignals);
[expectations[i] fulfill];
}];
}
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
- (void)testSetCustomSignalsMultipleTimes {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] = [self
expectationWithDescription:
[NSString stringWithFormat:@"Set custom signals multiple times - instance %d", i]];
// First set of signals
NSDictionary<NSString *, NSObject *> *testSignals1 = @{
@"signal1" : @"stringValue1",
@"signal2" : @"stringValue2",
};
// Second set of signals (overwrites, remove and adds new)
NSDictionary<NSString *, NSObject *> *testSignals2 = @{
@"signal1" : @"updatedValue1",
@"signal2" : [NSNull null],
@"signal3" : @5,
};
// Expected final set of signals
NSDictionary<NSString *, NSString *> *expectedSignals = @{
@"signal1" : @"updatedValue1",
@"signal3" : @"5",
};
[_configInstances[i] setCustomSignals:testSignals1
withCompletion:^(NSError *_Nullable error) {
XCTAssertNil(error);
[self->_configInstances[i]
setCustomSignals:testSignals2
withCompletion:^(NSError *_Nullable error) {
XCTAssertNil(error);
NSDictionary<NSString *, NSString *> *retrievedSignals =
self->_configInstances[i].settings.customSignals;
XCTAssertEqualObjects(retrievedSignals, expectedSignals);
[expectations[i] fulfill];
}];
}];
}
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
- (void)testSetCustomSignals_invalidInput_throwsException {
NSMutableArray<XCTestExpectation *> *expectations =
[[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
expectations[i] =
[self expectationWithDescription:
[NSString stringWithFormat:@"Set custom signals expects error - instance %d", i]];
// Invalid value type.
NSDictionary<NSString *, NSObject *> *invalidSignals1 = @{@"name" : [NSDate date]};
// Key length exceeds limit.
NSDictionary<NSString *, NSObject *> *invalidSignals2 =
@{[@"a" stringByPaddingToLength:251 withString:@"a" startingAtIndex:0] : @"value"};
// Value length exceeds limit.
NSDictionary<NSString *, NSObject *> *invalidSignals3 =
@{@"key" : [@"a" stringByPaddingToLength:501 withString:@"a" startingAtIndex:0]};
[_configInstances[i]
setCustomSignals:invalidSignals1
withCompletion:^(NSError *_Nullable error) {
XCTAssertNotNil(error);
XCTAssertEqual(error.code, FIRRemoteConfigCustomSignalsErrorInvalidValueType);
}];
[_configInstances[i]
setCustomSignals:invalidSignals2
withCompletion:^(NSError *_Nullable error) {
XCTAssertNotNil(error);
XCTAssertEqual(error.code, FIRRemoteConfigCustomSignalsErrorLimitExceeded);
}];
[_configInstances[i]
setCustomSignals:invalidSignals3
withCompletion:^(NSError *_Nullable error) {
XCTAssertNotNil(error);
XCTAssertEqual(error.code, FIRRemoteConfigCustomSignalsErrorLimitExceeded);
[expectations[i] fulfill];
}];
}
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
}
#pragma mark - Memory Leak Tests
- (void)testActivateWithCompletionMemoryLeak {
XCTestExpectation *expectation = [self expectationWithDescription:@"activate completes"];
__weak FIRRemoteConfig *weakInstance = nil;
@autoreleasepool {
FIRApp *app = [FIRApp appNamed:@"fir-app-name-test"];
if (!app) {
[FIRApp configureWithName:@"fir-app-name-test" options:[self firstAppOptions]];
app = [FIRApp appNamed:@"fir-app-name-test"];
}
RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:_DBManager];
__block FIRRemoteConfig *instance = [[FIRRemoteConfig alloc] initWithAppName:app.name
FIROptions:app.options
namespace:@"test_namespace"
DBManager:_DBManager
configContent:configContent
analytics:nil];
weakInstance = instance;
// Set settings to ensure the activation path is fully executed and does not return early.
RCNConfigSettings *settings = [instance valueForKey:@"_settings"];
settings.lastETagUpdateTime = 100;
settings.lastApplyTimeInterval = 0;
[instance activateWithCompletion:^(BOOL changed, NSError *_Nullable error) {
instance = nil;
[expectation fulfill];
}];
}
[self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
XCTAssertNil(weakInstance,
@"FIRRemoteConfig instance was not deallocated. Retain cycle present!");
}
#pragma mark - Unexpected JSON Response Type Tests
- (void)testFetchWithTopLevelJSONArrayFailsGracefully {
// A single-element top-level array used to crash on the server-error check (`count == 1`).
[self fetchWithResponseObject:@[ @{@"some" : @"data"} ]
completionHandler:^(FIRRemoteConfig *config, FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(status, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(config.lastFetchStatus, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(error.code, FIRRemoteConfigErrorInternalError);
}];
}
- (void)testFetchWithNonDictionaryServerErrorFailsGracefully {
// A single-element response whose `error` value is a string (not a dictionary) used to crash on
// the server-error subscript (`errDict[...]`).
[self fetchWithResponseObject:@{@"error" : @"boom"}
completionHandler:^(FIRRemoteConfig *config, FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(status, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(error.code, FIRRemoteConfigErrorInternalError);
}];
}
- (void)testFetchWithNonDictionaryPersonalizationMetadataFailsGracefully {
// A non-dictionary `personalizationMetadata` used to throw in `NSJSONSerialization` when it was
// written to the database.
[self fetchWithResponseObject:@{
@"state" : @"UPDATE",
@"entries" : @{@"key1" : @"value1"},
RCNFetchResponseKeyPersonalizationMetadata : @"boom"
}
completionHandler:^(FIRRemoteConfig *config, FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(status, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(config.lastFetchStatus, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(error.code, FIRRemoteConfigErrorInternalError);
}];
}
- (void)testFetchWithNonDictionaryRolloutMetadataEntryFailsGracefully {
// A `rolloutMetadata` element that isn't a dictionary used to crash on the
// `rolloutId` subscript when the config update was computed.
[self fetchWithResponseObject:@{
@"state" : @"UPDATE",
@"entries" : @{@"key1" : @"value1"},
RCNFetchResponseKeyRolloutMetadata : @[ @"boom" ]
}
completionHandler:^(FIRRemoteConfig *config, FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(status, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(config.lastFetchStatus, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(error.code, FIRRemoteConfigErrorInternalError);
}];
}
- (void)testFetchWithNonArrayExperimentDescriptionsFailsGracefully {
// A non-array `experimentDescriptions` used to crash when the experiment payloads were
// enumerated.
[self fetchWithResponseObject:@{
@"state" : @"UPDATE",
@"entries" : @{@"key1" : @"value1"},
RCNFetchResponseKeyExperimentDescriptions : @"boom"
}
completionHandler:^(FIRRemoteConfig *config, FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(status, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(config.lastFetchStatus, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(error.code, FIRRemoteConfigErrorInternalError);
}];
}
- (void)testFetchWithNonStringStateFailsGracefully {
// A non-string `state` used to be dropped by RCNConfigContent while the fetch was still
// reported as a success and the ETag and template version advanced.
[self fetchWithResponseObject:@{@"state" : @5, @"entries" : @{@"key1" : @"value1"}}
completionHandler:^(FIRRemoteConfig *config, FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(status, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(config.lastFetchStatus, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(error.code, FIRRemoteConfigErrorInternalError);
}];
}
- (void)testFetchWithNonDictionaryEntriesFailsGracefully {
// A non-dictionary `entries` used to be dropped by RCNConfigContent while the fetch was still
// reported as a success and the ETag and template version advanced.
[self fetchWithResponseObject:@{@"state" : @"UPDATE", @"entries" : @"boom"}
completionHandler:^(FIRRemoteConfig *config, FIRRemoteConfigFetchStatus status,
NSError *error) {
XCTAssertEqual(status, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(config.lastFetchStatus, FIRRemoteConfigFetchStatusFailure);
XCTAssertEqual(error.code, FIRRemoteConfigErrorInternalError);
}];
}
#pragma mark - Test Helpers
- (FIROptions *)firstAppOptions {
// TODO: Evaluate if we want to hardcode things here instead.
FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"1:123:ios:123abc"
GCMSenderID:@"correct_gcm_sender_id"];
options.APIKey = @"AIzaSy-ApiKeyWithValidFormat_0123456789";
options.projectID = @"abc-xyz-123";
return options;
}
- (FIROptions *)secondAppOptions {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
#if SWIFT_PACKAGE
bundle = SWIFTPM_MODULE_BUNDLE;
#endif
NSString *plistPath = [bundle pathForResource:@"SecondApp-GoogleService-Info" ofType:@"plist"];
FIROptions *options = [[FIROptions alloc] initWithContentsOfFile:plistPath];
XCTAssertNotNil(options);
return options;
}
/// Fetches a stubbed network response whose body is `responseObject` serialized as JSON, using
/// fresh instances for the default app and namespace, and passes the status and error received by
/// the fetch completion handler to `completionHandler`.
- (void)fetchWithResponseObject:(id)responseObject
completionHandler:(void (^)(FIRRemoteConfig *config,
FIRRemoteConfigFetchStatus status,
NSError *error))completionHandler {
RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:_DBManager];
NSString *currentAppName = RCNTestsDefaultFIRAppName;
FIROptions *currentOptions = [self firstAppOptions];
NSString *currentNamespace = RCNTestsFIRNamespace;
NSString *fullyQualifiedNamespace =
[NSString stringWithFormat:@"%@:%@", currentNamespace, currentAppName];
FIRRemoteConfig *config = OCMPartialMock([[FIRRemoteConfig alloc] initWithAppName:currentAppName
FIROptions:currentOptions
namespace:currentNamespace
DBManager:_DBManager
configContent:configContent
analytics:nil]);
RCNConfigSettings *settings =
[[RCNConfigSettings alloc] initWithDatabaseManager:_DBManager
namespace:fullyQualifiedNamespace
firebaseAppName:currentAppName
googleAppID:currentOptions.googleAppID];
RCNConfigExperiment *experiment = [[RCNConfigExperiment alloc] initWithDBManager:_DBManager
experimentController:nil];
dispatch_queue_t queue = dispatch_queue_create(
[[NSString stringWithFormat:@"testqueue"] cStringUsingEncoding:NSUTF8StringEncoding],
DISPATCH_QUEUE_SERIAL);
RCNConfigFetch *configFetch =
OCMPartialMock([[RCNConfigFetch alloc] initWithContent:configContent
DBManager:_DBManager
settings:settings
analytics:nil
experiment:experiment
queue:queue
namespace:fullyQualifiedNamespace
options:currentOptions]);
OCMStub([configFetch fetchConfigWithExpirationDuration:43200 completionHandler:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
__unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status,
NSError *_Nullable error) = nil;
[invocation getArgument:&handler atIndex:3];
[configFetch fetchWithUserProperties:[[NSDictionary alloc] init]
fetchTypeHeader:@"Base/1"
completionHandler:handler
updateCompletionHandler:nil];
});
NSData *responseData = [NSJSONSerialization dataWithJSONObject:responseObject
options:0
error:nil];
NSHTTPURLResponse *URLResponse =
[[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"https://firebase.com"]
statusCode:200
HTTPVersion:nil
headerFields:@{@"etag" : @"etag1"}];
id completionBlock = [OCMArg invokeBlockWithArgs:responseData, URLResponse, [NSNull null], nil];
OCMStub([configFetch URLSessionDataTaskWithContent:[OCMArg any]
fetchTypeHeader:@"Base/1"
completionHandler:completionBlock])
.andReturn(nil);
[config updateWithNewInstancesForConfigFetch:configFetch
configContent:configContent
configSettings:settings
configExperiment:experiment];
XCTestExpectation *expectation =
[self expectationWithDescription:@"Fetch completion handler is called"];
XCTAssertEqual(config.lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet);
[config fetchWithExpirationDuration:43200
completionHandler:^(FIRRemoteConfigFetchStatus status, NSError *error) {
completionHandler(config, status, error);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
@end