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 "keychain/securityd/Regressions/SecdTestKeychainUtilities.h"
36 #include <utilities/SecFileLocations.h>
37 #include "keychain/securityd/SecItemServer.h"
40 #include "keychain/securityd/spi.h"
43 #include <Security/SecureObjectSync/SOSViews.h>
45 #include <utilities/SecDb.h>
46 #include "keychain/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 "tests/secdmockaks/mockaks.h"
58 #import "utilities/SecTapToRadar.h"
60 #import "MockCloudKit.h"
62 @interface BoolHolder : NSObject
66 @implementation BoolHolder
69 // Inform OCMock about the internals of CKContainer
70 @interface CKContainer ()
71 - (void)_checkSelfCloudServicesEntitlement;
75 @implementation CloudKitMockXCTest
76 @synthesize aksLockState = _aksLockState;
82 SecCKKSSetReduceRateLimiting(true);
86 securityd_init_local_spi();
90 - (BOOL)isRateLimited:(SecTapToRadar *)ttrRequest
92 return self.isTTRRatelimited;
95 - (BOOL)askUserIfTTR:(SecTapToRadar *)ttrRequest
100 - (void)triggerTapToRadar:(SecTapToRadar *)ttrRequest
102 [self.ttrExpectation fulfill];
108 NSString* testName = [self.name componentsSeparatedByString:@" "][1];
109 testName = [testName stringByReplacingOccurrencesOfString:@"]" withString:@""];
110 secnotice("ckkstest", "Beginning test %@", testName);
112 // All tests start with the same flag set.
113 SecCKKSTestResetFlags();
114 SecCKKSTestSetDisableSOS(true);
116 self.silentFetchesAllowed = true;
117 self.silentZoneDeletesAllowed = false; // Set to true if you want to do any deletes
119 __weak __typeof(self) weakSelf = self;
120 self.operationQueue = [[NSOperationQueue alloc] init];
121 self.operationQueue.maxConcurrentOperationCount = 1;
123 self.zones = [[NSMutableDictionary alloc] init];
125 self.apsEnvironment = @"fake APS push string";
127 // Static variables are a scourge. Let's reset this one...
128 [OctagonAPSReceiver resetGlobalEnviornmentMap];
130 self.mockDatabaseExceptionCatcher = OCMStrictClassMock([CKDatabase class]);
131 self.mockDatabase = OCMStrictClassMock([CKDatabase class]);
132 self.mockContainerExpectations = OCMStrictClassMock([CKContainer class]);
133 self.mockContainer = OCMClassMock([CKContainer class]);
134 OCMStub([self.mockContainer containerWithIdentifier:[OCMArg isKindOfClass:[NSString class]]]).andReturn(self.mockContainer);
135 OCMStub([self.mockContainer defaultContainer]).andReturn(self.mockContainer);
136 OCMStub([self.mockContainer alloc]).andReturn(self.mockContainer);
137 OCMStub([self.mockContainer containerIdentifier]).andReturn(SecCKKSContainerName);
138 OCMStub([self.mockContainer initWithContainerID: [OCMArg any] options: [OCMArg any]]).andReturn(self.mockContainer);
139 OCMStub([self.mockContainer privateCloudDatabase]).andReturn(self.mockDatabaseExceptionCatcher);
140 OCMStub([self.mockContainer serverPreferredPushEnvironmentWithCompletionHandler: ([OCMArg invokeBlockWithArgs:self.apsEnvironment, [NSNull null], nil])]);
141 OCMStub([self.mockContainer submitEventMetric:[OCMArg any]]).andCall(self, @selector(ckcontainerSubmitEventMetric:));
143 // Use two layers of mockDatabase here, so we can both add Expectations and catch the exception (instead of crash) when one fails.
144 OCMStub([self.mockDatabaseExceptionCatcher addOperation:[OCMArg any]]).andCall(self, @selector(ckdatabaseAddOperation:));
146 // If you want to change this, you'll need to update the mock
147 _ckDeviceID = [NSString stringWithFormat:@"fake-cloudkit-device-id-%@", testName];
148 OCMStub([self.mockContainer fetchCurrentDeviceIDWithCompletionHandler: ([OCMArg invokeBlockWithArgs:self.ckDeviceID, [NSNull null], nil])]);
150 self.accountStatus = CKAccountStatusAvailable;
151 self.iCloudHasValidCredentials = YES;
153 self.fakeHSA2AccountStatus = CKKSAccountStatusAvailable;
155 // Inject a fake operation dependency so we won't respond with the CloudKit account status immediately
156 // The CKKSAccountStateTracker won't send any login/logout calls without that information, so this blocks all CKKS setup
157 self.ckaccountHoldOperation = [NSBlockOperation named:@"ckaccount-hold" withBlock:^{
158 secnotice("ckks", "CKKS CK account status test hold released");
161 OCMStub([self.mockContainer accountStatusWithCompletionHandler:
162 [OCMArg checkWithBlock:^BOOL(void (^passedBlock) (CKAccountStatus accountStatus,
163 NSError * _Nullable error)) {
166 __strong __typeof(self) strongSelf = weakSelf;
167 NSBlockOperation* fulfillBlock = [NSBlockOperation named:@"account-status-completion" withBlock: ^{
168 passedBlock(weakSelf.accountStatus, nil);
170 [fulfillBlock addDependency: strongSelf.ckaccountHoldOperation];
171 [strongSelf.operationQueue addOperation: fulfillBlock];
178 OCMStub([self.mockContainer accountInfoWithCompletionHandler:
179 [OCMArg checkWithBlock:^BOOL(void (^passedBlock) (CKAccountInfo* accountInfo,
181 __strong __typeof(self) strongSelf = weakSelf;
182 if(passedBlock && strongSelf) {
183 NSBlockOperation* fulfillBlock = [NSBlockOperation named:@"account-info-completion" withBlock: ^{
184 __strong __typeof(self) blockStrongSelf = weakSelf;
185 CKAccountInfo* account = [[CKAccountInfo alloc] init];
186 account.accountStatus = blockStrongSelf.accountStatus;
187 account.hasValidCredentials = blockStrongSelf.iCloudHasValidCredentials;
188 account.accountPartition = CKAccountPartitionTypeProduction;
189 passedBlock((CKAccountInfo*)account, nil);
191 [fulfillBlock addDependency: strongSelf.ckaccountHoldOperation];
192 [strongSelf.operationQueue addOperation: fulfillBlock];
199 self.mockAccountStateTracker = OCMClassMock([CKKSAccountStateTracker class]);
200 OCMStub([self.mockAccountStateTracker getCircleStatus]).andCall(self, @selector(circleStatus));
202 // Fake out SOS peers
203 // One trusted non-self peer, but it doesn't have any Octagon keys. Your test can change this if it wants.
204 // However, note that [self putFakeDeviceStatusInCloudKit:] will likely not do what you want after you change this
205 CKKSSOSSelfPeer* currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"local-peer"
206 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
207 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
208 viewList:self.managedViewList];
210 self.mockSOSAdapter = [[CKKSMockSOSPresentAdapter alloc] initWithSelfPeer:currentSelfPeer
211 trustedPeers:[NSSet set]
214 // If we're in circle, come up with a fake circle id. Otherwise, return an error.
215 OCMStub([self.mockAccountStateTracker fetchCirclePeerID:
216 [OCMArg checkWithBlock:^BOOL(void (^passedBlock) (NSString* peerID,
218 __strong __typeof(self) strongSelf = weakSelf;
219 if(passedBlock && strongSelf) {
220 if(strongSelf.mockSOSAdapter.circleStatus == kSOSCCInCircle) {
221 passedBlock(strongSelf.mockSOSAdapter.selfPeer.peerID, nil);
223 passedBlock(nil, [NSError errorWithDomain:@"securityd" code:errSecInternalError userInfo:@{NSLocalizedDescriptionKey:@"no account, no circle id"}]);
231 self.aksLockState = false; // Lie and say AKS is always unlocked
232 self.mockLockStateTracker = OCMClassMock([CKKSLockStateTracker class]);
233 OCMStub([self.mockLockStateTracker queryAKSLocked]).andCall(self, @selector(aksLockState));
235 self.mockTTR = OCMClassMock([SecTapToRadar class]);
236 OCMStub([self.mockTTR isRateLimited:[OCMArg any]]).andCall(self, @selector(isRateLimited:));
237 OCMStub([self.mockTTR askUserIfTTR:[OCMArg any]]).andCall(self, @selector(askUserIfTTR:));
238 OCMStub([self.mockTTR triggerTapToRadar:[OCMArg any]]).andCall(self, @selector(triggerTapToRadar:));
239 self.isTTRRatelimited = true;
241 self.mockFakeCKModifyRecordZonesOperation = OCMClassMock([FakeCKModifyRecordZonesOperation class]);
242 OCMStub([self.mockFakeCKModifyRecordZonesOperation ckdb]).andReturn(self.zones);
243 OCMStub([self.mockFakeCKModifyRecordZonesOperation shouldFailModifyRecordZonesOperation]).andCall(self, @selector(shouldFailModifyRecordZonesOperation));
245 OCMStub([self.mockFakeCKModifyRecordZonesOperation ensureZoneDeletionAllowed:[OCMArg any]]).andCall(self, @selector(ensureZoneDeletionAllowed:));
247 self.mockFakeCKModifySubscriptionsOperation = OCMClassMock([FakeCKModifySubscriptionsOperation class]);
248 OCMStub([self.mockFakeCKModifySubscriptionsOperation ckdb]).andReturn(self.zones);
250 self.mockFakeCKFetchRecordZoneChangesOperation = OCMClassMock([FakeCKFetchRecordZoneChangesOperation class]);
251 OCMStub([self.mockFakeCKFetchRecordZoneChangesOperation ckdb]).andReturn(self.zones);
252 OCMStub([self.mockFakeCKFetchRecordZoneChangesOperation isNetworkReachable]).andCall(self, @selector(isNetworkReachable));
254 self.mockFakeCKFetchRecordsOperation = OCMClassMock([FakeCKFetchRecordsOperation class]);
255 OCMStub([self.mockFakeCKFetchRecordsOperation ckdb]).andReturn(self.zones);
257 self.mockFakeCKQueryOperation = OCMClassMock([FakeCKQueryOperation class]);
258 OCMStub([self.mockFakeCKQueryOperation ckdb]).andReturn(self.zones);
261 OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
262 __strong __typeof(self) strongSelf = weakSelf;
264 if ([obj isKindOfClass: [FakeCKFetchRecordZoneChangesOperation class]]) {
265 if(strongSelf.silentFetchesAllowed) {
268 FakeCKFetchRecordZoneChangesOperation *frzco = (FakeCKFetchRecordZoneChangesOperation *)obj;
269 [frzco addNullableDependency:strongSelf.ckFetchHoldOperation];
270 [strongSelf.operationQueue addOperation: frzco];
276 OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
277 __strong __typeof(self) strongSelf = weakSelf;
279 if ([obj isKindOfClass: [FakeCKFetchRecordsOperation class]]) {
280 if(strongSelf.silentFetchesAllowed) {
283 FakeCKFetchRecordsOperation *ffro = (FakeCKFetchRecordsOperation *)obj;
284 [ffro addNullableDependency:strongSelf.ckFetchHoldOperation];
285 [strongSelf.operationQueue addOperation: ffro];
291 OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
292 __strong __typeof(self) strongSelf = weakSelf;
294 if ([obj isKindOfClass: [FakeCKQueryOperation class]]) {
295 if(strongSelf.silentFetchesAllowed) {
298 FakeCKQueryOperation *fqo = (FakeCKQueryOperation *)obj;
299 [fqo addNullableDependency:strongSelf.ckFetchHoldOperation];
300 [strongSelf.operationQueue addOperation: fqo];
306 self.testZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"testzone" ownerName:CKCurrentUserDefaultName];
308 // We don't want to use class mocks here, because they don't play well with partial mocks
309 CKKSCloudKitClassDependencies* cloudKitClassDependencies = [[CKKSCloudKitClassDependencies alloc] initWithFetchRecordZoneChangesOperationClass:[FakeCKFetchRecordZoneChangesOperation class]
310 fetchRecordsOperationClass:[FakeCKFetchRecordsOperation class]
311 queryOperationClass:[FakeCKQueryOperation class]
312 modifySubscriptionsOperationClass:[FakeCKModifySubscriptionsOperation class]
313 modifyRecordZonesOperationClass:[FakeCKModifyRecordZonesOperation class]
314 apsConnectionClass:[FakeAPSConnection class]
315 nsnotificationCenterClass:[FakeNSNotificationCenter class]
316 nsdistributednotificationCenterClass:[FakeNSDistributedNotificationCenter class]
317 notifierClass:[FakeCKKSNotifier class]];
319 self.mockCKKSViewManager = OCMPartialMock(
320 [[CKKSViewManager alloc] initWithContainerName:SecCKKSContainerName
321 usePCS:SecCKKSContainerUsePCS
322 sosAdapter:self.mockSOSAdapter
323 cloudKitClassDependencies:cloudKitClassDependencies]);
325 OCMStub([self.mockCKKSViewManager defaultViewList]).andCall(self, @selector(managedViewList));
326 OCMStub([self.mockCKKSViewManager syncBackupAndNotifyAboutSync]);
327 OCMStub([self.mockCKKSViewManager waitForTrustReady]).andReturn(YES);
329 self.injectedManager = self.mockCKKSViewManager;
331 [CKKSViewManager resetManager:false setTo:self.injectedManager];
333 // Lie and say network is available
334 [self.reachabilityTracker setNetworkReachability:true];
336 // Make a new fake keychain
337 NSString* tmp_dir = [NSString stringWithFormat: @"/tmp/%@.%X", testName, arc4random()];
338 [[NSFileManager defaultManager] createDirectoryAtPath:[NSString stringWithFormat: @"%@/Library/Keychains", tmp_dir] withIntermediateDirectories:YES attributes:nil error:NULL];
340 SetCustomHomeURLString((__bridge CFStringRef) tmp_dir);
341 SecKeychainDbReset(NULL);
343 // Actually load the database.
344 kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) { return false; });
347 - (SOSAccountStatus*)circleStatus {
348 NSError* error = nil;
349 SOSCCStatus status = [self.mockSOSAdapter circleStatus:&error];
350 return [[SOSAccountStatus alloc] init:status error:error];
355 return _aksLockState;
358 - (void)setAksLockState:(bool)aksLockState
362 [SecMockAKS lockClassA];
364 [SecMockAKS unlockAllClasses];
366 _aksLockState = aksLockState;
369 - (bool)isNetworkReachable {
370 return self.reachabilityTracker.currentReachability;
373 - (void)ckcontainerSubmitEventMetric:(CKEventMetric*)metric {
375 [self.mockContainerExpectations submitEventMetric:metric];
376 } @catch (NSException *exception) {
377 XCTFail("Received an container exception when trying to add a metric: %@", exception);
381 - (void)ckdatabaseAddOperation:(NSOperation*)op {
383 [self.mockDatabase addOperation:op];
384 } @catch (NSException *exception) {
385 XCTFail("Received an database exception: %@", exception);
389 - (NSError* _Nullable)shouldFailModifyRecordZonesOperation {
390 NSError* error = self.nextModifyRecordZonesError;
392 self.nextModifyRecordZonesError = nil;
398 - (void)ensureZoneDeletionAllowed:(FakeCKZone*)zone {
399 XCTAssertTrue(self.silentZoneDeletesAllowed, "Should be allowing zone deletes");
402 -(CKKSAccountStateTracker*)accountStateTracker {
403 return self.injectedManager.accountTracker;
406 -(CKKSLockStateTracker*)lockStateTracker {
407 return self.injectedManager.lockStateTracker;
410 -(CKKSReachabilityTracker*)reachabilityTracker {
411 return self.injectedManager.reachabilityTracker;
414 -(NSSet*)managedViewList {
415 return (NSSet*) CFBridgingRelease(SOSViewCopyViewSet(kViewSetCKKS));
418 -(void)expectCKFetch {
419 [self expectCKFetchAndRunBeforeFinished: nil];
422 -(void)expectCKFetchAndRunBeforeFinished: (void (^)(void))blockAfterFetch {
423 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * op) {
426 runBeforeFinished:blockAfterFetch];
429 - (void)expectCKFetchWithFilter:(BOOL (^)(FakeCKFetchRecordZoneChangesOperation*))operationMatch
430 runBeforeFinished:(void (^)(void))blockAfterFetch
432 // Create an object for the block to retain and modify
433 BoolHolder* runAlready = [[BoolHolder alloc] init];
435 __weak __typeof(self) weakSelf = self;
436 [[self.mockDatabase expect] addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
437 __strong __typeof(self) strongSelf = weakSelf;
438 if(runAlready.state) {
442 secnotice("fakecloudkit", "Received an operation (%@), checking if it's a fetch changes", obj);
444 if ([obj isKindOfClass: [FakeCKFetchRecordZoneChangesOperation class]]) {
445 FakeCKFetchRecordZoneChangesOperation *frzco = (FakeCKFetchRecordZoneChangesOperation *)obj;
446 matches = operationMatch(frzco);
447 runAlready.state = true;
449 secnotice("fakecloudkit", "Running fetch changes: %@", obj);
450 frzco.blockAfterFetch = blockAfterFetch;
451 [frzco addNullableDependency: strongSelf.ckFetchHoldOperation];
452 [strongSelf.operationQueue addOperation: frzco];
458 -(void)expectCKFetchByRecordID {
459 // Create an object for the block to retain and modify
460 BoolHolder* runAlready = [[BoolHolder alloc] init];
462 __weak __typeof(self) weakSelf = self;
463 [[self.mockDatabase expect] addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
464 __strong __typeof(self) strongSelf = weakSelf;
465 if(runAlready.state) {
469 if ([obj isKindOfClass: [FakeCKFetchRecordsOperation class]]) {
471 runAlready.state = true;
473 FakeCKFetchRecordsOperation *ffro = (FakeCKFetchRecordsOperation *)obj;
474 [ffro addNullableDependency: strongSelf.ckFetchHoldOperation];
475 [strongSelf.operationQueue addOperation: ffro];
482 -(void)expectCKFetchByQuery {
483 // Create an object for the block to retain and modify
484 BoolHolder* runAlready = [[BoolHolder alloc] init];
486 __weak __typeof(self) weakSelf = self;
487 [[self.mockDatabase expect] addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
488 __strong __typeof(self) strongSelf = weakSelf;
489 if(runAlready.state) {
493 if ([obj isKindOfClass: [FakeCKQueryOperation class]]) {
495 runAlready.state = true;
497 FakeCKQueryOperation *fqo = (FakeCKQueryOperation *)obj;
498 [fqo addNullableDependency: strongSelf.ckFetchHoldOperation];
499 [strongSelf.operationQueue addOperation: fqo];
505 - (void)startCKKSSubsystem {
506 if(self.fakeHSA2AccountStatus != CKKSAccountStatusUnknown) {
507 [self.accountStateTracker setHSA2iCloudAccountStatus:self.fakeHSA2AccountStatus];
509 [self startCKAccountStatusMock];
512 - (void)startCKAccountStatusMock {
513 // Note: currently, based on how we're mocking up the zone creation and zone subscription operation,
514 // they will 'fire' before this method is called. It's harmless, since the mocks immediately succeed
515 // and return; it's just a tad confusing.
516 if([self.ckaccountHoldOperation isPending]) {
517 [self.operationQueue addOperation: self.ckaccountHoldOperation];
520 [self.accountStateTracker performInitialDispatches];
523 -(void)holdCloudKitModifications {
524 XCTAssertFalse([self.ckModifyHoldOperation isPending], "Shouldn't already be a pending cloudkit modify hold operation");
525 self.ckModifyHoldOperation = [NSBlockOperation blockOperationWithBlock:^{
526 secnotice("ckks", "Released CloudKit modification hold.");
529 -(void)releaseCloudKitModificationHold {
530 if([self.ckModifyHoldOperation isPending]) {
531 [self.operationQueue addOperation: self.ckModifyHoldOperation];
535 -(void)holdCloudKitFetches {
536 XCTAssertFalse([self.ckFetchHoldOperation isPending], "Shouldn't already be a pending cloudkit fetch hold operation");
537 self.ckFetchHoldOperation = [NSBlockOperation blockOperationWithBlock:^{
538 secnotice("ckks", "Released CloudKit fetch hold.");
541 -(void)releaseCloudKitFetchHold {
542 if([self.ckFetchHoldOperation isPending]) {
543 [self.operationQueue addOperation: self.ckFetchHoldOperation];
547 - (void)expectCKModifyItemRecords: (NSUInteger) expectedNumberOfRecords currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords zoneID: (CKRecordZoneID*) zoneID {
548 [self expectCKModifyItemRecords:expectedNumberOfRecords
549 currentKeyPointerRecords:expectedCurrentKeyRecords
554 - (void)expectCKModifyItemRecords: (NSUInteger) expectedNumberOfRecords currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords zoneID: (CKRecordZoneID*) zoneID checkItem: (BOOL (^)(CKRecord*)) checkItem {
555 [self expectCKModifyItemRecords:expectedNumberOfRecords
557 currentKeyPointerRecords:expectedCurrentKeyRecords
559 checkItem:checkItem];
562 - (void)expectCKModifyItemRecords:(NSUInteger)expectedNumberOfModifiedRecords
563 deletedRecords:(NSUInteger)expectedNumberOfDeletedRecords
564 currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
565 zoneID:(CKRecordZoneID*)zoneID
566 checkItem:(BOOL (^)(CKRecord*))checkItem {
567 // We're updating the device state type on every update, so add it in here
568 NSMutableDictionary* expectedRecords = [@{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfModifiedRecords],
569 SecCKRecordCurrentKeyType: [NSNumber numberWithUnsignedInteger: expectedCurrentKeyRecords],
570 SecCKRecordDeviceStateType: [NSNumber numberWithUnsignedInt: 1],
573 if(SecCKKSSyncManifests()) {
574 expectedRecords[SecCKRecordManifestType] = [NSNumber numberWithInt: 1];
575 expectedRecords[SecCKRecordManifestLeafType] = [NSNumber numberWithInt: 72];
578 NSDictionary* deletedRecords = nil;
579 if(expectedNumberOfDeletedRecords != 0) {
580 deletedRecords = @{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfDeletedRecords]};
583 [self expectCKModifyRecords:expectedRecords
584 deletedRecordTypeCounts:deletedRecords
586 checkModifiedRecord: ^BOOL (CKRecord* record){
587 if([record.recordType isEqualToString: SecCKRecordItemType] && checkItem) {
588 return checkItem(record);
593 runAfterModification:nil];
598 - (void)expectCKModifyKeyRecords:(NSUInteger)expectedNumberOfRecords
599 currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
600 tlkShareRecords:(NSUInteger)expectedTLKShareRecords
601 zoneID:(CKRecordZoneID*)zoneID
603 return [self expectCKModifyKeyRecords:expectedNumberOfRecords
604 currentKeyPointerRecords:expectedCurrentKeyRecords
605 tlkShareRecords:expectedTLKShareRecords
607 checkModifiedRecord:nil];
610 - (void)expectCKModifyKeyRecords:(NSUInteger)expectedNumberOfRecords
611 currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
612 tlkShareRecords:(NSUInteger)expectedTLKShareRecords
613 zoneID:(CKRecordZoneID*)zoneID
614 checkModifiedRecord:(BOOL (^_Nullable)(CKRecord*))checkModifiedRecord
616 NSNumber* nkeys = [NSNumber numberWithUnsignedInteger: expectedNumberOfRecords];
617 NSNumber* ncurrentkeys = [NSNumber numberWithUnsignedInteger: expectedCurrentKeyRecords];
618 NSNumber* ntlkshares = [NSNumber numberWithUnsignedInteger: expectedTLKShareRecords];
620 [self expectCKModifyRecords:@{SecCKRecordIntermediateKeyType: nkeys,
621 SecCKRecordCurrentKeyType: ncurrentkeys,
622 SecCKRecordTLKShareType: ntlkshares,
624 deletedRecordTypeCounts:nil
626 checkModifiedRecord:checkModifiedRecord
627 runAfterModification:nil];
630 - (void)expectCKModifyRecords:(NSDictionary<NSString*, NSNumber*>*) expectedRecordTypeCounts
631 deletedRecordTypeCounts:(NSDictionary<NSString*, NSNumber*>*) expectedDeletedRecordTypeCounts
632 zoneID:(CKRecordZoneID*) zoneID
633 checkModifiedRecord:(BOOL (^)(CKRecord*)) checkModifiedRecord
634 runAfterModification:(void (^) (void))afterModification
636 __weak __typeof(self) weakSelf = self;
638 // Create an object for the block to retain and modify
639 BoolHolder* runAlready = [[BoolHolder alloc] init];
641 secnotice("fakecloudkit", "expecting an operation matching modifications: %@ deletions: %@",
642 expectedRecordTypeCounts, expectedDeletedRecordTypeCounts);
644 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
645 secnotice("fakecloudkit", "Received an operation (%@), checking if it's a modification", obj);
646 __block bool matches = false;
647 if(runAlready.state) {
648 secnotice("fakecloudkit", "Run already, skipping");
652 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
653 __strong __typeof(weakSelf) strongSelf = weakSelf;
654 XCTAssertNotNil(strongSelf, "self exists");
656 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
659 NSMutableDictionary<NSString*, NSNumber*>* modifiedRecordTypeCounts = [[NSMutableDictionary alloc] init];
660 NSMutableDictionary<NSString*, NSNumber*>* deletedRecordTypeCounts = [[NSMutableDictionary alloc] init];
662 // First: check if it matches. If it does, _then_ execute the operation.
663 // Supports single-zone atomic writes only
666 // We only care about atomic operations
667 secnotice("fakecloudkit", "Not an atomic operation; quitting: %@", op);
671 FakeCKZone* zone = strongSelf.zones[zoneID];
672 XCTAssertNotNil(zone, "Have a zone for these records");
674 __block BOOL result = YES;
675 dispatch_sync(zone.queue, ^{
677 for(CKRecord* record in op.recordsToSave) {
678 if(![record.recordID.zoneID isEqual: zoneID]) {
679 secnotice("fakecloudkit", "Modified record zone ID mismatch: %@ %@", zoneID, record.recordID.zoneID);
684 NSError* recordError = [zone errorFromSavingRecord: record];
686 secnotice("fakecloudkit", "Record zone rejected record write: %@ %@", recordError, record);
687 XCTFail(@"Record zone rejected record write: %@ %@", recordError, record);
692 NSNumber* currentCountNumber = modifiedRecordTypeCounts[record.recordType];
693 NSUInteger currentCount = currentCountNumber ? [currentCountNumber unsignedIntegerValue] : 0;
694 modifiedRecordTypeCounts[record.recordType] = [NSNumber numberWithUnsignedInteger: currentCount + 1];
697 for(CKRecordID* recordID in op.recordIDsToDelete) {
698 if(![recordID.zoneID isEqual: zoneID]) {
700 secnotice("fakecloudkit", "Deleted record zone ID mismatch: %@ %@", zoneID, recordID.zoneID);
703 // Find the object in CloudKit, and record its type
704 CKRecord* record = strongSelf.zones[zoneID].currentDatabase[recordID];
706 NSNumber* currentCountNumber = deletedRecordTypeCounts[record.recordType];
707 NSUInteger currentCount = currentCountNumber ? [currentCountNumber unsignedIntegerValue] : 0;
708 deletedRecordTypeCounts[record.recordType] = [NSNumber numberWithUnsignedInteger: currentCount + 1];
712 NSMutableDictionary* filteredExpectedRecordTypeCounts = [expectedRecordTypeCounts mutableCopy];
713 for(NSString* key in filteredExpectedRecordTypeCounts.allKeys) {
714 if([filteredExpectedRecordTypeCounts[key] isEqual: [NSNumber numberWithInt:0]]) {
715 filteredExpectedRecordTypeCounts[key] = nil;
718 filteredExpectedRecordTypeCounts[SecCKRecordManifestType] = modifiedRecordTypeCounts[SecCKRecordManifestType];
719 filteredExpectedRecordTypeCounts[SecCKRecordManifestLeafType] = modifiedRecordTypeCounts[SecCKRecordManifestLeafType];
721 // Inspect that we have exactly the same records as we expect
722 if(expectedRecordTypeCounts) {
723 matches &= !![modifiedRecordTypeCounts isEqual: filteredExpectedRecordTypeCounts];
725 secnotice("fakecloudkit", "Record number mismatch: %@ %@", modifiedRecordTypeCounts, filteredExpectedRecordTypeCounts);
730 matches &= op.recordsToSave.count == 0u;
732 secnotice("fakecloudkit", "Record number mismatch: %@ 0", modifiedRecordTypeCounts);
737 if(expectedDeletedRecordTypeCounts) {
738 matches &= !![deletedRecordTypeCounts isEqual: expectedDeletedRecordTypeCounts];
740 secnotice("fakecloudkit", "Deleted record number mismatch: %@ %@", deletedRecordTypeCounts, expectedDeletedRecordTypeCounts);
745 matches &= op.recordIDsToDelete.count == 0u;
747 secnotice("fakecloudkit", "Deleted record number mismatch: %@ 0", deletedRecordTypeCounts);
753 // We have the right number of things, and their etags match. Ensure that they have the right etags
754 if(matches && checkModifiedRecord) {
755 // Clearly we have the right number of things. Call checkRecord on them...
756 for(CKRecord* record in op.recordsToSave) {
757 matches &= !!(checkModifiedRecord(record));
759 secnotice("fakecloudkit", "Check record reports NO: %@ 0", record);
767 // Emulate cloudkit and schedule the operation for execution. Be sure to wait for this operation
768 // if you'd like to read the data from this write.
769 NSBlockOperation* ckop = [NSBlockOperation named:@"cloudkit-write" withBlock: ^{
770 @synchronized(zone.currentDatabase) {
771 NSMutableArray* savedRecords = [[NSMutableArray alloc] init];
772 for(CKRecord* record in op.recordsToSave) {
773 CKRecord* reflectedRecord = [record copy];
774 reflectedRecord.modificationDate = [NSDate date];
776 [zone addToZone: reflectedRecord];
778 [savedRecords addObject:reflectedRecord];
779 op.perRecordCompletionBlock(reflectedRecord, nil);
781 for(CKRecordID* recordID in op.recordIDsToDelete) {
782 // I don't believe CloudKit fails an operation if you delete a record that's not there, so:
783 [zone deleteCKRecordIDFromZone: recordID];
786 if(afterModification) {
790 op.modifyRecordsCompletionBlock(savedRecords, op.recordIDsToDelete, nil);
794 [ckop addNullableDependency:strongSelf.ckModifyHoldOperation];
795 [strongSelf.operationQueue addOperation: ckop];
803 runAlready.state = true;
805 return matches ? YES : NO;
809 - (void)failNextZoneCreation:(CKRecordZoneID*)zoneID {
810 XCTAssertNil(self.zones[zoneID], "Zone does not exist yet");
811 self.zones[zoneID] = [[FakeCKZone alloc] initZone: zoneID];
812 self.zones[zoneID].creationError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
813 code:CKErrorNetworkUnavailable
815 CKErrorRetryAfterKey: @(0.5),
819 // Report success, but don't actually create the zone.
820 // This way, you can find ZoneNotFound errors later on
821 - (void)failNextZoneCreationSilently:(CKRecordZoneID*)zoneID {
822 XCTAssertNil(self.zones[zoneID], "Zone does not exist yet");
823 self.zones[zoneID] = [[FakeCKZone alloc] initZone: zoneID];
824 self.zones[zoneID].failCreationSilently = true;
827 - (void)failNextZoneSubscription:(CKRecordZoneID*)zoneID {
828 XCTAssertNotNil(self.zones[zoneID], "Zone exists");
829 self.zones[zoneID].subscriptionError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
832 - (void)failNextZoneSubscription:(CKRecordZoneID*)zoneID withError:(NSError*)error {
833 XCTAssertNotNil(self.zones[zoneID], "Zone exists");
834 self.zones[zoneID].subscriptionError = error;
837 - (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID {
838 [self failNextCKAtomicModifyItemRecordsUpdateFailure:zoneID blockAfterReject:nil];
841 - (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID blockAfterReject: (void (^)(void))blockAfterReject {
842 [self failNextCKAtomicModifyItemRecordsUpdateFailure:zoneID blockAfterReject:blockAfterReject withError:nil];
845 - (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID blockAfterReject: (void (^)(void))blockAfterReject withError:(NSError*)error {
846 __weak __typeof(self) weakSelf = self;
848 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
849 __strong __typeof(weakSelf) strongSelf = weakSelf;
850 XCTAssertNotNil(strongSelf, "self exists");
852 __block bool rejected = false;
853 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
854 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
857 // We only care about atomic operations
861 // We want to only match zone updates pertaining to this zone
862 for(CKRecord* record in op.recordsToSave) {
863 if(![record.recordID.zoneID isEqual: zoneID]) {
868 FakeCKZone* zone = strongSelf.zones[zoneID];
869 XCTAssertNotNil(zone, "Have a zone for these records");
874 [strongSelf rejectWrite: op withError:error];
876 NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = [[NSMutableDictionary alloc] init];
877 [strongSelf rejectWrite: op failedRecords:failedRecords];
880 if(blockAfterReject) {
884 return rejected ? YES : NO;
888 - (void)expectCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID {
889 __weak __typeof(self) weakSelf = self;
891 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
892 __strong __typeof(weakSelf) strongSelf = weakSelf;
893 XCTAssertNotNil(strongSelf, "self exists");
895 __block bool rejected = false;
896 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
897 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
899 secnotice("fakecloudkit", "checking for expectCKAtomicModifyItemRecordsUpdateFailure");
902 // We only care about atomic operations
903 secnotice("fakecloudkit", "expectCKAtomicModifyItemRecordsUpdateFailure: update not atomic");
907 // We want to only match zone updates pertaining to this zone
908 for(CKRecord* record in op.recordsToSave) {
909 if(![record.recordID.zoneID isEqual: zoneID]) {
910 secnotice("fakecloudkit", "expectCKAtomicModifyItemRecordsUpdateFailure: %@ is not %@", record.recordID.zoneID, zoneID);
915 FakeCKZone* zone = strongSelf.zones[zoneID];
916 XCTAssertNotNil(zone, "Have a zone for these records");
918 NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = [[NSMutableDictionary alloc] init];
920 @synchronized(zone.currentDatabase) {
921 for(CKRecord* record in op.recordsToSave) {
922 // Check if we should allow this transaction
923 NSError* recordSaveError = [zone errorFromSavingRecord: record];
924 if(recordSaveError) {
925 failedRecords[record.recordID] = recordSaveError;
932 [strongSelf rejectWrite: op failedRecords:failedRecords];
934 secnotice("fakecloudkit", "expectCKAtomicModifyItemRecordsUpdateFailure: doesn't seem like an error to us");
937 return rejected ? YES : NO;
941 -(void)rejectWrite:(CKModifyRecordsOperation*)op withError:(NSError*)error {
942 // Emulate cloudkit and schedule the operation for execution. Be sure to wait for this operation
943 // if you'd like to read the data from this write.
944 NSBlockOperation* ckop = [NSBlockOperation named:@"cloudkit-reject-write-error" withBlock: ^{
945 op.modifyRecordsCompletionBlock(nil, nil, error);
948 [ckop addNullableDependency: self.ckModifyHoldOperation];
949 [self.operationQueue addOperation: ckop];
952 -(void)rejectWrite:(CKModifyRecordsOperation*)op failedRecords:(NSMutableDictionary<CKRecordID*, NSError*>*)failedRecords {
953 // Add the batch request failed errors
954 for(CKRecord* record in op.recordsToSave) {
955 NSError* exists = failedRecords[record.recordID];
957 // TODO: might have important userInfo, but we're not mocking that yet
958 failedRecords[record.recordID] = [[CKPrettyError alloc] initWithDomain: CKErrorDomain code: CKErrorBatchRequestFailed userInfo: @{}];
962 NSError* error = [[CKPrettyError alloc] initWithDomain: CKErrorDomain code: CKErrorPartialFailure userInfo: @{CKPartialErrorsByItemIDKey: failedRecords}];
964 // Emulate cloudkit and schedule the operation for execution. Be sure to wait for this operation
965 // if you'd like to read the data from this write.
966 NSBlockOperation* ckop = [NSBlockOperation named:@"cloudkit-reject-write" withBlock: ^{
967 op.modifyRecordsCompletionBlock(nil, nil, error);
970 [ckop addNullableDependency: self.ckModifyHoldOperation];
971 [self.operationQueue addOperation: ckop];
974 - (void)expectCKDeleteItemRecords:(NSUInteger)expectedNumberOfRecords
975 zoneID:(CKRecordZoneID*) zoneID {
977 // We're updating the device state type on every update, so add it in here
978 NSMutableDictionary* expectedRecords = [@{
979 SecCKRecordDeviceStateType: [NSNumber numberWithUnsignedInteger:expectedNumberOfRecords],
981 if(SecCKKSSyncManifests()) {
982 // TODO: this really shouldn't be 2.
983 expectedRecords[SecCKRecordManifestType] = [NSNumber numberWithInt: 2];
984 expectedRecords[SecCKRecordManifestLeafType] = [NSNumber numberWithInt: 72];
987 [self expectCKModifyRecords:expectedRecords
988 deletedRecordTypeCounts:@{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfRecords]}
990 checkModifiedRecord:nil
991 runAfterModification:nil];
994 -(void)waitForCKModifications {
995 // CloudKit modifications are put on the local queue.
996 // This is heavyweight but should suffice.
997 [self.operationQueue waitUntilAllOperationsAreFinished];
1001 NSString* testName = [self.name componentsSeparatedByString:@" "][1];
1002 testName = [testName stringByReplacingOccurrencesOfString:@"]" withString:@""];
1003 secnotice("ckkstest", "Ending test %@", testName);
1005 if(SecCKKSIsEnabled()) {
1006 self.accountStatus = CKAccountStatusCouldNotDetermine;
1008 // If the test never initialized the account state, don't call status later
1009 bool callStatus = [self.ckaccountHoldOperation isFinished];
1010 [self.ckaccountHoldOperation cancel];
1011 self.ckaccountHoldOperation = nil;
1013 // Ensure we don't have any blocking operations left
1014 [self.operationQueue cancelAllOperations];
1015 [self waitForCKModifications];
1017 XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:20*NSEC_PER_SEC],
1018 "Timeout did not occur waiting for SecCKKSInitialize");
1020 // Ensure that we can fetch zone status for all zones
1022 XCTestExpectation *statusReturned = [self expectationWithDescription:@"status returned"];
1023 [self.injectedManager rpcStatus:nil reply:^(NSArray<NSDictionary *> *result, NSError *error) {
1024 XCTAssertNil(error, "Should be no error fetching status");
1025 [statusReturned fulfill];
1027 [self waitForExpectations: @[statusReturned] timeout:20];
1029 // Make sure this happens before teardown.
1030 XCTAssertEqual(0, [self.accountStateTracker.finishedInitialDispatches wait:20*NSEC_PER_SEC], "Account state tracker initialized itself");
1032 dispatch_group_t accountChangesDelivered = [self.accountStateTracker checkForAllDeliveries];
1033 XCTAssertEqual(0, dispatch_group_wait(accountChangesDelivered, dispatch_time(DISPATCH_TIME_NOW, 10*NSEC_PER_SEC)), "Account state tracker finished delivering everything");
1039 [self.injectedManager cancelPendingOperations];
1040 [CKKSViewManager resetManager:true setTo:nil];
1041 self.injectedManager = nil;
1042 [self.mockCKKSViewManager stopMocking];
1043 self.mockCKKSViewManager = nil;
1045 [self.mockAccountStateTracker stopMocking];
1046 self.mockAccountStateTracker = nil;
1048 [self.mockLockStateTracker stopMocking];
1049 self.mockLockStateTracker = nil;
1051 [self.mockFakeCKModifyRecordZonesOperation stopMocking];
1052 self.mockFakeCKModifyRecordZonesOperation = nil;
1054 [self.mockFakeCKModifySubscriptionsOperation stopMocking];
1055 self.mockFakeCKModifySubscriptionsOperation = nil;
1057 [self.mockFakeCKFetchRecordZoneChangesOperation stopMocking];
1058 self.mockFakeCKFetchRecordZoneChangesOperation = nil;
1060 [self.mockFakeCKFetchRecordsOperation stopMocking];
1061 self.mockFakeCKFetchRecordsOperation = nil;
1063 [self.mockFakeCKQueryOperation stopMocking];
1064 self.mockFakeCKQueryOperation = nil;
1066 [self.mockDatabase stopMocking];
1067 self.mockDatabase = nil;
1069 [self.mockDatabaseExceptionCatcher stopMocking];
1070 self.mockDatabaseExceptionCatcher = nil;
1072 [self.mockContainer stopMocking];
1073 self.mockContainer = nil;
1075 [self.mockTTR stopMocking];
1077 self.ttrExpectation = nil;
1078 self.isTTRRatelimited = true;
1082 _mockSOSAdapter = nil;
1083 _mockOctagonAdapter = nil;
1085 SecCKKSTestResetFlags();
1088 - (CKKSKey*) fakeTLK: (CKRecordZoneID*)zoneID {
1089 CKKSKey* key = [[CKKSKey alloc] initSelfWrappedWithAESKey:[[CKKSAESSIVKey alloc] initWithBase64: @"uImdbZ7Zg+6WJXScTnRBfNmoU1UiMkSYxWc+d1Vuq3IFn2RmTRkTdWTe3HmeWo1pAomqy+upK8KHg2PGiRGhqg=="]
1090 uuid:[[NSUUID UUID] UUIDString]
1091 keyclass:SecCKKSKeyClassTLK
1092 state: SecCKKSProcessedStateLocal
1094 encodedCKRecord: nil
1096 [key CKRecordWithZoneID: zoneID];
1100 - (NSError*)ckInternalServerExtensionError:(NSInteger)code description:(NSString*)desc {
1101 NSError* extensionError = [[CKPrettyError alloc] initWithDomain:@"CloudkitKeychainService"
1104 CKErrorServerDescriptionKey: desc,
1105 NSLocalizedDescriptionKey: desc,
1107 NSError* internalError = [[CKPrettyError alloc] initWithDomain:CKInternalErrorDomain
1108 code:CKErrorInternalPluginError
1109 userInfo:@{CKErrorServerDescriptionKey: desc,
1110 NSLocalizedDescriptionKey: desc,
1111 NSUnderlyingErrorKey: extensionError,
1113 NSError* error = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
1114 code:CKErrorServerRejectedRequest
1115 userInfo:@{NSUnderlyingErrorKey: internalError,
1116 CKErrorServerDescriptionKey: desc,
1117 NSLocalizedDescriptionKey: desc,
1118 CKContainerIDKey: SecCKKSContainerName,