]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CloudKitMockXCTest.m
Security-58286.200.222.tar.gz
[apple/security.git] / keychain / ckks / tests / CloudKitMockXCTest.m
1 /*
2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #import "CloudKitMockXCTest.h"
27
28 #import <ApplePushService/ApplePushService.h>
29 #import <Foundation/Foundation.h>
30 #import <CloudKit/CloudKit.h>
31 #import <CloudKit/CloudKit_Private.h>
32 #import <CloudKit/CKContainer_Private.h>
33 #import <OCMock/OCMock.h>
34
35 #include "OSX/sec/securityd/Regressions/SecdTestKeychainUtilities.h"
36 #include <utilities/SecFileLocations.h>
37 #include <securityd/SecItemServer.h>
38
39 #if NO_SERVER
40 #include <securityd/spi.h>
41 #endif
42
43 #include <Security/SecureObjectSync/SOSViews.h>
44
45 #include <utilities/SecDb.h>
46 #include <securityd/SecItemServer.h>
47 #include <keychain/ckks/CKKS.h>
48 #include <keychain/ckks/CKKSViewManager.h>
49 #include <keychain/ckks/CKKSKeychainView.h>
50 #include <keychain/ckks/CKKSItem.h>
51 #include <keychain/ckks/CKKSOutgoingQueueEntry.h>
52 #include <keychain/ckks/CKKSKey.h>
53 #include "keychain/ckks/CKKSGroupOperation.h"
54 #include "keychain/ckks/CKKSLockStateTracker.h"
55 #include "keychain/ckks/CKKSReachabilityTracker.h"
56
57 #import "MockCloudKit.h"
58
59 @interface BoolHolder : NSObject
60 @property bool state;
61 @end
62
63 @implementation BoolHolder
64 @end
65
66 // Inform OCMock about the internals of CKContainer
67 @interface CKContainer ()
68 - (void)_checkSelfCloudServicesEntitlement;
69 @end
70
71
72 @implementation CloudKitMockXCTest
73
74 + (void)setUp {
75 // Turn on testing
76 SecCKKSTestsEnable();
77 SecCKKSSetReduceRateLimiting(true);
78 [super setUp];
79
80 #if NO_SERVER
81 securityd_init_local_spi();
82 #endif
83 }
84
85 - (void)setUp {
86 [super setUp];
87
88 NSString* testName = [self.name componentsSeparatedByString:@" "][1];
89 testName = [testName stringByReplacingOccurrencesOfString:@"]" withString:@""];
90 secnotice("ckkstest", "Beginning test %@", testName);
91
92 // All tests start with the same flag set.
93 SecCKKSTestResetFlags();
94 SecCKKSTestSetDisableSOS(true);
95
96 self.silentFetchesAllowed = true;
97 self.silentZoneDeletesAllowed = false; // Set to true if you want to do any deletes
98
99 __weak __typeof(self) weakSelf = self;
100 self.operationQueue = [[NSOperationQueue alloc] init];
101 self.operationQueue.maxConcurrentOperationCount = 1;
102
103 self.zones = [[NSMutableDictionary alloc] init];
104
105 self.apsEnvironment = @"fake APS push string";
106
107 self.mockDatabaseExceptionCatcher = OCMStrictClassMock([CKDatabase class]);
108 self.mockDatabase = OCMStrictClassMock([CKDatabase class]);
109 self.mockContainerExpectations = OCMStrictClassMock([CKContainer class]);
110 self.mockContainer = OCMClassMock([CKContainer class]);
111 OCMStub([self.mockContainer containerWithIdentifier:[OCMArg isKindOfClass:[NSString class]]]).andReturn(self.mockContainer);
112 OCMStub([self.mockContainer defaultContainer]).andReturn(self.mockContainer);
113 OCMStub([self.mockContainer alloc]).andReturn(self.mockContainer);
114 OCMStub([self.mockContainer containerIdentifier]).andReturn(SecCKKSContainerName);
115 OCMStub([self.mockContainer initWithContainerID: [OCMArg any] options: [OCMArg any]]).andReturn(self.mockContainer);
116 OCMStub([self.mockContainer privateCloudDatabase]).andReturn(self.mockDatabaseExceptionCatcher);
117 OCMStub([self.mockContainer serverPreferredPushEnvironmentWithCompletionHandler: ([OCMArg invokeBlockWithArgs:self.apsEnvironment, [NSNull null], nil])]);
118 OCMStub([self.mockContainer submitEventMetric:[OCMArg any]]).andCall(self, @selector(ckcontainerSubmitEventMetric:));
119
120 // Use two layers of mockDatabase here, so we can both add Expectations and catch the exception (instead of crash) when one fails.
121 OCMStub([self.mockDatabaseExceptionCatcher addOperation:[OCMArg any]]).andCall(self, @selector(ckdatabaseAddOperation:));
122
123 // If you want to change this, you'll need to update the mock
124 _ckDeviceID = [NSString stringWithFormat:@"fake-cloudkit-device-id-%@", testName];
125 OCMStub([self.mockContainer fetchCurrentDeviceIDWithCompletionHandler: ([OCMArg invokeBlockWithArgs:self.ckDeviceID, [NSNull null], nil])]);
126
127 self.accountStatus = CKAccountStatusAvailable;
128 self.supportsDeviceToDeviceEncryption = YES;
129 self.iCloudHasValidCredentials = YES;
130
131 // Inject a fake operation dependency so we won't respond with the CloudKit account status immediately
132 // The CKKSCKAccountStateTracker won't send any login/logout calls without that information, so this blocks all CKKS setup
133 self.ckaccountHoldOperation = [NSBlockOperation named:@"ckaccount-hold" withBlock:^{
134 secnotice("ckks", "CKKS CK account status test hold released");
135 }];
136
137 OCMStub([self.mockContainer accountStatusWithCompletionHandler:
138 [OCMArg checkWithBlock:^BOOL(void (^passedBlock) (CKAccountStatus accountStatus,
139 NSError * _Nullable error)) {
140
141 if(passedBlock) {
142 __strong __typeof(self) strongSelf = weakSelf;
143 NSBlockOperation* fulfillBlock = [NSBlockOperation named:@"account-status-completion" withBlock: ^{
144 passedBlock(weakSelf.accountStatus, nil);
145 }];
146 [fulfillBlock addDependency: strongSelf.ckaccountHoldOperation];
147 [strongSelf.operationQueue addOperation: fulfillBlock];
148
149 return YES;
150 }
151 return NO;
152 }]]);
153
154 OCMStub([self.mockContainer accountInfoWithCompletionHandler:
155 [OCMArg checkWithBlock:^BOOL(void (^passedBlock) (CKAccountInfo* accountInfo,
156 NSError * error)) {
157 __strong __typeof(self) strongSelf = weakSelf;
158 if(passedBlock && strongSelf) {
159 NSBlockOperation* fulfillBlock = [NSBlockOperation named:@"account-info-completion" withBlock: ^{
160 __strong __typeof(self) blockStrongSelf = weakSelf;
161 CKAccountInfo* account = [[CKAccountInfo alloc] init];
162 account.accountStatus = blockStrongSelf.accountStatus;
163 account.supportsDeviceToDeviceEncryption = blockStrongSelf.supportsDeviceToDeviceEncryption;
164 account.hasValidCredentials = blockStrongSelf.iCloudHasValidCredentials;
165 account.accountPartition = CKAccountPartitionTypeProduction;
166 passedBlock((CKAccountInfo*)account, nil);
167 }];
168 [fulfillBlock addDependency: strongSelf.ckaccountHoldOperation];
169 [strongSelf.operationQueue addOperation: fulfillBlock];
170
171 return YES;
172 }
173 return NO;
174 }]]);
175
176 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
177 self.mockAccountStateTracker = OCMClassMock([CKKSCKAccountStateTracker class]);
178 OCMStub([self.mockAccountStateTracker getCircleStatus]).andCall(self, @selector(circleStatus));
179
180 // If we're in circle, come up with a fake circle id. Otherwise, return an error.
181 self.circlePeerID = [NSString stringWithFormat:@"fake-circle-id-%@", testName];
182 OCMStub([self.mockAccountStateTracker fetchCirclePeerID:
183 [OCMArg checkWithBlock:^BOOL(void (^passedBlock) (NSString* peerID,
184 NSError * error)) {
185 __strong __typeof(self) strongSelf = weakSelf;
186 if(passedBlock && strongSelf) {
187 if(strongSelf.circleStatus.status == kSOSCCInCircle) {
188 passedBlock(strongSelf.circlePeerID, nil);
189 } else {
190 passedBlock(nil, [NSError errorWithDomain:@"securityd" code:errSecInternalError userInfo:@{NSLocalizedDescriptionKey:@"no account, no circle id"}]);
191 }
192
193 return YES;
194 }
195 return NO;
196 }]]);
197
198 self.aksLockState = false; // Lie and say AKS is always unlocked
199 self.mockLockStateTracker = OCMClassMock([CKKSLockStateTracker class]);
200 OCMStub([self.mockLockStateTracker queryAKSLocked]).andCall(self, @selector(aksLockState));
201
202 self.reachabilityFlags = kSCNetworkReachabilityFlagsReachable; // Lie and say network is available
203 self.mockReachabilityTracker = OCMClassMock([CKKSReachabilityTracker class]);
204 OCMStub([self.mockReachabilityTracker getReachabilityFlags:[OCMArg anyPointer]]).andCall(self, @selector(reachabilityFlags));
205
206 self.mockFakeCKModifyRecordZonesOperation = OCMClassMock([FakeCKModifyRecordZonesOperation class]);
207 OCMStub([self.mockFakeCKModifyRecordZonesOperation ckdb]).andReturn(self.zones);
208 OCMStub([self.mockFakeCKModifyRecordZonesOperation ensureZoneDeletionAllowed:[OCMArg any]]).andCall(self, @selector(ensureZoneDeletionAllowed:));
209
210 self.mockFakeCKModifySubscriptionsOperation = OCMClassMock([FakeCKModifySubscriptionsOperation class]);
211 OCMStub([self.mockFakeCKModifySubscriptionsOperation ckdb]).andReturn(self.zones);
212
213 self.mockFakeCKFetchRecordZoneChangesOperation = OCMClassMock([FakeCKFetchRecordZoneChangesOperation class]);
214 OCMStub([self.mockFakeCKFetchRecordZoneChangesOperation ckdb]).andReturn(self.zones);
215
216 self.mockFakeCKFetchRecordsOperation = OCMClassMock([FakeCKFetchRecordsOperation class]);
217 OCMStub([self.mockFakeCKFetchRecordsOperation ckdb]).andReturn(self.zones);
218
219 self.mockFakeCKQueryOperation = OCMClassMock([FakeCKQueryOperation class]);
220 OCMStub([self.mockFakeCKQueryOperation ckdb]).andReturn(self.zones);
221
222
223 OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
224 __strong __typeof(self) strongSelf = weakSelf;
225 BOOL matches = NO;
226 if ([obj isKindOfClass: [FakeCKFetchRecordZoneChangesOperation class]]) {
227 if(strongSelf.silentFetchesAllowed) {
228 matches = YES;
229
230 FakeCKFetchRecordZoneChangesOperation *frzco = (FakeCKFetchRecordZoneChangesOperation *)obj;
231 [frzco addNullableDependency:strongSelf.ckFetchHoldOperation];
232 [strongSelf.operationQueue addOperation: frzco];
233 }
234 }
235 return matches;
236 }]]);
237
238 OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
239 __strong __typeof(self) strongSelf = weakSelf;
240 BOOL matches = NO;
241 if ([obj isKindOfClass: [FakeCKFetchRecordsOperation class]]) {
242 if(strongSelf.silentFetchesAllowed) {
243 matches = YES;
244
245 FakeCKFetchRecordsOperation *ffro = (FakeCKFetchRecordsOperation *)obj;
246 [ffro addNullableDependency:strongSelf.ckFetchHoldOperation];
247 [strongSelf.operationQueue addOperation: ffro];
248 }
249 }
250 return matches;
251 }]]);
252
253 OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
254 __strong __typeof(self) strongSelf = weakSelf;
255 BOOL matches = NO;
256 if ([obj isKindOfClass: [FakeCKQueryOperation class]]) {
257 if(strongSelf.silentFetchesAllowed) {
258 matches = YES;
259
260 FakeCKQueryOperation *fqo = (FakeCKQueryOperation *)obj;
261 [fqo addNullableDependency:strongSelf.ckFetchHoldOperation];
262 [strongSelf.operationQueue addOperation: fqo];
263 }
264 }
265 return matches;
266 }]]);
267
268
269 self.testZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"testzone" ownerName:CKCurrentUserDefaultName];
270
271 // We don't want to use class mocks here, because they don't play well with partial mocks
272 self.mockCKKSViewManager = OCMPartialMock(
273 [[CKKSViewManager alloc] initWithContainerName:SecCKKSContainerName
274 usePCS:SecCKKSContainerUsePCS
275 fetchRecordZoneChangesOperationClass:[FakeCKFetchRecordZoneChangesOperation class]
276 fetchRecordsOperationClass:[FakeCKFetchRecordsOperation class]
277 queryOperationClass:[FakeCKQueryOperation class]
278 modifySubscriptionsOperationClass:[FakeCKModifySubscriptionsOperation class]
279 modifyRecordZonesOperationClass:[FakeCKModifyRecordZonesOperation class]
280 apsConnectionClass:[FakeAPSConnection class]
281 nsnotificationCenterClass:[FakeNSNotificationCenter class]
282 notifierClass:[FakeCKKSNotifier class]]);
283
284 OCMStub([self.mockCKKSViewManager viewList]).andCall(self, @selector(managedViewList));
285 OCMStub([self.mockCKKSViewManager syncBackupAndNotifyAboutSync]);
286
287 self.injectedManager = self.mockCKKSViewManager;
288
289 [CKKSViewManager resetManager:false setTo:self.injectedManager];
290
291 // Make a new fake keychain
292 NSString* tmp_dir = [NSString stringWithFormat: @"/tmp/%@.%X", testName, arc4random()];
293 [[NSFileManager defaultManager] createDirectoryAtPath:[NSString stringWithFormat: @"%@/Library/Keychains", tmp_dir] withIntermediateDirectories:YES attributes:nil error:NULL];
294
295 SetCustomHomeURLString((__bridge CFStringRef) tmp_dir);
296 SecKeychainDbReset(NULL);
297
298 // Actually load the database.
299 kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) { return false; });
300 }
301
302 - (void)ckcontainerSubmitEventMetric:(CKEventMetric*)metric {
303 @try {
304 [self.mockContainerExpectations submitEventMetric:metric];
305 } @catch (NSException *exception) {
306 XCTFail("Received an container exception when trying to add a metric: %@", exception);
307 }
308 }
309
310 - (void)ckdatabaseAddOperation:(NSOperation*)op {
311 @try {
312 [self.mockDatabase addOperation:op];
313 } @catch (NSException *exception) {
314 XCTFail("Received an database exception: %@", exception);
315 }
316 }
317
318
319 - (void)ensureZoneDeletionAllowed:(FakeCKZone*)zone {
320 XCTAssertTrue(self.silentZoneDeletesAllowed, "Should be allowing zone deletes");
321 }
322
323 -(CKKSCKAccountStateTracker*)accountStateTracker {
324 return self.injectedManager.accountTracker;
325 }
326
327 -(CKKSLockStateTracker*)lockStateTracker {
328 return self.injectedManager.lockStateTracker;
329 }
330
331 -(CKKSReachabilityTracker*)reachabilityTracker {
332 return self.injectedManager.reachabilityTracker;
333 }
334
335 -(NSSet*)managedViewList {
336 return (NSSet*) CFBridgingRelease(SOSViewCopyViewSet(kViewSetCKKS));
337 }
338
339 -(void)expectCKFetch {
340 [self expectCKFetchAndRunBeforeFinished: nil];
341 }
342
343 -(void)expectCKFetchAndRunBeforeFinished: (void (^)(void))blockAfterFetch {
344 // Create an object for the block to retain and modify
345 BoolHolder* runAlready = [[BoolHolder alloc] init];
346
347 __weak __typeof(self) weakSelf = self;
348 [[self.mockDatabase expect] addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
349 __strong __typeof(self) strongSelf = weakSelf;
350 if(runAlready.state) {
351 return NO;
352 }
353
354 secnotice("fakecloudkit", "Received an operation (%@), checking if it's a fetch changes", obj);
355 BOOL matches = NO;
356 if ([obj isKindOfClass: [FakeCKFetchRecordZoneChangesOperation class]]) {
357 matches = YES;
358 runAlready.state = true;
359
360 secnotice("fakecloudkit", "Running fetch changes: %@", obj);
361 FakeCKFetchRecordZoneChangesOperation *frzco = (FakeCKFetchRecordZoneChangesOperation *)obj;
362 frzco.blockAfterFetch = blockAfterFetch;
363 [frzco addNullableDependency: strongSelf.ckFetchHoldOperation];
364 [strongSelf.operationQueue addOperation: frzco];
365 }
366 return matches;
367 }]];
368 }
369
370 -(void)expectCKFetchByRecordID {
371 // Create an object for the block to retain and modify
372 BoolHolder* runAlready = [[BoolHolder alloc] init];
373
374 __weak __typeof(self) weakSelf = self;
375 [[self.mockDatabase expect] addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
376 __strong __typeof(self) strongSelf = weakSelf;
377 if(runAlready.state) {
378 return NO;
379 }
380 BOOL matches = NO;
381 if ([obj isKindOfClass: [FakeCKFetchRecordsOperation class]]) {
382 matches = YES;
383 runAlready.state = true;
384
385 FakeCKFetchRecordsOperation *ffro = (FakeCKFetchRecordsOperation *)obj;
386 [ffro addNullableDependency: strongSelf.ckFetchHoldOperation];
387 [strongSelf.operationQueue addOperation: ffro];
388 }
389 return matches;
390 }]];
391 }
392
393
394 -(void)expectCKFetchByQuery {
395 // Create an object for the block to retain and modify
396 BoolHolder* runAlready = [[BoolHolder alloc] init];
397
398 __weak __typeof(self) weakSelf = self;
399 [[self.mockDatabase expect] addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
400 __strong __typeof(self) strongSelf = weakSelf;
401 if(runAlready.state) {
402 return NO;
403 }
404 BOOL matches = NO;
405 if ([obj isKindOfClass: [FakeCKQueryOperation class]]) {
406 matches = YES;
407 runAlready.state = true;
408
409 FakeCKQueryOperation *fqo = (FakeCKQueryOperation *)obj;
410 [fqo addNullableDependency: strongSelf.ckFetchHoldOperation];
411 [strongSelf.operationQueue addOperation: fqo];
412 }
413 return matches;
414 }]];
415 }
416
417 - (void)startCKKSSubsystem {
418 [self startCKAccountStatusMock];
419 }
420
421 - (void)startCKAccountStatusMock {
422 // Note: currently, based on how we're mocking up the zone creation and zone subscription operation,
423 // they will 'fire' before this method is called. It's harmless, since the mocks immediately succeed
424 // and return; it's just a tad confusing.
425 if([self.ckaccountHoldOperation isPending]) {
426 [self.operationQueue addOperation: self.ckaccountHoldOperation];
427 }
428 }
429
430 -(void)holdCloudKitModifications {
431 XCTAssertFalse([self.ckModifyHoldOperation isPending], "Shouldn't already be a pending cloudkit modify hold operation");
432 self.ckModifyHoldOperation = [NSBlockOperation blockOperationWithBlock:^{
433 secnotice("ckks", "Released CloudKit modification hold.");
434 }];
435 }
436 -(void)releaseCloudKitModificationHold {
437 if([self.ckModifyHoldOperation isPending]) {
438 [self.operationQueue addOperation: self.ckModifyHoldOperation];
439 }
440 }
441
442 -(void)holdCloudKitFetches {
443 XCTAssertFalse([self.ckFetchHoldOperation isPending], "Shouldn't already be a pending cloudkit fetch hold operation");
444 self.ckFetchHoldOperation = [NSBlockOperation blockOperationWithBlock:^{
445 secnotice("ckks", "Released CloudKit fetch hold.");
446 }];
447 }
448 -(void)releaseCloudKitFetchHold {
449 if([self.ckFetchHoldOperation isPending]) {
450 [self.operationQueue addOperation: self.ckFetchHoldOperation];
451 }
452 }
453
454 - (void)expectCKModifyItemRecords: (NSUInteger) expectedNumberOfRecords currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords zoneID: (CKRecordZoneID*) zoneID {
455 [self expectCKModifyItemRecords:expectedNumberOfRecords
456 currentKeyPointerRecords:expectedCurrentKeyRecords
457 zoneID:zoneID
458 checkItem:nil];
459 }
460
461 - (void)expectCKModifyItemRecords: (NSUInteger) expectedNumberOfRecords currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords zoneID: (CKRecordZoneID*) zoneID checkItem: (BOOL (^)(CKRecord*)) checkItem {
462 [self expectCKModifyItemRecords:expectedNumberOfRecords
463 deletedRecords:0
464 currentKeyPointerRecords:expectedCurrentKeyRecords
465 zoneID:zoneID
466 checkItem:checkItem];
467 }
468
469 - (void)expectCKModifyItemRecords:(NSUInteger)expectedNumberOfModifiedRecords
470 deletedRecords:(NSUInteger)expectedNumberOfDeletedRecords
471 currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
472 zoneID:(CKRecordZoneID*)zoneID
473 checkItem:(BOOL (^)(CKRecord*))checkItem {
474 // We're updating the device state type on every update, so add it in here
475 NSMutableDictionary* expectedRecords = [@{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfModifiedRecords],
476 SecCKRecordCurrentKeyType: [NSNumber numberWithUnsignedInteger: expectedCurrentKeyRecords],
477 SecCKRecordDeviceStateType: [NSNumber numberWithUnsignedInt: 1],
478 } mutableCopy];
479
480 if(SecCKKSSyncManifests()) {
481 expectedRecords[SecCKRecordManifestType] = [NSNumber numberWithInt: 1];
482 expectedRecords[SecCKRecordManifestLeafType] = [NSNumber numberWithInt: 72];
483 }
484
485 NSDictionary* deletedRecords = nil;
486 if(expectedNumberOfDeletedRecords != 0) {
487 deletedRecords = @{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfDeletedRecords]};
488 }
489
490 [self expectCKModifyRecords:expectedRecords
491 deletedRecordTypeCounts:deletedRecords
492 zoneID:zoneID
493 checkModifiedRecord: ^BOOL (CKRecord* record){
494 if([record.recordType isEqualToString: SecCKRecordItemType] && checkItem) {
495 return checkItem(record);
496 } else {
497 return YES;
498 }
499 }
500 runAfterModification:nil];
501 }
502
503
504
505 - (void)expectCKModifyKeyRecords:(NSUInteger)expectedNumberOfRecords
506 currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
507 tlkShareRecords:(NSUInteger)expectedTLKShareRecords
508 zoneID:(CKRecordZoneID*)zoneID
509 {
510 return [self expectCKModifyKeyRecords:expectedNumberOfRecords
511 currentKeyPointerRecords:expectedCurrentKeyRecords
512 tlkShareRecords:expectedTLKShareRecords
513 zoneID:zoneID
514 checkModifiedRecord:nil];
515 }
516
517 - (void)expectCKModifyKeyRecords:(NSUInteger)expectedNumberOfRecords
518 currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
519 tlkShareRecords:(NSUInteger)expectedTLKShareRecords
520 zoneID:(CKRecordZoneID*)zoneID
521 checkModifiedRecord:(BOOL (^_Nullable)(CKRecord*))checkModifiedRecord
522 {
523 NSNumber* nkeys = [NSNumber numberWithUnsignedInteger: expectedNumberOfRecords];
524 NSNumber* ncurrentkeys = [NSNumber numberWithUnsignedInteger: expectedCurrentKeyRecords];
525 NSNumber* ntlkshares = [NSNumber numberWithUnsignedInteger: expectedTLKShareRecords];
526
527 [self expectCKModifyRecords:@{SecCKRecordIntermediateKeyType: nkeys,
528 SecCKRecordCurrentKeyType: ncurrentkeys,
529 SecCKRecordTLKShareType: ntlkshares,
530 }
531 deletedRecordTypeCounts:nil
532 zoneID:zoneID
533 checkModifiedRecord:checkModifiedRecord
534 runAfterModification:nil];
535 }
536
537 - (void)expectCKModifyRecords:(NSDictionary<NSString*, NSNumber*>*) expectedRecordTypeCounts
538 deletedRecordTypeCounts:(NSDictionary<NSString*, NSNumber*>*) expectedDeletedRecordTypeCounts
539 zoneID:(CKRecordZoneID*) zoneID
540 checkModifiedRecord:(BOOL (^)(CKRecord*)) checkModifiedRecord
541 runAfterModification:(void (^) (void))afterModification
542 {
543 __weak __typeof(self) weakSelf = self;
544
545 // Create an object for the block to retain and modify
546 BoolHolder* runAlready = [[BoolHolder alloc] init];
547
548 secnotice("fakecloudkit", "expecting an operation matching modifications: %@ deletions: %@",
549 expectedRecordTypeCounts, expectedDeletedRecordTypeCounts);
550
551 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
552 secnotice("fakecloudkit", "Received an operation (%@), checking if it's a modification", obj);
553 __block bool matches = false;
554 if(runAlready.state) {
555 secnotice("fakecloudkit", "Run already, skipping");
556 return NO;
557 }
558
559 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
560 __strong __typeof(weakSelf) strongSelf = weakSelf;
561 XCTAssertNotNil(strongSelf, "self exists");
562
563 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
564 matches = true;
565
566 NSMutableDictionary<NSString*, NSNumber*>* modifiedRecordTypeCounts = [[NSMutableDictionary alloc] init];
567 NSMutableDictionary<NSString*, NSNumber*>* deletedRecordTypeCounts = [[NSMutableDictionary alloc] init];
568
569 // First: check if it matches. If it does, _then_ execute the operation.
570 // Supports single-zone atomic writes only
571
572 if(!op.atomic) {
573 // We only care about atomic operations
574 secnotice("fakecloudkit", "Not an atomic operation; quitting: %@", op);
575 return NO;
576 }
577
578 FakeCKZone* zone = strongSelf.zones[zoneID];
579 XCTAssertNotNil(zone, "Have a zone for these records");
580
581 __block BOOL result = YES;
582 dispatch_sync(zone.queue, ^{
583
584 for(CKRecord* record in op.recordsToSave) {
585 if(![record.recordID.zoneID isEqual: zoneID]) {
586 secnotice("fakecloudkit", "Modified record zone ID mismatch: %@ %@", zoneID, record.recordID.zoneID);
587 result = NO;
588 return;
589 }
590
591 NSError* recordError = [zone errorFromSavingRecord: record];
592 if(recordError) {
593 secnotice("fakecloudkit", "Record zone rejected record write: %@ %@", recordError, record);
594 XCTFail(@"Record zone rejected record write: %@ %@", recordError, record);
595 result = NO;
596 return;
597 }
598
599 NSNumber* currentCountNumber = modifiedRecordTypeCounts[record.recordType];
600 NSUInteger currentCount = currentCountNumber ? [currentCountNumber unsignedIntegerValue] : 0;
601 modifiedRecordTypeCounts[record.recordType] = [NSNumber numberWithUnsignedInteger: currentCount + 1];
602 }
603
604 for(CKRecordID* recordID in op.recordIDsToDelete) {
605 if(![recordID.zoneID isEqual: zoneID]) {
606 matches = false;
607 secnotice("fakecloudkit", "Deleted record zone ID mismatch: %@ %@", zoneID, recordID.zoneID);
608 }
609
610 // Find the object in CloudKit, and record its type
611 CKRecord* record = strongSelf.zones[zoneID].currentDatabase[recordID];
612 if(record) {
613 NSNumber* currentCountNumber = deletedRecordTypeCounts[record.recordType];
614 NSUInteger currentCount = currentCountNumber ? [currentCountNumber unsignedIntegerValue] : 0;
615 deletedRecordTypeCounts[record.recordType] = [NSNumber numberWithUnsignedInteger: currentCount + 1];
616 }
617 }
618
619 NSMutableDictionary* filteredExpectedRecordTypeCounts = [expectedRecordTypeCounts mutableCopy];
620 for(NSString* key in filteredExpectedRecordTypeCounts.allKeys) {
621 if([filteredExpectedRecordTypeCounts[key] isEqual: [NSNumber numberWithInt:0]]) {
622 filteredExpectedRecordTypeCounts[key] = nil;
623 }
624 }
625 filteredExpectedRecordTypeCounts[SecCKRecordManifestType] = modifiedRecordTypeCounts[SecCKRecordManifestType];
626 filteredExpectedRecordTypeCounts[SecCKRecordManifestLeafType] = modifiedRecordTypeCounts[SecCKRecordManifestLeafType];
627
628 // Inspect that we have exactly the same records as we expect
629 if(expectedRecordTypeCounts) {
630 matches &= !![modifiedRecordTypeCounts isEqual: filteredExpectedRecordTypeCounts];
631 if(!matches) {
632 secnotice("fakecloudkit", "Record number mismatch: %@ %@", modifiedRecordTypeCounts, filteredExpectedRecordTypeCounts);
633 result = NO;
634 return;
635 }
636 } else {
637 matches &= op.recordsToSave.count == 0u;
638 if(!matches) {
639 secnotice("fakecloudkit", "Record number mismatch: %@ 0", modifiedRecordTypeCounts);
640 result = NO;
641 return;
642 }
643 }
644 if(expectedDeletedRecordTypeCounts) {
645 matches &= !![deletedRecordTypeCounts isEqual: expectedDeletedRecordTypeCounts];
646 if(!matches) {
647 secnotice("fakecloudkit", "Deleted record number mismatch: %@ %@", deletedRecordTypeCounts, expectedDeletedRecordTypeCounts);
648 result = NO;
649 return;
650 }
651 } else {
652 matches &= op.recordIDsToDelete.count == 0u;
653 if(!matches) {
654 secnotice("fakecloudkit", "Deleted record number mismatch: %@ 0", deletedRecordTypeCounts);
655 result = NO;
656 return;
657 }
658 }
659
660 // We have the right number of things, and their etags match. Ensure that they have the right etags
661 if(matches && checkModifiedRecord) {
662 // Clearly we have the right number of things. Call checkRecord on them...
663 for(CKRecord* record in op.recordsToSave) {
664 matches &= !!(checkModifiedRecord(record));
665 if(!matches) {
666 secnotice("fakecloudkit", "Check record reports NO: %@ 0", record);
667 result = NO;
668 return;
669 }
670 }
671 }
672
673 if(matches) {
674 // Emulate cloudkit and schedule the operation for execution. Be sure to wait for this operation
675 // if you'd like to read the data from this write.
676 NSBlockOperation* ckop = [NSBlockOperation named:@"cloudkit-write" withBlock: ^{
677 @synchronized(zone.currentDatabase) {
678 NSMutableArray* savedRecords = [[NSMutableArray alloc] init];
679 for(CKRecord* record in op.recordsToSave) {
680 CKRecord* reflectedRecord = [record copy];
681 reflectedRecord.modificationDate = [NSDate date];
682
683 [zone addToZone: reflectedRecord];
684
685 [savedRecords addObject:reflectedRecord];
686 op.perRecordCompletionBlock(reflectedRecord, nil);
687 }
688 for(CKRecordID* recordID in op.recordIDsToDelete) {
689 // I don't believe CloudKit fails an operation if you delete a record that's not there, so:
690 [zone deleteCKRecordIDFromZone: recordID];
691 }
692
693 if(afterModification) {
694 afterModification();
695 }
696
697 op.modifyRecordsCompletionBlock(savedRecords, op.recordIDsToDelete, nil);
698 op.isFinished = YES;
699 }
700 }];
701 [ckop addNullableDependency:strongSelf.ckModifyHoldOperation];
702 [strongSelf.operationQueue addOperation: ckop];
703 }
704 });
705 if(result != YES) {
706 return result;
707 }
708 }
709 if(matches) {
710 runAlready.state = true;
711 }
712 return matches ? YES : NO;
713 }]];
714 }
715
716 - (void)failNextZoneCreation:(CKRecordZoneID*)zoneID {
717 XCTAssertNil(self.zones[zoneID], "Zone does not exist yet");
718 self.zones[zoneID] = [[FakeCKZone alloc] initZone: zoneID];
719 self.zones[zoneID].creationError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
720 }
721
722 // Report success, but don't actually create the zone.
723 // This way, you can find ZoneNotFound errors later on
724 - (void)failNextZoneCreationSilently:(CKRecordZoneID*)zoneID {
725 XCTAssertNil(self.zones[zoneID], "Zone does not exist yet");
726 self.zones[zoneID] = [[FakeCKZone alloc] initZone: zoneID];
727 self.zones[zoneID].failCreationSilently = true;
728 }
729
730 - (void)failNextZoneSubscription:(CKRecordZoneID*)zoneID {
731 XCTAssertNotNil(self.zones[zoneID], "Zone exists");
732 self.zones[zoneID].subscriptionError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
733 }
734
735 - (void)failNextZoneSubscription:(CKRecordZoneID*)zoneID withError:(NSError*)error {
736 XCTAssertNotNil(self.zones[zoneID], "Zone exists");
737 self.zones[zoneID].subscriptionError = error;
738 }
739
740 - (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID {
741 [self failNextCKAtomicModifyItemRecordsUpdateFailure:zoneID blockAfterReject:nil];
742 }
743
744 - (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID blockAfterReject: (void (^)(void))blockAfterReject {
745 [self failNextCKAtomicModifyItemRecordsUpdateFailure:zoneID blockAfterReject:blockAfterReject withError:nil];
746 }
747
748 - (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID blockAfterReject: (void (^)(void))blockAfterReject withError:(NSError*)error {
749 __weak __typeof(self) weakSelf = self;
750
751 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
752 __strong __typeof(weakSelf) strongSelf = weakSelf;
753 XCTAssertNotNil(strongSelf, "self exists");
754
755 __block bool rejected = false;
756 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
757 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
758
759 if(!op.atomic) {
760 // We only care about atomic operations
761 return NO;
762 }
763
764 // We want to only match zone updates pertaining to this zone
765 for(CKRecord* record in op.recordsToSave) {
766 if(![record.recordID.zoneID isEqual: zoneID]) {
767 return NO;
768 }
769 }
770
771 FakeCKZone* zone = strongSelf.zones[zoneID];
772 XCTAssertNotNil(zone, "Have a zone for these records");
773
774 rejected = true;
775
776 if(error) {
777 [strongSelf rejectWrite: op withError:error];
778 } else {
779 NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = [[NSMutableDictionary alloc] init];
780 [strongSelf rejectWrite: op failedRecords:failedRecords];
781 }
782
783 if(blockAfterReject) {
784 blockAfterReject();
785 }
786 }
787 return rejected ? YES : NO;
788 }]];
789 }
790
791 - (void)expectCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID {
792 __weak __typeof(self) weakSelf = self;
793
794 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
795 __strong __typeof(weakSelf) strongSelf = weakSelf;
796 XCTAssertNotNil(strongSelf, "self exists");
797
798 __block bool rejected = false;
799 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
800 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
801
802 secnotice("fakecloudkit", "checking for expectCKAtomicModifyItemRecordsUpdateFailure");
803
804 if(!op.atomic) {
805 // We only care about atomic operations
806 secnotice("fakecloudkit", "expectCKAtomicModifyItemRecordsUpdateFailure: update not atomic");
807 return NO;
808 }
809
810 // We want to only match zone updates pertaining to this zone
811 for(CKRecord* record in op.recordsToSave) {
812 if(![record.recordID.zoneID isEqual: zoneID]) {
813 secnotice("fakecloudkit", "expectCKAtomicModifyItemRecordsUpdateFailure: %@ is not %@", record.recordID.zoneID, zoneID);
814 return NO;
815 }
816 }
817
818 FakeCKZone* zone = strongSelf.zones[zoneID];
819 XCTAssertNotNil(zone, "Have a zone for these records");
820
821 NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = [[NSMutableDictionary alloc] init];
822
823 @synchronized(zone.currentDatabase) {
824 for(CKRecord* record in op.recordsToSave) {
825 // Check if we should allow this transaction
826 NSError* recordSaveError = [zone errorFromSavingRecord: record];
827 if(recordSaveError) {
828 failedRecords[record.recordID] = recordSaveError;
829 rejected = true;
830 }
831 }
832 }
833
834 if(rejected) {
835 [strongSelf rejectWrite: op failedRecords:failedRecords];
836 } else {
837 secnotice("fakecloudkit", "expectCKAtomicModifyItemRecordsUpdateFailure: doesn't seem like an error to us");
838 }
839 }
840 return rejected ? YES : NO;
841 }]];
842 }
843
844 -(void)rejectWrite:(CKModifyRecordsOperation*)op withError:(NSError*)error {
845 // Emulate cloudkit and schedule the operation for execution. Be sure to wait for this operation
846 // if you'd like to read the data from this write.
847 NSBlockOperation* ckop = [NSBlockOperation named:@"cloudkit-reject-write-error" withBlock: ^{
848 op.modifyRecordsCompletionBlock(nil, nil, error);
849 op.isFinished = YES;
850 }];
851 [ckop addNullableDependency: self.ckModifyHoldOperation];
852 [self.operationQueue addOperation: ckop];
853 }
854
855 -(void)rejectWrite:(CKModifyRecordsOperation*)op failedRecords:(NSMutableDictionary<CKRecordID*, NSError*>*)failedRecords {
856 // Add the batch request failed errors
857 for(CKRecord* record in op.recordsToSave) {
858 NSError* exists = failedRecords[record.recordID];
859 if(!exists) {
860 // TODO: might have important userInfo, but we're not mocking that yet
861 failedRecords[record.recordID] = [[CKPrettyError alloc] initWithDomain: CKErrorDomain code: CKErrorBatchRequestFailed userInfo: @{}];
862 }
863 }
864
865 NSError* error = [[CKPrettyError alloc] initWithDomain: CKErrorDomain code: CKErrorPartialFailure userInfo: @{CKPartialErrorsByItemIDKey: failedRecords}];
866
867 // Emulate cloudkit and schedule the operation for execution. Be sure to wait for this operation
868 // if you'd like to read the data from this write.
869 NSBlockOperation* ckop = [NSBlockOperation named:@"cloudkit-reject-write" withBlock: ^{
870 op.modifyRecordsCompletionBlock(nil, nil, error);
871 op.isFinished = YES;
872 }];
873 [ckop addNullableDependency: self.ckModifyHoldOperation];
874 [self.operationQueue addOperation: ckop];
875 }
876
877 - (void)expectCKDeleteItemRecords:(NSUInteger)expectedNumberOfRecords
878 zoneID:(CKRecordZoneID*) zoneID {
879
880 // We're updating the device state type on every update, so add it in here
881 NSMutableDictionary* expectedRecords = [@{
882 SecCKRecordDeviceStateType: [NSNumber numberWithUnsignedInteger:expectedNumberOfRecords],
883 } mutableCopy];
884 if(SecCKKSSyncManifests()) {
885 // TODO: this really shouldn't be 2.
886 expectedRecords[SecCKRecordManifestType] = [NSNumber numberWithInt: 2];
887 expectedRecords[SecCKRecordManifestLeafType] = [NSNumber numberWithInt: 72];
888 }
889
890 [self expectCKModifyRecords:expectedRecords
891 deletedRecordTypeCounts:@{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfRecords]}
892 zoneID:zoneID
893 checkModifiedRecord:nil
894 runAfterModification:nil];
895 }
896
897 -(void)waitForCKModifications {
898 // CloudKit modifications are put on the local queue.
899 // This is heavyweight but should suffice.
900 [self.operationQueue waitUntilAllOperationsAreFinished];
901 }
902
903 - (void)tearDown {
904 NSString* testName = [self.name componentsSeparatedByString:@" "][1];
905 testName = [testName stringByReplacingOccurrencesOfString:@"]" withString:@""];
906 secnotice("ckkstest", "Ending test %@", testName);
907
908 if(SecCKKSIsEnabled()) {
909 self.accountStatus = CKAccountStatusCouldNotDetermine;
910
911 // If the test never initialized the account state, don't call status later
912 bool callStatus = [self.ckaccountHoldOperation isFinished];
913 [self.ckaccountHoldOperation cancel];
914 self.ckaccountHoldOperation = nil;
915
916 // Ensure we don't have any blocking operations left
917 [self.operationQueue cancelAllOperations];
918 [self waitForCKModifications];
919
920 XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:20*NSEC_PER_SEC],
921 "Timeout did not occur waiting for SecCKKSInitialize");
922
923 // Ensure that we can fetch zone status for all zones
924 if(callStatus) {
925 XCTestExpectation *statusReturned = [self expectationWithDescription:@"status returned"];
926 [self.injectedManager rpcStatus:nil reply:^(NSArray<NSDictionary *> *result, NSError *error) {
927 XCTAssertNil(error, "Should be no error fetching status");
928 [statusReturned fulfill];
929 }];
930 [self waitForExpectations: @[statusReturned] timeout:20];
931 }
932
933 // Make sure this happens before teardown.
934 XCTAssertEqual(0, [self.accountStateTracker.finishedInitialDispatches wait:20*NSEC_PER_SEC], "Account state tracker initialized itself");
935
936 dispatch_group_t accountChangesDelivered = [self.accountStateTracker checkForAllDeliveries];
937 XCTAssertEqual(0, dispatch_group_wait(accountChangesDelivered, dispatch_time(DISPATCH_TIME_NOW, 10*NSEC_PER_SEC)), "Account state tracker finished delivering everything");
938 }
939
940 [super tearDown];
941
942 [self.injectedManager cancelPendingOperations];
943 [CKKSViewManager resetManager:true setTo:nil];
944 self.injectedManager = nil;
945 [self.mockCKKSViewManager stopMocking];
946 self.mockCKKSViewManager = nil;
947
948 [self.mockAccountStateTracker stopMocking];
949 self.mockAccountStateTracker = nil;
950
951 [self.mockLockStateTracker stopMocking];
952 self.mockLockStateTracker = nil;
953
954 [self.mockReachabilityTracker stopMocking];
955 self.mockReachabilityTracker = nil;
956
957 [self.mockFakeCKModifyRecordZonesOperation stopMocking];
958 self.mockFakeCKModifyRecordZonesOperation = nil;
959
960 [self.mockFakeCKModifySubscriptionsOperation stopMocking];
961 self.mockFakeCKModifySubscriptionsOperation = nil;
962
963 [self.mockFakeCKFetchRecordZoneChangesOperation stopMocking];
964 self.mockFakeCKFetchRecordZoneChangesOperation = nil;
965
966 [self.mockFakeCKFetchRecordsOperation stopMocking];
967 self.mockFakeCKFetchRecordsOperation = nil;
968
969 [self.mockFakeCKQueryOperation stopMocking];
970 self.mockFakeCKQueryOperation = nil;
971
972 [self.mockDatabase stopMocking];
973 self.mockDatabase = nil;
974
975 [self.mockDatabaseExceptionCatcher stopMocking];
976 self.mockDatabaseExceptionCatcher = nil;
977
978 [self.mockContainer stopMocking];
979 self.mockContainer = nil;
980
981 self.zones = nil;
982
983 self.operationQueue = nil;
984
985 SecCKKSTestResetFlags();
986 }
987
988 - (CKKSKey*) fakeTLK: (CKRecordZoneID*)zoneID {
989 CKKSKey* key = [[CKKSKey alloc] initSelfWrappedWithAESKey:[[CKKSAESSIVKey alloc] initWithBase64: @"uImdbZ7Zg+6WJXScTnRBfNmoU1UiMkSYxWc+d1Vuq3IFn2RmTRkTdWTe3HmeWo1pAomqy+upK8KHg2PGiRGhqg=="]
990 uuid:[[NSUUID UUID] UUIDString]
991 keyclass:SecCKKSKeyClassTLK
992 state: SecCKKSProcessedStateLocal
993 zoneID:zoneID
994 encodedCKRecord: nil
995 currentkey: true];
996 [key CKRecordWithZoneID: zoneID];
997 return key;
998 }
999
1000 - (NSError*)ckInternalServerExtensionError:(NSInteger)code description:(NSString*)desc {
1001 NSError* extensionError = [[CKPrettyError alloc] initWithDomain:@"CloudkitKeychainService"
1002 code:code
1003 userInfo:@{
1004 CKErrorServerDescriptionKey: desc,
1005 NSLocalizedDescriptionKey: desc,
1006 }];
1007 NSError* internalError = [[CKPrettyError alloc] initWithDomain:CKInternalErrorDomain
1008 code:CKErrorInternalPluginError
1009 userInfo:@{CKErrorServerDescriptionKey: desc,
1010 NSLocalizedDescriptionKey: desc,
1011 NSUnderlyingErrorKey: extensionError,
1012 }];
1013 NSError* error = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
1014 code:CKErrorServerRejectedRequest
1015 userInfo:@{NSUnderlyingErrorKey: internalError,
1016 CKErrorServerDescriptionKey: desc,
1017 NSLocalizedDescriptionKey: desc,
1018 CKContainerIDKey: SecCKKSContainerName,
1019 }];
1020 return error;
1021 }
1022
1023 @end
1024
1025 #endif