SecExperimentTests.m   [plain text]

//
// SecExperimentTests.m
//
//

#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import <CoreFoundation/CFXPCBridge.h>
#import <nw/private.h>
#import <Security/SecProtocolPriv.h>
#import "SecExperimentInternal.h"
#import "SecExperimentPriv.h"

@interface SecExperimentTests : XCTestCase
@end

@implementation SecExperimentTests

- (NSDictionary *)copyRandomConfiguration
{
const char *testKey = "test_defaults_experiment_key";
const char *testValue = "test_value";
NSString *testKeyString = [NSString stringWithUTF8String:testKey];
NSString *testValueString = [NSString stringWithUTF8String:testValue];
NSDictionary *testConfigData = @{testKeyString: testValueString};
NSDictionary *testConfig = @{SecExperimentConfigurationKeyFleetSampleRate : @(100),
SecExperimentConfigurationKeyDeviceSampleRate : @(100),
SecExperimentConfigurationKeyExperimentIdentifier : @"identifier",
SecExperimentConfigurationKeyConfigurationData : testConfigData};
return testConfig;
}

- (void)testCopyFromDefaults {
const char *testKey = "test_defaults_experiment_key";
const char *testValue = "test_value";
NSString *testKeyString = [NSString stringWithUTF8String:testKey];
NSString *testValueString = [NSString stringWithUTF8String:testValue];
NSDictionary *testConfigData = @{testKeyString: testValueString};
NSDictionary *testConfig = @{SecExperimentConfigurationKeyFleetSampleRate : @(100),
SecExperimentConfigurationKeyDeviceSampleRate : @(100),
SecExperimentConfigurationKeyExperimentIdentifier : @"identifier",
SecExperimentConfigurationKeyConfigurationData : testConfigData};

SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(testConfig);
sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);

sec_experiment_set_sampling_disabled(experiment, true);
xpc_object_t actualConfig = sec_experiment_copy_configuration(experiment);
XCTAssertNotNil(actualConfig);

xpc_type_t configType = xpc_get_type(actualConfig);
XCTAssertTrue(configType == XPC_TYPE_DICTIONARY);

const char *actualValue = xpc_dictionary_get_string(actualConfig, testKey);
XCTAssertTrue(actualValue != NULL && strncmp(actualValue, testValue, strlen(testValue)) == 0);
}

- (void)testInitializeWithIdentifier {
sec_experiment_t experiment = sec_experiment_create(kSecExperimentTLSProbe);
const char *identifier = sec_experiment_get_identifier(experiment);
XCTAssertTrue(identifier == NULL);
}

- (void)testExperimentRunAsynchronously_SkipSampling {
dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);

__block dispatch_semaphore_t blocker = dispatch_semaphore_create(0);
__block bool experiment_invoked = false;
sec_experiment_run_block_t run_block = ^bool(const char *identifier, xpc_object_t config) {
experiment_invoked = identifier != NULL && config != NULL;
dispatch_semaphore_signal(blocker);
return experiment_invoked;
};

NSDictionary *testConfig = [self copyRandomConfiguration];

SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(testConfig);
sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);

sec_experiment_run_internal(experiment, true, test_queue, run_block, nil, false);
XCTAssertTrue(dispatch_semaphore_wait(blocker, dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(5 * NSEC_PER_SEC))) == 0L);
XCTAssertTrue(experiment_invoked);
}

- (void)testExperimentRun_SkipWithoutConfig {
SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
OCMStub([mockExperiment experimentIsAllowedForProcess]).andReturn(YES);
OCMStub([mockExperiment copyRemoteExperimentAsset]).andReturn(nil);
OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(nil);
sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);

dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
__block bool run = false;
sec_experiment_run_block_t run_block = ^bool(__unused const char *identifier, __unused xpc_object_t config) {
run = true;
return true;
};
__block BOOL skipped = false;
sec_experiment_skip_block_t skip_block = ^(__unused const char *identifier) {
skipped = true;
};

bool result = sec_experiment_run_internal(experiment, true, test_queue, run_block, skip_block, true);
XCTAssertTrue(result);
XCTAssertTrue(skipped);
XCTAssertFalse(run);
}

- (void)testExperimentRunSuccess {
NSDictionary *testConfig = [self copyRandomConfiguration];

SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
OCMStub([mockExperiment isSamplingDisabled]).andReturn(YES);
OCMStub([mockExperiment experimentIsAllowedForProcess]).andReturn(YES);
OCMStub([mockExperiment copyRemoteExperimentAsset]).andReturn(testConfig);
OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(nil);
OCMStub([mockExperiment copyRandomExperimentConfigurationFromAsset:[OCMArg any]]).andReturn(testConfig);
sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);

dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
__block bool run = false;
sec_experiment_run_block_t run_block = ^bool(__unused const char *identifier, __unused xpc_object_t config) {
run = true;
return true; // Signal that the experiment run was successful
};
__block BOOL skipped = false;
sec_experiment_skip_block_t skip_block = ^(__unused const char *identifier) {
skipped = true;
};

bool result = sec_experiment_run_internal(experiment, true, test_queue, run_block, skip_block, true);
XCTAssertTrue(result);
XCTAssertFalse(skipped);
XCTAssertTrue(run);
XCTAssertTrue(sec_experiment_get_run_count(experiment) == 1);
XCTAssertTrue(sec_experiment_get_successful_run_count(experiment) == 1);
}

- (void)testExperimentRunFailure {
NSDictionary *testConfig = [self copyRandomConfiguration];

SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
OCMStub([mockExperiment experimentIsAllowedForProcess]).andReturn(YES);
OCMStub([mockExperiment isSamplingDisabled]).andReturn(YES);
OCMStub([mockExperiment copyRemoteExperimentAsset]).andReturn(testConfig);
OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(nil);
OCMStub([mockExperiment copyRandomExperimentConfigurationFromAsset:[OCMArg any]]).andReturn(testConfig);
sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);

dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
__block bool run = false;
sec_experiment_run_block_t run_block = ^bool(__unused const char *identifier, __unused xpc_object_t config) {
run = true;
return false; // Signal that the experiment run failed
};
__block BOOL skipped = false;
sec_experiment_skip_block_t skip_block = ^(__unused const char *identifier) {
skipped = true;
};

bool result = sec_experiment_run_internal(experiment, true, test_queue, run_block, skip_block, true);
XCTAssertTrue(result);
XCTAssertFalse(skipped);
XCTAssertTrue(run);
XCTAssertTrue(sec_experiment_get_run_count(experiment) == 1);
XCTAssertTrue(sec_experiment_get_successful_run_count(experiment) == 0);
}

- (void)testExperimentRun_SamplingEnabled_NotInFleet {
size_t fleetNumber = 10;
NSDictionary *testConfig = @{SecExperimentConfigurationKeyFleetSampleRate: @(fleetNumber), SecExperimentConfigurationKeyDeviceSampleRate : @(2)};

SecExperimentConfig *mockConfig = OCMPartialMock([[SecExperimentConfig alloc] initWithConfiguration:testConfig]);
OCMStub([mockConfig hostHash]).andReturn(fleetNumber + 1); // Ensure that fleetNumber < hostHash

SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
OCMStub([mockExperiment experimentIsAllowedForProcess]).andReturn(YES);
OCMStub([mockExperiment isSamplingDisabled]).andReturn(NO);
OCMStub([mockExperiment copyExperimentConfiguration]).andReturn(mockConfig);

sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);

dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
__block bool run = false;
sec_experiment_run_block_t run_block = ^bool(__unused const char *identifier, __unused xpc_object_t config) {
run = true;
return false; // Signal that the experiment run failed
};
__block BOOL skipped = false;
sec_experiment_skip_block_t skip_block = ^(__unused const char *identifier) {
skipped = true;
};

bool result = sec_experiment_run_internal(experiment, true, test_queue, run_block, skip_block, true);
XCTAssertTrue(result);
XCTAssertTrue(skipped);
XCTAssertFalse(run);
XCTAssertTrue(sec_experiment_get_run_count(experiment) == 0);
XCTAssertTrue(sec_experiment_get_successful_run_count(experiment) == 0);
}

- (void)testExperimentRun_SamplingEnabled_InFleetNotInSample {
size_t fleetNumber = 10;
NSDictionary *testConfig = @{SecExperimentConfigurationKeyFleetSampleRate: @(fleetNumber), SecExperimentConfigurationKeyDeviceSampleRate : @(2)};

SecExperimentConfig *mockConfig = OCMPartialMock([[SecExperimentConfig alloc] initWithConfiguration:testConfig]);
OCMStub([mockConfig hostHash]).andReturn(fleetNumber - 1); // Ensure that fleetNumber > hostHash
OCMStub([mockConfig shouldRunWithSamplingRate:[OCMArg any]]).andReturn(NO); // Determine that we're not in the fleet

SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
OCMStub([mockExperiment experimentIsAllowedForProcess]).andReturn(YES);
OCMStub([mockExperiment isSamplingDisabled]).andReturn(NO);
OCMStub([mockExperiment copyExperimentConfiguration]).andReturn(mockConfig);

sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);

dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
__block bool run = false;
sec_experiment_run_block_t run_block = ^bool(__unused const char *identifier, __unused xpc_object_t config) {
run = true;
return false; // Signal that the experiment run failed
};
__block BOOL skipped = false;
sec_experiment_skip_block_t skip_block = ^(__unused const char *identifier) {
skipped = true;
};

bool result = sec_experiment_run_internal(experiment, true, test_queue, run_block, skip_block, true);
XCTAssertTrue(result);
XCTAssertTrue(skipped);
XCTAssertFalse(run);
XCTAssertTrue(sec_experiment_get_run_count(experiment) == 0);
XCTAssertTrue(sec_experiment_get_successful_run_count(experiment) == 0);
}

- (void)testExperimentRun_DisallowedProcess {
SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
OCMStub([mockExperiment experimentIsAllowedForProcess]).andReturn(NO);
sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);

dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
sec_experiment_run_block_t run_block = ^bool(__unused const char *identifier, __unused xpc_object_t config) {
return true;
};

bool result = sec_experiment_run_internal(experiment, true, test_queue, run_block, nil, false);
XCTAssertFalse(result);
}

- (void)testExperimentRunSynchronously_SkipSampling {
dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);

__block bool experiment_invoked = false;
sec_experiment_run_block_t run_block = ^bool(const char *identifier, xpc_object_t config) {
experiment_invoked = identifier != NULL && config != NULL;
return experiment_invoked;
};

NSDictionary *testConfig = [self copyRandomConfiguration];

SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(testConfig);
sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);

sec_experiment_run_internal(experiment, true, test_queue, run_block, nil, true);
XCTAssertTrue(experiment_invoked);
}

- (void)testDefaultsConfigCopy {
const char *testKey = "test_defaults_experiment_key";
const char *testValue = "test_value";
NSString *testKeyString = [NSString stringWithUTF8String:testKey];
NSString *testValueString = [NSString stringWithUTF8String:testValue];
NSDictionary *testConfigData = @{testKeyString: testValueString};
NSString *experimentName = @"TestExperiment";
NSDictionary *testConfig = @{experimentName: @{SecExperimentConfigurationKeyFleetSampleRate : @(100),
SecExperimentConfigurationKeyDeviceSampleRate : @(100),
SecExperimentConfigurationKeyExperimentIdentifier : @"identifier",
SecExperimentConfigurationKeyConfigurationData : testConfigData}};

sec_experiment_t experiment = sec_experiment_create([experimentName UTF8String]);
XCTAssert(experiment, @"sec_experiment_create");

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setPersistentDomain:testConfig forName:[NSString stringWithUTF8String:kSecExperimentDefaultsDomain]];

sec_experiment_set_sampling_disabled(experiment, true);
xpc_object_t tlsconfig = sec_experiment_copy_configuration(experiment);
XCTAssertNotNil(tlsconfig);
if (tlsconfig) {
XCTAssertTrue(xpc_get_type(tlsconfig) == XPC_TYPE_DICTIONARY);
if (xpc_get_type(tlsconfig) == XPC_TYPE_DICTIONARY) {
const char *actualValue = xpc_dictionary_get_string(tlsconfig, testKey);
XCTAssertTrue(actualValue != NULL);
if (actualValue != NULL) {
XCTAssertTrue(strcmp(actualValue, testValue) == 0);
}
}
}

// Clear the persistent domain
[defaults removePersistentDomainForName:[NSString stringWithUTF8String:kSecExperimentDefaultsDomain]];
}

- (void)testRunWithIdentifier {
const char *testKey = "test_defaults_experiment_key";
const char *testValue = "test_value";
NSString *testKeyString = [NSString stringWithUTF8String:testKey];
NSString *testValueString = [NSString stringWithUTF8String:testValue];
NSDictionary *testConfigData = @{testKeyString: testValueString};
NSString *experimentName = @"TestExperiment";
NSString *identifierName = @"ExperimentIdentifier";
NSDictionary *testConfig = @{experimentName: @{SecExperimentConfigurationKeyFleetSampleRate : @(100),
SecExperimentConfigurationKeyDeviceSampleRate : @(100),
SecExperimentConfigurationKeyExperimentIdentifier : identifierName,
SecExperimentConfigurationKeyConfigurationData : testConfigData}};

SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:[experimentName UTF8String]]);
OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(testConfig);
sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);

sec_experiment_run_internal(experiment, true, nil, ^bool(const char * _Nonnull identifier, xpc_object_t _Nonnull experiment_config) {
XCTAssertTrue(identifier != NULL);
if (identifier != NULL) {
XCTAssertTrue(strcmp([identifierName UTF8String], identifier) == 0);
}
}, ^(const char * _Nonnull identifier) {
XCTAssertFalse(true);
}, true);
}

- (void)testGeneration {
nw_protocol_options_t tls_options = nw_tls_create_options();
sec_protocol_options_t sec_options = nw_tls_copy_sec_protocol_options(tls_options);

sec_protocol_options_set_tls_grease_enabled(sec_options, true);
xpc_object_t config = sec_protocol_options_create_config(sec_options);

xpc_dictionary_apply(config, ^bool(const char * _Nonnull key, xpc_object_t _Nonnull value) {
if (xpc_get_type(value) == XPC_TYPE_BOOL) {
NSLog(@"%s -> %d", key, xpc_bool_get_value(value));
} else if (xpc_get_type(value) == XPC_TYPE_UINT64) {
NSLog(@"%s -> %zu", key, (size_t)xpc_uint64_get_value(value));
} else {
NSLog(@"%s -> %d", key, (int)xpc_get_type(value));
}

return true;
});

CFDictionaryRef config_dictionary = _CFXPCCreateCFObjectFromXPCObject(config);
XCTAssertTrue(config_dictionary != NULL);
NSLog(@"%@", (__bridge NSDictionary *)config_dictionary);
}

@end