ABTConditionalUserPropertyControllerTest.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. // Copyright 2019 Google
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import <OCMock/OCMock.h>
  15. #import <XCTest/XCTest.h>
  16. #import "FirebaseABTesting/Sources/ABTConditionalUserPropertyController.h"
  17. #import "FirebaseABTesting/Sources/ABTConstants.h"
  18. #import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h"
  19. #import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h"
  20. #import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRLifecycleEvents.h"
  21. #import "FirebaseABTesting/Tests/Unit/ABTFakeFIRAConditionalUserPropertyController.h"
  22. #import "FirebaseABTesting/Tests/Unit/ABTTestUniversalConstants.h"
  23. #import "FirebaseABTesting/Tests/Unit/Utilities/ABTTestUtilities.h"
  24. #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
  25. @interface ABTConditionalUserPropertyController (ExposedForTest)
  26. - (NSInteger)maxNumberOfExperimentsOfOrigin:(NSString *)origin;
  27. - (void)maxNumberOfExperimentsOfOrigin:(NSString *)origin
  28. completionHandler:(void (^)(int32_t))completionHandler;
  29. - (id)createExperimentFromOrigin:(NSString *)origin
  30. payload:(ABTExperimentPayload *)payload
  31. events:(FIRLifecycleEvents *)events;
  32. - (ABTExperimentPayloadExperimentOverflowPolicy)
  33. overflowPolicyWithPayload:(ABTExperimentPayload *)payload
  34. originalPolicy:(ABTExperimentPayloadExperimentOverflowPolicy)originalPolicy;
  35. /// Surface internal initializer to avoid singleton usage during tests.
  36. - (instancetype)initWithAnalytics:(nullable id<FIRAnalyticsInterop>)analytics;
  37. @end
  38. typedef void (^FakeAnalyticsLogEventWithOriginNameParametersHandler)(
  39. NSString *origin, NSString *name, NSDictionary<NSString *, id> *parameters);
  40. @interface ABTConditionalUserPropertyControllerTest : XCTestCase {
  41. ABTConditionalUserPropertyController *_ABTCUPController;
  42. ABTFakeFIRAConditionalUserPropertyController *_fakeController;
  43. id _mockCUPController;
  44. }
  45. @end
  46. @interface ABTExperimentPayload (Testing)
  47. @property(nonatomic, readwrite) ABTExperimentPayloadExperimentOverflowPolicy overflowPolicy;
  48. @end
  49. @implementation ABTConditionalUserPropertyControllerTest
  50. - (void)setUp {
  51. [super setUp];
  52. _fakeController = [ABTFakeFIRAConditionalUserPropertyController sharedInstance];
  53. _ABTCUPController = [[ABTConditionalUserPropertyController alloc]
  54. initWithAnalytics:[[FakeAnalytics alloc] initWithFakeController:_fakeController]];
  55. _mockCUPController = OCMPartialMock(_ABTCUPController);
  56. OCMStub([_mockCUPController maxNumberOfExperimentsOfOrigin:[OCMArg any]]).andReturn(3);
  57. // Must initialize FIRApp before calling set experiment as Firebase Analytics internal event
  58. // logging requires it.
  59. NSDictionary *optionsDictionary = @{
  60. kFIRGoogleAppID : @"1:123456789012:ios:1234567890123456",
  61. @"GCM_SENDER_ID" : @"123456789012"
  62. };
  63. FIROptions *options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
  64. [FIRApp configureWithOptions:options];
  65. }
  66. - (void)tearDown {
  67. [_fakeController resetExperiments];
  68. [_mockCUPController stopMocking];
  69. [FIRApp resetApps];
  70. [super tearDown];
  71. }
  72. #pragma mark - test proxy methods on Firebase Analytics
  73. - (void)testSetExperiment {
  74. id payload = OCMClassMock([ABTExperimentPayload class]);
  75. OCMStub([payload experimentId]).andReturn(@"exp_0");
  76. OCMStub([payload variantId]).andReturn(@"v1");
  77. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  78. [_ABTCUPController
  79. setExperimentWithOrigin:gABTTestOrigin
  80. payload:payload
  81. events:events
  82. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  83. NSArray *experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  84. XCTAssertEqual(experiments.count, 1);
  85. }
  86. - (void)testSetExperimentWhenOverflow {
  87. id payload1 = OCMClassMock([ABTExperimentPayload class]);
  88. OCMStub([payload1 experimentId]).andReturn(@"exp_1");
  89. OCMStub([payload1 variantId]).andReturn(@"v1");
  90. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  91. [_ABTCUPController
  92. setExperimentWithOrigin:gABTTestOrigin
  93. payload:payload1
  94. events:events
  95. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  96. NSArray *experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  97. XCTAssertEqual(experiments.count, 1);
  98. id payload2 = OCMClassMock([ABTExperimentPayload class]);
  99. OCMStub([payload2 experimentId]).andReturn(@"exp_2");
  100. OCMStub([payload2 variantId]).andReturn(@"v1");
  101. [_ABTCUPController
  102. setExperimentWithOrigin:gABTTestOrigin
  103. payload:payload2
  104. events:events
  105. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  106. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  107. XCTAssertEqual(experiments.count, 2);
  108. id payload3 = OCMClassMock([ABTExperimentPayload class]);
  109. OCMStub([payload3 experimentId]).andReturn(@"exp_3");
  110. OCMStub([payload3 variantId]).andReturn(@"v1");
  111. [_ABTCUPController
  112. setExperimentWithOrigin:gABTTestOrigin
  113. payload:payload3
  114. events:events
  115. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  116. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  117. XCTAssertEqual(experiments.count, 3);
  118. // Now it's overflowed, try setting a new experiment exp_4.
  119. id payload4 = OCMClassMock([ABTExperimentPayload class]);
  120. OCMStub([payload4 experimentId]).andReturn(@"exp_4");
  121. OCMStub([payload4 variantId]).andReturn(@"v1");
  122. // Try setting a new experiment with ignore newest policy.
  123. [_ABTCUPController
  124. setExperimentWithOrigin:gABTTestOrigin
  125. payload:payload4
  126. events:events
  127. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  128. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  129. XCTAssertEqual(experiments.count, 3);
  130. XCTAssertTrue([self isExperimentID:@"exp_1" variantID:@"v1" inExperiments:experiments]);
  131. XCTAssertTrue([self isExperimentID:@"exp_2" variantID:@"v1" inExperiments:experiments]);
  132. XCTAssertTrue([self isExperimentID:@"exp_3" variantID:@"v1" inExperiments:experiments]);
  133. XCTAssertFalse([self isExperimentID:@"exp_4" variantID:@"v1" inExperiments:experiments]);
  134. // Try setting a new experiment with discard oldest policy.
  135. [_ABTCUPController
  136. setExperimentWithOrigin:gABTTestOrigin
  137. payload:payload4
  138. events:events
  139. policy:ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest];
  140. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  141. XCTAssertEqual(experiments.count, 3);
  142. XCTAssertFalse([self isExperimentID:@"exp_1" variantID:@"v1" inExperiments:experiments]);
  143. XCTAssertTrue([self isExperimentID:@"exp_4" variantID:@"v1" inExperiments:experiments]);
  144. // Try setting a new experiment with unspecified policy
  145. id payload5 = OCMClassMock([ABTExperimentPayload class]);
  146. OCMStub([payload5 experimentId]).andReturn(@"exp_5");
  147. OCMStub([payload5 variantId]).andReturn(@"v1");
  148. [_ABTCUPController
  149. setExperimentWithOrigin:gABTTestOrigin
  150. payload:payload5
  151. events:events
  152. policy:ABTExperimentPayloadExperimentOverflowPolicyUnspecified];
  153. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  154. XCTAssertEqual(experiments.count, 3);
  155. XCTAssertFalse([self isExperimentID:@"exp_2" variantID:@"v1" inExperiments:experiments]);
  156. XCTAssertTrue([self isExperimentID:@"exp_3" variantID:@"v1" inExperiments:experiments]);
  157. XCTAssertTrue([self isExperimentID:@"exp_4" variantID:@"v1" inExperiments:experiments]);
  158. XCTAssertTrue([self isExperimentID:@"exp_5" variantID:@"v1" inExperiments:experiments]);
  159. }
  160. - (void)testSetExperimentWithTheSameVariantID {
  161. id payload1 = OCMClassMock([ABTExperimentPayload class]);
  162. OCMStub([payload1 experimentId]).andReturn(@"exp_1");
  163. OCMStub([payload1 variantId]).andReturn(@"v1");
  164. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  165. [_ABTCUPController
  166. setExperimentWithOrigin:gABTTestOrigin
  167. payload:payload1
  168. events:events
  169. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  170. NSArray *experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  171. XCTAssertEqual(experiments.count, 1);
  172. XCTAssertTrue([self isExperimentID:@"exp_1" variantID:@"v1" inExperiments:experiments]);
  173. id payload2 = OCMClassMock([ABTExperimentPayload class]);
  174. OCMStub([payload2 experimentId]).andReturn(@"exp_1");
  175. OCMStub([payload2 variantId]).andReturn(@"v2");
  176. [_ABTCUPController
  177. setExperimentWithOrigin:gABTTestOrigin
  178. payload:payload2
  179. events:events
  180. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  181. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  182. XCTAssertEqual(experiments.count, 1);
  183. XCTAssertTrue([self isExperimentID:@"exp_1" variantID:@"v2" inExperiments:experiments]);
  184. }
  185. - (BOOL)isExperimentID:(NSString *)experimentID
  186. variantID:(NSString *)variantID
  187. inExperiments:(NSArray *)experiments {
  188. for (NSDictionary<NSString *, NSString *> *experiment in experiments) {
  189. if ([experiment[@"name"] isEqualToString:experimentID] &&
  190. [experiment[@"value"] isEqualToString:variantID]) {
  191. return YES;
  192. }
  193. }
  194. return NO;
  195. }
  196. - (void)testClearExperiment {
  197. id payload = OCMClassMock([ABTExperimentPayload class]);
  198. OCMStub([payload experimentId]).andReturn(@"exp_1");
  199. OCMStub([payload variantId]).andReturn(@"v1");
  200. OCMStub([payload clearEventToLog]).andReturn(@"override_clear_event");
  201. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  202. [_ABTCUPController
  203. setExperimentWithOrigin:gABTTestOrigin
  204. payload:payload
  205. events:events
  206. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  207. NSArray *experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  208. XCTAssertEqual(experiments.count, 1);
  209. [_ABTCUPController clearExperiment:@"exp_1"
  210. variantID:@"v1"
  211. withOrigin:gABTTestOrigin
  212. payload:payload
  213. events:events];
  214. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  215. XCTAssertEqual(experiments.count, 0);
  216. }
  217. - (void)testMaxNumberOfExperiments {
  218. XCTAssertEqual([_ABTCUPController maxNumberOfExperimentsOfOrigin:gABTTestOrigin], 3);
  219. }
  220. - (void)testCreateExperiment {
  221. NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
  222. id payload = OCMClassMock([ABTExperimentPayload class]);
  223. OCMStub([payload experimentId]).andReturn(@"exp_1");
  224. OCMStub([payload variantId]).andReturn(@"variant_B");
  225. int64_t startTimeMillis = now * ABT_MSEC_PER_SEC;
  226. OCMStub([payload experimentStartTimeMillis]).andReturn(startTimeMillis);
  227. OCMStub([payload triggerEvent]).andReturn(@"");
  228. int64_t triggerTimeoutMillis = (now + 1500) * ABT_MSEC_PER_SEC;
  229. OCMStub([payload triggerTimeoutMillis]).andReturn(triggerTimeoutMillis);
  230. int64_t timeToLiveMillis = (now + 60000) * ABT_MSEC_PER_SEC;
  231. OCMStub([payload timeToLiveMillis]).andReturn(timeToLiveMillis);
  232. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  233. events.activateExperimentEventName = @"_lifecycle_override_activate";
  234. events.expireExperimentEventName = @"lifecycle_override_time_to_live";
  235. NSDictionary<NSString *, id> *experiment =
  236. [_ABTCUPController createExperimentFromOrigin:gABTTestOrigin payload:payload events:events];
  237. NSDictionary<NSString *, id> *triggeredEvent = [experiment objectForKey:@"triggeredEvent"];
  238. XCTAssertEqualObjects([experiment objectForKey:@"name"], @"exp_1");
  239. XCTAssertEqualObjects([experiment objectForKey:@"value"], @"variant_B");
  240. XCTAssertEqualObjects(gABTTestOrigin, [experiment objectForKey:@"origin"]);
  241. XCTAssertEqualWithAccuracy(now, [[experiment objectForKey:@"creationTimestamp"] longLongValue],
  242. 1.0);
  243. // Trigger event
  244. XCTAssertEqualObjects(gABTTestOrigin, triggeredEvent[@"origin"]);
  245. XCTAssertEqualObjects(triggeredEvent[@"name"], @"_lifecycle_override_activate",
  246. @"Activate event name is overrided by lifecycle events.");
  247. // Timeout event
  248. NSDictionary<NSString *, id> *timedOutEvent = [experiment objectForKey:@"timedOutEvent"];
  249. XCTAssertEqualObjects(gABTTestOrigin, timedOutEvent[@"origin"]);
  250. XCTAssertEqualObjects(FIRTimeoutExperimentEventName, timedOutEvent[@"name"],
  251. @"payload doesn't have timeout event name, use default one");
  252. // Expired event
  253. NSDictionary<NSString *, id> *expiredEvent = [experiment objectForKey:@"expiredEvent"];
  254. XCTAssertEqualObjects(gABTTestOrigin, expiredEvent[@"origin"]);
  255. XCTAssertEqualObjects(
  256. @"lifecycle_override_time_to_live", expiredEvent[@"name"],
  257. @"payload doesn't have expiry event name, but lifecycle event does, use lifecycle event");
  258. // Trigger event name
  259. XCTAssertEqualObjects(nil, [experiment objectForKey:@"triggerEventName"],
  260. @"Empty trigger event must be set to nil");
  261. // trigger timeout
  262. XCTAssertEqualWithAccuracy(now + 1500,
  263. [[experiment objectForKey:@"triggerTimeout"] longLongValue], 1.0);
  264. // time to live
  265. XCTAssertEqualWithAccuracy(now + 60000, [[experiment objectForKey:@"timeToLive"] longLongValue],
  266. 1.0);
  267. // Overwrite all event names
  268. id payloadWithCustomEventNames = OCMClassMock([ABTExperimentPayload class]);
  269. OCMStub([payloadWithCustomEventNames experimentId]).andReturn(@"exp_1");
  270. OCMStub([payloadWithCustomEventNames variantId]).andReturn(@"variant_B");
  271. OCMStub([payloadWithCustomEventNames activateEventToLog]).andReturn(@"payload_override_activate");
  272. OCMStub([payloadWithCustomEventNames ttlExpiryEventToLog])
  273. .andReturn(@"payload_override_time_to_live");
  274. OCMStub([payloadWithCustomEventNames timeoutEventToLog]).andReturn(@"payload_override_timeout");
  275. OCMStub([payloadWithCustomEventNames triggerEvent]).andReturn(@"payload_override_trigger_event");
  276. experiment = [_ABTCUPController createExperimentFromOrigin:gABTTestOrigin
  277. payload:payloadWithCustomEventNames
  278. events:events];
  279. triggeredEvent = [experiment objectForKey:@"triggeredEvent"];
  280. XCTAssertEqual(triggeredEvent[@"name"], @"payload_override_activate");
  281. timedOutEvent = [experiment objectForKey:@"timedOutEvent"];
  282. XCTAssertEqualObjects(timedOutEvent[@"name"], @"payload_override_timeout");
  283. expiredEvent = [experiment objectForKey:@"expiredEvent"];
  284. XCTAssertEqual(expiredEvent[@"name"], @"payload_override_time_to_live");
  285. XCTAssertEqualObjects([experiment objectForKey:@"triggerEventName"],
  286. @"payload_override_trigger_event");
  287. }
  288. #pragma mark - helpers
  289. - (void)testIsExperimentTheSameAsPayload {
  290. NSDictionary<NSString *, NSString *> *experiment =
  291. @{@"name" : @"exp_1", @"value" : @"variant_control_group"};
  292. id payload2 = OCMClassMock([ABTExperimentPayload class]);
  293. OCMStub([payload2 experimentId]).andReturn(@"exp_2");
  294. OCMStub([payload2 variantId]).andReturn(@"variant_group_A");
  295. XCTAssertFalse([_ABTCUPController isExperiment:experiment theSameAsPayload:payload2]);
  296. id payload1 = OCMClassMock([ABTExperimentPayload class]);
  297. OCMStub([payload1 experimentId]).andReturn(@"exp_1");
  298. OCMStub([payload1 variantId]).andReturn(@"variant_group_A");
  299. XCTAssertFalse([_ABTCUPController isExperiment:experiment theSameAsPayload:payload1]);
  300. id payloadJustRight = OCMClassMock([ABTExperimentPayload class]);
  301. OCMStub([payloadJustRight experimentId]).andReturn(@"exp_1");
  302. OCMStub([payloadJustRight variantId]).andReturn(@"variant_control_group");
  303. XCTAssertTrue([_ABTCUPController isExperiment:experiment theSameAsPayload:payloadJustRight]);
  304. }
  305. - (void)testOverflowPolicyWithPayload {
  306. ABTExperimentPayload *payloadUnspecifiedPolicy =
  307. [ABTTestUtilities payloadFromTestFilename:@"TestABTPayload3"];
  308. XCTAssertEqual(ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest,
  309. [_ABTCUPController overflowPolicyWithPayload:payloadUnspecifiedPolicy
  310. originalPolicy:-1000],
  311. @"Payload policy is unspecified, original policy is invalid, should return "
  312. @"default: DiscardOldest.");
  313. XCTAssertEqual(
  314. ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest,
  315. [_ABTCUPController
  316. overflowPolicyWithPayload:payloadUnspecifiedPolicy
  317. originalPolicy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest],
  318. @"Payload policy is unspecified, original policy is valid, use "
  319. @"original policy.");
  320. ABTExperimentPayload *payloadDiscardOldest =
  321. [ABTTestUtilities payloadFromTestFilename:@"TestABTPayload1"];
  322. payloadDiscardOldest.overflowPolicy = ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest;
  323. XCTAssertEqual(
  324. ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest,
  325. [_ABTCUPController
  326. overflowPolicyWithPayload:payloadDiscardOldest
  327. originalPolicy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest],
  328. @"Payload policy is specified, original policy is valid, but "
  329. @"use Payload because Payload always wins.");
  330. }
  331. @end