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

477 lines
21 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 <XCTest/XCTest.h>
#import "FirebaseRemoteConfig/Sources/Protos/wireless/android/config/proto/Config.pbobjc.h"
#import <FirebaseInstanceID/FIRInstanceID+Private.h>
#import <FirebaseInstanceID/FIRInstanceIDCheckinPreferences.h>
#import <OCMock/OCMock.h>
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigFetch.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"
#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"
static NSString *const RCNFakeSenderID = @"855865492447";
static NSString *const RCNFakeToken = @"ctToAh17Exk:"
@"APA91bFpX1aucYk5ONWt3MxyVxTyDKV8PKjaY2X3DPCZOOTHIBNf3ybxV5"
@"aMQ7G8zUTrduobNLEUoSvGsncthR27gDF_qqZELqp2eEi1BL8k_"
@"4AkeP2dLBq4f8MvuJTOEv2P5ChTdByr";
static NSString *const RCNFakeDeviceID = @"4421690866479820589";
static NSString *const RCNFakeSecretToken = @"6377571288467228941";
@interface RCNConfigFetch (ForTest)
// Exposes fetching user property method in the category.
//- (void)fetchWithUserPropertiesCompletionHandler:(RCNAnalyticsUserPropertiesCompletion)block;
- (void)refreshInstanceIDTokenAndFetchCheckInInfoWithCompletionHandler:
(FIRRemoteConfigFetchCompletion)completionHandler;
- (void)fetchCheckinInfoWithCompletionHandler:(FIRRemoteConfigFetchCompletion)completionHandler;
@end
@interface RCNConfigTest : XCTestCase {
NSTimeInterval _expectationTimeout;
RCNConfigSettings *_settings;
RCNConfigFetchResponse *_response;
RCNConfigContent *_configContent;
RCNConfigExperiment *_experiment;
RCNConfigFetch *_configFetch;
dispatch_queue_t _queue;
}
@end
@implementation RCNConfigTest
- (void)setUp {
[super setUp];
_expectationTimeout = 1.0;
// Mock the singleton to an instance that is reset for each unit test
_configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
_settings = [[RCNConfigSettings alloc] initWithDatabaseManager:nil];
_experiment = [[RCNConfigExperiment alloc] initWithDBManager:nil];
_queue = dispatch_queue_create("com.google.GoogleConfigService.FIRRemoteConfigTest",
DISPATCH_QUEUE_CONCURRENT);
RCNConfigFetch *fetcher = [[RCNConfigFetch alloc] initWithContent:_configContent
settings:_settings
experiment:_experiment
queue:_queue];
_configFetch = OCMPartialMock(fetcher);
// Fake a response with a default namespace and a custom namespace.
NSDictionary *namespaceToConfig = @{
FIRNamespaceGoogleMobilePlatform : @{@"key1" : @"value1", @"key2" : @"value2"},
FIRNamespaceGooglePlayPlatform : @{@"playerID" : @"36", @"gameLevel" : @"87"},
};
_response =
[RCNTestUtilities responseWithNamespaceToConfig:namespaceToConfig
statusArray:@[
@(RCNAppNamespaceConfigTable_NamespaceStatus_Update),
@(RCNAppNamespaceConfigTable_NamespaceStatus_Update)
]];
NSData *responseData = [NSData gtm_dataByDeflatingData:_response.data error:nil];
// Mock successful network fetches with an empty config response.
RCNConfigFetcherTestBlock testBlock = ^(RCNConfigFetcherCompletion completion) {
completion(responseData, nil, nil);
};
[RCNConfigFetch setGlobalTestBlock:testBlock];
// Mocks the user property fetch with a predefined dictionary.
NSDictionary *userProperties = @{@"userProperty1" : @"100", @"userProperty2" : @"200"};
OCMStub([_configFetch
fetchWithUserPropertiesCompletionHandler:([OCMArg invokeBlockWithArgs:userProperties, nil])]);
}
- (void)tearDown {
[RCNConfigFetch setGlobalTestBlock:nil];
[super tearDown];
}
- (void)testInitMethod {
RCNConfigFetch *fetcher = [[RCNConfigFetch alloc] init];
XCTAssertNotNil(fetcher);
}
- (void)testFetchAllConfigsFailedWithoutCachedResult {
XCTestExpectation *fetchFailedExpectation = [self
expectationWithDescription:@"Test first config fetch failed without any cached result."];
// Mock a failed network fetch.
NSError *error = [NSError errorWithDomain:@"testDomain" code:1 userInfo:nil];
RCNConfigFetcherTestBlock testBlock = ^(RCNConfigFetcherCompletion completion) {
completion(nil, nil, error);
};
[RCNConfigFetch setGlobalTestBlock:testBlock];
FIRRemoteConfigFetchCompletion fetchAllConfigsCompletion =
^void(FIRRemoteConfigFetchStatus status, NSError *error) {
XCTAssertNotNil(error);
XCTAssertEqual(self->_configContent.fetchedConfig.count,
0); // There's no cached result yet since this is the first fetch.
XCTAssertEqual(status, FIRRemoteConfigFetchStatusFailure,
@"Fetch config failed, there is no cached config result yet. Status must "
@"equal to FIRRemoteConfigFetchStatusNotAvailable.");
XCTAssertEqual(self->_settings.expirationInSeconds, 0,
@"expirationInSeconds is set successfully during fetch.");
XCTAssertEqual(self->_settings.lastFetchTimeInterval, 0,
@"last fetch time interval should not be set.");
XCTAssertEqual(self->_settings.lastApplyTimeInterval, 0,
@"last apply time interval should not be set.");
[fetchFailedExpectation fulfill];
};
[_configFetch fetchAllConfigsWithExpirationDuration:0
completionHandler:fetchAllConfigsCompletion];
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testFetchAllConfigsSuccessfully {
XCTestExpectation *fetchAllConfigsExpectation =
[self expectationWithDescription:@"Test fetch all configs successfully."];
FIRRemoteConfigFetchCompletion fetchAllConfigsCompletion =
^void(FIRRemoteConfigFetchStatus status, NSError *error) {
XCTAssertNil(error);
NSDictionary *result = self->_configContent.fetchedConfig;
XCTAssertNotNil(result);
[self checkConfigResult:result
withNamespace:FIRNamespaceGoogleMobilePlatform
key:@"key1"
value:@"value1"];
[self checkConfigResult:result
withNamespace:FIRNamespaceGoogleMobilePlatform
key:@"key2"
value:@"value2"];
[self checkConfigResult:result
withNamespace:FIRNamespaceGooglePlayPlatform
key:@"playerID"
value:@"36"];
[self checkConfigResult:result
withNamespace:FIRNamespaceGooglePlayPlatform
key:@"gameLevel"
value:@"87"];
XCTAssertEqual(self->_settings.expirationInSeconds, 43200,
@"expirationInSeconds is set successfully during fetch.");
XCTAssertGreaterThan(self->_settings.lastFetchTimeInterval, 0,
@"last fetch time interval should be set.");
XCTAssertEqual(self->_settings.lastApplyTimeInterval, 0,
@"last apply time interval should not be set.");
XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess,
@"Callback of first successful config "
@"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccess.");
[fetchAllConfigsExpectation fulfill];
};
[_configFetch fetchAllConfigsWithExpirationDuration:43200
completionHandler:fetchAllConfigsCompletion];
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testFetchConfigInCachedResults {
XCTestExpectation *fetchConfigExpectation =
[self expectationWithDescription:@"Test fetch config within expiration duration, meaning "
@"use fresh cached result instead of fetching from server."];
FIRRemoteConfigFetchCompletion firstFetchCompletion =
^void(FIRRemoteConfigFetchStatus status, NSError *error) {
XCTAssertNil(error);
FIRRemoteConfigFetchCompletion secondFetchCompletion = ^void(
FIRRemoteConfigFetchStatus status, NSError *error) {
XCTAssertNil(error);
NSDictionary *result = self->_configContent.fetchedConfig;
XCTAssertNotNil(result);
[self checkConfigResult:result
withNamespace:FIRNamespaceGoogleMobilePlatform
key:@"key1"
value:@"value1"];
[self checkConfigResult:result
withNamespace:FIRNamespaceGoogleMobilePlatform
key:@"key2"
value:@"value2"];
XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess,
"Config fetch's expiration duration is 43200 seconds, which means the"
"config cached data hasn't expired. Return cached result. Status must be "
"FIRRemoteConfigFetchStatusSuccess.");
[fetchConfigExpectation fulfill];
};
[_configFetch fetchAllConfigsWithExpirationDuration:43200
completionHandler:secondFetchCompletion];
};
[_configFetch fetchAllConfigsWithExpirationDuration:43200 completionHandler:firstFetchCompletion];
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testFetchFailedWithCachedResult {
XCTestExpectation *fetchFailedExpectation =
[self expectationWithDescription:@"Test fetch failed from server, use cached result."];
// Mock a failed network fetch.
NSError *error = [NSError errorWithDomain:@"testDomain" code:1 userInfo:nil];
RCNConfigFetcherTestBlock testBlock = ^(RCNConfigFetcherCompletion completion) {
completion(nil, nil, error);
};
[RCNConfigFetch setGlobalTestBlock:testBlock];
// Mock previous fetch succeed with cached data.
[_settings updateMetadata:YES namespaceToDigest:nil];
[_configContent updateConfigContentWithResponse:[_response copy]];
FIRRemoteConfigFetchCompletion fetchAllConfigsCompletion =
^void(FIRRemoteConfigFetchStatus status, NSError *error) {
XCTAssertNotNil(error);
NSDictionary *result = self->_configContent.fetchedConfig;
XCTAssertNotNil(result);
[self checkConfigResult:result
withNamespace:FIRNamespaceGoogleMobilePlatform
key:@"key1"
value:@"value1"];
[self checkConfigResult:result
withNamespace:FIRNamespaceGoogleMobilePlatform
key:@"key2"
value:@"value2"];
[self checkConfigResult:result
withNamespace:FIRNamespaceGooglePlayPlatform
key:@"playerID"
value:@"36"];
[self checkConfigResult:result
withNamespace:FIRNamespaceGooglePlayPlatform
key:@"gameLevel"
value:@"87"];
[fetchFailedExpectation fulfill];
};
// Expiration duration is set to 0, meaning always fetch from server because the cached result
// expired in 0 seconds.
[_configFetch fetchAllConfigsWithExpirationDuration:0
completionHandler:fetchAllConfigsCompletion];
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testFetchThrottledWithCachedResult {
XCTestExpectation *fetchAllConfigsExpectation =
[self expectationWithDescription:
@"Test fetch being throttled after exceeding throttling limit, use cached result."];
// Fake a new response with a different custom namespace.
NSDictionary *namespaceToConfig = @{
@"configns:MY_OWN_APP" :
@{@"columnID" : @"28", @"columnName" : @"height", @"columnValue" : @"2"}
};
RCNConfigFetchResponse *newResponse = [RCNTestUtilities
responseWithNamespaceToConfig:namespaceToConfig
statusArray:@[ @(RCNAppNamespaceConfigTable_NamespaceStatus_Update) ]];
// Mock 5 fetches ahead.
for (int i = 0; i < RCNThrottledSuccessFetchCountDefault; i++) {
[_settings updateMetadata:YES namespaceToDigest:nil];
[_configContent updateConfigContentWithResponse:[newResponse copy]];
}
FIRRemoteConfigFetchCompletion fetchAllConfigsCompletion =
^void(FIRRemoteConfigFetchStatus status, NSError *error) {
XCTAssertNotNil(error);
NSDictionary *result = self->_configContent.fetchedConfig;
XCTAssertNotNil(result); // Cached result is not nil.
[self checkConfigResult:result
withNamespace:@"configns:MY_OWN_APP"
key:@"columnID"
value:@"28"];
[self checkConfigResult:result
withNamespace:@"configns:MY_OWN_APP"
key:@"columnName"
value:@"height"];
[self checkConfigResult:result
withNamespace:@"configns:MY_OWN_APP"
key:@"columnValue"
value:@"2"];
XCTAssertEqual(error.code, (int)FIRRemoteConfigErrorThrottled,
@"Default success throttling rate is 5. Mocked 5 successful fetches from "
@"server, this fetch will be throttled. Status must equal to "
@"FIRRemoteConfigFetchStatusFetchThrottled.");
[fetchAllConfigsExpectation fulfill];
};
[_configFetch fetchAllConfigsWithExpirationDuration:-1
completionHandler:fetchAllConfigsCompletion];
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testFetchThrottledWithStaledCachedResult {
XCTestExpectation *fetchAllConfigsExpectation =
[self expectationWithDescription:@"Test fetch being throttled, use staled cache result."];
// Mock 5 fetches ahead.
for (int i = 0; i < RCNThrottledSuccessFetchCountDefault; i++) {
[_settings updateMetadata:YES namespaceToDigest:nil];
[_configContent updateConfigContentWithResponse:[_response copy]];
}
FIRRemoteConfigFetchCompletion fetchAllConfigsCompletion =
^void(FIRRemoteConfigFetchStatus status, NSError *error) {
XCTAssertNotNil(error);
NSDictionary *result = self->_configContent.fetchedConfig;
XCTAssertNotNil(result);
[self checkConfigResult:result
withNamespace:FIRNamespaceGoogleMobilePlatform
key:@"key1"
value:@"value1"];
[self checkConfigResult:result
withNamespace:FIRNamespaceGoogleMobilePlatform
key:@"key2"
value:@"value2"];
[self checkConfigResult:result
withNamespace:FIRNamespaceGooglePlayPlatform
key:@"playerID"
value:@"36"];
[self checkConfigResult:result
withNamespace:FIRNamespaceGooglePlayPlatform
key:@"gameLevel"
value:@"87"];
XCTAssertEqual(
error.code, FIRRemoteConfigErrorThrottled,
@"Request fetching within throttling time interval, so this fetch will still be "
@"throttled. "
@"However, the App context (custom variables) has changed, meaning the return cached "
@"result is staled. The status must equal to RCNConfigStatusFetchThrottledStale.");
[fetchAllConfigsExpectation fulfill];
};
// Fetch with new custom variables.
[_configFetch fetchAllConfigsWithExpirationDuration:0
completionHandler:fetchAllConfigsCompletion];
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testRefreshInstanceIDToken {
XCTestExpectation *refreshTokenExpectation =
[self expectationWithDescription:@"Test refresh Instance ID token."];
FIRInstanceID *instanceID = [FIRInstanceID instanceID];
_settings.senderID = RCNFakeSenderID;
id mock = OCMPartialMock(instanceID);
OCMStub([mock
tokenWithAuthorizedEntity:RCNFakeSenderID
scope:@"*"
options:nil
handler:([OCMArg invokeBlockWithArgs:RCNFakeToken, [NSNull null], nil])]);
FIRRemoteConfigFetchCompletion completionHandler =
^void(FIRRemoteConfigFetchStatus status, NSError *error) {
XCTAssertEqualObjects(RCNFakeToken, _settings.configInstanceIDToken);
[mock stopMocking];
[refreshTokenExpectation fulfill];
};
[_configFetch refreshInstanceIDTokenAndFetchCheckInInfoWithCompletionHandler:completionHandler];
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testRefreshInstanceIDTokenWithError {
XCTestExpectation *refreshTokenExpectation =
[self expectationWithDescription:@"Test refresh Instance ID token."];
FIRInstanceID *instanceID = [FIRInstanceID instanceID];
_settings.senderID = RCNFakeSenderID;
NSError *error = [NSError errorWithDomain:@"errorDomain" code:20 userInfo:nil];
id mock = OCMPartialMock(instanceID);
OCMStub([mock
tokenWithAuthorizedEntity:RCNFakeSenderID
scope:@"*"
options:nil
handler:([OCMArg invokeBlockWithArgs:[NSNull null], error, nil])]);
FIRRemoteConfigFetchCompletion completionHandler =
^void(FIRRemoteConfigFetchStatus status, NSError *error) {
XCTAssertNil(_settings.configInstanceIDToken);
XCTAssertNil(_settings.configInstanceID);
[mock stopMocking];
[refreshTokenExpectation fulfill];
};
[_configFetch refreshInstanceIDTokenAndFetchCheckInInfoWithCompletionHandler:completionHandler];
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
- (void)testFetchCheckin {
XCTestExpectation *refreshTokenExpectation =
[self expectationWithDescription:@"Test fetch checkin."];
FIRInstanceID *instanceID = [FIRInstanceID instanceID];
_settings.senderID = RCNFakeSenderID;
id mock = OCMPartialMock(instanceID);
FIRInstanceIDCheckinPreferences *preferences =
[[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:RCNFakeDeviceID
secretToken:RCNFakeSecretToken];
OCMStub([mock
fetchCheckinInfoWithHandler:([OCMArg invokeBlockWithArgs:preferences, [NSNull null], nil])]);
FIRRemoteConfigFetchCompletion completionHandler =
^void(FIRRemoteConfigFetchStatus status, NSError *error) {
XCTAssertEqualObjects(RCNFakeDeviceID, _settings.deviceAuthID);
XCTAssertEqualObjects(RCNFakeSecretToken, _settings.secretToken);
[mock stopMocking];
[refreshTokenExpectation fulfill];
};
[_configFetch refreshInstanceIDTokenAndFetchCheckInInfoWithCompletionHandler:completionHandler];
[self waitForExpectationsWithTimeout:_expectationTimeout
handler:^(NSError *error) {
XCTAssertNil(error);
}];
}
#pragma mark - helpers
- (void)checkConfigResult:(NSDictionary *)result
withNamespace:(NSString *)namespace
key:(NSString *)key
value:(NSString *)value {
if (result[namespace]) {
FIRRemoteConfigValue *configValue = result[namespace][key];
XCTAssertEqualObjects(configValue.stringValue, value,
@"Config result missing the key value pair.");
} else {
XCTAssertNotNil(result[namespace], @"Config result missing the namespace.");
}
}
@end