RCNConfigContentTest.m 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  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/RCNConfigConstants.h"
  21. #import "FirebaseRemoteConfig/Sources/RCNConfigContent.h"
  22. #import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
  23. #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"
  24. #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"
  25. @interface RCNConfigContent (Testing)
  26. - (NSMutableSet<NSString *> *)
  27. getKeysAffectedByChangedExperiments:(NSMutableArray *)activeExperimentPayloads
  28. fetchedExperimentPayloads:(NSMutableArray *)experimentPayloads;
  29. - (BOOL)checkAndWaitForInitialDatabaseLoad;
  30. @end
  31. extern const NSTimeInterval kDatabaseLoadTimeoutSecs;
  32. @interface RCNConfigDBManagerMock : RCNConfigDBManager
  33. @property(nonatomic, assign) BOOL isLoadMainCompleted;
  34. @property(nonatomic, assign) BOOL isLoadPersonalizationCompleted;
  35. @end
  36. @implementation RCNConfigDBManagerMock
  37. - (void)createOrOpenDatabase {
  38. }
  39. - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier
  40. completionHandler:(RCNDBLoadCompletion)handler {
  41. double justSmallDelay = 0.008;
  42. XCTAssertTrue(justSmallDelay < kDatabaseLoadTimeoutSecs);
  43. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(justSmallDelay * NSEC_PER_SEC)),
  44. dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  45. self.isLoadMainCompleted = YES;
  46. handler(YES, nil, nil, nil);
  47. });
  48. }
  49. - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler {
  50. double justOtherSmallDelay = 0.009;
  51. XCTAssertTrue(justOtherSmallDelay < kDatabaseLoadTimeoutSecs);
  52. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(justOtherSmallDelay * NSEC_PER_SEC)),
  53. dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  54. self.isLoadPersonalizationCompleted = YES;
  55. handler(YES, nil, nil, nil);
  56. });
  57. }
  58. @end
  59. @interface RCNConfigContentTest : XCTestCase {
  60. NSTimeInterval _expectationTimeout;
  61. RCNConfigContent *_configContent;
  62. NSString *namespaceApp1, *namespaceApp2;
  63. }
  64. @end
  65. /// Unit Tests for RCNConfigContent methods.
  66. @implementation RCNConfigContentTest
  67. - (void)setUp {
  68. [super setUp];
  69. _expectationTimeout = 1.0;
  70. namespaceApp1 = [NSString
  71. stringWithFormat:@"%@:%@", FIRNamespaceGoogleMobilePlatform, RCNTestsDefaultFIRAppName];
  72. namespaceApp2 = [NSString
  73. stringWithFormat:@"%@:%@", FIRNamespaceGoogleMobilePlatform, RCNTestsSecondFIRAppName];
  74. _configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
  75. id partialMock = OCMPartialMock(_configContent);
  76. OCMStub([partialMock checkAndWaitForInitialDatabaseLoad]).andDo(nil);
  77. }
  78. /// Passing in a nil bundleID should not crash the app
  79. - (void)testCrashShouldNotHappenWithoutMainBundleID {
  80. id mockBundle = OCMPartialMock([NSBundle mainBundle]);
  81. OCMStub([mockBundle bundleIdentifier]).andReturn(nil);
  82. _configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
  83. [mockBundle stopMocking];
  84. }
  85. /// Standard test case of receiving updated config from fetch.
  86. - (void)testUpdateConfigContentForMultipleApps {
  87. NSMutableDictionary<NSString *, id> *config1ToSet =
  88. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  89. NSDictionary<NSString *, NSString *> *entries = @{@"key1" : @"value1", @"key2" : @"value2"};
  90. [config1ToSet setValue:entries forKey:@"entries"];
  91. [_configContent updateConfigContentWithResponse:config1ToSet forNamespace:namespaceApp1];
  92. // Update for second app.
  93. NSMutableDictionary<NSString *, id> *config2ToSet =
  94. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  95. NSDictionary<NSString *, NSString *> *entries2 = @{@"key11" : @"value11", @"key21" : @"value21"};
  96. [config2ToSet setValue:entries2 forKey:@"entries"];
  97. [_configContent updateConfigContentWithResponse:config2ToSet forNamespace:namespaceApp2];
  98. // Check config for first app.
  99. NSDictionary *fetchedConfig = _configContent.fetchedConfig;
  100. XCTAssertNotNil(fetchedConfig[namespaceApp1][@"key1"]);
  101. XCTAssertEqualObjects([fetchedConfig[namespaceApp1][@"key1"] stringValue], @"value1");
  102. XCTAssertNotNil(fetchedConfig[namespaceApp1][@"key2"]);
  103. XCTAssertEqualObjects([fetchedConfig[namespaceApp1][@"key2"] stringValue], @"value2");
  104. // Check config for second app.
  105. fetchedConfig = _configContent.fetchedConfig;
  106. XCTAssertNotNil(fetchedConfig[namespaceApp2][@"key11"]);
  107. XCTAssertEqualObjects([fetchedConfig[namespaceApp2][@"key11"] stringValue], @"value11");
  108. XCTAssertNotNil(fetchedConfig[namespaceApp2][@"key21"]);
  109. XCTAssertEqualObjects([fetchedConfig[namespaceApp2][@"key21"] stringValue], @"value21");
  110. }
  111. /// Standard test case of receiving updated config from fetch.
  112. - (void)testUpdateConfigContentWithResponse {
  113. NSMutableDictionary *configToSet =
  114. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  115. NSDictionary *entries = @{@"key1" : @"value1", @"key2" : @"value2"};
  116. [configToSet setValue:entries forKey:@"entries"];
  117. [_configContent updateConfigContentWithResponse:configToSet
  118. forNamespace:FIRNamespaceGoogleMobilePlatform];
  119. NSDictionary *fetchedConfig = _configContent.fetchedConfig;
  120. XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"]);
  121. XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"] stringValue],
  122. @"value1");
  123. XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"]);
  124. XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"] stringValue],
  125. @"value2");
  126. }
  127. /// Verify that fetchedConfig is overwritten for a new fetch call.
  128. - (void)testUpdateConfigContentWithStatusUpdateWithDifferentKeys {
  129. NSMutableDictionary *configToSet =
  130. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  131. NSDictionary *entries = @{@"key1" : @"value1"};
  132. [configToSet setValue:entries forKey:@"entries"];
  133. [_configContent updateConfigContentWithResponse:configToSet
  134. forNamespace:FIRNamespaceGoogleMobilePlatform];
  135. configToSet = [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  136. entries = @{@"key2" : @"value2", @"key3" : @"value3"};
  137. [configToSet setValue:entries forKey:@"entries"];
  138. [_configContent updateConfigContentWithResponse:configToSet
  139. forNamespace:FIRNamespaceGoogleMobilePlatform];
  140. NSDictionary *fetchedConfig = _configContent.fetchedConfig;
  141. XCTAssertNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"]);
  142. XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"]);
  143. XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"] stringValue],
  144. @"value2");
  145. XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key3"]);
  146. XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key3"] stringValue],
  147. @"value3");
  148. }
  149. /// Verify fetchedConfig is available across different namespaces.
  150. - (void)testUpdateConfigContentWithStatusUpdateWithDifferentNamespaces {
  151. NSMutableDictionary *configToSet =
  152. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  153. NSMutableDictionary *configToSet2 =
  154. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  155. NSDictionary *entries = @{@"key1" : @"value1"};
  156. NSDictionary *entries2 = @{@"key2" : @"value2"};
  157. [configToSet setValue:entries forKey:@"entries"];
  158. [configToSet2 setValue:entries2 forKey:@"entries"];
  159. [_configContent updateConfigContentWithResponse:configToSet forNamespace:@"namespace_1"];
  160. [_configContent updateConfigContentWithResponse:configToSet2 forNamespace:@"namespace_2"];
  161. [_configContent updateConfigContentWithResponse:configToSet forNamespace:@"namespace_3"];
  162. [_configContent updateConfigContentWithResponse:configToSet2 forNamespace:@"namespace_4"];
  163. NSDictionary *fetchedConfig = _configContent.fetchedConfig;
  164. XCTAssertNotNil(fetchedConfig[@"namespace_1"][@"key1"]);
  165. XCTAssertEqualObjects([fetchedConfig[@"namespace_1"][@"key1"] stringValue], @"value1");
  166. XCTAssertNotNil(fetchedConfig[@"namespace_2"][@"key2"]);
  167. XCTAssertEqualObjects([fetchedConfig[@"namespace_2"][@"key2"] stringValue], @"value2");
  168. XCTAssertNotNil(fetchedConfig[@"namespace_3"][@"key1"]);
  169. XCTAssertEqualObjects([fetchedConfig[@"namespace_3"][@"key1"] stringValue], @"value1");
  170. XCTAssertNotNil(fetchedConfig[@"namespace_4"][@"key2"]);
  171. XCTAssertEqualObjects([fetchedConfig[@"namespace_4"][@"key2"] stringValue], @"value2");
  172. }
  173. - (void)skip_testUpdateConfigContentWithStatusNoChange {
  174. // TODO: Add test case once new eTag based logic is implemented.
  175. }
  176. - (void)skip_testUpdateConfigContentWithRemoveNamespaceStatus {
  177. // TODO: Add test case once new eTag based logic is implemented.
  178. }
  179. - (void)skip_testUpdateConfigContentWithEmptyConfig {
  180. // TODO: Add test case once new eTag based logic is implemented.
  181. }
  182. - (void)testCopyFromDictionaryDoesNotUpdateFetchedConfig {
  183. NSMutableDictionary *configToSet =
  184. [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil];
  185. NSDictionary *entries = @{@"key1" : @"value1", @"key2" : @"value2"};
  186. [configToSet setValue:entries forKey:@"entries"];
  187. [_configContent updateConfigContentWithResponse:configToSet forNamespace:@"dummy_namespace"];
  188. NSDictionary *namespaceToConfig = @{
  189. @"dummy_namespace" : @{
  190. @"new_key" : @"new_value",
  191. }
  192. };
  193. [_configContent copyFromDictionary:namespaceToConfig
  194. toSource:RCNDBSourceFetched
  195. forNamespace:@"dummy_namespace"];
  196. XCTAssertEqual(((NSDictionary *)_configContent.fetchedConfig[@"dummy_namespace"]).count, 2);
  197. XCTAssertEqual(_configContent.activeConfig.count, 0);
  198. XCTAssertEqual(_configContent.defaultConfig.count, 0);
  199. }
  200. - (void)testCopyFromDictionaryUpdatesDefaultConfig {
  201. NSDictionary *embeddedDictionary = @{@"default_embedded_key" : @"default_embedded_Value"};
  202. NSData *dataValue = [NSJSONSerialization dataWithJSONObject:embeddedDictionary
  203. options:NSJSONWritingPrettyPrinted
  204. error:nil];
  205. NSDate *now = [NSDate date];
  206. NSError *error;
  207. NSData *JSONData = [NSJSONSerialization dataWithJSONObject:@{@"key1" : @"value1"}
  208. options:0
  209. error:&error];
  210. NSString *JSONString = [[NSString alloc] initWithData:JSONData encoding:NSUTF8StringEncoding];
  211. NSDictionary *namespaceToConfig = @{
  212. @"default_namespace" : @{
  213. @"new_string_key" : @"new_string_value",
  214. @"new_number_key" : @1234,
  215. @"new_data_key" : dataValue,
  216. @"new_date_key" : now,
  217. @"new_json_key" : JSONString
  218. }
  219. };
  220. [_configContent copyFromDictionary:namespaceToConfig
  221. toSource:RCNDBSourceDefault
  222. forNamespace:@"default_namespace"];
  223. NSDictionary *defaultConfig = _configContent.defaultConfig;
  224. XCTAssertEqual(_configContent.fetchedConfig.count, 0);
  225. XCTAssertEqual(_configContent.activeConfig.count, 0);
  226. XCTAssertNotNil(defaultConfig[@"default_namespace"]);
  227. XCTAssertEqual(((NSDictionary *)defaultConfig[@"default_namespace"]).count, 5);
  228. XCTAssertEqualObjects(@"new_string_value",
  229. [defaultConfig[@"default_namespace"][@"new_string_key"] stringValue]);
  230. XCTAssertEqualObjects(
  231. @1234, [((FIRRemoteConfigValue *)defaultConfig[@"default_namespace"][@"new_number_key"])
  232. numberValue]);
  233. NSDictionary<NSString *, NSString *> *sampleJSON = @{@"key1" : @"value1"};
  234. id configJSON = [(defaultConfig[@"default_namespace"][@"new_json_key"]) JSONValue];
  235. XCTAssertTrue([configJSON isKindOfClass:[NSDictionary class]]);
  236. XCTAssertTrue([sampleJSON isKindOfClass:[NSDictionary class]]);
  237. XCTAssertEqualObjects(sampleJSON, (NSDictionary *)configJSON);
  238. XCTAssertEqualObjects(dataValue,
  239. [defaultConfig[@"default_namespace"][@"new_data_key"] dataValue]);
  240. NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  241. [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
  242. NSString *strValueForDate = [dateFormatter stringFromDate:now];
  243. XCTAssertEqualObjects(strValueForDate,
  244. [defaultConfig[@"default_namespace"][@"new_date_key"] stringValue]);
  245. }
  246. - (void)testCopyFromDictionaryUpdatesActiveConfig {
  247. // Active config values must be RCNConfigValue format
  248. NSDictionary *embeddedDictionary = @{@"active_embedded_key" : @"active_embedded_Value"};
  249. NSData *dataValue = [NSJSONSerialization dataWithJSONObject:embeddedDictionary
  250. options:NSJSONWritingPrettyPrinted
  251. error:nil];
  252. NSDictionary *namespaceToConfig = @{
  253. @"dummy_namespace" : @{
  254. @"new_key" : [[FIRRemoteConfigValue alloc] initWithData:dataValue source:-1],
  255. }
  256. };
  257. [_configContent copyFromDictionary:namespaceToConfig
  258. toSource:RCNDBSourceActive
  259. forNamespace:@"dummy_namespace"];
  260. XCTAssertEqual(((NSDictionary *)_configContent.activeConfig[@"dummy_namespace"]).count, 1);
  261. XCTAssertEqual(_configContent.fetchedConfig.count, 0);
  262. XCTAssertEqual(_configContent.defaultConfig.count, 0);
  263. XCTAssertEqualObjects(dataValue,
  264. [_configContent.activeConfig[@"dummy_namespace"][@"new_key"] dataValue]);
  265. }
  266. - (void)testCheckAndWaitForInitialDatabaseLoad {
  267. RCNConfigDBManagerMock *mockDBManager = [[RCNConfigDBManagerMock alloc] init];
  268. RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:mockDBManager];
  269. // Check that no one of first three calls of `-checkAndWaitForInitialDatabaseLoad` do not produce
  270. // timeout error <begin>
  271. XCTestExpectation *expectation1 =
  272. [self expectationWithDescription:
  273. @"1st `checkAndWaitForInitialDatabaseLoad` return without timeout"];
  274. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  275. XCTAssertTrue([configContent checkAndWaitForInitialDatabaseLoad]);
  276. [expectation1 fulfill];
  277. });
  278. XCTestExpectation *expectation2 =
  279. [self expectationWithDescription:
  280. @"2nd `checkAndWaitForInitialDatabaseLoad` return without timeout"];
  281. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  282. XCTAssertTrue([configContent checkAndWaitForInitialDatabaseLoad]);
  283. [expectation2 fulfill];
  284. });
  285. XCTAssertTrue([configContent checkAndWaitForInitialDatabaseLoad]);
  286. // Check that both `-load...` methods already completed after 1st wait.
  287. // This make us sure that both `-loadMainWithBundleIdentifier` and
  288. // `-loadPersonalizationWithCompletionHandler` methods synched with
  289. // `-checkAndWaitForInitialDatabaseLoad`.
  290. XCTAssertTrue(mockDBManager.isLoadMainCompleted);
  291. XCTAssertTrue(mockDBManager.isLoadPersonalizationCompleted);
  292. // Check that no one of first three calls of `-checkAndWaitForInitialDatabaseLoad` do not produce
  293. // timeout error <end>.
  294. // This make us sure that there no threads "stucked" on `-checkAndWaitForInitialDatabaseLoad`.
  295. [self waitForExpectationsWithTimeout:0.5 * kDatabaseLoadTimeoutSecs handler:nil];
  296. }
  297. - (void)testConfigUpdate_noChange_emptyResponse {
  298. NSString *namespace = @"test_namespace";
  299. // populate fetched config
  300. NSMutableDictionary *fetchResponse =
  301. [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} p13nMetadata:nil];
  302. [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace];
  303. // active config is the same as fetched config
  304. FIRRemoteConfigValue *value =
  305. [[FIRRemoteConfigValue alloc] initWithData:[@"value1" dataUsingEncoding:NSUTF8StringEncoding]
  306. source:FIRRemoteConfigSourceRemote];
  307. NSDictionary *namespaceToConfig = @{namespace : @{@"key1" : value}};
  308. [_configContent copyFromDictionary:namespaceToConfig
  309. toSource:RCNDBSourceActive
  310. forNamespace:namespace];
  311. FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace];
  312. XCTAssertTrue([update updatedKeys].count == 0);
  313. }
  314. - (void)testConfigUpdate_noParamChange_butExperimentChange {
  315. NSString *namespace = @"test_namespace";
  316. RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
  317. NSMutableSet<NSString *> *experimentKeys = [[NSMutableSet alloc] init];
  318. [experimentKeys addObject:@"key_2"];
  319. id configMock = OCMPartialMock(configContent);
  320. OCMStub([configMock getKeysAffectedByChangedExperiments:OCMOCK_ANY
  321. fetchedExperimentPayloads:OCMOCK_ANY])
  322. .andReturn(experimentKeys);
  323. // populate fetched config
  324. NSMutableDictionary *fetchResponse =
  325. [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} p13nMetadata:nil];
  326. [configMock updateConfigContentWithResponse:fetchResponse forNamespace:namespace];
  327. // active config is the same as fetched config
  328. FIRRemoteConfigValue *value =
  329. [[FIRRemoteConfigValue alloc] initWithData:[@"value1" dataUsingEncoding:NSUTF8StringEncoding]
  330. source:FIRRemoteConfigSourceRemote];
  331. NSDictionary *namespaceToConfig = @{namespace : @{@"key1" : value}};
  332. [configMock copyFromDictionary:namespaceToConfig
  333. toSource:RCNDBSourceActive
  334. forNamespace:namespace];
  335. FIRRemoteConfigUpdate *update = [configMock getConfigUpdateForNamespace:namespace];
  336. XCTAssertTrue([update updatedKeys].count == 1);
  337. XCTAssertTrue([[update updatedKeys] containsObject:@"key_2"]);
  338. }
  339. - (void)testExperimentDiff_addedExperiment {
  340. NSData *payloadData1 = [[self class] payloadDataFromTestFile];
  341. NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];
  342. NSError *dataError;
  343. NSMutableDictionary *payload =
  344. [NSJSONSerialization JSONObjectWithData:payloadData1
  345. options:NSJSONReadingMutableContainers
  346. error:&dataError];
  347. [payload setValue:@"exp_2" forKey:@"experimentId"];
  348. NSError *jsonError;
  349. NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload
  350. options:kNilOptions
  351. error:&jsonError];
  352. NSMutableArray *experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy];
  353. RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
  354. NSMutableSet<NSString *> *changedKeys =
  355. [configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
  356. fetchedExperimentPayloads:experimentPayloads];
  357. XCTAssertTrue([changedKeys containsObject:@"test_key_1"]);
  358. }
  359. - (void)testExperimentDiff_changedExperimentMetadata {
  360. NSData *payloadData1 = [[self class] payloadDataFromTestFile];
  361. NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];
  362. NSError *dataError;
  363. NSMutableDictionary *payload =
  364. [NSJSONSerialization JSONObjectWithData:payloadData1
  365. options:NSJSONReadingMutableContainers
  366. error:&dataError];
  367. [payload setValue:@"var_2" forKey:@"variantId"];
  368. NSError *jsonError;
  369. NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload
  370. options:kNilOptions
  371. error:&jsonError];
  372. NSMutableArray *experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy];
  373. RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
  374. NSMutableSet<NSString *> *changedKeys =
  375. [configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
  376. fetchedExperimentPayloads:experimentPayloads];
  377. XCTAssertTrue([changedKeys containsObject:@"test_key_1"]);
  378. }
  379. - (void)testExperimentDiff_changedExperimentKeys {
  380. NSData *payloadData1 = [[self class] payloadDataFromTestFile];
  381. NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];
  382. NSError *dataError;
  383. NSMutableDictionary *payload =
  384. [NSJSONSerialization JSONObjectWithData:payloadData1
  385. options:NSJSONReadingMutableContainers
  386. error:&dataError];
  387. [payload setValue:@[ @"test_key_1", @"test_key_2" ] forKey:@"affectedParameterKeys"];
  388. NSError *jsonError;
  389. NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload
  390. options:kNilOptions
  391. error:&jsonError];
  392. NSMutableArray *experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy];
  393. RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
  394. NSMutableSet<NSString *> *changedKeys =
  395. [configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
  396. fetchedExperimentPayloads:experimentPayloads];
  397. XCTAssertTrue([changedKeys containsObject:@"test_key_2"]);
  398. }
  399. - (void)testExperimentDiff_deletedExperiment {
  400. NSData *payloadData1 = [[self class] payloadDataFromTestFile];
  401. NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];
  402. NSMutableArray *experimentPayloads = [@[] mutableCopy];
  403. RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
  404. NSMutableSet<NSString *> *changedKeys =
  405. [configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
  406. fetchedExperimentPayloads:experimentPayloads];
  407. XCTAssertTrue([changedKeys containsObject:@"test_key_1"]);
  408. }
  409. - (void)testExperimentDiff_noChange {
  410. NSData *payloadData1 = [[self class] payloadDataFromTestFile];
  411. NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];
  412. NSMutableArray *experimentPayloads = [@[ payloadData1 ] mutableCopy];
  413. RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
  414. NSMutableSet<NSString *> *changedKeys =
  415. [configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
  416. fetchedExperimentPayloads:experimentPayloads];
  417. XCTAssertTrue([changedKeys count] == 0);
  418. }
  419. - (void)testConfigUpdate_paramAdded_returnsNewKey {
  420. NSString *namespace = @"test_namespace";
  421. NSString *newParam = @"key2";
  422. // populate active config
  423. FIRRemoteConfigValue *value =
  424. [[FIRRemoteConfigValue alloc] initWithData:[@"value1" dataUsingEncoding:NSUTF8StringEncoding]
  425. source:FIRRemoteConfigSourceRemote];
  426. NSDictionary *namespaceToConfig = @{namespace : @{@"key1" : value}};
  427. [_configContent copyFromDictionary:namespaceToConfig
  428. toSource:RCNDBSourceActive
  429. forNamespace:namespace];
  430. // fetch response has new param
  431. NSMutableDictionary *fetchResponse =
  432. [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1", newParam : @"value2"}
  433. p13nMetadata:nil];
  434. [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace];
  435. FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace];
  436. XCTAssertTrue([update updatedKeys].count == 1);
  437. XCTAssertTrue([[update updatedKeys] containsObject:newParam]);
  438. }
  439. - (void)testConfigUpdate_paramValueChanged_returnsUpdatedKey {
  440. NSString *namespace = @"test_namespace";
  441. NSString *existingParam = @"key1";
  442. NSString *oldValue = @"value1";
  443. NSString *updatedValue = @"value2";
  444. // active config contains old value
  445. FIRRemoteConfigValue *value =
  446. [[FIRRemoteConfigValue alloc] initWithData:[oldValue dataUsingEncoding:NSUTF8StringEncoding]
  447. source:FIRRemoteConfigSourceRemote];
  448. NSDictionary *namespaceToConfig = @{namespace : @{existingParam : value}};
  449. [_configContent copyFromDictionary:namespaceToConfig
  450. toSource:RCNDBSourceActive
  451. forNamespace:namespace];
  452. // fetch response contains updated value
  453. NSMutableDictionary *fetchResponse =
  454. [self createFetchResponseWithConfigEntries:@{existingParam : updatedValue} p13nMetadata:nil];
  455. [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace];
  456. FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace];
  457. XCTAssertTrue([update updatedKeys].count == 1);
  458. XCTAssertTrue([[update updatedKeys] containsObject:existingParam]);
  459. }
  460. - (void)testConfigUpdate_paramDeleted_returnsDeletedKey {
  461. NSString *namespace = @"test_namespace";
  462. NSString *existingParam = @"key1";
  463. NSString *newParam = @"key2";
  464. NSString *value1 = @"value1";
  465. // populate active config
  466. FIRRemoteConfigValue *value =
  467. [[FIRRemoteConfigValue alloc] initWithData:[value1 dataUsingEncoding:NSUTF8StringEncoding]
  468. source:FIRRemoteConfigSourceRemote];
  469. NSDictionary *namespaceToConfig = @{namespace : @{existingParam : value}};
  470. [_configContent copyFromDictionary:namespaceToConfig
  471. toSource:RCNDBSourceActive
  472. forNamespace:namespace];
  473. // fetch response does not contain existing param
  474. NSMutableDictionary *fetchResponse =
  475. [self createFetchResponseWithConfigEntries:@{newParam : value1} p13nMetadata:nil];
  476. [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace];
  477. FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace];
  478. XCTAssertTrue([update updatedKeys].count == 2);
  479. XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); // deleted
  480. XCTAssertTrue([[update updatedKeys] containsObject:newParam]); // added
  481. }
  482. - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey {
  483. NSString *namespace = @"test_namespace";
  484. NSString *existingParam = @"key1";
  485. NSString *value1 = @"value1";
  486. NSDictionary *oldMetadata = @{@"arm_index" : @"1"};
  487. NSDictionary *updatedMetadata = @{@"arm_index" : @"2"};
  488. // popuate fetched config
  489. NSMutableDictionary *fetchResponse =
  490. [self createFetchResponseWithConfigEntries:@{existingParam : value1}
  491. p13nMetadata:@{existingParam : oldMetadata}];
  492. [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace];
  493. // populate active config with the same content
  494. [_configContent activatePersonalization];
  495. FIRRemoteConfigValue *value =
  496. [[FIRRemoteConfigValue alloc] initWithData:[value1 dataUsingEncoding:NSUTF8StringEncoding]
  497. source:FIRRemoteConfigSourceRemote];
  498. NSDictionary *namespaceToConfig = @{namespace : @{existingParam : value}};
  499. [_configContent copyFromDictionary:namespaceToConfig
  500. toSource:RCNDBSourceActive
  501. forNamespace:namespace];
  502. // fetched response has updated p13n metadata
  503. [fetchResponse setValue:@{existingParam : updatedMetadata}
  504. forKey:RCNFetchResponseKeyPersonalizationMetadata];
  505. [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace];
  506. FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace];
  507. XCTAssertTrue([update updatedKeys].count == 1);
  508. XCTAssertTrue([[update updatedKeys] containsObject:existingParam]);
  509. }
  510. - (void)testConfigUpdate_valueSourceChanged_returnsKey {
  511. NSString *namespace = @"test_namespace";
  512. NSString *existingParam = @"key1";
  513. NSString *value1 = @"value1";
  514. // set default config
  515. FIRRemoteConfigValue *value =
  516. [[FIRRemoteConfigValue alloc] initWithData:[value1 dataUsingEncoding:NSUTF8StringEncoding]
  517. source:FIRRemoteConfigSourceDefault];
  518. NSDictionary *namespaceToConfig = @{namespace : @{existingParam : value}};
  519. [_configContent copyFromDictionary:namespaceToConfig
  520. toSource:RCNDBSourceDefault
  521. forNamespace:namespace];
  522. // fetch response contains same key->value
  523. NSMutableDictionary *fetchResponse =
  524. [self createFetchResponseWithConfigEntries:@{existingParam : value1} p13nMetadata:nil];
  525. [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace];
  526. FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace];
  527. XCTAssertTrue([update updatedKeys].count == 1);
  528. XCTAssertTrue([[update updatedKeys] containsObject:existingParam]);
  529. }
  530. #pragma mark - Test Helpers
  531. - (NSMutableDictionary *)createFetchResponseWithConfigEntries:(NSDictionary *)config
  532. p13nMetadata:(NSDictionary *)metadata {
  533. NSMutableDictionary *fetchResponse = [[NSMutableDictionary alloc]
  534. initWithObjectsAndKeys:RCNFetchResponseKeyStateUpdate, RCNFetchResponseKeyState, nil];
  535. if (config) {
  536. [fetchResponse setValue:config forKey:RCNFetchResponseKeyEntries];
  537. }
  538. if (metadata) {
  539. [fetchResponse setValue:metadata forKey:RCNFetchResponseKeyPersonalizationMetadata];
  540. }
  541. return fetchResponse;
  542. }
  543. + (NSData *)payloadDataFromTestFile {
  544. #if SWIFT_PACKAGE
  545. NSBundle *bundle = SWIFTPM_MODULE_BUNDLE;
  546. #else
  547. NSBundle *bundle = [NSBundle bundleForClass:[self class]];
  548. #endif
  549. NSString *testJsonDataFilePath = [bundle pathForResource:@"TestABTPayload" ofType:@"txt"];
  550. NSError *readTextError = nil;
  551. NSString *fileText = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath
  552. encoding:NSUTF8StringEncoding
  553. error:&readTextError];
  554. NSData *fileData = [fileText dataUsingEncoding:kCFStringEncodingUTF8];
  555. NSError *jsonDictionaryError = nil;
  556. NSMutableDictionary *jsonDictionary =
  557. [[NSJSONSerialization JSONObjectWithData:fileData
  558. options:kNilOptions
  559. error:&jsonDictionaryError] mutableCopy];
  560. NSError *jsonDataError = nil;
  561. return [NSJSONSerialization dataWithJSONObject:jsonDictionary
  562. options:kNilOptions
  563. error:&jsonDataError];
  564. }
  565. @end