ABTConditionalUserPropertyControllerTest.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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. FIROptions *options =
  60. [[FIROptions alloc] initWithGoogleAppID:@"1:123456789012:ios:1234567890123456"
  61. GCMSenderID:@"123456789012"];
  62. [FIRApp configureWithOptions:options];
  63. }
  64. - (void)tearDown {
  65. [_fakeController resetExperiments];
  66. [_mockCUPController stopMocking];
  67. [FIRApp resetApps];
  68. [super tearDown];
  69. }
  70. #pragma mark - test proxy methods on Firebase Analytics
  71. - (void)testSetExperiment {
  72. id payload = OCMClassMock([ABTExperimentPayload class]);
  73. OCMStub([payload experimentId]).andReturn(@"exp_0");
  74. OCMStub([payload variantId]).andReturn(@"v1");
  75. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  76. [_ABTCUPController
  77. setExperimentWithOrigin:gABTTestOrigin
  78. payload:payload
  79. events:events
  80. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  81. NSArray *experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  82. XCTAssertEqual(experiments.count, 1);
  83. }
  84. - (void)testSetExperimentWhenOverflow {
  85. id payload1 = OCMClassMock([ABTExperimentPayload class]);
  86. OCMStub([payload1 experimentId]).andReturn(@"exp_1");
  87. OCMStub([payload1 variantId]).andReturn(@"v1");
  88. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  89. [_ABTCUPController
  90. setExperimentWithOrigin:gABTTestOrigin
  91. payload:payload1
  92. events:events
  93. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  94. NSArray *experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  95. XCTAssertEqual(experiments.count, 1);
  96. id payload2 = OCMClassMock([ABTExperimentPayload class]);
  97. OCMStub([payload2 experimentId]).andReturn(@"exp_2");
  98. OCMStub([payload2 variantId]).andReturn(@"v1");
  99. [_ABTCUPController
  100. setExperimentWithOrigin:gABTTestOrigin
  101. payload:payload2
  102. events:events
  103. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  104. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  105. XCTAssertEqual(experiments.count, 2);
  106. id payload3 = OCMClassMock([ABTExperimentPayload class]);
  107. OCMStub([payload3 experimentId]).andReturn(@"exp_3");
  108. OCMStub([payload3 variantId]).andReturn(@"v1");
  109. [_ABTCUPController
  110. setExperimentWithOrigin:gABTTestOrigin
  111. payload:payload3
  112. events:events
  113. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  114. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  115. XCTAssertEqual(experiments.count, 3);
  116. // Now it's overflowed, try setting a new experiment exp_4.
  117. id payload4 = OCMClassMock([ABTExperimentPayload class]);
  118. OCMStub([payload4 experimentId]).andReturn(@"exp_4");
  119. OCMStub([payload4 variantId]).andReturn(@"v1");
  120. // Try setting a new experiment with ignore newest policy.
  121. [_ABTCUPController
  122. setExperimentWithOrigin:gABTTestOrigin
  123. payload:payload4
  124. events:events
  125. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  126. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  127. XCTAssertEqual(experiments.count, 3);
  128. XCTAssertTrue([self isExperimentID:@"exp_1" variantID:@"v1" inExperiments:experiments]);
  129. XCTAssertTrue([self isExperimentID:@"exp_2" variantID:@"v1" inExperiments:experiments]);
  130. XCTAssertTrue([self isExperimentID:@"exp_3" variantID:@"v1" inExperiments:experiments]);
  131. XCTAssertFalse([self isExperimentID:@"exp_4" variantID:@"v1" inExperiments:experiments]);
  132. // Try setting a new experiment with discard oldest policy.
  133. [_ABTCUPController
  134. setExperimentWithOrigin:gABTTestOrigin
  135. payload:payload4
  136. events:events
  137. policy:ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest];
  138. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  139. XCTAssertEqual(experiments.count, 3);
  140. XCTAssertFalse([self isExperimentID:@"exp_1" variantID:@"v1" inExperiments:experiments]);
  141. XCTAssertTrue([self isExperimentID:@"exp_4" variantID:@"v1" inExperiments:experiments]);
  142. // Try setting a new experiment with unspecified policy
  143. id payload5 = OCMClassMock([ABTExperimentPayload class]);
  144. OCMStub([payload5 experimentId]).andReturn(@"exp_5");
  145. OCMStub([payload5 variantId]).andReturn(@"v1");
  146. [_ABTCUPController
  147. setExperimentWithOrigin:gABTTestOrigin
  148. payload:payload5
  149. events:events
  150. policy:ABTExperimentPayloadExperimentOverflowPolicyUnspecified];
  151. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  152. XCTAssertEqual(experiments.count, 3);
  153. XCTAssertFalse([self isExperimentID:@"exp_2" variantID:@"v1" inExperiments:experiments]);
  154. XCTAssertTrue([self isExperimentID:@"exp_3" variantID:@"v1" inExperiments:experiments]);
  155. XCTAssertTrue([self isExperimentID:@"exp_4" variantID:@"v1" inExperiments:experiments]);
  156. XCTAssertTrue([self isExperimentID:@"exp_5" variantID:@"v1" inExperiments:experiments]);
  157. }
  158. - (void)testSetExperimentWithTheSameVariantID {
  159. id payload1 = OCMClassMock([ABTExperimentPayload class]);
  160. OCMStub([payload1 experimentId]).andReturn(@"exp_1");
  161. OCMStub([payload1 variantId]).andReturn(@"v1");
  162. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  163. [_ABTCUPController
  164. setExperimentWithOrigin:gABTTestOrigin
  165. payload:payload1
  166. events:events
  167. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  168. NSArray *experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  169. XCTAssertEqual(experiments.count, 1);
  170. XCTAssertTrue([self isExperimentID:@"exp_1" variantID:@"v1" inExperiments:experiments]);
  171. id payload2 = OCMClassMock([ABTExperimentPayload class]);
  172. OCMStub([payload2 experimentId]).andReturn(@"exp_1");
  173. OCMStub([payload2 variantId]).andReturn(@"v2");
  174. [_ABTCUPController
  175. setExperimentWithOrigin:gABTTestOrigin
  176. payload:payload2
  177. events:events
  178. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  179. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  180. XCTAssertEqual(experiments.count, 1);
  181. XCTAssertTrue([self isExperimentID:@"exp_1" variantID:@"v2" inExperiments:experiments]);
  182. }
  183. - (BOOL)isExperimentID:(NSString *)experimentID
  184. variantID:(NSString *)variantID
  185. inExperiments:(NSArray *)experiments {
  186. for (NSDictionary<NSString *, NSString *> *experiment in experiments) {
  187. if ([experiment[@"name"] isEqualToString:experimentID] &&
  188. [experiment[@"value"] isEqualToString:variantID]) {
  189. return YES;
  190. }
  191. }
  192. return NO;
  193. }
  194. - (void)testClearExperiment {
  195. id payload = OCMClassMock([ABTExperimentPayload class]);
  196. OCMStub([payload experimentId]).andReturn(@"exp_1");
  197. OCMStub([payload variantId]).andReturn(@"v1");
  198. OCMStub([payload clearEventToLog]).andReturn(@"override_clear_event");
  199. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  200. [_ABTCUPController
  201. setExperimentWithOrigin:gABTTestOrigin
  202. payload:payload
  203. events:events
  204. policy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest];
  205. NSArray *experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  206. XCTAssertEqual(experiments.count, 1);
  207. [_ABTCUPController clearExperiment:@"exp_1"
  208. variantID:@"v1"
  209. withOrigin:gABTTestOrigin
  210. payload:payload
  211. events:events];
  212. experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin];
  213. XCTAssertEqual(experiments.count, 0);
  214. }
  215. - (void)testMaxNumberOfExperiments {
  216. XCTAssertEqual([_ABTCUPController maxNumberOfExperimentsOfOrigin:gABTTestOrigin], 3);
  217. }
  218. - (void)testCreateExperiment {
  219. NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
  220. id payload = OCMClassMock([ABTExperimentPayload class]);
  221. OCMStub([payload experimentId]).andReturn(@"exp_1");
  222. OCMStub([payload variantId]).andReturn(@"variant_B");
  223. int64_t startTimeMillis = now * ABT_MSEC_PER_SEC;
  224. OCMStub([payload experimentStartTimeMillis]).andReturn(startTimeMillis);
  225. OCMStub([payload triggerEvent]).andReturn(@"");
  226. int64_t triggerTimeoutMillis = (now + 1500) * ABT_MSEC_PER_SEC;
  227. OCMStub([payload triggerTimeoutMillis]).andReturn(triggerTimeoutMillis);
  228. int64_t timeToLiveMillis = (now + 60000) * ABT_MSEC_PER_SEC;
  229. OCMStub([payload timeToLiveMillis]).andReturn(timeToLiveMillis);
  230. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  231. events.activateExperimentEventName = @"_lifecycle_override_activate";
  232. events.expireExperimentEventName = @"lifecycle_override_time_to_live";
  233. NSDictionary<NSString *, id> *experiment =
  234. [_ABTCUPController createExperimentFromOrigin:gABTTestOrigin payload:payload events:events];
  235. NSDictionary<NSString *, id> *triggeredEvent = [experiment objectForKey:@"triggeredEvent"];
  236. XCTAssertEqualObjects([experiment objectForKey:@"name"], @"exp_1");
  237. XCTAssertEqualObjects([experiment objectForKey:@"value"], @"variant_B");
  238. XCTAssertEqualObjects(gABTTestOrigin, [experiment objectForKey:@"origin"]);
  239. XCTAssertEqualWithAccuracy(now, [[experiment objectForKey:@"creationTimestamp"] longLongValue],
  240. 1.0);
  241. // Trigger event
  242. XCTAssertEqualObjects(gABTTestOrigin, triggeredEvent[@"origin"]);
  243. XCTAssertEqualObjects(triggeredEvent[@"name"], @"_lifecycle_override_activate",
  244. @"Activate event name is overrided by lifecycle events.");
  245. // Timeout event
  246. NSDictionary<NSString *, id> *timedOutEvent = [experiment objectForKey:@"timedOutEvent"];
  247. XCTAssertEqualObjects(gABTTestOrigin, timedOutEvent[@"origin"]);
  248. XCTAssertEqualObjects(FIRTimeoutExperimentEventName, timedOutEvent[@"name"],
  249. @"payload doesn't have timeout event name, use default one");
  250. // Expired event
  251. NSDictionary<NSString *, id> *expiredEvent = [experiment objectForKey:@"expiredEvent"];
  252. XCTAssertEqualObjects(gABTTestOrigin, expiredEvent[@"origin"]);
  253. XCTAssertEqualObjects(
  254. @"lifecycle_override_time_to_live", expiredEvent[@"name"],
  255. @"payload doesn't have expiry event name, but lifecycle event does, use lifecycle event");
  256. // Trigger event name
  257. XCTAssertEqualObjects(nil, [experiment objectForKey:@"triggerEventName"],
  258. @"Empty trigger event must be set to nil");
  259. // trigger timeout
  260. XCTAssertEqualWithAccuracy(now + 1500,
  261. [[experiment objectForKey:@"triggerTimeout"] longLongValue], 1.0);
  262. // time to live
  263. XCTAssertEqualWithAccuracy(now + 60000, [[experiment objectForKey:@"timeToLive"] longLongValue],
  264. 1.0);
  265. // Overwrite all event names
  266. id payloadWithCustomEventNames = OCMClassMock([ABTExperimentPayload class]);
  267. OCMStub([payloadWithCustomEventNames experimentId]).andReturn(@"exp_1");
  268. OCMStub([payloadWithCustomEventNames variantId]).andReturn(@"variant_B");
  269. OCMStub([payloadWithCustomEventNames activateEventToLog]).andReturn(@"payload_override_activate");
  270. OCMStub([payloadWithCustomEventNames ttlExpiryEventToLog])
  271. .andReturn(@"payload_override_time_to_live");
  272. OCMStub([payloadWithCustomEventNames timeoutEventToLog]).andReturn(@"payload_override_timeout");
  273. OCMStub([payloadWithCustomEventNames triggerEvent]).andReturn(@"payload_override_trigger_event");
  274. experiment = [_ABTCUPController createExperimentFromOrigin:gABTTestOrigin
  275. payload:payloadWithCustomEventNames
  276. events:events];
  277. triggeredEvent = [experiment objectForKey:@"triggeredEvent"];
  278. XCTAssertEqual(triggeredEvent[@"name"], @"payload_override_activate");
  279. timedOutEvent = [experiment objectForKey:@"timedOutEvent"];
  280. XCTAssertEqualObjects(timedOutEvent[@"name"], @"payload_override_timeout");
  281. expiredEvent = [experiment objectForKey:@"expiredEvent"];
  282. XCTAssertEqual(expiredEvent[@"name"], @"payload_override_time_to_live");
  283. XCTAssertEqualObjects([experiment objectForKey:@"triggerEventName"],
  284. @"payload_override_trigger_event");
  285. }
  286. #pragma mark - helpers
  287. - (void)testIsExperimentTheSameAsPayload {
  288. NSDictionary<NSString *, NSString *> *experiment =
  289. @{@"name" : @"exp_1", @"value" : @"variant_control_group"};
  290. id payload2 = OCMClassMock([ABTExperimentPayload class]);
  291. OCMStub([payload2 experimentId]).andReturn(@"exp_2");
  292. OCMStub([payload2 variantId]).andReturn(@"variant_group_A");
  293. XCTAssertFalse([_ABTCUPController isExperiment:experiment theSameAsPayload:payload2]);
  294. id payload1 = OCMClassMock([ABTExperimentPayload class]);
  295. OCMStub([payload1 experimentId]).andReturn(@"exp_1");
  296. OCMStub([payload1 variantId]).andReturn(@"variant_group_A");
  297. XCTAssertFalse([_ABTCUPController isExperiment:experiment theSameAsPayload:payload1]);
  298. id payloadJustRight = OCMClassMock([ABTExperimentPayload class]);
  299. OCMStub([payloadJustRight experimentId]).andReturn(@"exp_1");
  300. OCMStub([payloadJustRight variantId]).andReturn(@"variant_control_group");
  301. XCTAssertTrue([_ABTCUPController isExperiment:experiment theSameAsPayload:payloadJustRight]);
  302. }
  303. - (void)testOverflowPolicyWithPayload {
  304. ABTExperimentPayload *payloadUnspecifiedPolicy =
  305. [ABTTestUtilities payloadFromTestFilename:@"TestABTPayload3"];
  306. XCTAssertEqual(ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest,
  307. [_ABTCUPController overflowPolicyWithPayload:payloadUnspecifiedPolicy
  308. originalPolicy:-1000],
  309. @"Payload policy is unspecified, original policy is invalid, should return "
  310. @"default: DiscardOldest.");
  311. XCTAssertEqual(
  312. ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest,
  313. [_ABTCUPController
  314. overflowPolicyWithPayload:payloadUnspecifiedPolicy
  315. originalPolicy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest],
  316. @"Payload policy is unspecified, original policy is valid, use "
  317. @"original policy.");
  318. ABTExperimentPayload *payloadDiscardOldest =
  319. [ABTTestUtilities payloadFromTestFilename:@"TestABTPayload1"];
  320. payloadDiscardOldest.overflowPolicy = ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest;
  321. XCTAssertEqual(
  322. ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest,
  323. [_ABTCUPController
  324. overflowPolicyWithPayload:payloadDiscardOldest
  325. originalPolicy:ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest],
  326. @"Payload policy is specified, original policy is valid, but "
  327. @"use Payload because Payload always wins.");
  328. }
  329. @end