RCNConfigContentTest.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. /*
  2. * Copyright 2019 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <OCMock/OCMock.h>
  17. #import <XCTest/XCTest.h>
  18. #import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
  19. #import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
  20. #import "FirebaseRemoteConfig/Sources/RCNConfigContent.h"
  21. #import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
  22. #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"
  23. #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"
  24. @interface RCNConfigContent (Testing)
  25. - (BOOL)checkAndWaitForInitialDatabaseLoad;
  26. @end
  27. extern const NSTimeInterval kDatabaseLoadTimeoutSecs;
  28. @interface RCNConfigDBManagerMock : RCNConfigDBManager
  29. @property(nonatomic, assign) BOOL isLoadMainCompleted;
  30. @property(nonatomic, assign) BOOL isLoadPersonalizationCompleted;
  31. @end
  32. @implementation RCNConfigDBManagerMock
  33. - (void)createOrOpenDatabase {
  34. }
  35. - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier
  36. completionHandler:(RCNDBLoadCompletion)handler {
  37. double justSmallDelay = 0.008;
  38. XCTAssertTrue(justSmallDelay < kDatabaseLoadTimeoutSecs);
  39. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(justSmallDelay * NSEC_PER_SEC)),
  40. dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  41. self.isLoadMainCompleted = YES;
  42. handler(YES, nil, nil, nil);
  43. });
  44. }
  45. - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler {
  46. double justOtherSmallDelay = 0.009;
  47. XCTAssertTrue(justOtherSmallDelay < kDatabaseLoadTimeoutSecs);
  48. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(justOtherSmallDelay * NSEC_PER_SEC)),
  49. dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  50. self.isLoadPersonalizationCompleted = YES;
  51. handler(YES, nil, nil, nil);
  52. });
  53. }
  54. @end
  55. @interface RCNConfigContentTest : XCTestCase {
  56. NSTimeInterval _expectationTimeout;
  57. RCNConfigContent *_configContent;
  58. NSString *namespaceApp1, *namespaceApp2;
  59. }
  60. @end
  61. /// Unit Tests for RCNConfigContent methods.
  62. @implementation RCNConfigContentTest
  63. - (void)setUp {
  64. [super setUp];
  65. _expectationTimeout = 1.0;
  66. namespaceApp1 = [NSString
  67. stringWithFormat:@"%@:%@", FIRNamespaceGoogleMobilePlatform, RCNTestsDefaultFIRAppName];
  68. namespaceApp2 = [NSString
  69. stringWithFormat:@"%@:%@", FIRNamespaceGoogleMobilePlatform, RCNTestsSecondFIRAppName];
  70. _configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
  71. id partialMock = OCMPartialMock(_configContent);
  72. OCMStub([partialMock checkAndWaitForInitialDatabaseLoad]).andDo(nil);
  73. }
  74. /// Passing in a nil bundleID should not crash the app
  75. - (void)testCrashShouldNotHappenWithoutMainBundleID {
  76. id mockBundle = OCMPartialMock([NSBundle mainBundle]);
  77. OCMStub([mockBundle bundleIdentifier]).andReturn(nil);
  78. _configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
  79. [mockBundle stopMocking];
  80. }
  81. /// Standard test case of receiving updated config from fetch.
  82. - (void)testUpdateConfigContentForMultipleApps {
  83. NSMutableDictionary<NSString *, id> *config1ToSet =
  84. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  85. NSDictionary<NSString *, NSString *> *entries = @{@"key1" : @"value1", @"key2" : @"value2"};
  86. [config1ToSet setValue:entries forKey:@"entries"];
  87. [_configContent updateConfigContentWithResponse:config1ToSet forNamespace:namespaceApp1];
  88. // Update for second app.
  89. NSMutableDictionary<NSString *, id> *config2ToSet =
  90. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  91. NSDictionary<NSString *, NSString *> *entries2 = @{@"key11" : @"value11", @"key21" : @"value21"};
  92. [config2ToSet setValue:entries2 forKey:@"entries"];
  93. [_configContent updateConfigContentWithResponse:config2ToSet forNamespace:namespaceApp2];
  94. // Check config for first app.
  95. NSDictionary *fetchedConfig = _configContent.fetchedConfig;
  96. XCTAssertNotNil(fetchedConfig[namespaceApp1][@"key1"]);
  97. XCTAssertEqualObjects([fetchedConfig[namespaceApp1][@"key1"] stringValue], @"value1");
  98. XCTAssertNotNil(fetchedConfig[namespaceApp1][@"key2"]);
  99. XCTAssertEqualObjects([fetchedConfig[namespaceApp1][@"key2"] stringValue], @"value2");
  100. // Check config for second app.
  101. fetchedConfig = _configContent.fetchedConfig;
  102. XCTAssertNotNil(fetchedConfig[namespaceApp2][@"key11"]);
  103. XCTAssertEqualObjects([fetchedConfig[namespaceApp2][@"key11"] stringValue], @"value11");
  104. XCTAssertNotNil(fetchedConfig[namespaceApp2][@"key21"]);
  105. XCTAssertEqualObjects([fetchedConfig[namespaceApp2][@"key21"] stringValue], @"value21");
  106. }
  107. /// Standard test case of receiving updated config from fetch.
  108. - (void)testUpdateConfigContentWithResponse {
  109. NSMutableDictionary *configToSet =
  110. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  111. NSDictionary *entries = @{@"key1" : @"value1", @"key2" : @"value2"};
  112. [configToSet setValue:entries forKey:@"entries"];
  113. [_configContent updateConfigContentWithResponse:configToSet
  114. forNamespace:FIRNamespaceGoogleMobilePlatform];
  115. NSDictionary *fetchedConfig = _configContent.fetchedConfig;
  116. XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"]);
  117. XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"] stringValue],
  118. @"value1");
  119. XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"]);
  120. XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"] stringValue],
  121. @"value2");
  122. }
  123. /// Verify that fetchedConfig is overwritten for a new fetch call.
  124. - (void)testUpdateConfigContentWithStatusUpdateWithDifferentKeys {
  125. NSMutableDictionary *configToSet =
  126. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  127. NSDictionary *entries = @{@"key1" : @"value1"};
  128. [configToSet setValue:entries forKey:@"entries"];
  129. [_configContent updateConfigContentWithResponse:configToSet
  130. forNamespace:FIRNamespaceGoogleMobilePlatform];
  131. configToSet = [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  132. entries = @{@"key2" : @"value2", @"key3" : @"value3"};
  133. [configToSet setValue:entries forKey:@"entries"];
  134. [_configContent updateConfigContentWithResponse:configToSet
  135. forNamespace:FIRNamespaceGoogleMobilePlatform];
  136. NSDictionary *fetchedConfig = _configContent.fetchedConfig;
  137. XCTAssertNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"]);
  138. XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"]);
  139. XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"] stringValue],
  140. @"value2");
  141. XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key3"]);
  142. XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key3"] stringValue],
  143. @"value3");
  144. }
  145. /// Verify fetchedConfig is available across different namespaces.
  146. - (void)testUpdateConfigContentWithStatusUpdateWithDifferentNamespaces {
  147. NSMutableDictionary *configToSet =
  148. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  149. NSMutableDictionary *configToSet2 =
  150. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  151. NSDictionary *entries = @{@"key1" : @"value1"};
  152. NSDictionary *entries2 = @{@"key2" : @"value2"};
  153. [configToSet setValue:entries forKey:@"entries"];
  154. [configToSet2 setValue:entries2 forKey:@"entries"];
  155. [_configContent updateConfigContentWithResponse:configToSet forNamespace:@"namespace_1"];
  156. [_configContent updateConfigContentWithResponse:configToSet2 forNamespace:@"namespace_2"];
  157. [_configContent updateConfigContentWithResponse:configToSet forNamespace:@"namespace_3"];
  158. [_configContent updateConfigContentWithResponse:configToSet2 forNamespace:@"namespace_4"];
  159. NSDictionary *fetchedConfig = _configContent.fetchedConfig;
  160. XCTAssertNotNil(fetchedConfig[@"namespace_1"][@"key1"]);
  161. XCTAssertEqualObjects([fetchedConfig[@"namespace_1"][@"key1"] stringValue], @"value1");
  162. XCTAssertNotNil(fetchedConfig[@"namespace_2"][@"key2"]);
  163. XCTAssertEqualObjects([fetchedConfig[@"namespace_2"][@"key2"] stringValue], @"value2");
  164. XCTAssertNotNil(fetchedConfig[@"namespace_3"][@"key1"]);
  165. XCTAssertEqualObjects([fetchedConfig[@"namespace_3"][@"key1"] stringValue], @"value1");
  166. XCTAssertNotNil(fetchedConfig[@"namespace_4"][@"key2"]);
  167. XCTAssertEqualObjects([fetchedConfig[@"namespace_4"][@"key2"] stringValue], @"value2");
  168. }
  169. - (void)skip_testUpdateConfigContentWithStatusNoChange {
  170. // TODO: Add test case once new eTag based logic is implemented.
  171. }
  172. - (void)skip_testUpdateConfigContentWithRemoveNamespaceStatus {
  173. // TODO: Add test case once new eTag based logic is implemented.
  174. }
  175. - (void)skip_testUpdateConfigContentWithEmptyConfig {
  176. // TODO: Add test case once new eTag based logic is implemented.
  177. }
  178. - (void)testCopyFromDictionaryDoesNotUpdateFetchedConfig {
  179. NSMutableDictionary *configToSet =
  180. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  181. NSDictionary *entries = @{@"key1" : @"value1", @"key2" : @"value2"};
  182. [configToSet setValue:entries forKey:@"entries"];
  183. [_configContent updateConfigContentWithResponse:configToSet forNamespace:@"dummy_namespace"];
  184. NSDictionary *namespaceToConfig = @{
  185. @"dummy_namespace" : @{
  186. @"new_key" : @"new_value",
  187. }
  188. };
  189. [_configContent copyFromDictionary:namespaceToConfig
  190. toSource:RCNDBSourceFetched
  191. forNamespace:@"dummy_namespace"];
  192. XCTAssertEqual(((NSDictionary *)_configContent.fetchedConfig[@"dummy_namespace"]).count, 2);
  193. XCTAssertEqual(_configContent.activeConfig.count, 0);
  194. XCTAssertEqual(_configContent.defaultConfig.count, 0);
  195. }
  196. - (void)testCopyFromDictionaryUpdatesDefaultConfig {
  197. NSDictionary *embeddedDictionary = @{@"default_embedded_key" : @"default_embedded_Value"};
  198. NSData *dataValue = [NSJSONSerialization dataWithJSONObject:embeddedDictionary
  199. options:NSJSONWritingPrettyPrinted
  200. error:nil];
  201. NSDate *now = [NSDate date];
  202. NSError *error;
  203. NSData *JSONData = [NSJSONSerialization dataWithJSONObject:@{@"key1" : @"value1"}
  204. options:0
  205. error:&error];
  206. NSString *JSONString = [[NSString alloc] initWithData:JSONData encoding:NSUTF8StringEncoding];
  207. NSDictionary *namespaceToConfig = @{
  208. @"default_namespace" : @{
  209. @"new_string_key" : @"new_string_value",
  210. @"new_number_key" : @1234,
  211. @"new_data_key" : dataValue,
  212. @"new_date_key" : now,
  213. @"new_json_key" : JSONString
  214. }
  215. };
  216. [_configContent copyFromDictionary:namespaceToConfig
  217. toSource:RCNDBSourceDefault
  218. forNamespace:@"default_namespace"];
  219. NSDictionary *defaultConfig = _configContent.defaultConfig;
  220. XCTAssertEqual(_configContent.fetchedConfig.count, 0);
  221. XCTAssertEqual(_configContent.activeConfig.count, 0);
  222. XCTAssertNotNil(defaultConfig[@"default_namespace"]);
  223. XCTAssertEqual(((NSDictionary *)defaultConfig[@"default_namespace"]).count, 5);
  224. XCTAssertEqualObjects(@"new_string_value",
  225. [defaultConfig[@"default_namespace"][@"new_string_key"] stringValue]);
  226. XCTAssertEqualObjects(
  227. @1234, [((FIRRemoteConfigValue *)defaultConfig[@"default_namespace"][@"new_number_key"])
  228. numberValue]);
  229. NSDictionary<NSString *, NSString *> *sampleJSON = @{@"key1" : @"value1"};
  230. id configJSON = [(defaultConfig[@"default_namespace"][@"new_json_key"]) JSONValue];
  231. XCTAssertTrue([configJSON isKindOfClass:[NSDictionary class]]);
  232. XCTAssertTrue([sampleJSON isKindOfClass:[NSDictionary class]]);
  233. XCTAssertEqualObjects(sampleJSON, (NSDictionary *)configJSON);
  234. XCTAssertEqualObjects(dataValue,
  235. [defaultConfig[@"default_namespace"][@"new_data_key"] dataValue]);
  236. NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  237. [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
  238. NSString *strValueForDate = [dateFormatter stringFromDate:now];
  239. XCTAssertEqualObjects(strValueForDate,
  240. [defaultConfig[@"default_namespace"][@"new_date_key"] stringValue]);
  241. }
  242. - (void)testCopyFromDictionaryUpdatesActiveConfig {
  243. // Active config values must be RCNConfigValue format
  244. NSDictionary *embeddedDictionary = @{@"active_embedded_key" : @"active_embedded_Value"};
  245. NSData *dataValue = [NSJSONSerialization dataWithJSONObject:embeddedDictionary
  246. options:NSJSONWritingPrettyPrinted
  247. error:nil];
  248. NSDictionary *namespaceToConfig = @{
  249. @"dummy_namespace" : @{
  250. @"new_key" : [[FIRRemoteConfigValue alloc] initWithData:dataValue source:-1],
  251. }
  252. };
  253. [_configContent copyFromDictionary:namespaceToConfig
  254. toSource:RCNDBSourceActive
  255. forNamespace:@"dummy_namespace"];
  256. XCTAssertEqual(((NSDictionary *)_configContent.activeConfig[@"dummy_namespace"]).count, 1);
  257. XCTAssertEqual(_configContent.fetchedConfig.count, 0);
  258. XCTAssertEqual(_configContent.defaultConfig.count, 0);
  259. XCTAssertEqualObjects(dataValue,
  260. [_configContent.activeConfig[@"dummy_namespace"][@"new_key"] dataValue]);
  261. }
  262. - (void)testCheckAndWaitForInitialDatabaseLoad {
  263. RCNConfigDBManagerMock *mockDBManager = [[RCNConfigDBManagerMock alloc] init];
  264. RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:mockDBManager];
  265. // Check that no one of first three calls of `-checkAndWaitForInitialDatabaseLoad` do not produce
  266. // timeout error <begin>
  267. XCTestExpectation *expectation1 =
  268. [self expectationWithDescription:
  269. @"1st `checkAndWaitForInitialDatabaseLoad` return without timeout"];
  270. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  271. XCTAssertTrue([configContent checkAndWaitForInitialDatabaseLoad]);
  272. [expectation1 fulfill];
  273. });
  274. XCTestExpectation *expectation2 =
  275. [self expectationWithDescription:
  276. @"2nd `checkAndWaitForInitialDatabaseLoad` return without timeout"];
  277. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  278. XCTAssertTrue([configContent checkAndWaitForInitialDatabaseLoad]);
  279. [expectation2 fulfill];
  280. });
  281. XCTAssertTrue([configContent checkAndWaitForInitialDatabaseLoad]);
  282. // Check that both `-load...` methods already completed after 1st wait.
  283. // This make us sure that both `-loadMainWithBundleIdentifier` and
  284. // `-loadPersonalizationWithCompletionHandler` methods synched with
  285. // `-checkAndWaitForInitialDatabaseLoad`.
  286. XCTAssertTrue(mockDBManager.isLoadMainCompleted);
  287. XCTAssertTrue(mockDBManager.isLoadPersonalizationCompleted);
  288. // Check that no one of first three calls of `-checkAndWaitForInitialDatabaseLoad` do not produce
  289. // timeout error <end>.
  290. // This make us sure that there no threads "stucked" on `-checkAndWaitForInitialDatabaseLoad`.
  291. [self waitForExpectationsWithTimeout:0.5 * kDatabaseLoadTimeoutSecs handler:nil];
  292. }
  293. @end