// Copyright 2019 Google // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #import #import #import #import #import "FirebaseABTesting/Sources/ABTConditionalUserPropertyController.h" #import "FirebaseABTesting/Sources/ABTConstants.h" #import "FirebaseABTesting/Tests/Unit/ABTFakeFIRAConditionalUserPropertyController.h" #import "FirebaseABTesting/Tests/Unit/ABTTestUniversalConstants.h" #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" @interface ABTConditionalUserPropertyController (ExposedForTest) - (NSInteger)maxNumberOfExperimentsOfOrigin:(NSString *)origin; - (void)maxNumberOfExperimentsOfOrigin:(NSString *)origin completionHandler:(void (^)(int32_t))completionHandler; - (id)createExperimentFromOrigin:(NSString *)origin payload:(ABTExperimentPayload *)payload events:(FIRLifecycleEvents *)events; - (ABTExperimentPayload_ExperimentOverflowPolicy) overflowPolicyWithPayload:(ABTExperimentPayload *)payload originalPolicy:(ABTExperimentPayload_ExperimentOverflowPolicy)originalPolicy; /// Surface internal initializer to avoid singleton usage during tests. - (instancetype)initWithAnalytics:(nullable id)analytics; @end typedef void (^FakeAnalyticsLogEventWithOriginNameParametersHandler)( NSString *origin, NSString *name, NSDictionary *parameters); @interface ABTConditionalUserPropertyControllerTest : XCTestCase { ABTConditionalUserPropertyController *_ABTCUPController; ABTFakeFIRAConditionalUserPropertyController *_fakeController; id _mockCUPController; } @end @implementation ABTConditionalUserPropertyControllerTest - (void)setUp { [super setUp]; _fakeController = [ABTFakeFIRAConditionalUserPropertyController sharedInstance]; _ABTCUPController = [[ABTConditionalUserPropertyController alloc] initWithAnalytics:[[FakeAnalytics alloc] initWithFakeController:_fakeController]]; _mockCUPController = OCMPartialMock(_ABTCUPController); OCMStub([_mockCUPController maxNumberOfExperimentsOfOrigin:[OCMArg any]]).andReturn(3); // Must initialize FIRApp before calling set experiment as Firebase Analytics internal event // logging requires it. NSDictionary *optionsDictionary = @{ kFIRGoogleAppID : @"1:123456789012:ios:1234567890123456", @"GCM_SENDER_ID" : @"123456789012" }; FIROptions *options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; [FIRApp configureWithOptions:options]; } - (void)tearDown { [_fakeController resetExperiments]; [_mockCUPController stopMocking]; [FIRApp resetApps]; [super tearDown]; } #pragma mark - test proxy methods on Firebase Analytics - (void)testSetExperiment { ABTExperimentPayload *payload = [[ABTExperimentPayload alloc] init]; payload.experimentId = @"exp_0"; FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init]; [_ABTCUPController setExperimentWithOrigin:gABTTestOrigin payload:payload events:events policy:ABTExperimentPayload_ExperimentOverflowPolicy_IgnoreNewest]; NSArray *experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin]; XCTAssertEqual(experiments.count, 1); } - (void)testSetExperimentWhenOverflow { ABTExperimentPayload *payload = [[ABTExperimentPayload alloc] init]; payload.experimentId = @"exp_1"; payload.variantId = @"v1"; FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init]; [_ABTCUPController setExperimentWithOrigin:gABTTestOrigin payload:payload events:events policy:ABTExperimentPayload_ExperimentOverflowPolicy_IgnoreNewest]; NSArray *experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin]; XCTAssertEqual(experiments.count, 1); payload.experimentId = @"exp_2"; payload.variantId = @"v1"; [_ABTCUPController setExperimentWithOrigin:gABTTestOrigin payload:payload events:events policy:ABTExperimentPayload_ExperimentOverflowPolicy_IgnoreNewest]; experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin]; XCTAssertEqual(experiments.count, 2); payload.experimentId = @"exp_3"; payload.variantId = @"v1"; [_ABTCUPController setExperimentWithOrigin:gABTTestOrigin payload:payload events:events policy:ABTExperimentPayload_ExperimentOverflowPolicy_IgnoreNewest]; experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin]; XCTAssertEqual(experiments.count, 3); // Now it's overflowed, try setting a new experiment exp_4. payload.experimentId = @"exp_4"; payload.variantId = @"v1"; // Try setting a new experiment with ignore newest policy. [_ABTCUPController setExperimentWithOrigin:gABTTestOrigin payload:payload events:events policy:ABTExperimentPayload_ExperimentOverflowPolicy_IgnoreNewest]; experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin]; XCTAssertEqual(experiments.count, 3); XCTAssertTrue([self isExperimentID:@"exp_1" variantID:@"v1" inExperiments:experiments]); XCTAssertTrue([self isExperimentID:@"exp_2" variantID:@"v1" inExperiments:experiments]); XCTAssertTrue([self isExperimentID:@"exp_3" variantID:@"v1" inExperiments:experiments]); XCTAssertFalse([self isExperimentID:@"exp_4" variantID:@"v1" inExperiments:experiments]); // Try setting a new experiment with discard oldest policy. [_ABTCUPController setExperimentWithOrigin:gABTTestOrigin payload:payload events:events policy:ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest]; experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin]; XCTAssertEqual(experiments.count, 3); XCTAssertFalse([self isExperimentID:@"exp_1" variantID:@"v1" inExperiments:experiments]); XCTAssertTrue([self isExperimentID:@"exp_4" variantID:@"v1" inExperiments:experiments]); // Try setting a new experiment with unspecified policy payload.experimentId = @"exp_5"; payload.variantId = @"v1"; [_ABTCUPController setExperimentWithOrigin:gABTTestOrigin payload:payload events:events policy:ABTExperimentPayload_ExperimentOverflowPolicy_PolicyUnspecified]; experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin]; XCTAssertEqual(experiments.count, 3); XCTAssertFalse([self isExperimentID:@"exp_2" variantID:@"v1" inExperiments:experiments]); XCTAssertTrue([self isExperimentID:@"exp_3" variantID:@"v1" inExperiments:experiments]); XCTAssertTrue([self isExperimentID:@"exp_4" variantID:@"v1" inExperiments:experiments]); XCTAssertTrue([self isExperimentID:@"exp_5" variantID:@"v1" inExperiments:experiments]); } - (void)testSetExperimentWithTheSameVariantID { ABTExperimentPayload *payload = [[ABTExperimentPayload alloc] init]; payload.experimentId = @"exp_1"; payload.variantId = @"v1"; FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init]; [_ABTCUPController setExperimentWithOrigin:gABTTestOrigin payload:payload events:events policy:ABTExperimentPayload_ExperimentOverflowPolicy_IgnoreNewest]; NSArray *experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin]; XCTAssertEqual(experiments.count, 1); XCTAssertTrue([self isExperimentID:@"exp_1" variantID:@"v1" inExperiments:experiments]); payload.experimentId = @"exp_1"; payload.variantId = @"v2"; [_ABTCUPController setExperimentWithOrigin:gABTTestOrigin payload:payload events:events policy:ABTExperimentPayload_ExperimentOverflowPolicy_IgnoreNewest]; experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin]; XCTAssertEqual(experiments.count, 1); XCTAssertTrue([self isExperimentID:@"exp_1" variantID:@"v2" inExperiments:experiments]); } - (BOOL)isExperimentID:(NSString *)experimentID variantID:(NSString *)variantID inExperiments:(NSArray *)experiments { for (NSDictionary *experiment in experiments) { if ([experiment[@"name"] isEqualToString:experimentID] && [experiment[@"value"] isEqualToString:variantID]) { return YES; } } return NO; } - (void)testClearExperiment { ABTExperimentPayload *payload = [[ABTExperimentPayload alloc] init]; payload.experimentId = @"exp_1"; payload.variantId = @"v1"; // TODO(chliang) to check this name is logged in scion. payload.clearEventToLog = @"override_clear_event"; FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init]; [_ABTCUPController setExperimentWithOrigin:gABTTestOrigin payload:payload events:events policy:ABTExperimentPayload_ExperimentOverflowPolicy_IgnoreNewest]; NSArray *experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin]; XCTAssertEqual(experiments.count, 1); [_ABTCUPController clearExperiment:@"exp_1" variantID:@"v1" withOrigin:gABTTestOrigin payload:payload events:events]; experiments = [_ABTCUPController experimentsWithOrigin:gABTTestOrigin]; XCTAssertEqual(experiments.count, 0); } - (void)testMaxNumberOfExperiments { XCTAssertEqual([_ABTCUPController maxNumberOfExperimentsOfOrigin:gABTTestOrigin], 3); } - (void)testCreateExperiment { ABTExperimentPayload *payload = [[ABTExperimentPayload alloc] init]; payload.experimentId = @"exp_1"; payload.variantId = @"variant_B"; NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; payload.experimentStartTimeMillis = now * ABT_MSEC_PER_SEC; payload.triggerEvent = @""; int64_t triggerTimeout = now + 1500; payload.triggerTimeoutMillis = triggerTimeout * ABT_MSEC_PER_SEC; payload.timeToLiveMillis = (now + 60000) * ABT_MSEC_PER_SEC; FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init]; events.activateExperimentEventName = @"_lifecycle_override_activate"; events.expireExperimentEventName = @"lifecycle_override_time_to_live"; NSDictionary *experiment = [_ABTCUPController createExperimentFromOrigin:gABTTestOrigin payload:payload events:events]; NSDictionary *triggeredEvent = [experiment objectForKey:@"triggeredEvent"]; XCTAssertEqualObjects([experiment objectForKey:@"name"], @"exp_1"); XCTAssertEqualObjects([experiment objectForKey:@"value"], @"variant_B"); XCTAssertEqualObjects(gABTTestOrigin, [experiment objectForKey:@"origin"]); XCTAssertEqualWithAccuracy( now, [(NSNumber *)[experiment objectForKey:@"creationTimestamp"] doubleValue], 1.0); // Trigger event XCTAssertEqualObjects(gABTTestOrigin, triggeredEvent[@"origin"]); XCTAssertEqualObjects(triggeredEvent[@"name"], @"_lifecycle_override_activate", @"Activate event name is overrided by lifecycle events."); // Timeout event NSDictionary *timedOutEvent = [experiment objectForKey:@"timedOutEvent"]; XCTAssertEqualObjects(gABTTestOrigin, timedOutEvent[@"origin"]); XCTAssertEqualObjects(FIRTimeoutExperimentEventName, timedOutEvent[@"name"], @"payload doesn't have timeout event name, use default one"); // Expired event NSDictionary *expiredEvent = [experiment objectForKey:@"expiredEvent"]; XCTAssertEqualObjects(gABTTestOrigin, expiredEvent[@"origin"]); XCTAssertEqualObjects( @"lifecycle_override_time_to_live", expiredEvent[@"name"], @"payload doesn't have expiry event name, but lifecycle event does, use lifecycle event"); // Trigger event name XCTAssertEqualObjects(nil, [experiment objectForKey:@"triggerEventName"], @"Empty trigger event must be set to nil"); // trigger timeout XCTAssertEqualWithAccuracy( now + 1500, [(NSNumber *)([experiment objectForKey:@"triggerTimeout"]) doubleValue], 1.0); // time to live XCTAssertEqualWithAccuracy( now + 60000, [(NSNumber *)[experiment objectForKey:@"timeToLive"] doubleValue], 1.0); // Overwrite all event names payload.activateEventToLog = @"payload_override_activate"; payload.ttlExpiryEventToLog = @"payload_override_time_to_live"; payload.timeoutEventToLog = @"payload_override_timeout"; payload.triggerEvent = @"payload_override_trigger_event"; experiment = [_ABTCUPController createExperimentFromOrigin:gABTTestOrigin payload:payload events:events]; triggeredEvent = [experiment objectForKey:@"triggeredEvent"]; XCTAssertEqual(triggeredEvent[@"name"], @"payload_override_activate"); timedOutEvent = [experiment objectForKey:@"timedOutEvent"]; XCTAssertEqualObjects(timedOutEvent[@"name"], @"payload_override_timeout"); expiredEvent = [experiment objectForKey:@"expiredEvent"]; XCTAssertEqual(expiredEvent[@"name"], @"payload_override_time_to_live"); XCTAssertEqual([experiment objectForKey:@"triggerEventName"], @"payload_override_trigger_event"); } #pragma mark - helpers - (void)testIsExperimentTheSameAsPayload { NSDictionary *experiment = @{@"name" : @"exp_1", @"value" : @"variant_control_group"}; ABTExperimentPayload *payload = [[ABTExperimentPayload alloc] init]; payload.experimentId = @"exp_2"; payload.variantId = @"variant_group_A"; XCTAssertFalse([_ABTCUPController isExperiment:experiment theSameAsPayload:payload]); payload.experimentId = @"exp_1"; XCTAssertFalse([_ABTCUPController isExperiment:experiment theSameAsPayload:payload]); payload.variantId = @"variant_control_group"; XCTAssertTrue([_ABTCUPController isExperiment:experiment theSameAsPayload:payload]); } - (void)testOverflowPolicyWithPayload { ABTExperimentPayload *payload = [[ABTExperimentPayload alloc] init]; XCTAssertEqual(ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest, [_ABTCUPController overflowPolicyWithPayload:payload originalPolicy:-1000], @"Payload policy is unspecified, original policy is invalid, should return " @"default: DiscardOldest."); XCTAssertEqual( ABTExperimentPayload_ExperimentOverflowPolicy_IgnoreNewest, [_ABTCUPController overflowPolicyWithPayload:payload originalPolicy:ABTExperimentPayload_ExperimentOverflowPolicy_IgnoreNewest], @"Payload policy is unspecified, original policy is valid, use " @"original policy."); payload.overflowPolicy = ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest; XCTAssertEqual( ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest, [_ABTCUPController overflowPolicyWithPayload:payload originalPolicy:ABTExperimentPayload_ExperimentOverflowPolicy_IgnoreNewest], @"Payload policy is specified, original policy is valid, but " @"use Payload because Payload always wins."); } @end