RCNPersonalizationTest.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  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;
  19. #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
  20. #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"
  21. #import "Interop/Analytics/Public/FIRAnalyticsInterop.h"
  22. #define RCNExperimentTableKeyPayload "experiment_payload"
  23. #define RCNExperimentTableKeyMetadata "experiment_metadata"
  24. #define RCNExperimentTableKeyActivePayload "experiment_active_payload"
  25. #define RCNRolloutTableKeyActiveMetadata "active_rollout_metadata"
  26. #define RCNRolloutTableKeyFetchedMetadata "fetched_rollout_metadata"
  27. typedef void (^FIRRemoteConfigFetchAndActivateCompletion)(
  28. FIRRemoteConfigFetchAndActivateStatus status, NSError *_Nullable error);
  29. static NSString *const RCNFetchResponseKeyEntries = @"entries";
  30. /// Key that includes data for experiment descriptions in ABT.
  31. static NSString *const RCNFetchResponseKeyExperimentDescriptions = @"experimentDescriptions";
  32. /// Key that includes data for Personalization metadata.
  33. static NSString *const RCNFetchResponseKeyPersonalizationMetadata = @"personalizationMetadata";
  34. /// Key that includes data for Rollout metadata.
  35. static NSString *const RCNFetchResponseKeyRolloutMetadata = @"rolloutMetadata";
  36. /// Key that indicates rollout id in Rollout metadata.
  37. static NSString *const RCNFetchResponseKeyRolloutID = @"rolloutId";
  38. /// Key that indicates variant id in Rollout metadata.
  39. static NSString *const RCNFetchResponseKeyVariantID = @"variantId";
  40. /// Key that indicates affected parameter keys in Rollout Metadata.
  41. static NSString *const RCNFetchResponseKeyAffectedParameterKeys = @"affectedParameterKeys";
  42. @import FirebaseRemoteConfig;
  43. static NSString *const kAnalyticsOriginPersonalization = @"fp";
  44. static NSString *const kExternalEvent = @"personalization_assignment";
  45. static NSString *const kExternalRcParameterParam = @"arm_key";
  46. static NSString *const kExternalArmValueParam = @"arm_value";
  47. static NSString *const kPersonalizationId = @"personalizationId";
  48. static NSString *const kExternalPersonalizationIdParam = @"personalization_id";
  49. static NSString *const kArmIndex = @"armIndex";
  50. static NSString *const kExternalArmIndexParam = @"arm_index";
  51. static NSString *const kGroup = @"group";
  52. static NSString *const kExternalGroupParam = @"group";
  53. static NSString *const kInternalEvent = @"_fpc";
  54. static NSString *const kChoiceId = @"choiceId";
  55. static NSString *const kInternalChoiceIdParam = @"_fpid";
  56. //@interface RCNConfigFetch (ForTest)
  57. //- (NSURLSessionDataTask *)URLSessionDataTaskWithContent:(NSData *)content
  58. // fetchTypeHeader:(NSString *)fetchTypeHeader
  59. // completionHandler:(void (^)(NSData *data,
  60. // NSURLResponse *response,
  61. // NSError
  62. // *error))fetcherCompletion;
  63. //
  64. //- (void)fetchWithUserProperties:(NSDictionary *)userProperties
  65. // fetchTypeHeader:(NSString *)fetchTypeHeader
  66. // completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler
  67. // updateCompletionHandler:(void (^)(FIRRemoteConfigFetchStatus status,
  68. // FIRRemoteConfigUpdate *update,
  69. // NSError *error))updateCompletionHandler;
  70. //@end
  71. @interface RCNPersonalizationTest : XCTestCase {
  72. NSDictionary *_configContainer;
  73. NSMutableArray<NSDictionary *> *_fakeLogs;
  74. id _analyticsMock;
  75. RCNPersonalization *_personalization;
  76. FIRRemoteConfig *_configInstance;
  77. }
  78. @end
  79. @implementation RCNPersonalizationTest
  80. - (void)setUp {
  81. [super setUp];
  82. _configContainer = @{
  83. RCNFetchResponseKeyEntries : @{
  84. @"key1" : [[FIRRemoteConfigValue alloc]
  85. initWithData:[@"value1" dataUsingEncoding:NSUTF8StringEncoding]
  86. source:FIRRemoteConfigSourceRemote],
  87. @"key2" : [[FIRRemoteConfigValue alloc]
  88. initWithData:[@"value2" dataUsingEncoding:NSUTF8StringEncoding]
  89. source:FIRRemoteConfigSourceRemote],
  90. @"key3" : [[FIRRemoteConfigValue alloc]
  91. initWithData:[@"value3" dataUsingEncoding:NSUTF8StringEncoding]
  92. source:FIRRemoteConfigSourceRemote]
  93. },
  94. RCNFetchResponseKeyPersonalizationMetadata : @{
  95. @"key1" : @{
  96. kPersonalizationId : @"p13n1",
  97. kArmIndex : @0,
  98. kChoiceId : @"id1",
  99. kGroup : @"BASELINE"
  100. },
  101. @"key2" :
  102. @{kPersonalizationId : @"p13n2", kArmIndex : @1, kChoiceId : @"id2", kGroup : @"P13N"}
  103. }
  104. };
  105. _fakeLogs = [[NSMutableArray alloc] init];
  106. _analyticsMock = OCMProtocolMock(@protocol(FIRAnalyticsInterop));
  107. OCMStub([_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization
  108. name:[OCMArg isKindOfClass:[NSString class]]
  109. parameters:[OCMArg isKindOfClass:[NSDictionary class]]])
  110. .andDo(^(NSInvocation *invocation) {
  111. __unsafe_unretained NSDictionary *bundle;
  112. [invocation getArgument:&bundle atIndex:4];
  113. [self->_fakeLogs addObject:bundle];
  114. });
  115. _personalization = [[RCNPersonalization alloc] initWithAnalytics:_analyticsMock];
  116. // Always remove the database at the start of testing.
  117. NSString *DBPath = [RCNTestUtilities remoteConfigPathForTestDatabase];
  118. RCNConfigDBManager *DBManager = [[RCNConfigDBManager alloc] initWithDbPath:DBPath];
  119. RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:DBManager];
  120. // Create a mock FIRRemoteConfig instance.
  121. FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"1:123:ios:test"
  122. GCMSenderID:@"testSender"];
  123. options.APIKey = @"test API key";
  124. _configInstance = OCMPartialMock([[FIRRemoteConfig alloc] initWithAppName:@"testApp"
  125. FIROptions:options
  126. namespace:@"namespace"
  127. DBManager:DBManager
  128. configContent:configContent
  129. analytics:_analyticsMock]);
  130. // [_configInstance setValue:[RCNPersonalizationTest mockFetchRequest] forKey:@"_configFetch"];
  131. }
  132. - (void)tearDown {
  133. [super tearDown];
  134. }
  135. - (void)testNonPersonalizationKey {
  136. [_fakeLogs removeAllObjects];
  137. [_personalization logArmActiveWithRcParameter:@"key3" config:_configContainer];
  138. OCMVerify(never(),
  139. [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization
  140. name:[OCMArg checkWithBlock:^BOOL(NSString *value) {
  141. return [value isEqualToString:kExternalEvent] ||
  142. [value isEqualToString:kInternalEvent];
  143. }]
  144. parameters:[OCMArg isKindOfClass:[NSDictionary class]]]);
  145. XCTAssertEqual([_fakeLogs count], 0);
  146. }
  147. - (void)testSinglePersonalizationKey {
  148. [_fakeLogs removeAllObjects];
  149. [_personalization logArmActiveWithRcParameter:@"key1" config:_configContainer];
  150. OCMVerify(times(2),
  151. [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization
  152. name:[OCMArg checkWithBlock:^BOOL(NSString *value) {
  153. return [value isEqualToString:kExternalEvent] ||
  154. [value isEqualToString:kInternalEvent];
  155. }]
  156. parameters:[OCMArg isKindOfClass:[NSDictionary class]]]);
  157. XCTAssertEqual([_fakeLogs count], 2);
  158. NSDictionary *logParams = @{
  159. kExternalRcParameterParam : @"key1",
  160. kExternalArmValueParam : @"value1",
  161. kExternalPersonalizationIdParam : @"p13n1",
  162. kExternalArmIndexParam : @0,
  163. kExternalGroupParam : @"BASELINE"
  164. };
  165. XCTAssertEqualObjects(_fakeLogs[0], logParams);
  166. NSDictionary *internalLogParams = @{kInternalChoiceIdParam : @"id1"};
  167. XCTAssertEqualObjects(_fakeLogs[1], internalLogParams);
  168. }
  169. - (void)testMultiplePersonalizationKeys {
  170. [_fakeLogs removeAllObjects];
  171. [_personalization logArmActiveWithRcParameter:@"key1" config:_configContainer];
  172. [_personalization logArmActiveWithRcParameter:@"key2" config:_configContainer];
  173. [_personalization logArmActiveWithRcParameter:@"key1" config:_configContainer];
  174. OCMVerify(times(4),
  175. [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization
  176. name:[OCMArg checkWithBlock:^BOOL(NSString *value) {
  177. return [value isEqualToString:kExternalEvent] ||
  178. [value isEqualToString:kInternalEvent];
  179. }]
  180. parameters:[OCMArg isKindOfClass:[NSDictionary class]]]);
  181. XCTAssertEqual([_fakeLogs count], 4);
  182. NSDictionary *logParams1 = @{
  183. kExternalRcParameterParam : @"key1",
  184. kExternalArmValueParam : @"value1",
  185. kExternalPersonalizationIdParam : @"p13n1",
  186. kExternalArmIndexParam : @0,
  187. kExternalGroupParam : @"BASELINE"
  188. };
  189. XCTAssertEqualObjects(_fakeLogs[0], logParams1);
  190. NSDictionary *internalLogParams1 = @{kInternalChoiceIdParam : @"id1"};
  191. XCTAssertEqualObjects(_fakeLogs[1], internalLogParams1);
  192. NSDictionary *logParams2 = @{
  193. kExternalRcParameterParam : @"key2",
  194. kExternalArmValueParam : @"value2",
  195. kExternalPersonalizationIdParam : @"p13n2",
  196. kExternalArmIndexParam : @1,
  197. kExternalGroupParam : @"P13N"
  198. };
  199. XCTAssertEqualObjects(_fakeLogs[2], logParams2);
  200. NSDictionary *internalLogParams2 = @{kInternalChoiceIdParam : @"id2"};
  201. XCTAssertEqualObjects(_fakeLogs[3], internalLogParams2);
  202. }
  203. // Skip very slow test while iterating
  204. - (void)SKIPtestRemoteConfigIntegration {
  205. [_fakeLogs removeAllObjects];
  206. FIRRemoteConfigFetchAndActivateCompletion fetchAndActivateCompletion =
  207. ^void(FIRRemoteConfigFetchAndActivateStatus status, NSError *error) {
  208. OCMVerify(times(4), [self->_analyticsMock
  209. logEventWithOrigin:kAnalyticsOriginPersonalization
  210. name:[OCMArg checkWithBlock:^BOOL(NSString *value) {
  211. return [value isEqualToString:kExternalEvent] ||
  212. [value isEqualToString:kInternalEvent];
  213. }]
  214. parameters:[OCMArg isKindOfClass:[NSDictionary class]]]);
  215. XCTAssertEqual([self->_fakeLogs count], 4);
  216. NSDictionary *logParams1 = @{
  217. kExternalRcParameterParam : @"key1",
  218. kExternalArmValueParam : @"value1",
  219. kExternalPersonalizationIdParam : @"p13n1",
  220. kExternalArmIndexParam : @0,
  221. kExternalGroupParam : @"BASELINE"
  222. };
  223. XCTAssertEqualObjects(self->_fakeLogs[0], logParams1);
  224. NSDictionary *internalLogParams1 = @{kInternalChoiceIdParam : @"id1"};
  225. XCTAssertEqualObjects(self->_fakeLogs[1], internalLogParams1);
  226. NSDictionary *logParams2 = @{
  227. kExternalRcParameterParam : @"key1",
  228. kExternalArmValueParam : @"value1",
  229. kExternalPersonalizationIdParam : @"p13n1",
  230. kExternalArmIndexParam : @0,
  231. kExternalGroupParam : @"BASELINE"
  232. };
  233. XCTAssertEqualObjects(self->_fakeLogs[2], logParams2);
  234. NSDictionary *internalLogParams2 = @{kInternalChoiceIdParam : @"id2"};
  235. XCTAssertEqualObjects(self->_fakeLogs[3], internalLogParams2);
  236. };
  237. [_configInstance fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion];
  238. [_configInstance configValueForKey:@"key1"];
  239. [_configInstance configValueForKey:@"key2"];
  240. }
  241. //+ (id)mockFetchRequest {
  242. // id configFetch = OCMClassMock([RCNConfigFetch class]);
  243. // OCMStub([configFetch fetchConfigWithExpirationDuration:0 completionHandler:OCMOCK_ANY])
  244. // .ignoringNonObjectArgs()
  245. // .andDo(^(NSInvocation *invocation) {
  246. // __unsafe_unretained FIRRemoteConfigFetchCompletion handler;
  247. // [invocation getArgument:&handler atIndex:3];
  248. // [configFetch fetchWithUserProperties:[[NSDictionary alloc] init]
  249. // fetchTypeHeader:@"Base/1"
  250. // completionHandler:handler
  251. // updateCompletionHandler:nil];
  252. // });
  253. // OCMExpect([configFetch
  254. // URLSessionDataTaskWithContent:[OCMArg any]
  255. // fetchTypeHeader:@"Base/1"
  256. // completionHandler:[RCNPersonalizationTest mockResponseHandler]])
  257. // .andReturn(nil);
  258. // return configFetch;
  259. //}
  260. //+ (id)mockResponseHandler {
  261. // NSDictionary *response = @{
  262. // RCNFetchResponseKeyState : RCNFetchResponseKeyStateUpdate,
  263. // RCNFetchResponseKeyEntries : @{@"key1" : @"value1", @"key2" : @"value2", @"key3" : @"value3"},
  264. // RCNFetchResponseKeyPersonalizationMetadata : @{
  265. // @"key1" : @{
  266. // kPersonalizationId : @"p13n1",
  267. // kArmIndex : @0,
  268. // kChoiceId : @"id1",
  269. // kGroup : @"BASELINE"
  270. // },
  271. // @"key2" :
  272. // @{kPersonalizationId : @"p13n2", kArmIndex : @1, kChoiceId : @"id2", kGroup : @"P13N"}
  273. // }
  274. //
  275. // };
  276. // return [OCMArg invokeBlockWithArgs:[NSJSONSerialization dataWithJSONObject:response
  277. // options:0
  278. // error:nil],
  279. // [[NSHTTPURLResponse alloc]
  280. // initWithURL:[NSURL
  281. // URLWithString:@"https://firebase.com"]
  282. // statusCode:200
  283. // HTTPVersion:nil
  284. // headerFields:@{@"etag" : @"etag1"}],
  285. // [NSNull null], nil];
  286. //}
  287. @end