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