2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
26 #import "CloudKitMockXCTest.h"
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>
35 #include "OSX/sec/securityd/Regressions/SecdTestKeychainUtilities.h"
36 #include <utilities/SecFileLocations.h>
37 #include <securityd/SecItemServer.h>
40 #include <securityd/spi.h>
43 #include <Security/SecureObjectSync/SOSViews.h>
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"
57 #import "MockCloudKit.h"
59 @interface BoolHolder : NSObject
63 @implementation BoolHolder
66 // Inform OCMock about the internals of CKContainer
67 @interface CKContainer ()
68 - (void)_checkSelfCloudServicesEntitlement;
72 @implementation CloudKitMockXCTest
77 SecCKKSSetReduceRateLimiting(true);
81 securityd_init_local_spi();
88 NSString* testName = [self.name componentsSeparatedByString:@" "][1];
89 testName = [testName stringByReplacingOccurrencesOfString:@"]" withString:@""];
90 secnotice("ckkstest", "Beginning test %@", testName);
92 // All tests start with the same flag set.
93 SecCKKSTestResetFlags();
94 SecCKKSTestSetDisableSOS(true);
96 self.silentFetchesAllowed = true;
97 self.silentZoneDeletesAllowed = false; // Set to true if you want to do any deletes
99 __weak __typeof(self) weakSelf = self;
100 self.operationQueue = [[NSOperationQueue alloc] init];
101 self.operationQueue.maxConcurrentOperationCount = 1;
103 self.zones = [[NSMutableDictionary alloc] init];
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])]);
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:));
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])]);
123 self.accountStatus = CKAccountStatusAvailable;
124 self.supportsDeviceToDeviceEncryption = YES;
125 self.iCloudHasValidCredentials = YES;
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");
133 OCMStub([self.mockContainer accountStatusWithCompletionHandler:
134 [OCMArg checkWithBlock:^BOOL(void (^passedBlock) (CKAccountStatus accountStatus,
135 NSError * _Nullable error)) {
138 __strong __typeof(self) strongSelf = weakSelf;
139 NSBlockOperation* fulfillBlock = [NSBlockOperation named:@"account-status-completion" withBlock: ^{
140 passedBlock(weakSelf.accountStatus, nil);
142 [fulfillBlock addDependency: strongSelf.ckaccountHoldOperation];
143 [strongSelf.operationQueue addOperation: fulfillBlock];
150 OCMStub([self.mockContainer accountInfoWithCompletionHandler:
151 [OCMArg checkWithBlock:^BOOL(void (^passedBlock) (CKAccountInfo* accountInfo,
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);
164 [fulfillBlock addDependency: strongSelf.ckaccountHoldOperation];
165 [strongSelf.operationQueue addOperation: fulfillBlock];
172 self.circleStatus = kSOSCCInCircle;
173 self.mockAccountStateTracker = OCMClassMock([CKKSCKAccountStateTracker class]);
174 OCMStub([self.mockAccountStateTracker getCircleStatus]).andCall(self, @selector(circleStatus));
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,
181 __strong __typeof(self) strongSelf = weakSelf;
182 if(passedBlock && strongSelf) {
183 if(strongSelf.circleStatus == kSOSCCInCircle) {
184 passedBlock(strongSelf.circlePeerID, nil);
186 passedBlock(nil, [NSError errorWithDomain:@"securityd" code:errSecInternalError userInfo:@{NSLocalizedDescriptionKey:@"no account, no circle id"}]);
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));
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));
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:));
206 self.mockFakeCKModifySubscriptionsOperation = OCMClassMock([FakeCKModifySubscriptionsOperation class]);
207 OCMStub([self.mockFakeCKModifySubscriptionsOperation ckdb]).andReturn(self.zones);
209 self.mockFakeCKFetchRecordZoneChangesOperation = OCMClassMock([FakeCKFetchRecordZoneChangesOperation class]);
210 OCMStub([self.mockFakeCKFetchRecordZoneChangesOperation ckdb]).andReturn(self.zones);
212 self.mockFakeCKFetchRecordsOperation = OCMClassMock([FakeCKFetchRecordsOperation class]);
213 OCMStub([self.mockFakeCKFetchRecordsOperation ckdb]).andReturn(self.zones);
215 self.mockFakeCKQueryOperation = OCMClassMock([FakeCKQueryOperation class]);
216 OCMStub([self.mockFakeCKQueryOperation ckdb]).andReturn(self.zones);
219 OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
220 __strong __typeof(self) strongSelf = weakSelf;
222 if ([obj isKindOfClass: [FakeCKFetchRecordZoneChangesOperation class]]) {
223 if(strongSelf.silentFetchesAllowed) {
226 FakeCKFetchRecordZoneChangesOperation *frzco = (FakeCKFetchRecordZoneChangesOperation *)obj;
227 [frzco addNullableDependency:strongSelf.ckFetchHoldOperation];
228 [strongSelf.operationQueue addOperation: frzco];
234 OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
235 __strong __typeof(self) strongSelf = weakSelf;
237 if ([obj isKindOfClass: [FakeCKFetchRecordsOperation class]]) {
238 if(strongSelf.silentFetchesAllowed) {
241 FakeCKFetchRecordsOperation *ffro = (FakeCKFetchRecordsOperation *)obj;
242 [ffro addNullableDependency:strongSelf.ckFetchHoldOperation];
243 [strongSelf.operationQueue addOperation: ffro];
249 OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
250 __strong __typeof(self) strongSelf = weakSelf;
252 if ([obj isKindOfClass: [FakeCKQueryOperation class]]) {
253 if(strongSelf.silentFetchesAllowed) {
256 FakeCKQueryOperation *fqo = (FakeCKQueryOperation *)obj;
257 [fqo addNullableDependency:strongSelf.ckFetchHoldOperation];
258 [strongSelf.operationQueue addOperation: fqo];
265 self.testZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"testzone" ownerName:CKCurrentUserDefaultName];
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]]);
280 OCMStub([self.mockCKKSViewManager viewList]).andCall(self, @selector(managedViewList));
281 OCMStub([self.mockCKKSViewManager syncBackupAndNotifyAboutSync]);
283 self.injectedManager = self.mockCKKSViewManager;
285 [CKKSViewManager resetManager:false setTo:self.injectedManager];
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];
291 SetCustomHomeURLString((__bridge CFStringRef) tmp_dir);
292 SecKeychainDbReset(NULL);
294 // Actually load the database.
295 kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) { return false; });
298 - (void)ckdatabaseAddOperation:(NSOperation*)op {
300 [self.mockDatabase addOperation:op];
301 } @catch (NSException *exception) {
302 XCTFail("Received an database exception: %@", exception);
307 - (void)ensureZoneDeletionAllowed:(FakeCKZone*)zone {
308 XCTAssertTrue(self.silentZoneDeletesAllowed, "Should be allowing zone deletes");
311 -(CKKSCKAccountStateTracker*)accountStateTracker {
312 return self.injectedManager.accountTracker;
315 -(CKKSLockStateTracker*)lockStateTracker {
316 return self.injectedManager.lockStateTracker;
319 -(CKKSReachabilityTracker*)reachabilityTracker {
320 return self.injectedManager.reachabilityTracker;
323 -(NSSet*)managedViewList {
324 return (NSSet*) CFBridgingRelease(SOSViewCopyViewSet(kViewSetCKKS));
327 -(void)expectCKFetch {
328 [self expectCKFetchAndRunBeforeFinished: nil];
331 -(void)expectCKFetchAndRunBeforeFinished: (void (^)())blockAfterFetch {
332 // Create an object for the block to retain and modify
333 BoolHolder* runAlready = [[BoolHolder alloc] init];
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) {
342 if ([obj isKindOfClass: [FakeCKFetchRecordZoneChangesOperation class]]) {
344 runAlready.state = true;
346 FakeCKFetchRecordZoneChangesOperation *frzco = (FakeCKFetchRecordZoneChangesOperation *)obj;
347 frzco.blockAfterFetch = blockAfterFetch;
348 [frzco addNullableDependency: strongSelf.ckFetchHoldOperation];
349 [strongSelf.operationQueue addOperation: frzco];
355 -(void)expectCKFetchByRecordID {
356 // Create an object for the block to retain and modify
357 BoolHolder* runAlready = [[BoolHolder alloc] init];
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) {
366 if ([obj isKindOfClass: [FakeCKFetchRecordsOperation class]]) {
368 runAlready.state = true;
370 FakeCKFetchRecordsOperation *ffro = (FakeCKFetchRecordsOperation *)obj;
371 [ffro addNullableDependency: strongSelf.ckFetchHoldOperation];
372 [strongSelf.operationQueue addOperation: ffro];
379 -(void)expectCKFetchByQuery {
380 // Create an object for the block to retain and modify
381 BoolHolder* runAlready = [[BoolHolder alloc] init];
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) {
390 if ([obj isKindOfClass: [FakeCKQueryOperation class]]) {
392 runAlready.state = true;
394 FakeCKQueryOperation *fqo = (FakeCKQueryOperation *)obj;
395 [fqo addNullableDependency: strongSelf.ckFetchHoldOperation];
396 [strongSelf.operationQueue addOperation: fqo];
402 - (void)startCKKSSubsystem {
403 [self startCKAccountStatusMock];
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];
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.");
421 -(void)releaseCloudKitModificationHold {
422 if([self.ckModifyHoldOperation isPending]) {
423 [self.operationQueue addOperation: self.ckModifyHoldOperation];
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.");
433 -(void)releaseCloudKitFetchHold {
434 if([self.ckFetchHoldOperation isPending]) {
435 [self.operationQueue addOperation: self.ckFetchHoldOperation];
439 - (void)expectCKModifyItemRecords: (NSUInteger) expectedNumberOfRecords currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords zoneID: (CKRecordZoneID*) zoneID {
440 [self expectCKModifyItemRecords:expectedNumberOfRecords
441 currentKeyPointerRecords:expectedCurrentKeyRecords
446 - (void)expectCKModifyItemRecords: (NSUInteger) expectedNumberOfRecords currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords zoneID: (CKRecordZoneID*) zoneID checkItem: (BOOL (^)(CKRecord*)) checkItem {
447 [self expectCKModifyItemRecords:expectedNumberOfRecords
449 currentKeyPointerRecords:expectedCurrentKeyRecords
451 checkItem:checkItem];
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],
465 if(SecCKKSSyncManifests()) {
466 expectedRecords[SecCKRecordManifestType] = [NSNumber numberWithInt: 1];
467 expectedRecords[SecCKRecordManifestLeafType] = [NSNumber numberWithInt: 72];
470 NSDictionary* deletedRecords = nil;
471 if(expectedNumberOfDeletedRecords != 0) {
472 deletedRecords = @{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfDeletedRecords]};
475 [self expectCKModifyRecords:expectedRecords
476 deletedRecordTypeCounts:deletedRecords
478 checkModifiedRecord: ^BOOL (CKRecord* record){
479 if([record.recordType isEqualToString: SecCKRecordItemType] && checkItem) {
480 return checkItem(record);
485 runAfterModification:nil];
490 - (void)expectCKModifyKeyRecords:(NSUInteger)expectedNumberOfRecords
491 currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
492 tlkShareRecords:(NSUInteger)expectedTLKShareRecords
493 zoneID:(CKRecordZoneID*)zoneID
495 return [self expectCKModifyKeyRecords:expectedNumberOfRecords
496 currentKeyPointerRecords:expectedCurrentKeyRecords
497 tlkShareRecords:expectedTLKShareRecords
499 checkModifiedRecord:nil];
502 - (void)expectCKModifyKeyRecords:(NSUInteger)expectedNumberOfRecords
503 currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
504 tlkShareRecords:(NSUInteger)expectedTLKShareRecords
505 zoneID:(CKRecordZoneID*)zoneID
506 checkModifiedRecord:(BOOL (^_Nullable)(CKRecord*))checkModifiedRecord
508 NSNumber* nkeys = [NSNumber numberWithUnsignedInteger: expectedNumberOfRecords];
509 NSNumber* ncurrentkeys = [NSNumber numberWithUnsignedInteger: expectedCurrentKeyRecords];
510 NSNumber* ntlkshares = [NSNumber numberWithUnsignedInteger: expectedTLKShareRecords];
512 [self expectCKModifyRecords:@{SecCKRecordIntermediateKeyType: nkeys,
513 SecCKRecordCurrentKeyType: ncurrentkeys,
514 SecCKRecordTLKShareType: ntlkshares,
516 deletedRecordTypeCounts:nil
518 checkModifiedRecord:checkModifiedRecord
519 runAfterModification:nil];
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
528 __weak __typeof(self) weakSelf = self;
530 // Create an object for the block to retain and modify
531 BoolHolder* runAlready = [[BoolHolder alloc] init];
533 secnotice("fakecloudkit", "expecting an operation matching modifications: %@ deletions: %@",
534 expectedRecordTypeCounts, expectedDeletedRecordTypeCounts);
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");
544 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
545 __strong __typeof(weakSelf) strongSelf = weakSelf;
546 XCTAssertNotNil(strongSelf, "self exists");
548 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
551 NSMutableDictionary<NSString*, NSNumber*>* modifiedRecordTypeCounts = [[NSMutableDictionary alloc] init];
552 NSMutableDictionary<NSString*, NSNumber*>* deletedRecordTypeCounts = [[NSMutableDictionary alloc] init];
554 // First: check if it matches. If it does, _then_ execute the operation.
555 // Supports single-zone atomic writes only
558 // We only care about atomic operations
559 secnotice("fakecloudkit", "Not an atomic operation; quitting: %@", op);
563 FakeCKZone* zone = strongSelf.zones[zoneID];
564 XCTAssertNotNil(zone, "Have a zone for these records");
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);
572 NSError* recordError = [zone errorFromSavingRecord: record];
574 secnotice("fakecloudkit", "Record zone rejected record write: %@ %@", recordError, record);
575 XCTFail(@"Record zone rejected record write: %@ %@", recordError, record);
579 NSNumber* currentCountNumber = modifiedRecordTypeCounts[record.recordType];
580 NSUInteger currentCount = currentCountNumber ? [currentCountNumber unsignedIntegerValue] : 0;
581 modifiedRecordTypeCounts[record.recordType] = [NSNumber numberWithUnsignedInteger: currentCount + 1];
584 for(CKRecordID* recordID in op.recordIDsToDelete) {
585 if(![recordID.zoneID isEqual: zoneID]) {
587 secnotice("fakecloudkit", "Deleted record zone ID mismatch: %@ %@", zoneID, recordID.zoneID);
590 // Find the object in CloudKit, and record its type
591 CKRecord* record = strongSelf.zones[zoneID].currentDatabase[recordID];
593 NSNumber* currentCountNumber = deletedRecordTypeCounts[record.recordType];
594 NSUInteger currentCount = currentCountNumber ? [currentCountNumber unsignedIntegerValue] : 0;
595 deletedRecordTypeCounts[record.recordType] = [NSNumber numberWithUnsignedInteger: currentCount + 1];
599 NSMutableDictionary* filteredExpectedRecordTypeCounts = [expectedRecordTypeCounts mutableCopy];
600 for(NSString* key in filteredExpectedRecordTypeCounts.allKeys) {
601 if([filteredExpectedRecordTypeCounts[key] isEqual: [NSNumber numberWithInt:0]]) {
602 filteredExpectedRecordTypeCounts[key] = nil;
605 filteredExpectedRecordTypeCounts[SecCKRecordManifestType] = modifiedRecordTypeCounts[SecCKRecordManifestType];
606 filteredExpectedRecordTypeCounts[SecCKRecordManifestLeafType] = modifiedRecordTypeCounts[SecCKRecordManifestLeafType];
608 // Inspect that we have exactly the same records as we expect
609 if(expectedRecordTypeCounts) {
610 matches &= !![modifiedRecordTypeCounts isEqual: filteredExpectedRecordTypeCounts];
612 secnotice("fakecloudkit", "Record number mismatch: %@ %@", modifiedRecordTypeCounts, filteredExpectedRecordTypeCounts);
616 matches &= op.recordsToSave.count == 0u;
618 secnotice("fakecloudkit", "Record number mismatch: %@ 0", modifiedRecordTypeCounts);
622 if(expectedDeletedRecordTypeCounts) {
623 matches &= !![deletedRecordTypeCounts isEqual: expectedDeletedRecordTypeCounts];
625 secnotice("fakecloudkit", "Deleted record number mismatch: %@ %@", deletedRecordTypeCounts, expectedDeletedRecordTypeCounts);
629 matches &= op.recordIDsToDelete.count == 0u;
631 secnotice("fakecloudkit", "Deleted record number mismatch: %@ 0", deletedRecordTypeCounts);
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));
642 secnotice("fakecloudkit", "Check record reports NO: %@ 0", record);
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];
658 [zone addToZone: reflectedRecord];
660 [savedRecords addObject:reflectedRecord];
661 op.perRecordCompletionBlock(reflectedRecord, nil);
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];
668 if(afterModification) {
672 op.modifyRecordsCompletionBlock(savedRecords, op.recordIDsToDelete, nil);
676 [ckop addNullableDependency:strongSelf.ckModifyHoldOperation];
677 [strongSelf.operationQueue addOperation: ckop];
681 runAlready.state = true;
683 return matches ? YES : NO;
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:@{}];
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;
701 - (void)failNextZoneSubscription:(CKRecordZoneID*)zoneID {
702 XCTAssertNotNil(self.zones[zoneID], "Zone exists");
703 self.zones[zoneID].subscriptionError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
706 - (void)failNextZoneSubscription:(CKRecordZoneID*)zoneID withError:(NSError*)error {
707 XCTAssertNotNil(self.zones[zoneID], "Zone exists");
708 self.zones[zoneID].subscriptionError = error;
711 - (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID {
712 [self failNextCKAtomicModifyItemRecordsUpdateFailure:zoneID blockAfterReject:nil];
715 - (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID blockAfterReject: (void (^)())blockAfterReject {
716 [self failNextCKAtomicModifyItemRecordsUpdateFailure:zoneID blockAfterReject:blockAfterReject withError:nil];
719 - (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID blockAfterReject: (void (^)())blockAfterReject withError:(NSError*)error {
720 __weak __typeof(self) weakSelf = self;
722 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
723 __strong __typeof(weakSelf) strongSelf = weakSelf;
724 XCTAssertNotNil(strongSelf, "self exists");
726 __block bool rejected = false;
727 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
728 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
731 // We only care about atomic operations
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]) {
742 FakeCKZone* zone = strongSelf.zones[zoneID];
743 XCTAssertNotNil(zone, "Have a zone for these records");
748 [strongSelf rejectWrite: op withError:error];
750 NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = [[NSMutableDictionary alloc] init];
751 [strongSelf rejectWrite: op failedRecords:failedRecords];
754 if(blockAfterReject) {
758 return rejected ? YES : NO;
762 - (void)expectCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID {
763 __weak __typeof(self) weakSelf = self;
765 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
766 __strong __typeof(weakSelf) strongSelf = weakSelf;
767 XCTAssertNotNil(strongSelf, "self exists");
769 __block bool rejected = false;
770 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
771 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
773 secnotice("fakecloudkit", "checking for expectCKAtomicModifyItemRecordsUpdateFailure");
776 // We only care about atomic operations
777 secnotice("fakecloudkit", "expectCKAtomicModifyItemRecordsUpdateFailure: update not atomic");
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);
789 FakeCKZone* zone = strongSelf.zones[zoneID];
790 XCTAssertNotNil(zone, "Have a zone for these records");
792 NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = [[NSMutableDictionary alloc] init];
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;
806 [strongSelf rejectWrite: op failedRecords:failedRecords];
808 secnotice("fakecloudkit", "expectCKAtomicModifyItemRecordsUpdateFailure: doesn't seem like an error to us");
811 return rejected ? YES : NO;
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);
822 [ckop addNullableDependency: self.ckModifyHoldOperation];
823 [self.operationQueue addOperation: ckop];
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];
831 // TODO: might have important userInfo, but we're not mocking that yet
832 failedRecords[record.recordID] = [[CKPrettyError alloc] initWithDomain: CKErrorDomain code: CKErrorBatchRequestFailed userInfo: @{}];
836 NSError* error = [[CKPrettyError alloc] initWithDomain: CKErrorDomain code: CKErrorPartialFailure userInfo: @{CKPartialErrorsByItemIDKey: failedRecords}];
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);
844 [ckop addNullableDependency: self.ckModifyHoldOperation];
845 [self.operationQueue addOperation: ckop];
848 - (void)expectCKDeleteItemRecords:(NSUInteger)expectedNumberOfRecords
849 zoneID:(CKRecordZoneID*) zoneID {
851 // We're updating the device state type on every update, so add it in here
852 NSMutableDictionary* expectedRecords = [@{
853 SecCKRecordDeviceStateType: [NSNumber numberWithUnsignedInteger:expectedNumberOfRecords],
855 if(SecCKKSSyncManifests()) {
856 // TODO: this really shouldn't be 2.
857 expectedRecords[SecCKRecordManifestType] = [NSNumber numberWithInt: 2];
858 expectedRecords[SecCKRecordManifestLeafType] = [NSNumber numberWithInt: 72];
861 [self expectCKModifyRecords:expectedRecords
862 deletedRecordTypeCounts:@{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfRecords]}
864 checkModifiedRecord:nil
865 runAfterModification:nil];
868 -(void)waitForCKModifications {
869 // CloudKit modifications are put on the local queue.
870 // This is heavyweight but should suffice.
871 [self.operationQueue waitUntilAllOperationsAreFinished];
875 NSString* testName = [self.name componentsSeparatedByString:@" "][1];
876 testName = [testName stringByReplacingOccurrencesOfString:@"]" withString:@""];
877 secnotice("ckkstest", "Ending test %@", testName);
879 if(SecCKKSIsEnabled()) {
880 self.accountStatus = CKAccountStatusCouldNotDetermine;
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;
887 // Ensure we don't have any blocking operations left
888 [self.operationQueue cancelAllOperations];
889 [self waitForCKModifications];
891 XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:2*NSEC_PER_SEC],
892 "Timeout did not occur waiting for SecCKKSInitialize");
894 // Ensure that we can fetch zone status for all zones
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];
901 [self waitForExpectations: @[statusReturned] timeout:5];
904 // Make sure this happens before teardown.
905 XCTAssertEqual(0, [self.accountStateTracker.finishedInitialDispatches wait:1*NSEC_PER_SEC], "Account state tracker initialized itself");
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");
913 [self.injectedManager cancelPendingOperations];
914 [CKKSViewManager resetManager:true setTo:nil];
915 self.injectedManager = nil;
916 [self.mockCKKSViewManager stopMocking];
917 self.mockCKKSViewManager = nil;
919 [self.mockAccountStateTracker stopMocking];
920 self.mockAccountStateTracker = nil;
922 [self.mockLockStateTracker stopMocking];
923 self.mockLockStateTracker = nil;
925 [self.mockReachabilityTracker stopMocking];
926 self.mockReachabilityTracker = nil;
928 [self.mockFakeCKModifyRecordZonesOperation stopMocking];
929 self.mockFakeCKModifyRecordZonesOperation = nil;
931 [self.mockFakeCKModifySubscriptionsOperation stopMocking];
932 self.mockFakeCKModifySubscriptionsOperation = nil;
934 [self.mockFakeCKFetchRecordZoneChangesOperation stopMocking];
935 self.mockFakeCKFetchRecordZoneChangesOperation = nil;
937 [self.mockFakeCKFetchRecordsOperation stopMocking];
938 self.mockFakeCKFetchRecordsOperation = nil;
940 [self.mockFakeCKQueryOperation stopMocking];
941 self.mockFakeCKQueryOperation = nil;
943 [self.mockDatabase stopMocking];
944 self.mockDatabase = nil;
946 [self.mockDatabaseExceptionCatcher stopMocking];
947 self.mockDatabaseExceptionCatcher = nil;
949 [self.mockContainer stopMocking];
950 self.mockContainer = nil;
954 self.operationQueue = nil;
956 SecCKKSTestResetFlags();
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
967 [key CKRecordWithZoneID: zoneID];
971 - (NSError*)ckInternalServerExtensionError:(NSInteger)code description:(NSString*)desc {
972 NSError* extensionError = [[CKPrettyError alloc] initWithDomain:@"CloudkitKeychainService"
975 CKErrorServerDescriptionKey: desc,
976 NSLocalizedDescriptionKey: desc,
978 NSError* internalError = [[CKPrettyError alloc] initWithDomain:CKInternalErrorDomain
979 code:CKErrorInternalPluginError
980 userInfo:@{CKErrorServerDescriptionKey: desc,
981 NSLocalizedDescriptionKey: desc,
982 NSUnderlyingErrorKey: extensionError,
984 NSError* error = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
985 code:CKErrorServerRejectedRequest
986 userInfo:@{NSUnderlyingErrorKey: internalError,
987 CKErrorServerDescriptionKey: desc,
988 NSLocalizedDescriptionKey: desc,
989 CKContainerIDKey: SecCKKSContainerName,