FIRExperimentControllerTest.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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 <XCTest/XCTest.h>
  15. #import <FirebaseABTesting/FIRExperimentController.h>
  16. #import <FirebaseABTesting/FIRLifecycleEvents.h>
  17. #import <FirebaseAnalyticsInterop/FIRAnalyticsInterop.h>
  18. #import <FirebaseCore/FIRAppInternal.h>
  19. #import <FirebaseCore/FIROptionsInternal.h>
  20. #import <OCMock/OCMock.h>
  21. #import "FirebaseABTesting/Sources/ABTConditionalUserPropertyController.h"
  22. #import "FirebaseABTesting/Sources/ABTConstants.h"
  23. #import "FirebaseABTesting/Tests/Unit/ABTFakeFIRAConditionalUserPropertyController.h"
  24. #import "FirebaseABTesting/Tests/Unit/ABTTestUniversalConstants.h"
  25. extern ABTExperimentPayload *ABTDeserializeExperimentPayload(NSData *payload);
  26. extern NSArray<ABTExperimentPayload *> *ABTExperimentsToSetFromPayloads(
  27. NSArray<NSData *> *payloads,
  28. NSArray<NSDictionary<NSString *, NSString *> *> *experiments,
  29. id<FIRAnalyticsInterop> _Nullable analytics);
  30. extern NSArray *ABTExperimentsToClearFromPayloads(
  31. NSArray<NSData *> *payloads,
  32. NSArray<NSDictionary<NSString *, NSString *> *> *experiments,
  33. id<FIRAnalyticsInterop> _Nullable analytics);
  34. @interface FIRExperimentController (ExposedForTest)
  35. - (void)
  36. updateExperimentsInBackgroundQueueWithServiceOrigin:(NSString *)origin
  37. events:(FIRLifecycleEvents *)events
  38. policy:
  39. (ABTExperimentPayload_ExperimentOverflowPolicy)
  40. policy
  41. lastStartTime:(NSTimeInterval)lastStartTime
  42. payloads:(NSArray<NSData *> *)payloads;
  43. /// Surface internal initializer to avoid singleton usage during tests.
  44. - (instancetype)initWithAnalytics:(nullable id<FIRAnalyticsInterop>)analytics;
  45. @end
  46. @interface ABTConditionalUserPropertyController (ExposedForTest)
  47. - (void)maxNumberOfExperimentsOfOrigin:(NSString *)origin
  48. completionHandler:(void (^)(int32_t))completionHandler;
  49. - (int32_t)maxNumberOfExperimentsOfOrigin:(NSString *)origin;
  50. - (id)createExperimentFromOrigin:(NSString *)origin
  51. payload:(ABTExperimentPayload *)payload
  52. events:(FIRLifecycleEvents *)events;
  53. - (ABTExperimentPayload_ExperimentOverflowPolicy)
  54. overflowPolicyWithPayload:(ABTExperimentPayload *)payload
  55. originalPolicy:(ABTExperimentPayload_ExperimentOverflowPolicy)originalPolicy;
  56. @end
  57. @interface FIRExperimentControllerTest : XCTestCase {
  58. FIRExperimentController *_experimentController;
  59. ABTFakeFIRAConditionalUserPropertyController *_fakeController;
  60. id _mockCUPController;
  61. }
  62. @end
  63. @implementation FIRExperimentControllerTest
  64. - (void)setUp {
  65. [super setUp];
  66. _fakeController = [ABTFakeFIRAConditionalUserPropertyController sharedInstance];
  67. id<FIRAnalyticsInterop> fakeAnalytics =
  68. [[FakeAnalytics alloc] initWithFakeController:_fakeController];
  69. _experimentController = [[FIRExperimentController alloc] initWithAnalytics:fakeAnalytics];
  70. ABTConditionalUserPropertyController *controller =
  71. [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:fakeAnalytics];
  72. _mockCUPController = OCMPartialMock(controller);
  73. OCMStub([_mockCUPController maxNumberOfExperimentsOfOrigin:[OCMArg any]]).andReturn(3);
  74. }
  75. - (void)tearDown {
  76. [_fakeController resetExperiments];
  77. [_mockCUPController stopMocking];
  78. [super tearDown];
  79. }
  80. - (void)testDeserializeInvalidPayload {
  81. FIRExperimentController *controller = _experimentController;
  82. XCTAssertNotNil(controller);
  83. NSString *sampleString = @"sample_invalid_payload";
  84. NSData *invalidData = [sampleString dataUsingEncoding:NSUTF8StringEncoding];
  85. XCTAssertNil(ABTDeserializeExperimentPayload(invalidData));
  86. XCTAssertNotNil(ABTDeserializeExperimentPayload(nil));
  87. }
  88. - (void)testLifecycleEvents {
  89. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  90. XCTAssertEqualObjects(FIRSetExperimentEventName, events.setExperimentEventName);
  91. XCTAssertEqualObjects(FIRActivateExperimentEventName, events.activateExperimentEventName);
  92. XCTAssertEqualObjects(FIRTimeoutExperimentEventName, events.timeoutExperimentEventName);
  93. XCTAssertEqualObjects(FIRExpireExperimentEventName, events.expireExperimentEventName);
  94. XCTAssertEqualObjects(FIRClearExperimentEventName, events.clearExperimentEventName);
  95. // Should be able to override event name values.
  96. events.setExperimentEventName = @"_new_set_experiment";
  97. XCTAssertEqualObjects(events.setExperimentEventName, @"_new_set_experiment");
  98. events.setExperimentEventName = @"name_without_prefix";
  99. XCTAssertEqualObjects(FIRSetExperimentEventName, events.setExperimentEventName);
  100. events.activateExperimentEventName = @"_new_activate_experiment";
  101. XCTAssertEqualObjects(events.activateExperimentEventName, @"_new_activate_experiment");
  102. events.activateExperimentEventName = @"";
  103. XCTAssertEqualObjects(FIRActivateExperimentEventName, events.activateExperimentEventName);
  104. events.timeoutExperimentEventName = @"__";
  105. XCTAssertEqualObjects(events.timeoutExperimentEventName, @"__");
  106. events.timeoutExperimentEventName = @"name_with_";
  107. XCTAssertEqualObjects(FIRTimeoutExperimentEventName, events.timeoutExperimentEventName);
  108. events.expireExperimentEventName = @"_";
  109. XCTAssertEqualObjects(events.expireExperimentEventName, @"_");
  110. #pragma clang diagnostic push
  111. #pragma clang diagnostic ignored "-Wnonnull"
  112. events.expireExperimentEventName = nil;
  113. #pragma clang diagnostic pop
  114. XCTAssertEqualObjects(FIRExpireExperimentEventName, events.expireExperimentEventName);
  115. events.clearExperimentEventName = @"_new_set_experiment";
  116. XCTAssertEqualObjects(events.clearExperimentEventName, @"_new_set_experiment");
  117. events.clearExperimentEventName = @"";
  118. XCTAssertEqualObjects(FIRClearExperimentEventName, events.clearExperimentEventName);
  119. }
  120. - (void)testSetExperimentWithBadPayload {
  121. [[_mockCUPController reject]
  122. setExperimentWithOrigin:[OCMArg any]
  123. payload:[OCMArg any]
  124. events:[OCMArg any]
  125. policy:ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest];
  126. NSString *sampleString = @"sample_invalid_payload";
  127. NSData *invalidData = [sampleString dataUsingEncoding:NSUTF8StringEncoding];
  128. XCTAssertNil(ABTDeserializeExperimentPayload(invalidData));
  129. }
  130. - (void)testUpdateExperiments {
  131. NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
  132. ABTExperimentPayload *payload2 = [[ABTExperimentPayload alloc] init];
  133. payload2.experimentId = @"exp_2";
  134. payload2.variantId = @"v200";
  135. payload2.experimentStartTimeMillis =
  136. (now + 1500) * ABT_MSEC_PER_SEC; // start time > last start time, do set
  137. ABTExperimentLite *ongoingExperiment = [[ABTExperimentLite alloc] init];
  138. ongoingExperiment.experimentId = @"exp_1";
  139. [payload2.ongoingExperimentsArray addObject:ongoingExperiment];
  140. ABTExperimentPayload *payload3 = [[ABTExperimentPayload alloc] init];
  141. payload3.experimentId = @"exp_3";
  142. payload3.variantId = @"v200";
  143. payload3.experimentStartTimeMillis =
  144. (now + 900) * ABT_MSEC_PER_SEC; // start time > last start time, do set
  145. ongoingExperiment = [[ABTExperimentLite alloc] init];
  146. ongoingExperiment.experimentId = @"exp_2";
  147. [payload3.ongoingExperimentsArray addObject:ongoingExperiment];
  148. ABTExperimentPayload *payload4 = [[ABTExperimentPayload alloc] init];
  149. payload4.experimentId = @"exp_4";
  150. payload4.variantId = @"v200";
  151. payload4.experimentStartTimeMillis =
  152. (now - 900) * ABT_MSEC_PER_SEC; // start time < last start time, do not set.
  153. ongoingExperiment = [[ABTExperimentLite alloc] init];
  154. ongoingExperiment.experimentId = @"exp_2";
  155. [payload4.ongoingExperimentsArray addObject:ongoingExperiment];
  156. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  157. NSArray *payloads = @[ [payload2 data], [payload3 data], [payload4 data] ];
  158. [_experimentController
  159. updateExperimentsInBackgroundQueueWithServiceOrigin:gABTTestOrigin
  160. events:events
  161. policy:
  162. ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest // NOLINT
  163. lastStartTime:now
  164. payloads:payloads];
  165. XCTAssertEqual([_mockCUPController experimentsWithOrigin:gABTTestOrigin].count, 2);
  166. // Second time update exp_1 no longer exist, should be cleared from experiments.
  167. payloads = @[ [payload3 data], [payload4 data] ];
  168. [_experimentController
  169. updateExperimentsInBackgroundQueueWithServiceOrigin:gABTTestOrigin
  170. events:events
  171. policy:
  172. ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest // NOLINT
  173. lastStartTime:now
  174. payloads:payloads];
  175. XCTAssertEqual([_mockCUPController experimentsWithOrigin:gABTTestOrigin].count, 1);
  176. }
  177. - (void)testLatestExperimentStartTimestamps {
  178. // Mock incoming payloads
  179. NSMutableArray<NSData *> *payloads = [[NSMutableArray alloc] init];
  180. NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
  181. ABTExperimentPayload *payload1 = [[ABTExperimentPayload alloc] init];
  182. payload1.experimentId = @"exp_1";
  183. payload1.variantId = @"v3";
  184. payload1.experimentStartTimeMillis = now * ABT_MSEC_PER_SEC;
  185. [payloads addObject:[payload1 data]];
  186. ABTExperimentPayload *payload2 = [[ABTExperimentPayload alloc] init];
  187. payload2.experimentId = @"exp_2";
  188. payload2.variantId = @"v2";
  189. payload2.experimentStartTimeMillis = (now + 500) * ABT_MSEC_PER_SEC;
  190. [payloads addObject:[payload2 data]];
  191. NSString *sampleString = @"sample_invalid_payload";
  192. NSData *invalidPayload = [sampleString dataUsingEncoding:NSUTF8StringEncoding];
  193. [payloads addObject:invalidPayload];
  194. XCTAssertEqualWithAccuracy(
  195. now + 500,
  196. [_experimentController latestExperimentStartTimestampBetweenTimestamp:now + 200
  197. andPayloads:payloads],
  198. 1);
  199. XCTAssertEqualWithAccuracy(
  200. now + 1000,
  201. [_experimentController latestExperimentStartTimestampBetweenTimestamp:now + 1000
  202. andPayloads:payloads],
  203. 1);
  204. XCTAssertEqualWithAccuracy(
  205. now + 500,
  206. [_experimentController latestExperimentStartTimestampBetweenTimestamp:now - 10000
  207. andPayloads:payloads],
  208. 1);
  209. }
  210. - (void)testExperimentsToSetFromPayloads {
  211. // Mock conditional user property objects in experiments.
  212. NSMutableArray *currentExperiments = [[NSMutableArray alloc] init];
  213. NSDictionary<NSString *, NSString *> *CUP1 = @{@"name" : @"exp_1", @"value" : @"v1"};
  214. [currentExperiments addObject:CUP1];
  215. NSDictionary<NSString *, NSString *> *CUP2 = @{@"name" : @"exp_2", @"value" : @"v2"};
  216. [currentExperiments addObject:CUP2];
  217. // Mock incoming payloads
  218. NSMutableArray<NSData *> *payloads = [[NSMutableArray alloc] init];
  219. ABTExperimentPayload *payload1 = [[ABTExperimentPayload alloc] init];
  220. payload1.experimentId = @"exp_1";
  221. payload1.variantId = @"v3";
  222. [payloads addObject:[payload1 data]];
  223. ABTExperimentPayload *payload2 = [[ABTExperimentPayload alloc] init];
  224. payload2.experimentId = @"exp_2";
  225. payload2.variantId = @"v2";
  226. [payloads addObject:[payload2 data]];
  227. NSString *sampleString = @"sample_invalid_payload";
  228. NSData *invalidPayload = [sampleString dataUsingEncoding:NSUTF8StringEncoding];
  229. [payloads addObject:invalidPayload];
  230. NSArray<ABTExperimentPayload *> *experimentsToSet =
  231. ABTExperimentsToSetFromPayloads(payloads, currentExperiments, nil);
  232. XCTAssertEqual(experimentsToSet.count, 1);
  233. ABTExperimentPayload *payloadToAdd = experimentsToSet.firstObject;
  234. XCTAssertEqualObjects(payloadToAdd.experimentId, @"exp_1");
  235. XCTAssertEqualObjects(payloadToAdd.variantId, @"v3");
  236. }
  237. - (void)testExperimentsToClearFromPaylods {
  238. // Mock conditional user property objects in experiments.
  239. NSMutableArray *currentExperiments = [[NSMutableArray alloc] init];
  240. NSDictionary<NSString *, NSString *> *CUP1 = @{@"name" : @"exp_1", @"value" : @"v1"};
  241. [currentExperiments addObject:CUP1];
  242. NSDictionary<NSString *, NSString *> *CUP2 = @{@"name" : @"exp_2", @"value" : @"v2"};
  243. [currentExperiments addObject:CUP2];
  244. // Mock incoming payloads
  245. NSMutableArray<NSData *> *payloads = [[NSMutableArray alloc] init];
  246. ABTExperimentPayload *payload1 = [[ABTExperimentPayload alloc] init];
  247. payload1.experimentId = @"exp_1";
  248. payload1.variantId = @"v3";
  249. [payloads addObject:[payload1 data]];
  250. ABTExperimentPayload *payload2 = [[ABTExperimentPayload alloc] init];
  251. payload2.experimentId = @"exp_2";
  252. payload2.variantId = @"v2";
  253. [payloads addObject:[payload2 data]];
  254. NSString *sampleString = @"sample_invalid_payload";
  255. NSData *invalidPayload = [sampleString dataUsingEncoding:NSUTF8StringEncoding];
  256. [payloads addObject:invalidPayload];
  257. NSArray<NSDictionary<NSString *, NSString *> *> *experimentsToClear =
  258. ABTExperimentsToClearFromPayloads(payloads, currentExperiments, nil);
  259. XCTAssertEqual(experimentsToClear.count, 1);
  260. NSDictionary<NSString *, NSString *> *experimentToRemove = experimentsToClear.firstObject;
  261. XCTAssertEqualObjects(experimentToRemove[@"name"], @"exp_1");
  262. XCTAssertEqualObjects(experimentToRemove[@"value"], @"v1");
  263. }
  264. - (void)testInvalidExperiments {
  265. [[_mockCUPController reject]
  266. setExperimentWithOrigin:[OCMArg any]
  267. payload:[OCMArg any]
  268. events:[OCMArg any]
  269. policy:ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest];
  270. [[_mockCUPController reject]
  271. setExperimentWithOrigin:[OCMArg any]
  272. payload:[OCMArg any]
  273. events:[OCMArg any]
  274. policy:ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest];
  275. OCMStub([_mockCUPController experimentsWithOrigin:gABTTestOrigin]).andReturn(nil);
  276. NSMutableArray<NSData *> *payloads = [[NSMutableArray alloc] init];
  277. FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
  278. [_experimentController
  279. updateExperimentsInBackgroundQueueWithServiceOrigin:gABTTestOrigin
  280. events:events
  281. policy:
  282. ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest // NOLINT
  283. lastStartTime:-1
  284. payloads:payloads];
  285. }
  286. @end