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"
56 #import "MockCloudKit.h"
58 @interface BoolHolder : NSObject
62 @implementation BoolHolder
65 // Inform OCMock about the internals of CKContainer
66 @interface CKContainer ()
67 - (void)_checkSelfCloudServicesEntitlement;
71 @implementation CloudKitMockXCTest
79 securityd_init_local_spi();
86 // All tests start with the same flag set.
87 SecCKKSTestResetFlags();
88 SecCKKSTestSetDisableSOS(true);
90 self.silentFetchesAllowed = true;
92 __weak __typeof(self) weakSelf = self;
93 self.operationQueue = [[NSOperationQueue alloc] init];
94 self.operationQueue.maxConcurrentOperationCount = 1;
96 self.zones = [[NSMutableDictionary alloc] init];
98 self.mockDatabase = OCMStrictClassMock([CKDatabase class]);
99 self.mockContainer = OCMClassMock([CKContainer class]);
100 OCMStub([self.mockContainer containerWithIdentifier:[OCMArg isKindOfClass:[NSString class]]]).andReturn(self.mockContainer);
101 OCMStub([self.mockContainer defaultContainer]).andReturn(self.mockContainer);
102 OCMStub([self.mockContainer alloc]).andReturn(self.mockContainer);
103 OCMStub([self.mockContainer containerIdentifier]).andReturn(SecCKKSContainerName);
104 OCMStub([self.mockContainer initWithContainerID: [OCMArg any] options: [OCMArg any]]).andReturn(self.mockContainer);
105 OCMStub([self.mockContainer privateCloudDatabase]).andReturn(self.mockDatabase);
106 OCMStub([self.mockContainer serverPreferredPushEnvironmentWithCompletionHandler: ([OCMArg invokeBlockWithArgs:@"fake APS push string", [NSNull null], nil])]);
108 // If you want to change this, you'll need to update the mock
109 _ckDeviceID = @"fake-cloudkit-device-id";
110 OCMStub([self.mockContainer fetchCurrentDeviceIDWithCompletionHandler: ([OCMArg invokeBlockWithArgs:self.ckDeviceID, [NSNull null], nil])]);
112 self.accountStatus = CKAccountStatusAvailable;
113 self.supportsDeviceToDeviceEncryption = YES;
115 // Inject a fake operation dependency into the manager object, so that the tests can perform setup and mock expectations before zone setup begins
116 // Also blocks all CK account state retrieval operations (but not circle status ones)
117 self.ckaccountHoldOperation = [NSBlockOperation named:@"ckaccount-hold" withBlock:^{
118 secnotice("ckks", "CKKS CK account status test hold released");
121 OCMStub([self.mockContainer accountStatusWithCompletionHandler:
122 [OCMArg checkWithBlock:^BOOL(void (^passedBlock) (CKAccountStatus accountStatus,
123 NSError * _Nullable error)) {
126 __strong __typeof(self) strongSelf = weakSelf;
127 NSBlockOperation* fulfillBlock = [NSBlockOperation named:@"account-status-completion" withBlock: ^{
128 passedBlock(weakSelf.accountStatus, nil);
130 [fulfillBlock addDependency: strongSelf.ckaccountHoldOperation];
131 [strongSelf.operationQueue addOperation: fulfillBlock];
138 OCMStub([self.mockContainer accountInfoWithCompletionHandler:
139 [OCMArg checkWithBlock:^BOOL(void (^passedBlock) (CKAccountInfo* accountInfo,
141 __strong __typeof(self) strongSelf = weakSelf;
142 if(passedBlock && strongSelf) {
143 NSBlockOperation* fulfillBlock = [NSBlockOperation named:@"account-info-completion" withBlock: ^{
144 __strong __typeof(self) blockStrongSelf = weakSelf;
145 CKAccountInfo* account = [[CKAccountInfo alloc] init];
146 account.accountStatus = blockStrongSelf.accountStatus;
147 account.supportsDeviceToDeviceEncryption = blockStrongSelf.supportsDeviceToDeviceEncryption;
148 account.accountPartition = CKAccountPartitionTypeProduction;
149 passedBlock((CKAccountInfo*)account, nil);
151 [fulfillBlock addDependency: strongSelf.ckaccountHoldOperation];
152 [strongSelf.operationQueue addOperation: fulfillBlock];
159 self.circleStatus = kSOSCCInCircle;
160 self.mockAccountStateTracker = OCMClassMock([CKKSCKAccountStateTracker class]);
161 OCMStub([self.mockAccountStateTracker getCircleStatus]).andCall(self, @selector(circleStatus));
163 // If we're in circle, come up with a fake circle id. Otherwise, return an error.
164 self.circlePeerID = @"fake-circle-id";
165 OCMStub([self.mockAccountStateTracker fetchCirclePeerID:
166 [OCMArg checkWithBlock:^BOOL(void (^passedBlock) (NSString* peerID,
168 __strong __typeof(self) strongSelf = weakSelf;
169 if(passedBlock && strongSelf) {
170 if(strongSelf.circleStatus == kSOSCCInCircle) {
171 passedBlock(strongSelf.circlePeerID, nil);
173 passedBlock(nil, [NSError errorWithDomain:@"securityd" code:errSecInternalError userInfo:@{NSLocalizedDescriptionKey:@"no account, no circle id"}]);
181 self.aksLockState = false; // Lie and say AKS is always unlocked
182 self.mockLockStateTracker = OCMClassMock([CKKSLockStateTracker class]);
183 OCMStub([self.mockLockStateTracker queryAKSLocked]).andCall(self, @selector(aksLockState));
185 self.mockFakeCKModifyRecordZonesOperation = OCMClassMock([FakeCKModifyRecordZonesOperation class]);
186 OCMStub([self.mockFakeCKModifyRecordZonesOperation ckdb]).andReturn(self.zones);
188 self.mockFakeCKModifySubscriptionsOperation = OCMClassMock([FakeCKModifySubscriptionsOperation class]);
189 OCMStub([self.mockFakeCKModifySubscriptionsOperation ckdb]).andReturn(self.zones);
191 self.mockFakeCKFetchRecordZoneChangesOperation = OCMClassMock([FakeCKFetchRecordZoneChangesOperation class]);
192 OCMStub([self.mockFakeCKFetchRecordZoneChangesOperation ckdb]).andReturn(self.zones);
194 OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
195 __strong __typeof(self) strongSelf = weakSelf;
197 if ([obj isKindOfClass: [FakeCKFetchRecordZoneChangesOperation class]]) {
198 if(strongSelf.silentFetchesAllowed) {
201 FakeCKFetchRecordZoneChangesOperation *frzco = (FakeCKFetchRecordZoneChangesOperation *)obj;
202 [strongSelf.operationQueue addOperation: frzco];
209 self.testZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"testzone" ownerName:CKCurrentUserDefaultName];
211 // Inject a fake operation dependency into the manager object, so that the tests can perform setup and mock expectations before zone setup begins
212 // Also blocks all CK account state retrieval operations (but not circle status ones)
213 self.ckksHoldOperation = [[NSBlockOperation alloc] init];
214 [self.ckksHoldOperation addExecutionBlock:^{
215 secnotice("ckks", "CKKS testing hold released");
217 self.ckksHoldOperation.name = @"ckks-hold";
219 self.mockCKKSViewManager = OCMClassMock([CKKSViewManager class]);
220 OCMStub([self.mockCKKSViewManager viewList]).andCall(self, @selector(managedViewList));
221 OCMStub([self.mockCKKSViewManager syncBackupAndNotifyAboutSync]);
223 self.injectedManager = [[CKKSViewManager alloc] initWithContainerName:SecCKKSContainerName
224 usePCS:SecCKKSContainerUsePCS
225 fetchRecordZoneChangesOperationClass:[FakeCKFetchRecordZoneChangesOperation class]
226 modifySubscriptionsOperationClass:[FakeCKModifySubscriptionsOperation class]
227 modifyRecordZonesOperationClass:[FakeCKModifyRecordZonesOperation class]
228 apsConnectionClass:[FakeAPSConnection class]
229 nsnotificationCenterClass:[FakeNSNotificationCenter class]
230 notifierClass:[FakeCKKSNotifier class]
231 setupHold:self.ckksHoldOperation];
233 [CKKSViewManager resetManager:false setTo:self.injectedManager];
235 // Make a new fake keychain
236 NSString* smallName = [self.name componentsSeparatedByString:@" "][1];
237 smallName = [smallName stringByReplacingOccurrencesOfString:@"]" withString:@""];
239 NSString* tmp_dir = [NSString stringWithFormat: @"/tmp/%@.%X", smallName, arc4random()];
240 [[NSFileManager defaultManager] createDirectoryAtPath:[NSString stringWithFormat: @"%@/Library/Keychains", tmp_dir] withIntermediateDirectories:YES attributes:nil error:NULL];
242 SetCustomHomeURLString((__bridge CFStringRef) tmp_dir);
243 SecKeychainDbReset(NULL);
245 // Actually load the database.
246 kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) { return false; });
249 -(CKKSCKAccountStateTracker*)accountStateTracker {
250 return self.injectedManager.accountTracker;
253 -(CKKSLockStateTracker*)lockStateTracker {
254 return self.injectedManager.lockStateTracker;
257 -(NSSet*)managedViewList {
258 return (NSSet*) CFBridgingRelease(SOSViewCopyViewSet(kViewSetCKKS));
261 -(void)expectCKFetch {
262 // Create an object for the block to retain and modify
263 BoolHolder* runAlready = [[BoolHolder alloc] init];
265 __weak __typeof(self) weakSelf = self;
266 [[self.mockDatabase expect] addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
267 __strong __typeof(self) strongSelf = weakSelf;
268 if(runAlready.state) {
272 if ([obj isKindOfClass: [FakeCKFetchRecordZoneChangesOperation class]]) {
274 runAlready.state = true;
276 FakeCKFetchRecordZoneChangesOperation *frzco = (FakeCKFetchRecordZoneChangesOperation *)obj;
277 [strongSelf.operationQueue addOperation: frzco];
283 - (void)startCKKSSubsystem {
284 [self startCKAccountStatusMock];
285 [self startCKKSSubsystemOnly];
288 - (void)startCKKSSubsystemOnly {
289 // Note: currently, based on how we're mocking up the zone creation and zone subscription operation,
290 // they will 'fire' before this method is called. It's harmless, since the mocks immediately succeed
291 // and return; it's just a tad confusing.
292 if([self.ckksHoldOperation isPending]) {
293 [self.operationQueue addOperation: self.ckksHoldOperation];
297 - (void)startCKAccountStatusMock {
298 // Note: currently, based on how we're mocking up the zone creation and zone subscription operation,
299 // they will 'fire' before this method is called. It's harmless, since the mocks immediately succeed
300 // and return; it's just a tad confusing.
301 if([self.ckaccountHoldOperation isPending]) {
302 [self.operationQueue addOperation: self.ckaccountHoldOperation];
306 -(void)holdCloudKitModifications {
307 self.ckModifyHoldOperation = [NSBlockOperation blockOperationWithBlock:^{
308 secnotice("ckks", "Released CloudKit modification hold.");
311 -(void)releaseCloudKitModificationHold {
312 if([self.ckModifyHoldOperation isPending]) {
313 [self.operationQueue addOperation: self.ckModifyHoldOperation];
317 - (void)expectCKModifyItemRecords: (NSUInteger) expectedNumberOfRecords currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords zoneID: (CKRecordZoneID*) zoneID {
318 [self expectCKModifyItemRecords:expectedNumberOfRecords
319 currentKeyPointerRecords:expectedCurrentKeyRecords
324 - (void)expectCKModifyItemRecords: (NSUInteger) expectedNumberOfRecords currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords zoneID: (CKRecordZoneID*) zoneID checkItem: (BOOL (^)(CKRecord*)) checkItem {
325 [self expectCKModifyItemRecords:expectedNumberOfRecords
327 currentKeyPointerRecords:expectedCurrentKeyRecords
329 checkItem:checkItem];
332 - (void)expectCKModifyItemRecords:(NSUInteger)expectedNumberOfModifiedRecords
333 deletedRecords:(NSUInteger)expectedNumberOfDeletedRecords
334 currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
335 zoneID:(CKRecordZoneID*)zoneID
336 checkItem:(BOOL (^)(CKRecord*))checkItem {
337 // We're updating the device state type on every update, so add it in here
338 NSMutableDictionary* expectedRecords = [@{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfModifiedRecords],
339 SecCKRecordCurrentKeyType: [NSNumber numberWithUnsignedInteger: expectedCurrentKeyRecords],
340 SecCKRecordDeviceStateType: [NSNumber numberWithUnsignedInt: 1],
343 if(SecCKKSSyncManifests()) {
344 expectedRecords[SecCKRecordManifestType] = [NSNumber numberWithInt: 1];
345 expectedRecords[SecCKRecordManifestLeafType] = [NSNumber numberWithInt: 72];
348 NSDictionary* deletedRecords = nil;
349 if(expectedNumberOfDeletedRecords != 0) {
350 deletedRecords = @{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfDeletedRecords]};
353 [self expectCKModifyRecords:expectedRecords
354 deletedRecordTypeCounts:deletedRecords
356 checkModifiedRecord: ^BOOL (CKRecord* record){
357 if([record.recordType isEqualToString: SecCKRecordItemType] && checkItem) {
358 return checkItem(record);
363 runAfterModification:nil];
368 - (void)expectCKModifyKeyRecords: (NSUInteger) expectedNumberOfRecords currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords zoneID: (CKRecordZoneID*) zoneID {
369 NSNumber* nkeys = [NSNumber numberWithUnsignedInteger: expectedNumberOfRecords];
370 NSNumber* ncurrentkeys = [NSNumber numberWithUnsignedInteger: expectedCurrentKeyRecords];
372 [self expectCKModifyRecords:@{SecCKRecordIntermediateKeyType: nkeys, SecCKRecordCurrentKeyType: ncurrentkeys}
373 deletedRecordTypeCounts:nil
375 checkModifiedRecord:nil
376 runAfterModification:nil];
379 - (void)expectCKModifyRecords:(NSDictionary<NSString*, NSNumber*>*) expectedRecordTypeCounts
380 deletedRecordTypeCounts:(NSDictionary<NSString*, NSNumber*>*) expectedDeletedRecordTypeCounts
381 zoneID:(CKRecordZoneID*) zoneID
382 checkModifiedRecord:(BOOL (^)(CKRecord*)) checkModifiedRecord
383 runAfterModification:(void (^) ())afterModification
385 __weak __typeof(self) weakSelf = self;
387 // Create an object for the block to retain and modify
388 BoolHolder* runAlready = [[BoolHolder alloc] init];
390 secnotice("fakecloudkit", "expecting an operation matching modifications: %@ deletions: %@",
391 expectedRecordTypeCounts, expectedDeletedRecordTypeCounts);
393 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
394 secnotice("fakecloudkit", "Received an operation, checking");
395 __block bool matches = false;
396 if(runAlready.state) {
397 secnotice("fakecloudkit", "Run already, skipping");
401 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
402 __strong __typeof(weakSelf) strongSelf = weakSelf;
403 XCTAssertNotNil(strongSelf, "self exists");
405 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
408 NSMutableDictionary<NSString*, NSNumber*>* modifiedRecordTypeCounts = [[NSMutableDictionary alloc] init];
409 NSMutableDictionary<NSString*, NSNumber*>* deletedRecordTypeCounts = [[NSMutableDictionary alloc] init];
411 // First: check if it matches. If it does, _then_ execute the operation.
412 // Supports single-zone atomic writes only
415 // We only care about atomic operations
416 secnotice("fakecloudkit", "Not an atomic operation; quitting: %@", op);
420 FakeCKZone* zone = strongSelf.zones[zoneID];
421 XCTAssertNotNil(zone, "Have a zone for these records");
423 for(CKRecord* record in op.recordsToSave) {
424 if(![record.recordID.zoneID isEqual: zoneID]) {
425 secnotice("fakecloudkit", "Modified record zone ID mismatch: %@ %@", zoneID, record.recordID.zoneID);
429 if([zone errorFromSavingRecord: record]) {
430 secnotice("fakecloudkit", "Record zone rejected record write: %@", record);
434 NSNumber* currentCountNumber = modifiedRecordTypeCounts[record.recordType];
435 NSUInteger currentCount = currentCountNumber ? [currentCountNumber unsignedIntegerValue] : 0;
436 modifiedRecordTypeCounts[record.recordType] = [NSNumber numberWithUnsignedInteger: currentCount + 1];
439 for(CKRecordID* recordID in op.recordIDsToDelete) {
440 if(![recordID.zoneID isEqual: zoneID]) {
442 secnotice("fakecloudkit", "Deleted record zone ID mismatch: %@ %@", zoneID, recordID.zoneID);
445 // Find the object in CloudKit, and record its type
446 CKRecord* record = strongSelf.zones[zoneID].currentDatabase[recordID];
448 NSNumber* currentCountNumber = deletedRecordTypeCounts[record.recordType];
449 NSUInteger currentCount = currentCountNumber ? [currentCountNumber unsignedIntegerValue] : 0;
450 deletedRecordTypeCounts[record.recordType] = [NSNumber numberWithUnsignedInteger: currentCount + 1];
454 NSMutableDictionary* filteredExpectedRecordTypeCounts = [expectedRecordTypeCounts mutableCopy];
455 for(NSString* key in filteredExpectedRecordTypeCounts.allKeys) {
456 if([filteredExpectedRecordTypeCounts[key] isEqual: [NSNumber numberWithInt:0]]) {
457 filteredExpectedRecordTypeCounts[key] = nil;
460 filteredExpectedRecordTypeCounts[SecCKRecordManifestType] = modifiedRecordTypeCounts[SecCKRecordManifestType];
461 filteredExpectedRecordTypeCounts[SecCKRecordManifestLeafType] = modifiedRecordTypeCounts[SecCKRecordManifestLeafType];
463 // Inspect that we have exactly the same records as we expect
464 if(expectedRecordTypeCounts) {
465 matches &= !![modifiedRecordTypeCounts isEqual: filteredExpectedRecordTypeCounts];
467 secnotice("fakecloudkit", "Record number mismatch: %@ %@", modifiedRecordTypeCounts, filteredExpectedRecordTypeCounts);
471 matches &= op.recordsToSave.count == 0u;
473 secnotice("fakecloudkit", "Record number mismatch: %@ 0", modifiedRecordTypeCounts);
477 if(expectedDeletedRecordTypeCounts) {
478 matches &= !![deletedRecordTypeCounts isEqual: expectedDeletedRecordTypeCounts];
480 secnotice("fakecloudkit", "Deleted record number mismatch: %@ %@", deletedRecordTypeCounts, expectedDeletedRecordTypeCounts);
484 matches &= op.recordIDsToDelete.count == 0u;
486 secnotice("fakecloudkit", "Deleted record number mismatch: %@ 0", deletedRecordTypeCounts);
491 // We have the right number of things, and their etags match. Ensure that they have the right etags
492 if(matches && checkModifiedRecord) {
493 // Clearly we have the right number of things. Call checkRecord on them...
494 for(CKRecord* record in op.recordsToSave) {
495 matches &= !!(checkModifiedRecord(record));
497 secnotice("fakecloudkit", "Check record reports NO: %@ 0", record);
504 // Emulate cloudkit and schedule the operation for execution. Be sure to wait for this operation
505 // if you'd like to read the data from this write.
506 NSBlockOperation* ckop = [NSBlockOperation named:@"cloudkit-write" withBlock: ^{
507 @synchronized(zone.currentDatabase) {
508 NSMutableArray* savedRecords = [[NSMutableArray alloc] init];
509 for(CKRecord* record in op.recordsToSave) {
510 CKRecord* reflectedRecord = [record copy];
511 reflectedRecord.modificationDate = [NSDate date];
513 [zone addToZone: reflectedRecord];
515 [savedRecords addObject:reflectedRecord];
516 op.perRecordCompletionBlock(reflectedRecord, nil);
518 for(CKRecordID* recordID in op.recordIDsToDelete) {
519 // I don't believe CloudKit fails an operation if you delete a record that's not there, so:
520 [zone deleteCKRecordIDFromZone: recordID];
523 op.modifyRecordsCompletionBlock(savedRecords, op.recordIDsToDelete, nil);
525 if(afterModification) {
532 [ckop addNullableDependency:strongSelf.ckModifyHoldOperation];
533 [strongSelf.operationQueue addOperation: ckop];
537 runAlready.state = true;
539 return matches ? YES : NO;
543 - (void)failNextZoneCreation:(CKRecordZoneID*)zoneID {
544 XCTAssertNil(self.zones[zoneID], "Zone does not exist yet");
545 self.zones[zoneID] = [[FakeCKZone alloc] initZone: zoneID];
546 self.zones[zoneID].creationError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
549 // Report success, but don't actually create the zone.
550 // This way, you can find ZoneNotFound errors later on
551 - (void)failNextZoneCreationSilently:(CKRecordZoneID*)zoneID {
552 XCTAssertNil(self.zones[zoneID], "Zone does not exist yet");
553 self.zones[zoneID] = [[FakeCKZone alloc] initZone: zoneID];
554 self.zones[zoneID].failCreationSilently = true;
557 - (void)failNextZoneSubscription:(CKRecordZoneID*)zoneID {
558 XCTAssertNotNil(self.zones[zoneID], "Zone exists");
559 self.zones[zoneID].subscriptionError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
562 - (void)failNextZoneSubscription:(CKRecordZoneID*)zoneID withError:(NSError*)error {
563 XCTAssertNotNil(self.zones[zoneID], "Zone exists");
564 self.zones[zoneID].subscriptionError = error;
567 - (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID {
568 [self failNextCKAtomicModifyItemRecordsUpdateFailure:zoneID blockAfterReject:nil];
571 - (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID blockAfterReject: (void (^)())blockAfterReject {
572 [self failNextCKAtomicModifyItemRecordsUpdateFailure:zoneID blockAfterReject:blockAfterReject withError:nil];
575 - (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID blockAfterReject: (void (^)())blockAfterReject withError:(NSError*)error {
576 __weak __typeof(self) weakSelf = self;
578 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
579 __strong __typeof(weakSelf) strongSelf = weakSelf;
580 XCTAssertNotNil(strongSelf, "self exists");
582 __block bool rejected = false;
583 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
584 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
587 // We only care about atomic operations
591 // We want to only match zone updates pertaining to this zone
592 for(CKRecord* record in op.recordsToSave) {
593 if(![record.recordID.zoneID isEqual: zoneID]) {
598 FakeCKZone* zone = strongSelf.zones[zoneID];
599 XCTAssertNotNil(zone, "Have a zone for these records");
604 [strongSelf rejectWrite: op withError:error];
606 NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = [[NSMutableDictionary alloc] init];
607 [strongSelf rejectWrite: op failedRecords:failedRecords];
610 if(blockAfterReject) {
614 return rejected ? YES : NO;
618 - (void)expectCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID {
619 __weak __typeof(self) weakSelf = self;
621 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
622 __strong __typeof(weakSelf) strongSelf = weakSelf;
623 XCTAssertNotNil(strongSelf, "self exists");
625 __block bool rejected = false;
626 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
627 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
630 // We only care about atomic operations
634 // We want to only match zone updates pertaining to this zone
635 for(CKRecord* record in op.recordsToSave) {
636 if(![record.recordID.zoneID isEqual: zoneID]) {
641 FakeCKZone* zone = strongSelf.zones[zoneID];
642 XCTAssertNotNil(zone, "Have a zone for these records");
644 NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = [[NSMutableDictionary alloc] init];
646 @synchronized(zone.currentDatabase) {
647 for(CKRecord* record in op.recordsToSave) {
648 // Check if we should allow this transaction
649 NSError* recordSaveError = [zone errorFromSavingRecord: record];
650 if(recordSaveError) {
651 failedRecords[record.recordID] = recordSaveError;
658 [strongSelf rejectWrite: op failedRecords:failedRecords];
661 return rejected ? YES : NO;
665 -(void)rejectWrite:(CKModifyRecordsOperation*)op withError:(NSError*)error {
666 // Emulate cloudkit and schedule the operation for execution. Be sure to wait for this operation
667 // if you'd like to read the data from this write.
668 NSBlockOperation* ckop = [NSBlockOperation named:@"cloudkit-reject-write-error" withBlock: ^{
669 op.modifyRecordsCompletionBlock(nil, nil, error);
672 [ckop addNullableDependency: self.ckModifyHoldOperation];
673 [self.operationQueue addOperation: ckop];
676 -(void)rejectWrite:(CKModifyRecordsOperation*)op failedRecords:(NSMutableDictionary<CKRecordID*, NSError*>*)failedRecords {
677 // Add the batch request failed errors
678 for(CKRecord* record in op.recordsToSave) {
679 NSError* exists = failedRecords[record.recordID];
681 // TODO: might have important userInfo, but we're not mocking that yet
682 failedRecords[record.recordID] = [[CKPrettyError alloc] initWithDomain: CKErrorDomain code: CKErrorBatchRequestFailed userInfo: @{}];
686 NSError* error = [[CKPrettyError alloc] initWithDomain: CKErrorDomain code: CKErrorPartialFailure userInfo: @{CKPartialErrorsByItemIDKey: failedRecords}];
688 // Emulate cloudkit and schedule the operation for execution. Be sure to wait for this operation
689 // if you'd like to read the data from this write.
690 NSBlockOperation* ckop = [NSBlockOperation named:@"cloudkit-reject-write" withBlock: ^{
691 op.modifyRecordsCompletionBlock(nil, nil, error);
694 [ckop addNullableDependency: self.ckModifyHoldOperation];
695 [self.operationQueue addOperation: ckop];
698 - (void)expectCKDeleteItemRecords:(NSUInteger)expectedNumberOfRecords
699 zoneID:(CKRecordZoneID*) zoneID {
701 // We're updating the device state type on every update, so add it in here
702 NSMutableDictionary* expectedRecords = [@{
703 SecCKRecordDeviceStateType: [NSNumber numberWithUnsignedInt: 1],
705 if(SecCKKSSyncManifests()) {
706 // TODO: this really shouldn't be 2.
707 expectedRecords[SecCKRecordManifestType] = [NSNumber numberWithInt: 2];
708 expectedRecords[SecCKRecordManifestLeafType] = [NSNumber numberWithInt: 72];
711 [self expectCKModifyRecords:expectedRecords
712 deletedRecordTypeCounts:@{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfRecords]}
714 checkModifiedRecord:nil
715 runAfterModification:nil];
718 -(void)waitForCKModifications {
719 // CloudKit modifications are put on the local queue.
720 // This is heavyweight but should suffice.
721 [self.operationQueue waitUntilAllOperationsAreFinished];
725 // Put teardown code here. This method is called after the invocation of each test method in the class.
727 if(SecCKKSIsEnabled()) {
728 // Ensure we don't have any blocking operations
729 [self startCKKSSubsystem];
731 [self waitForCKModifications];
733 XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:2*NSEC_PER_SEC],
734 "Timeout did not occur waiting for SecCKKSInitialize");
736 // Make sure this happens before teardown.
737 XCTAssertEqual(0, [self.accountStateTracker.finishedInitialCalls wait:1*NSEC_PER_SEC], "Account state tracker initialized itself");
742 [self.injectedManager cancelPendingOperations];
743 [CKKSViewManager resetManager:true setTo:nil];
744 self.injectedManager = nil;
746 [self.mockAccountStateTracker stopMocking];
747 self.mockAccountStateTracker = nil;
749 [self.mockLockStateTracker stopMocking];
750 self.mockLockStateTracker = nil;
752 [self.mockCKKSViewManager stopMocking];
753 self.mockCKKSViewManager = nil;
755 [self.mockFakeCKModifyRecordZonesOperation stopMocking];
756 self.mockFakeCKModifyRecordZonesOperation = nil;
758 [self.mockFakeCKModifySubscriptionsOperation stopMocking];
759 self.mockFakeCKModifySubscriptionsOperation = nil;
761 [self.mockFakeCKFetchRecordZoneChangesOperation stopMocking];
762 self.mockFakeCKFetchRecordZoneChangesOperation = nil;
764 [self.mockDatabase stopMocking];
765 self.mockDatabase = nil;
767 [self.mockContainer stopMocking];
768 self.mockContainer = nil;
771 self.operationQueue = nil;
772 self.ckksHoldOperation = nil;
773 self.ckaccountHoldOperation = nil;
775 SecCKKSTestResetFlags();
778 - (CKKSKey*) fakeTLK: (CKRecordZoneID*)zoneID {
779 CKKSKey* key = [[CKKSKey alloc] initSelfWrappedWithAESKey:[[CKKSAESSIVKey alloc] initWithBase64: @"uImdbZ7Zg+6WJXScTnRBfNmoU1UiMkSYxWc+d1Vuq3IFn2RmTRkTdWTe3HmeWo1pAomqy+upK8KHg2PGiRGhqg=="]
780 uuid:[[NSUUID UUID] UUIDString]
781 keyclass:SecCKKSKeyClassTLK
782 state: SecCKKSProcessedStateLocal
786 [key CKRecordWithZoneID: zoneID];