| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 |
- // 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 <OCMock/OCMock.h>
- #import <XCTest/XCTest.h>
- #import "FirebaseABTesting/Sources/ABTConditionalUserPropertyController.h"
- #import "FirebaseABTesting/Sources/ABTConstants.h"
- #import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h"
- #import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h"
- #import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRLifecycleEvents.h"
- #import "FirebaseABTesting/Tests/Unit/ABTFakeFIRAConditionalUserPropertyController.h"
- #import "FirebaseABTesting/Tests/Unit/ABTTestUniversalConstants.h"
- #import "FirebaseABTesting/Tests/Unit/Utilities/ABTTestUtilities.h"
- #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
- #import "Interop/Analytics/Public/FIRAnalyticsInterop.h"
- extern ABTExperimentPayload *ABTDeserializeExperimentPayload(NSData *payload);
- extern NSArray<ABTExperimentPayload *> *ABTExperimentsToSetFromPayloads(
- NSArray<NSData *> *payloads,
- NSArray<NSDictionary<NSString *, NSString *> *> *experiments,
- id<FIRAnalyticsInterop> _Nullable analytics);
- extern NSArray *ABTExperimentsToClearFromPayloads(
- NSArray<NSData *> *payloads,
- NSArray<NSDictionary<NSString *, NSString *> *> *experiments,
- id<FIRAnalyticsInterop> _Nullable analytics);
- @interface FIRExperimentController (ExposedForTest)
- - (void)
- updateExperimentConditionalUserPropertiesWithServiceOrigin:(NSString *)origin
- events:(FIRLifecycleEvents *)events
- policy:
- (ABTExperimentPayloadExperimentOverflowPolicy)
- policy
- lastStartTime:(NSTimeInterval)lastStartTime
- payloads:(NSArray<NSData *> *)payloads
- completionHandler:
- (nullable void (^)(NSError *_Nullable error))
- completionHandler;
- /// Surface internal initializer to avoid singleton usage during tests.
- - (instancetype)initWithAnalytics:(nullable id<FIRAnalyticsInterop>)analytics;
- @end
- @interface ABTConditionalUserPropertyController (ExposedForTest)
- - (void)maxNumberOfExperimentsOfOrigin:(NSString *)origin
- completionHandler:(void (^)(int32_t))completionHandler;
- - (int32_t)maxNumberOfExperimentsOfOrigin:(NSString *)origin;
- - (id)createExperimentFromOrigin:(NSString *)origin
- payload:(ABTExperimentPayload *)payload
- events:(FIRLifecycleEvents *)events;
- - (ABTExperimentPayloadExperimentOverflowPolicy)
- overflowPolicyWithPayload:(ABTExperimentPayload *)payload
- originalPolicy:(ABTExperimentPayloadExperimentOverflowPolicy)originalPolicy;
- @end
- @interface FIRExperimentControllerTest : XCTestCase {
- FIRExperimentController *_experimentController;
- ABTFakeFIRAConditionalUserPropertyController *_fakeController;
- id _mockCUPController;
- }
- @end
- @implementation FIRExperimentControllerTest
- - (void)setUp {
- [super setUp];
- _fakeController = [ABTFakeFIRAConditionalUserPropertyController sharedInstance];
- id<FIRAnalyticsInterop> fakeAnalytics =
- [[FakeAnalytics alloc] initWithFakeController:_fakeController];
- _experimentController = [[FIRExperimentController alloc] initWithAnalytics:fakeAnalytics];
- ABTConditionalUserPropertyController *controller =
- [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:fakeAnalytics];
- _mockCUPController = OCMPartialMock(controller);
- OCMStub([_mockCUPController maxNumberOfExperimentsOfOrigin:[OCMArg any]]).andReturn(3);
- }
- - (void)tearDown {
- [_fakeController resetExperiments];
- [_mockCUPController stopMocking];
- [super tearDown];
- }
- - (void)testDeserializeInvalidPayload {
- FIRExperimentController *controller = _experimentController;
- XCTAssertNotNil(controller);
- NSString *sampleString = @"sample_invalid_payload";
- NSData *invalidData = [sampleString dataUsingEncoding:NSUTF8StringEncoding];
- XCTAssertNil(ABTDeserializeExperimentPayload(invalidData));
- }
- - (void)testLifecycleEvents {
- FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
- XCTAssertEqualObjects(FIRSetExperimentEventName, events.setExperimentEventName);
- XCTAssertEqualObjects(FIRActivateExperimentEventName, events.activateExperimentEventName);
- XCTAssertEqualObjects(FIRTimeoutExperimentEventName, events.timeoutExperimentEventName);
- XCTAssertEqualObjects(FIRExpireExperimentEventName, events.expireExperimentEventName);
- XCTAssertEqualObjects(FIRClearExperimentEventName, events.clearExperimentEventName);
- // Should be able to override event name values.
- events.setExperimentEventName = @"_new_set_experiment";
- XCTAssertEqualObjects(events.setExperimentEventName, @"_new_set_experiment");
- events.setExperimentEventName = @"name_without_prefix";
- XCTAssertEqualObjects(FIRSetExperimentEventName, events.setExperimentEventName);
- events.activateExperimentEventName = @"_new_activate_experiment";
- XCTAssertEqualObjects(events.activateExperimentEventName, @"_new_activate_experiment");
- events.activateExperimentEventName = @"";
- XCTAssertEqualObjects(FIRActivateExperimentEventName, events.activateExperimentEventName);
- events.timeoutExperimentEventName = @"__";
- XCTAssertEqualObjects(events.timeoutExperimentEventName, @"__");
- events.timeoutExperimentEventName = @"name_with_";
- XCTAssertEqualObjects(FIRTimeoutExperimentEventName, events.timeoutExperimentEventName);
- events.expireExperimentEventName = @"_";
- XCTAssertEqualObjects(events.expireExperimentEventName, @"_");
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wnonnull"
- events.expireExperimentEventName = nil;
- #pragma clang diagnostic pop
- XCTAssertEqualObjects(FIRExpireExperimentEventName, events.expireExperimentEventName);
- events.clearExperimentEventName = @"_new_set_experiment";
- XCTAssertEqualObjects(events.clearExperimentEventName, @"_new_set_experiment");
- events.clearExperimentEventName = @"";
- XCTAssertEqualObjects(FIRClearExperimentEventName, events.clearExperimentEventName);
- }
- - (void)testSetExperimentWithBadPayload {
- [[_mockCUPController reject]
- setExperimentWithOrigin:[OCMArg any]
- payload:[OCMArg any]
- events:[OCMArg any]
- policy:ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest];
- NSString *sampleString = @"sample_invalid_payload";
- NSData *invalidData = [sampleString dataUsingEncoding:NSUTF8StringEncoding];
- XCTAssertNil(ABTDeserializeExperimentPayload(invalidData));
- }
- - (void)testUpdateExperiments {
- NSDate *now = [NSDate date];
- NSData *payload2Data =
- [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload2"
- modifiedStartTime:[now dateByAddingTimeInterval:1500]];
- NSData *payload3Data =
- [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload3"
- modifiedStartTime:[now dateByAddingTimeInterval:900]];
- NSData *payload4Data =
- [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload4"
- modifiedStartTime:[now dateByAddingTimeInterval:-900]];
- __block BOOL completionHandlerCalled = NO;
- FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
- NSArray *payloads = @[ payload2Data, payload3Data, payload4Data ];
- [_experimentController
- updateExperimentConditionalUserPropertiesWithServiceOrigin:gABTTestOrigin
- events:events
- policy:
- ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest // NOLINT
- lastStartTime:[now timeIntervalSince1970]
- payloads:payloads
- completionHandler:^(NSError *_Nullable error) {
- completionHandlerCalled = YES;
- }];
- XCTAssertEqual([_mockCUPController experimentsWithOrigin:gABTTestOrigin].count, 2);
- XCTAssertTrue(completionHandlerCalled);
- // Second time update exp_1 no longer exist, should be cleared from experiments.
- payloads = @[ payload3Data, payload4Data ];
- [_experimentController
- updateExperimentConditionalUserPropertiesWithServiceOrigin:gABTTestOrigin
- events:events
- policy:
- ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest // NOLINT
- lastStartTime:[now timeIntervalSince1970]
- payloads:payloads
- completionHandler:nil];
- XCTAssertEqual([_mockCUPController experimentsWithOrigin:gABTTestOrigin].count, 1);
- }
- - (void)testLatestExperimentStartTimestamps {
- // Mock incoming payloads
- NSMutableArray<NSData *> *payloads = [[NSMutableArray alloc] init];
- NSDate *now = [NSDate date];
- NSTimeInterval nowInterval = [now timeIntervalSince1970];
- NSData *payload2Data = [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload2"
- modifiedStartTime:now];
- [payloads addObject:payload2Data];
- NSData *payload3Data =
- [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload3"
- modifiedStartTime:[now dateByAddingTimeInterval:500]];
- [payloads addObject:payload3Data];
- NSString *sampleString = @"sample_invalid_payload";
- NSData *invalidPayload = [sampleString dataUsingEncoding:NSUTF8StringEncoding];
- [payloads addObject:invalidPayload];
- XCTAssertEqualWithAccuracy(
- [now timeIntervalSince1970] + 500,
- [_experimentController latestExperimentStartTimestampBetweenTimestamp:nowInterval + 200
- andPayloads:payloads],
- 1);
- XCTAssertEqualWithAccuracy(
- [now timeIntervalSince1970] + 1000,
- [_experimentController latestExperimentStartTimestampBetweenTimestamp:nowInterval + 1000
- andPayloads:payloads],
- 1);
- XCTAssertEqualWithAccuracy(
- [now timeIntervalSince1970] + 500,
- [_experimentController latestExperimentStartTimestampBetweenTimestamp:nowInterval - 10000
- andPayloads:payloads],
- 1);
- }
- - (void)testExperimentsToSetFromPayloads {
- // Mock conditional user property objects in experiments.
- NSMutableArray *currentExperiments = [[NSMutableArray alloc] init];
- NSDictionary<NSString *, NSString *> *CUP1 = @{@"name" : @"exp_1", @"value" : @"v1"};
- [currentExperiments addObject:CUP1];
- NSDictionary<NSString *, NSString *> *CUP2 = @{@"name" : @"exp_2", @"value" : @"v200"};
- [currentExperiments addObject:CUP2];
- NSMutableArray<NSData *> *payloads = [[NSMutableArray alloc] init];
- NSData *payload1Data = [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload1"
- modifiedStartTime:nil];
- [payloads addObject:payload1Data];
- NSData *payload2Data = [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload2"
- modifiedStartTime:nil];
- [payloads addObject:payload2Data];
- NSString *sampleString = @"sample_invalid_payload";
- NSData *invalidPayload = [sampleString dataUsingEncoding:NSUTF8StringEncoding];
- [payloads addObject:invalidPayload];
- NSArray<ABTExperimentPayload *> *experimentsToSet =
- ABTExperimentsToSetFromPayloads(payloads, currentExperiments, nil);
- XCTAssertEqual(experimentsToSet.count, 1);
- ABTExperimentPayload *payloadToAdd = experimentsToSet.firstObject;
- XCTAssertEqualObjects(payloadToAdd.experimentId, @"exp_1");
- XCTAssertEqualObjects(payloadToAdd.variantId, @"var_1");
- }
- - (void)testExperimentsToClearFromPayloads {
- // Mock conditional user property objects in experiments.
- NSMutableArray *currentExperiments = [[NSMutableArray alloc] init];
- NSDictionary<NSString *, NSString *> *CUP1 = @{@"name" : @"exp_1", @"value" : @"v1"};
- [currentExperiments addObject:CUP1];
- NSDictionary<NSString *, NSString *> *CUP2 = @{@"name" : @"exp_2", @"value" : @"v2"};
- [currentExperiments addObject:CUP2];
- NSMutableArray<NSData *> *payloads = [[NSMutableArray alloc] init];
- NSData *payload1Data = [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload4"
- modifiedStartTime:nil];
- [payloads addObject:payload1Data];
- NSData *payload2Data = [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload5"
- modifiedStartTime:nil];
- [payloads addObject:payload2Data];
- NSString *sampleString = @"sample_invalid_payload";
- NSData *invalidPayload = [sampleString dataUsingEncoding:NSUTF8StringEncoding];
- [payloads addObject:invalidPayload];
- NSArray<NSDictionary<NSString *, NSString *> *> *experimentsToClear =
- ABTExperimentsToClearFromPayloads(payloads, currentExperiments, nil);
- XCTAssertEqual(experimentsToClear.count, 1);
- NSDictionary<NSString *, NSString *> *experimentToRemove = experimentsToClear.firstObject;
- XCTAssertEqualObjects(experimentToRemove[@"name"], @"exp_1");
- XCTAssertEqualObjects(experimentToRemove[@"value"], @"v1");
- }
- - (void)testInvalidExperiments {
- [[_mockCUPController reject]
- setExperimentWithOrigin:[OCMArg any]
- payload:[OCMArg any]
- events:[OCMArg any]
- policy:ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest];
- [[_mockCUPController reject]
- setExperimentWithOrigin:[OCMArg any]
- payload:[OCMArg any]
- events:[OCMArg any]
- policy:ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest];
- OCMStub([_mockCUPController experimentsWithOrigin:gABTTestOrigin]).andReturn(nil);
- NSMutableArray<NSData *> *payloads = [[NSMutableArray alloc] init];
- __block BOOL completionHandlerWithErrorCalled = NO;
- FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
- [_experimentController
- updateExperimentConditionalUserPropertiesWithServiceOrigin:gABTTestOrigin
- events:events
- policy:
- ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest // NOLINT
- lastStartTime:-1
- payloads:payloads
- completionHandler:^(NSError *_Nullable error) {
- if (error &&
- error.code ==
- kABTInternalErrorFailedToFetchConditionalUserProperties) {
- completionHandlerWithErrorCalled = YES;
- }
- }];
- // Verify completion handler is still called.
- XCTAssertTrue(completionHandlerWithErrorCalled);
- }
- - (void)testValidateRunningExperimentsWithEmptyArray {
- NSDate *now = [NSDate date];
- NSData *payload2Data =
- [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload2"
- modifiedStartTime:[now dateByAddingTimeInterval:1500]];
- NSData *payload3Data =
- [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload3"
- modifiedStartTime:[now dateByAddingTimeInterval:900]];
- FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
- NSArray *payloads = @[ payload2Data, payload3Data ];
- [_experimentController
- updateExperimentConditionalUserPropertiesWithServiceOrigin:gABTTestOrigin
- events:events
- policy:
- ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest // NOLINT
- lastStartTime:[now timeIntervalSince1970]
- payloads:payloads
- completionHandler:nil];
- XCTAssertEqual([_mockCUPController experimentsWithOrigin:gABTTestOrigin].count, 2);
- [_experimentController validateRunningExperimentsForServiceOrigin:gABTTestOrigin
- runningExperimentPayloads:[NSArray array]];
- // Expect all experiments have been cleared.
- XCTAssertEqual([_mockCUPController experimentsWithOrigin:gABTTestOrigin].count, 0);
- }
- - (void)testValidateRunningExperimentsClearingOne {
- NSDate *now = [NSDate date];
- NSData *payload2Data =
- [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload2"
- modifiedStartTime:[now dateByAddingTimeInterval:1500]];
- NSData *payload3Data =
- [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload3"
- modifiedStartTime:[now dateByAddingTimeInterval:900]];
- FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
- NSArray *payloads = @[ payload2Data, payload3Data ];
- [_experimentController
- updateExperimentConditionalUserPropertiesWithServiceOrigin:gABTTestOrigin
- events:events
- policy:
- ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest // NOLINT
- lastStartTime:[now timeIntervalSince1970]
- payloads:payloads
- completionHandler:nil];
- XCTAssertEqual([_mockCUPController experimentsWithOrigin:gABTTestOrigin].count, 2);
- ABTExperimentPayload *validatingPayload2 =
- [ABTTestUtilities payloadFromTestFilename:@"TestABTPayload2"];
- [_experimentController validateRunningExperimentsForServiceOrigin:gABTTestOrigin
- runningExperimentPayloads:@[ validatingPayload2 ]];
- // Expect no experiments have been cleared.
- XCTAssertEqual([_mockCUPController experimentsWithOrigin:gABTTestOrigin].count, 1);
- }
- - (void)testValidateRunningExperimentsKeepingAll {
- NSDate *now = [NSDate date];
- NSData *payload2Data =
- [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload2"
- modifiedStartTime:[now dateByAddingTimeInterval:1500]];
- NSData *payload3Data =
- [ABTTestUtilities payloadJSONDataFromFile:@"TestABTPayload3"
- modifiedStartTime:[now dateByAddingTimeInterval:900]];
- FIRLifecycleEvents *events = [[FIRLifecycleEvents alloc] init];
- NSArray *payloads = @[ payload2Data, payload3Data ];
- [_experimentController
- updateExperimentConditionalUserPropertiesWithServiceOrigin:gABTTestOrigin
- events:events
- policy:
- ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest // NOLINT
- lastStartTime:[now timeIntervalSince1970]
- payloads:payloads
- completionHandler:nil];
- XCTAssertEqual([_mockCUPController experimentsWithOrigin:gABTTestOrigin].count, 2);
- ABTExperimentPayload *validatingPayload2 =
- [ABTTestUtilities payloadFromTestFilename:@"TestABTPayload2"];
- ABTExperimentPayload *validatingPayload3 =
- [ABTTestUtilities payloadFromTestFilename:@"TestABTPayload3"];
- [_experimentController
- validateRunningExperimentsForServiceOrigin:gABTTestOrigin
- runningExperimentPayloads:@[ validatingPayload2, validatingPayload3 ]];
- // Expect no experiments have been cleared.
- XCTAssertEqual([_mockCUPController experimentsWithOrigin:gABTTestOrigin].count, 2);
- }
- - (void)testActivateExperiment {
- ABTExperimentPayload *activeExperiment =
- [ABTTestUtilities payloadFromTestFilename:@"TestABTPayload1"];
- [_experimentController activateExperiment:activeExperiment forServiceOrigin:gABTTestOrigin];
- NSArray *experiments = [_mockCUPController experimentsWithOrigin:gABTTestOrigin];
- NSDictionary *userPropertyForExperiment = [experiments firstObject];
- // Verify that the triggerEventName is cleared, making this experiment active.
- XCTAssertNil([userPropertyForExperiment valueForKeyPath:@"triggerEventName"]);
- }
- @end
|