2 // SecExperimentTests.m
6 #import <XCTest/XCTest.h>
7 #import <OCMock/OCMock.h>
8 #import <CoreFoundation/CFXPCBridge.h>
10 #import <Security/SecProtocolPriv.h>
11 #import "SecExperimentInternal.h"
12 #import "SecExperimentPriv.h"
14 @interface SecExperimentTests : XCTestCase
17 @implementation SecExperimentTests
19 - (NSDictionary *)copyRandomConfiguration
21 const char *testKey = "test_defaults_experiment_key";
22 const char *testValue = "test_value";
23 NSString *testKeyString = [NSString stringWithUTF8String:testKey];
24 NSString *testValueString = [NSString stringWithUTF8String:testValue];
25 NSDictionary *testConfigData = @{testKeyString: testValueString};
26 NSDictionary *testConfig = @{SecExperimentConfigurationKeyFleetSampleRate : @(100),
27 SecExperimentConfigurationKeyDeviceSampleRate : @(100),
28 SecExperimentConfigurationKeyExperimentIdentifier : @"identifier",
29 SecExperimentConfigurationKeyConfigurationData : testConfigData};
33 - (void)testCopyFromDefaults {
34 const char *testKey = "test_defaults_experiment_key";
35 const char *testValue = "test_value";
36 NSString *testKeyString = [NSString stringWithUTF8String:testKey];
37 NSString *testValueString = [NSString stringWithUTF8String:testValue];
38 NSDictionary *testConfigData = @{testKeyString: testValueString};
39 NSDictionary *testConfig = @{SecExperimentConfigurationKeyFleetSampleRate : @(100),
40 SecExperimentConfigurationKeyDeviceSampleRate : @(100),
41 SecExperimentConfigurationKeyExperimentIdentifier : @"identifier",
42 SecExperimentConfigurationKeyConfigurationData : testConfigData};
44 SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
45 OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(testConfig);
46 sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);
48 sec_experiment_set_sampling_disabled(experiment, true);
49 xpc_object_t actualConfig = sec_experiment_copy_configuration(experiment);
50 XCTAssertNotNil(actualConfig);
52 xpc_type_t configType = xpc_get_type(actualConfig);
53 XCTAssertTrue(configType == XPC_TYPE_DICTIONARY);
55 const char *actualValue = xpc_dictionary_get_string(actualConfig, testKey);
56 XCTAssertTrue(actualValue != NULL && strncmp(actualValue, testValue, strlen(testValue)) == 0);
59 - (void)testInitializeWithIdentifier {
60 sec_experiment_t experiment = sec_experiment_create(kSecExperimentTLSProbe);
61 const char *identifier = sec_experiment_get_identifier(experiment);
62 XCTAssertTrue(identifier == NULL);
65 - (void)testExperimentRunAsynchronously_SkipSampling {
66 dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
68 __block dispatch_semaphore_t blocker = dispatch_semaphore_create(0);
69 __block bool experiment_invoked = false;
70 sec_experiment_run_block_t run_block = ^bool(const char *identifier, xpc_object_t config) {
71 experiment_invoked = identifier != NULL && config != NULL;
72 dispatch_semaphore_signal(blocker);
73 return experiment_invoked;
76 NSDictionary *testConfig = [self copyRandomConfiguration];
78 SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
79 OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(testConfig);
80 sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);
82 sec_experiment_run_internal(experiment, true, test_queue, run_block, nil, false);
83 XCTAssertTrue(dispatch_semaphore_wait(blocker, dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(5 * NSEC_PER_SEC))) == 0L);
84 XCTAssertTrue(experiment_invoked);
87 - (void)testExperimentRun_SkipWithoutConfig {
88 SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
89 OCMStub([mockExperiment experimentIsAllowedForProcess]).andReturn(YES);
90 OCMStub([mockExperiment copyRemoteExperimentAsset]).andReturn(nil);
91 OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(nil);
92 sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);
94 dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
95 __block bool run = false;
96 sec_experiment_run_block_t run_block = ^bool(__unused const char *identifier, __unused xpc_object_t config) {
100 __block BOOL skipped = false;
101 sec_experiment_skip_block_t skip_block = ^(__unused const char *identifier) {
105 bool result = sec_experiment_run_internal(experiment, true, test_queue, run_block, skip_block, true);
106 XCTAssertTrue(result);
107 XCTAssertTrue(skipped);
111 - (void)testExperimentRunSuccess {
112 NSDictionary *testConfig = [self copyRandomConfiguration];
114 SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
115 OCMStub([mockExperiment isSamplingDisabled]).andReturn(YES);
116 OCMStub([mockExperiment experimentIsAllowedForProcess]).andReturn(YES);
117 OCMStub([mockExperiment copyRemoteExperimentAsset]).andReturn(testConfig);
118 OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(nil);
119 OCMStub([mockExperiment copyRandomExperimentConfigurationFromAsset:[OCMArg any]]).andReturn(testConfig);
120 sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);
122 dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
123 __block bool run = false;
124 sec_experiment_run_block_t run_block = ^bool(__unused const char *identifier, __unused xpc_object_t config) {
126 return true; // Signal that the experiment run was successful
128 __block BOOL skipped = false;
129 sec_experiment_skip_block_t skip_block = ^(__unused const char *identifier) {
133 bool result = sec_experiment_run_internal(experiment, true, test_queue, run_block, skip_block, true);
134 XCTAssertTrue(result);
135 XCTAssertFalse(skipped);
137 XCTAssertTrue(sec_experiment_get_run_count(experiment) == 1);
138 XCTAssertTrue(sec_experiment_get_successful_run_count(experiment) == 1);
141 - (void)testExperimentRunFailure {
142 NSDictionary *testConfig = [self copyRandomConfiguration];
144 SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
145 OCMStub([mockExperiment experimentIsAllowedForProcess]).andReturn(YES);
146 OCMStub([mockExperiment isSamplingDisabled]).andReturn(YES);
147 OCMStub([mockExperiment copyRemoteExperimentAsset]).andReturn(testConfig);
148 OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(nil);
149 OCMStub([mockExperiment copyRandomExperimentConfigurationFromAsset:[OCMArg any]]).andReturn(testConfig);
150 sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);
152 dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
153 __block bool run = false;
154 sec_experiment_run_block_t run_block = ^bool(__unused const char *identifier, __unused xpc_object_t config) {
156 return false; // Signal that the experiment run failed
158 __block BOOL skipped = false;
159 sec_experiment_skip_block_t skip_block = ^(__unused const char *identifier) {
163 bool result = sec_experiment_run_internal(experiment, true, test_queue, run_block, skip_block, true);
164 XCTAssertTrue(result);
165 XCTAssertFalse(skipped);
167 XCTAssertTrue(sec_experiment_get_run_count(experiment) == 1);
168 XCTAssertTrue(sec_experiment_get_successful_run_count(experiment) == 0);
171 - (void)testExperimentRun_SamplingEnabled_NotInFleet {
172 size_t fleetNumber = 10;
173 NSDictionary *testConfig = @{SecExperimentConfigurationKeyFleetSampleRate: @(fleetNumber), SecExperimentConfigurationKeyDeviceSampleRate : @(2)};
175 SecExperimentConfig *mockConfig = OCMPartialMock([[SecExperimentConfig alloc] initWithConfiguration:testConfig]);
176 OCMStub([mockConfig hostHash]).andReturn(fleetNumber + 1); // Ensure that fleetNumber < hostHash
178 SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
179 OCMStub([mockExperiment experimentIsAllowedForProcess]).andReturn(YES);
180 OCMStub([mockExperiment isSamplingDisabled]).andReturn(NO);
181 OCMStub([mockExperiment copyExperimentConfiguration]).andReturn(mockConfig);
183 sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);
185 dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
186 __block bool run = false;
187 sec_experiment_run_block_t run_block = ^bool(__unused const char *identifier, __unused xpc_object_t config) {
189 return false; // Signal that the experiment run failed
191 __block BOOL skipped = false;
192 sec_experiment_skip_block_t skip_block = ^(__unused const char *identifier) {
196 bool result = sec_experiment_run_internal(experiment, true, test_queue, run_block, skip_block, true);
197 XCTAssertTrue(result);
198 XCTAssertTrue(skipped);
200 XCTAssertTrue(sec_experiment_get_run_count(experiment) == 0);
201 XCTAssertTrue(sec_experiment_get_successful_run_count(experiment) == 0);
204 - (void)testExperimentRun_SamplingEnabled_InFleetNotInSample {
205 size_t fleetNumber = 10;
206 NSDictionary *testConfig = @{SecExperimentConfigurationKeyFleetSampleRate: @(fleetNumber), SecExperimentConfigurationKeyDeviceSampleRate : @(2)};
208 SecExperimentConfig *mockConfig = OCMPartialMock([[SecExperimentConfig alloc] initWithConfiguration:testConfig]);
209 OCMStub([mockConfig hostHash]).andReturn(fleetNumber - 1); // Ensure that fleetNumber > hostHash
210 OCMStub([mockConfig shouldRunWithSamplingRate:[OCMArg any]]).andReturn(NO); // Determine that we're not in the fleet
212 SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
213 OCMStub([mockExperiment experimentIsAllowedForProcess]).andReturn(YES);
214 OCMStub([mockExperiment isSamplingDisabled]).andReturn(NO);
215 OCMStub([mockExperiment copyExperimentConfiguration]).andReturn(mockConfig);
217 sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);
219 dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
220 __block bool run = false;
221 sec_experiment_run_block_t run_block = ^bool(__unused const char *identifier, __unused xpc_object_t config) {
223 return false; // Signal that the experiment run failed
225 __block BOOL skipped = false;
226 sec_experiment_skip_block_t skip_block = ^(__unused const char *identifier) {
230 bool result = sec_experiment_run_internal(experiment, true, test_queue, run_block, skip_block, true);
231 XCTAssertTrue(result);
232 XCTAssertTrue(skipped);
234 XCTAssertTrue(sec_experiment_get_run_count(experiment) == 0);
235 XCTAssertTrue(sec_experiment_get_successful_run_count(experiment) == 0);
238 - (void)testExperimentRun_DisallowedProcess {
239 SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
240 OCMStub([mockExperiment experimentIsAllowedForProcess]).andReturn(NO);
241 sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);
243 dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
244 sec_experiment_run_block_t run_block = ^bool(__unused const char *identifier, __unused xpc_object_t config) {
248 bool result = sec_experiment_run_internal(experiment, true, test_queue, run_block, nil, false);
249 XCTAssertFalse(result);
252 - (void)testExperimentRunSynchronously_SkipSampling {
253 dispatch_queue_t test_queue = dispatch_queue_create("test_queue", NULL);
255 __block bool experiment_invoked = false;
256 sec_experiment_run_block_t run_block = ^bool(const char *identifier, xpc_object_t config) {
257 experiment_invoked = identifier != NULL && config != NULL;
258 return experiment_invoked;
261 NSDictionary *testConfig = [self copyRandomConfiguration];
263 SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:kSecExperimentTLSProbe]);
264 OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(testConfig);
265 sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);
267 sec_experiment_run_internal(experiment, true, test_queue, run_block, nil, true);
268 XCTAssertTrue(experiment_invoked);
271 - (void)testDefaultsConfigCopy {
272 const char *testKey = "test_defaults_experiment_key";
273 const char *testValue = "test_value";
274 NSString *testKeyString = [NSString stringWithUTF8String:testKey];
275 NSString *testValueString = [NSString stringWithUTF8String:testValue];
276 NSDictionary *testConfigData = @{testKeyString: testValueString};
277 NSString *experimentName = @"TestExperiment";
278 NSDictionary *testConfig = @{experimentName: @{SecExperimentConfigurationKeyFleetSampleRate : @(100),
279 SecExperimentConfigurationKeyDeviceSampleRate : @(100),
280 SecExperimentConfigurationKeyExperimentIdentifier : @"identifier",
281 SecExperimentConfigurationKeyConfigurationData : testConfigData}};
283 sec_experiment_t experiment = sec_experiment_create([experimentName UTF8String]);
284 XCTAssert(experiment, @"sec_experiment_create");
286 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
287 [defaults setPersistentDomain:testConfig forName:[NSString stringWithUTF8String:kSecExperimentDefaultsDomain]];
289 sec_experiment_set_sampling_disabled(experiment, true);
290 xpc_object_t tlsconfig = sec_experiment_copy_configuration(experiment);
291 XCTAssertNotNil(tlsconfig);
293 XCTAssertTrue(xpc_get_type(tlsconfig) == XPC_TYPE_DICTIONARY);
294 if (xpc_get_type(tlsconfig) == XPC_TYPE_DICTIONARY) {
295 const char *actualValue = xpc_dictionary_get_string(tlsconfig, testKey);
296 XCTAssertTrue(actualValue != NULL);
297 if (actualValue != NULL) {
298 XCTAssertTrue(strcmp(actualValue, testValue) == 0);
303 // Clear the persistent domain
304 [defaults removePersistentDomainForName:[NSString stringWithUTF8String:kSecExperimentDefaultsDomain]];
307 - (void)testRunWithIdentifier {
308 const char *testKey = "test_defaults_experiment_key";
309 const char *testValue = "test_value";
310 NSString *testKeyString = [NSString stringWithUTF8String:testKey];
311 NSString *testValueString = [NSString stringWithUTF8String:testValue];
312 NSDictionary *testConfigData = @{testKeyString: testValueString};
313 NSString *experimentName = @"TestExperiment";
314 NSString *identifierName = @"ExperimentIdentifier";
315 NSDictionary *testConfig = @{experimentName: @{SecExperimentConfigurationKeyFleetSampleRate : @(100),
316 SecExperimentConfigurationKeyDeviceSampleRate : @(100),
317 SecExperimentConfigurationKeyExperimentIdentifier : identifierName,
318 SecExperimentConfigurationKeyConfigurationData : testConfigData}};
320 SecExperiment *mockExperiment = OCMPartialMock([[SecExperiment alloc] initWithName:[experimentName UTF8String]]);
321 OCMStub([mockExperiment copyExperimentConfigurationFromUserDefaults]).andReturn(testConfig);
322 sec_experiment_t experiment = sec_experiment_create_with_inner_experiment(mockExperiment);
324 sec_experiment_run_internal(experiment, true, nil, ^bool(const char * _Nonnull identifier, xpc_object_t _Nonnull experiment_config) {
325 XCTAssertTrue(identifier != NULL);
326 if (identifier != NULL) {
327 XCTAssertTrue(strcmp([identifierName UTF8String], identifier) == 0);
329 }, ^(const char * _Nonnull identifier) {
330 XCTAssertFalse(true);
334 - (void)testGeneration {
335 nw_protocol_options_t tls_options = nw_tls_create_options();
336 sec_protocol_options_t sec_options = nw_tls_copy_sec_protocol_options(tls_options);
338 sec_protocol_options_set_tls_grease_enabled(sec_options, true);
339 xpc_object_t config = sec_protocol_options_create_config(sec_options);
341 xpc_dictionary_apply(config, ^bool(const char * _Nonnull key, xpc_object_t _Nonnull value) {
342 if (xpc_get_type(value) == XPC_TYPE_BOOL) {
343 NSLog(@"%s -> %d", key, xpc_bool_get_value(value));
344 } else if (xpc_get_type(value) == XPC_TYPE_UINT64) {
345 NSLog(@"%s -> %zu", key, (size_t)xpc_uint64_get_value(value));
347 NSLog(@"%s -> %d", key, (int)xpc_get_type(value));
353 CFDictionaryRef config_dictionary = _CFXPCCreateCFObjectFromXPCObject(config);
354 XCTAssertTrue(config_dictionary != NULL);
355 NSLog(@"%@", (__bridge NSDictionary *)config_dictionary);