CloudKitMockXCTest.m [plain text]
/*
* Copyright (c) 2016 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/
#if OCTAGON
#import "CloudKitMockXCTest.h"
#import <ApplePushService/ApplePushService.h>
#import <Foundation/Foundation.h>
#import <CloudKit/CloudKit.h>
#import <CloudKit/CloudKit_Private.h>
#import <CloudKit/CKContainer_Private.h>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header"
#import <OCMock/OCMock.h>
#pragma clang diagnostic pop
#import <TrustedPeers/TrustedPeers.h>
#import <TrustedPeers/TPPBPolicyKeyViewMapping.h>
#import <TrustedPeers/TPDictionaryMatchingRules.h>
#include "keychain/securityd/Regressions/SecdTestKeychainUtilities.h"
#include <utilities/SecFileLocations.h>
#include "keychain/securityd/SecItemServer.h"
#include "keychain/securityd/SecItemDataSource.h"
#if NO_SERVER
#include "keychain/securityd/spi.h"
#endif
#include <Security/SecureObjectSync/SOSViews.h>
#include <utilities/SecDb.h>
#include "keychain/securityd/SecItemServer.h"
#include <keychain/ckks/CKKS.h>
#include <keychain/ckks/CKKSViewManager.h>
#include <keychain/ckks/CKKSKeychainView.h>
#include <keychain/ckks/CKKSItem.h>
#include <keychain/ckks/CKKSOutgoingQueueEntry.h>
#include <keychain/ckks/CKKSKey.h>
#include "keychain/ckks/CKKSGroupOperation.h"
#include "keychain/ckks/CKKSLockStateTracker.h"
#include "keychain/ckks/CKKSReachabilityTracker.h"
#include "keychain/ot/OT.h"
#include "keychain/ot/OTManager.h"
#import "tests/secdmockaks/mockaks.h"
#import "utilities/SecTapToRadar.h"
#import "MockCloudKit.h"
#include "keychain/ckks/CKKSAnalytics.h"
@interface BoolHolder : NSObject
@property bool state;
@end
@implementation BoolHolder
@end
// Inform OCMock about the internals of CKContainer
@interface CKContainer ()
- (void)_checkSelfCloudServicesEntitlement;
@end
@implementation CKKSTestFailureLogger
- (instancetype)init {
if((self = [super init])) {
}
return self;
}
- (void)testCase:(XCTestCase *)testCase didRecordIssue:(XCTIssue *)issue {
ckksnotice_global("ckkstests", "XCTest failure: ( testCase.name,
issue.sourceCodeContext.location.fileURL,
(unsigned long)issue.sourceCodeContext.location.lineNumber,
issue.compactDescription,
issue.detailedDescription,
issue.sourceCodeContext.callStack);
}
@end
@implementation CloudKitMockXCTest
static CKKSTestFailureLogger* _testFailureLoggerVariable;
+ (void)setUp {
// Turn on testing
SecCKKSEnable();
SecCKKSTestsEnable();
SecCKKSSetReduceRateLimiting(true);
self.testFailureLogger = [[CKKSTestFailureLogger alloc] init];
[[XCTestObservationCenter sharedTestObservationCenter] addTestObserver:self.testFailureLogger];
[super setUp];
#if NO_SERVER
securityd_init_local_spi();
#endif
}
+ (void)tearDown {
[super tearDown];
[[XCTestObservationCenter sharedTestObservationCenter] removeTestObserver:self.testFailureLogger];
}
+ (CKKSTestFailureLogger*)testFailureLogger {
return _testFailureLoggerVariable;
}
+ (void)setTestFailureLogger:(CKKSTestFailureLogger*)logger {
_testFailureLoggerVariable = logger;
}
- (BOOL)isRateLimited:(SecTapToRadar *)ttrRequest
{
return self.isTTRRatelimited;
}
- (BOOL)askUserIfTTR:(SecTapToRadar *)ttrRequest
{
return YES;
}
- (void)triggerTapToRadar:(SecTapToRadar *)ttrRequest
{
[self.ttrExpectation fulfill];
}
- (void)setUp {
[super setUp];
NSString* testName = [self.name componentsSeparatedByString:@" "][1];
testName = [testName stringByReplacingOccurrencesOfString:@"]" withString:@""];
secnotice("ckkstest", "Beginning test
// All tests start with the same flag set.
SecCKKSTestResetFlags();
SecCKKSTestSetDisableSOS(true);
self.silentFetchesAllowed = true;
self.silentZoneDeletesAllowed = false; // Set to true if you want to do any deletes
__weak __typeof(self) weakSelf = self;
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.zones = self.zones ?: [[NSMutableDictionary alloc] init];
self.apsEnvironment = @"fake APS push string";
// Static variables are a scourge. Let's reset this one...
[OctagonAPSReceiver resetGlobalDelegatePortMap];
self.mockDatabaseExceptionCatcher = OCMStrictClassMock([CKDatabase class]);
self.mockDatabase = OCMStrictClassMock([CKDatabase class]);
self.mockContainerExpectations = OCMStrictClassMock([CKContainer class]);
self.mockContainer = OCMClassMock([CKContainer class]);
OCMStub([self.mockContainer containerWithIdentifier:[OCMArg isKindOfClass:[NSString class]]]).andReturn(self.mockContainer);
OCMStub([self.mockContainer defaultContainer]).andReturn(self.mockContainer);
OCMStub([self.mockContainer alloc]).andReturn(self.mockContainer);
OCMStub([self.mockContainer containerIdentifier]).andReturn(SecCKKSContainerName);
OCMStub([self.mockContainer initWithContainerID: [OCMArg any] options: [OCMArg any]]).andReturn(self.mockContainer);
OCMStub([self.mockContainer privateCloudDatabase]).andReturn(self.mockDatabaseExceptionCatcher);
OCMStub([self.mockContainer serverPreferredPushEnvironmentWithCompletionHandler: ([OCMArg invokeBlockWithArgs:self.apsEnvironment, [NSNull null], nil])]);
OCMStub([self.mockContainer submitEventMetric:[OCMArg any]]).andCall(self, @selector(ckcontainerSubmitEventMetric:));
// Use two layers of mockDatabase here, so we can both add Expectations and catch the exception (instead of crash) when one fails.
OCMStub([self.mockDatabaseExceptionCatcher addOperation:[OCMArg any]]).andCall(self, @selector(ckdatabaseAddOperation:));
// If you want to change this, you'll need to update the mock
_ckDeviceID = [NSString stringWithFormat:@"fake-cloudkit-device-id- OCMStub([self.mockContainer fetchCurrentDeviceIDWithCompletionHandler: ([OCMArg invokeBlockWithArgs:self.ckDeviceID, [NSNull null], nil])]);
self.accountStatus = CKAccountStatusAvailable;
self.iCloudHasValidCredentials = YES;
self.fakeHSA2AccountStatus = CKKSAccountStatusAvailable;
// Inject a fake operation dependency so we won't respond with the CloudKit account status immediately
// The CKKSAccountStateTracker won't send any login/logout calls without that information, so this blocks all CKKS setup
self.ckaccountHoldOperation = [NSBlockOperation named:@"ckaccount-hold" withBlock:^{
ckksnotice_global("ckks", "CKKS CK account status test hold released");
}];
OCMStub([self.mockContainer accountStatusWithCompletionHandler:[OCMArg any]]).andCall(self, @selector(ckcontainerAccountStatusWithCompletionHandler:));
OCMStub([self.mockContainer accountInfoWithCompletionHandler:[OCMArg any]]).andCall(self, @selector(ckcontainerAccountInfoWithCompletionHandler:));
self.mockAccountStateTracker = OCMClassMock([CKKSAccountStateTracker class]);
OCMStub([self.mockAccountStateTracker getCircleStatus]).andCall(self, @selector(circleStatus));
// Fake out SOS peers
// One trusted non-self peer, but it doesn't have any Octagon keys. Your test can change this if it wants.
// However, note that [self putFakeDeviceStatusInCloudKit:] will likely not do what you want after you change this
CKKSSOSSelfPeer* currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"local-peer"
encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
viewList:self.managedViewList];
self.mockSOSAdapter = [[CKKSMockSOSPresentAdapter alloc] initWithSelfPeer:currentSelfPeer
trustedPeers:[NSSet set]
essential:YES];
OCMStub([self.mockAccountStateTracker fetchCirclePeerID:[OCMArg any]]).andCall(self, @selector(sosFetchCirclePeerID:));
self.lockStateProvider = [[CKKSMockLockStateProvider alloc] initWithCurrentLockStatus:NO];
self.aksLockState = false; // Lie and say AKS is always unlocked
self.mockTTR = OCMClassMock([SecTapToRadar class]);
OCMStub([self.mockTTR isRateLimited:[OCMArg any]]).andCall(self, @selector(isRateLimited:));
OCMStub([self.mockTTR askUserIfTTR:[OCMArg any]]).andCall(self, @selector(askUserIfTTR:));
OCMStub([self.mockTTR triggerTapToRadar:[OCMArg any]]).andCall(self, @selector(triggerTapToRadar:));
self.isTTRRatelimited = true;
self.mockFakeCKModifyRecordZonesOperation = OCMClassMock([FakeCKModifyRecordZonesOperation class]);
OCMStub([self.mockFakeCKModifyRecordZonesOperation ckdb]).andReturn(self.zones);
OCMStub([self.mockFakeCKModifyRecordZonesOperation shouldFailModifyRecordZonesOperation]).andCall(self, @selector(shouldFailModifyRecordZonesOperation));
OCMStub([self.mockFakeCKModifyRecordZonesOperation ensureZoneDeletionAllowed:[OCMArg any]]).andCall(self, @selector(ensureZoneDeletionAllowed:));
self.mockFakeCKModifySubscriptionsOperation = OCMClassMock([FakeCKModifySubscriptionsOperation class]);
OCMStub([self.mockFakeCKModifySubscriptionsOperation ckdb]).andReturn(self.zones);
self.mockFakeCKFetchRecordZoneChangesOperation = OCMClassMock([FakeCKFetchRecordZoneChangesOperation class]);
OCMStub([self.mockFakeCKFetchRecordZoneChangesOperation ckdb]).andReturn(self.zones);
OCMStub([self.mockFakeCKFetchRecordZoneChangesOperation isNetworkReachable]).andCall(self, @selector(isNetworkReachable));
self.mockFakeCKFetchRecordsOperation = OCMClassMock([FakeCKFetchRecordsOperation class]);
OCMStub([self.mockFakeCKFetchRecordsOperation ckdb]).andReturn(self.zones);
self.mockFakeCKQueryOperation = OCMClassMock([FakeCKQueryOperation class]);
OCMStub([self.mockFakeCKQueryOperation ckdb]).andReturn(self.zones);
OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
__strong __typeof(self) strongSelf = weakSelf;
BOOL matches = NO;
if ([obj isKindOfClass: [FakeCKFetchRecordZoneChangesOperation class]]) {
if(strongSelf.silentFetchesAllowed) {
matches = YES;
FakeCKFetchRecordZoneChangesOperation *frzco = (FakeCKFetchRecordZoneChangesOperation *)obj;
[frzco addNullableDependency:strongSelf.ckFetchHoldOperation];
[strongSelf.operationQueue addOperation: frzco];
}
}
return matches;
}]]);
OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
__strong __typeof(self) strongSelf = weakSelf;
BOOL matches = NO;
if ([obj isKindOfClass: [FakeCKFetchRecordsOperation class]]) {
if(strongSelf.silentFetchesAllowed) {
matches = YES;
FakeCKFetchRecordsOperation *ffro = (FakeCKFetchRecordsOperation *)obj;
[ffro addNullableDependency:strongSelf.ckFetchHoldOperation];
[strongSelf.operationQueue addOperation: ffro];
}
}
return matches;
}]]);
OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
__strong __typeof(self) strongSelf = weakSelf;
BOOL matches = NO;
if ([obj isKindOfClass: [FakeCKQueryOperation class]]) {
if(strongSelf.silentFetchesAllowed) {
matches = YES;
FakeCKQueryOperation *fqo = (FakeCKQueryOperation *)obj;
[fqo addNullableDependency:strongSelf.ckFetchHoldOperation];
[strongSelf.operationQueue addOperation: fqo];
}
}
return matches;
}]]);
OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
__strong __typeof(self) strongSelf = weakSelf;
BOOL matches = NO;
if ([obj isKindOfClass: [FakeCKModifyRecordZonesOperation class]]) {
FakeCKModifyRecordZonesOperation *frzco = (FakeCKModifyRecordZonesOperation *)obj;
[frzco addNullableDependency:strongSelf.ckModifyRecordZonesHoldOperation];
[strongSelf.operationQueue addOperation: frzco];
matches = YES;
}
return matches;
}]]);
OCMStub([self.mockDatabase addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
__strong __typeof(self) strongSelf = weakSelf;
BOOL matches = NO;
if ([obj isKindOfClass: [FakeCKModifySubscriptionsOperation class]]) {
FakeCKModifySubscriptionsOperation *frzco = (FakeCKModifySubscriptionsOperation *)obj;
[frzco addNullableDependency:strongSelf.ckModifySubscriptionsHoldOperation];
[strongSelf.operationQueue addOperation: frzco];
matches = YES;
}
return matches;
}]]);
self.testZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"testzone" ownerName:CKCurrentUserDefaultName];
// We don't want to use class mocks here, because they don't play well with partial mocks
CKKSCloudKitClassDependencies* cloudKitClassDependencies = [[CKKSCloudKitClassDependencies alloc] initWithFetchRecordZoneChangesOperationClass:[FakeCKFetchRecordZoneChangesOperation class]
fetchRecordsOperationClass:[FakeCKFetchRecordsOperation class]
queryOperationClass:[FakeCKQueryOperation class]
modifySubscriptionsOperationClass:[FakeCKModifySubscriptionsOperation class]
modifyRecordZonesOperationClass:[FakeCKModifyRecordZonesOperation class]
apsConnectionClass:[FakeAPSConnection class]
nsnotificationCenterClass:[FakeNSNotificationCenter class]
nsdistributednotificationCenterClass:[FakeNSDistributedNotificationCenter class]
notifierClass:[FakeCKKSNotifier class]];
self.injectedOTManager = [self setUpOTManager:cloudKitClassDependencies];
[OTManager resetManager:false to:self.injectedOTManager];
self.mockCKKSViewManager = OCMPartialMock(self.injectedOTManager.viewManager);
self.injectedManager = self.mockCKKSViewManager;
[self.mockCKKSViewManager setOverrideCKKSViewsFromPolicy:!self.setCKKSViewsFromPolicyToNo];
OCMStub([self.mockCKKSViewManager defaultViewList]).andCall(self, @selector(managedViewList));
OCMStub([self.mockCKKSViewManager syncBackupAndNotifyAboutSync]);
OCMStub([self.mockCKKSViewManager waitForTrustReady]).andReturn(YES);
// Lie and say network is available
[self.reachabilityTracker setNetworkReachability:true];
// Make a new fake keychain
NSString* tmp_dir = [NSString stringWithFormat: @"/tmp/ [[NSFileManager defaultManager] createDirectoryAtPath:[NSString stringWithFormat: @"
SecSetCustomHomeURLString((__bridge CFStringRef) tmp_dir);
SecKeychainDbReset(NULL);
// Actually load the database.
kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) { return false; });
if(!self.disableConfigureCKKSViewManagerWithViews) {
// Normally, the Octagon state machine calls this. But, since we won't be running that, help it out.
// CKKS might try to take a DB lock, so do this after the DB load above
[self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
}
}
- (OTManager*)setUpOTManager:(CKKSCloudKitClassDependencies*)cloudKitClassDependencies
{
return [[OTManager alloc] initWithSOSAdapter:self.mockSOSAdapter
lockStateTracker:[[CKKSLockStateTracker alloc] initWithProvider:self.lockStateProvider]
cloudKitClassDependencies:cloudKitClassDependencies];
}
- (SOSAccountStatus*)circleStatus {
NSError* error = nil;
SOSCCStatus status = [self.mockSOSAdapter circleStatus:&error];
return [[SOSAccountStatus alloc] init:status error:error];
}
- (bool)aksLockState
{
return self.lockStateProvider.aksCurrentlyLocked;
}
- (void)setAksLockState:(bool)aksLockState
{
ckksnotice_global("ckkstests", "Setting mock AKS lock state to: self.mockSOSAdapter.aksLocked = aksLockState ? YES : NO;
self.lockStateProvider.aksCurrentlyLocked = aksLockState;
}
- (bool)isNetworkReachable {
return self.reachabilityTracker.currentReachability;
}
- (void)sosFetchCirclePeerID:(void (^)(NSString* _Nullable peerID, NSError* _Nullable error))callback
{
if(callback) {
// If we're in circle, come up with a fake circle id. Otherwise, return an error.
if(self.mockSOSAdapter.circleStatus == kSOSCCInCircle) {
callback(self.mockSOSAdapter.selfPeer.peerID, nil);
} else {
callback(nil, [NSError errorWithDomain:@"securityd"
code:errSecInternalError
userInfo:@{NSLocalizedDescriptionKey:@"no account, no circle id"}]);
}
}
}
- (void)ckcontainerSubmitEventMetric:(CKEventMetric*)metric {
@try {
[self.mockContainerExpectations submitEventMetric:metric];
} @catch (NSException *exception) {
XCTFail("Received an container exception when trying to add a metric: }
}
- (void)ckcontainerAccountStatusWithCompletionHandler:(void (^)(CKAccountStatus accountStatus, NSError * _Nullable error))completionHandler
{
__weak __typeof(self) weakSelf = self;
NSBlockOperation* fulfillBlock = [NSBlockOperation named:@"account-status-completion" withBlock: ^{
__strong __typeof(self) strongOperationSelf = weakSelf;
if(completionHandler) {
completionHandler(strongOperationSelf.accountStatus, nil);
}
}];
[fulfillBlock addNullableDependency:self.ckaccountHoldOperation];
[self.operationQueue addOperation:fulfillBlock];
}
- (void)ckcontainerAccountInfoWithCompletionHandler:(void (^)(CKAccountInfo * _Nullable accountInfo, NSError * _Nullable error))completionHandler
{
__weak __typeof(self) weakSelf = self;
NSBlockOperation* fulfillBlock = [NSBlockOperation named:@"account-info-completion" withBlock: ^{
__strong __typeof(self) blockStrongSelf = weakSelf;
CKAccountInfo* account = [[CKAccountInfo alloc] init];
account.accountStatus = blockStrongSelf.accountStatus;
account.hasValidCredentials = blockStrongSelf.iCloudHasValidCredentials;
account.accountPartition = CKAccountPartitionTypeProduction;
if(completionHandler) {
completionHandler(account, nil);
}
}];
[fulfillBlock addNullableDependency:self.ckaccountHoldOperation];
[self.operationQueue addOperation:fulfillBlock];
}
- (void)ckdatabaseAddOperation:(NSOperation*)op {
@try {
[self.mockDatabase addOperation:op];
} @catch (NSException *exception) {
XCTFail("Received an database exception: }
}
- (NSError* _Nullable)shouldFailModifyRecordZonesOperation {
NSError* error = self.nextModifyRecordZonesError;
if(error) {
self.nextModifyRecordZonesError = nil;
return error;
}
return nil;
}
- (void)ensureZoneDeletionAllowed:(FakeCKZone*)zone {
XCTAssertTrue(self.silentZoneDeletesAllowed, "Should be allowing zone deletes");
}
- (CKKSAccountStateTracker*)accountStateTracker {
return self.injectedOTManager.accountStateTracker;
}
-(CKKSLockStateTracker*)lockStateTracker {
return self.injectedOTManager.lockStateTracker;
}
-(CKKSReachabilityTracker*)reachabilityTracker {
return self.injectedManager.reachabilityTracker;
}
-(NSSet*)managedViewList {
return (NSSet*) CFBridgingRelease(SOSViewCopyViewSet(kViewSetCKKS));
}
- (TPSyncingPolicy*)viewSortingPolicyForManagedViewList
{
return [self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet set]
syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_ENABLED];
}
- (TPSyncingPolicy*)viewSortingPolicyForManagedViewListWithUserControllableViews:(NSSet<NSString*>*)ucv
syncUserControllableViews:(TPPBPeerStableInfo_UserControllableViewStatus)syncUserControllableViews
{
NSMutableArray<TPPBPolicyKeyViewMapping*>* rules = [NSMutableArray array];
for(NSString* viewName in self.managedViewList) {
TPPBPolicyKeyViewMapping* mapping = [[TPPBPolicyKeyViewMapping alloc] init];
mapping.view = viewName;
mapping.matchingRule = [TPDictionaryMatchingRule fieldMatch:@"vwht"
fieldRegex:[NSString stringWithFormat:@"^
[rules addObject:mapping];
}
TPSyncingPolicy* policy = [[TPSyncingPolicy alloc] initWithModel:@"test-policy"
version:[[TPPolicyVersion alloc] initWithVersion:1 hash:@"fake-policy-for-views"]
viewList:[self managedViewList]
priorityViews:[NSSet set]
userControllableViews:ucv
syncUserControllableViews:syncUserControllableViews
viewsToPiggybackTLKs:[NSSet set]
keyViewMapping:rules];
return policy;
}
-(void)expectCKFetch {
[self expectCKFetchAndRunBeforeFinished: nil];
}
-(void)expectCKFetchAndRunBeforeFinished: (void (^)(void))blockAfterFetch {
[self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * op) {
return YES;
}
runBeforeFinished:blockAfterFetch];
}
- (void)expectCKFetchWithFilter:(BOOL (^)(FakeCKFetchRecordZoneChangesOperation*))operationMatch
runBeforeFinished:(void (^)(void))blockAfterFetch
{
// Create an object for the block to retain and modify
BoolHolder* runAlready = [[BoolHolder alloc] init];
__weak __typeof(self) weakSelf = self;
[[self.mockDatabase expect] addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
__strong __typeof(self) strongSelf = weakSelf;
if(runAlready.state) {
return NO;
}
secnotice("fakecloudkit", "Received an operation ( BOOL matches = NO;
if ([obj isKindOfClass: [FakeCKFetchRecordZoneChangesOperation class]]) {
FakeCKFetchRecordZoneChangesOperation *frzco = (FakeCKFetchRecordZoneChangesOperation *)obj;
matches = operationMatch(frzco);
runAlready.state = true;
secnotice("fakecloudkit", "Running fetch changes: frzco.blockAfterFetch = blockAfterFetch;
[frzco addNullableDependency: strongSelf.ckFetchHoldOperation];
[strongSelf.operationQueue addOperation: frzco];
}
return matches;
}]];
}
-(void)expectCKFetchByRecordID {
// Create an object for the block to retain and modify
BoolHolder* runAlready = [[BoolHolder alloc] init];
__weak __typeof(self) weakSelf = self;
[[self.mockDatabase expect] addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
__strong __typeof(self) strongSelf = weakSelf;
if(runAlready.state) {
return NO;
}
BOOL matches = NO;
if ([obj isKindOfClass: [FakeCKFetchRecordsOperation class]]) {
matches = YES;
runAlready.state = true;
FakeCKFetchRecordsOperation *ffro = (FakeCKFetchRecordsOperation *)obj;
[ffro addNullableDependency: strongSelf.ckFetchHoldOperation];
[strongSelf.operationQueue addOperation: ffro];
}
return matches;
}]];
}
-(void)expectCKFetchByQuery {
// Create an object for the block to retain and modify
BoolHolder* runAlready = [[BoolHolder alloc] init];
__weak __typeof(self) weakSelf = self;
[[self.mockDatabase expect] addOperation: [OCMArg checkWithBlock:^BOOL(id obj) {
__strong __typeof(self) strongSelf = weakSelf;
if(runAlready.state) {
return NO;
}
BOOL matches = NO;
if ([obj isKindOfClass: [FakeCKQueryOperation class]]) {
matches = YES;
runAlready.state = true;
FakeCKQueryOperation *fqo = (FakeCKQueryOperation *)obj;
[fqo addNullableDependency: strongSelf.ckFetchHoldOperation];
[strongSelf.operationQueue addOperation: fqo];
}
return matches;
}]];
}
- (void)startCKKSSubsystem {
if(self.fakeHSA2AccountStatus != CKKSAccountStatusUnknown) {
[self.accountStateTracker setHSA2iCloudAccountStatus:self.fakeHSA2AccountStatus];
}
[self startCKAccountStatusMock];
}
- (void)startCKAccountStatusMock {
// Note: currently, based on how we're mocking up the zone creation and zone subscription operation,
// they will 'fire' before this method is called. It's harmless, since the mocks immediately succeed
// and return; it's just a tad confusing.
if([self.ckaccountHoldOperation isPending]) {
[self.operationQueue addOperation: self.ckaccountHoldOperation];
}
[self.accountStateTracker performInitialDispatches];
}
-(void)holdCloudKitModifications {
XCTAssertFalse([self.ckModifyHoldOperation isPending], "Shouldn't already be a pending cloudkit modify hold operation");
self.ckModifyHoldOperation = [NSBlockOperation blockOperationWithBlock:^{
ckksnotice_global("ckks", "Released CloudKit modification hold.");
}];
}
-(void)releaseCloudKitModificationHold {
if([self.ckModifyHoldOperation isPending]) {
[self.operationQueue addOperation: self.ckModifyHoldOperation];
}
}
-(void)holdCloudKitFetches {
XCTAssertFalse([self.ckFetchHoldOperation isPending], "Shouldn't already be a pending cloudkit fetch hold operation");
self.ckFetchHoldOperation = [NSBlockOperation blockOperationWithBlock:^{
ckksnotice_global("ckks", "Released CloudKit fetch hold.");
}];
}
-(void)releaseCloudKitFetchHold {
if([self.ckFetchHoldOperation isPending]) {
[self.operationQueue addOperation: self.ckFetchHoldOperation];
}
}
-(void)holdCloudKitModifyRecordZones {
XCTAssertFalse([self.ckModifyRecordZonesHoldOperation isPending], "Shouldn't already be a pending cloudkit zone create hold operation");
self.ckModifyRecordZonesHoldOperation = [NSBlockOperation blockOperationWithBlock:^{
ckksnotice_global("ckks", "Released CloudKit zone create hold.");
}];
}
-(void)releaseCloudKitModifyRecordZonesHold {
if([self.ckModifyRecordZonesHoldOperation isPending]) {
[self.operationQueue addOperation: self.ckModifyRecordZonesHoldOperation];
}
}
-(void)holdCloudKitModifySubscription {
XCTAssertFalse([self.ckModifySubscriptionsHoldOperation isPending], "Shouldn't already be a pending cloudkit subscription hold operation");
self.ckModifySubscriptionsHoldOperation = [NSBlockOperation blockOperationWithBlock:^{
ckksnotice_global("ckks", "Released CloudKit zone create hold.");
}];
}
-(void)releaseCloudKitModifySubscriptionHold {
if([self.ckModifySubscriptionsHoldOperation isPending]) {
[self.operationQueue addOperation: self.ckModifySubscriptionsHoldOperation];
}
}
- (void)expectCKModifyItemRecords: (NSUInteger) expectedNumberOfRecords currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords zoneID: (CKRecordZoneID*) zoneID {
[self expectCKModifyItemRecords:expectedNumberOfRecords
currentKeyPointerRecords:expectedCurrentKeyRecords
zoneID:zoneID
checkItem:nil];
}
- (void)expectCKModifyItemRecords: (NSUInteger) expectedNumberOfRecords currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords zoneID: (CKRecordZoneID*) zoneID checkItem: (BOOL (^)(CKRecord*)) checkItem {
[self expectCKModifyItemRecords:expectedNumberOfRecords
deletedRecords:0
currentKeyPointerRecords:expectedCurrentKeyRecords
zoneID:zoneID
checkItem:checkItem];
}
- (void)expectCKModifyItemRecords:(NSUInteger)expectedNumberOfModifiedRecords
deletedRecords:(NSUInteger)expectedNumberOfDeletedRecords
currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
zoneID:(CKRecordZoneID*)zoneID
checkItem:(BOOL (^)(CKRecord*))checkItem {
[self expectCKModifyItemRecords:expectedNumberOfModifiedRecords
deletedRecords:expectedNumberOfDeletedRecords
currentKeyPointerRecords:expectedCurrentKeyRecords
zoneID:zoneID
checkItem:checkItem
expectedOperationGroupName:nil];
}
- (void)expectCKModifyItemRecords:(NSUInteger)expectedNumberOfModifiedRecords
deletedRecords:(NSUInteger)expectedNumberOfDeletedRecords
currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
zoneID:(CKRecordZoneID*)zoneID
checkItem:(BOOL (^ _Nullable)(CKRecord*))checkItem
expectedOperationGroupName:(NSString* _Nullable)operationGroupName
{
// We're updating the device state type on every update, so add it in here
NSMutableDictionary* expectedRecords = [@{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfModifiedRecords],
SecCKRecordCurrentKeyType: [NSNumber numberWithUnsignedInteger: expectedCurrentKeyRecords],
SecCKRecordDeviceStateType: [NSNumber numberWithUnsignedInt: 1],
} mutableCopy];
if(SecCKKSSyncManifests()) {
expectedRecords[SecCKRecordManifestType] = [NSNumber numberWithInt: 1];
expectedRecords[SecCKRecordManifestLeafType] = [NSNumber numberWithInt: 72];
}
NSDictionary* deletedRecords = nil;
if(expectedNumberOfDeletedRecords != 0) {
deletedRecords = @{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfDeletedRecords]};
}
[self expectCKModifyRecords:expectedRecords
deletedRecordTypeCounts:deletedRecords
zoneID:zoneID
checkModifiedRecord: ^BOOL (CKRecord* record){
if([record.recordType isEqualToString: SecCKRecordItemType] && checkItem) {
return checkItem(record);
} else {
return YES;
}
}
inspectOperationGroup:operationGroupName != nil ? ^(CKOperationGroup* group) {
XCTAssertEqualObjects(group.name, operationGroupName, "Should have expected group name");
} : nil
runAfterModification:nil];
}
- (void)expectCKModifyKeyRecords:(NSUInteger)expectedNumberOfRecords
currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
tlkShareRecords:(NSUInteger)expectedTLKShareRecords
zoneID:(CKRecordZoneID*)zoneID
{
return [self expectCKModifyKeyRecords:expectedNumberOfRecords
currentKeyPointerRecords:expectedCurrentKeyRecords
tlkShareRecords:expectedTLKShareRecords
zoneID:zoneID
checkModifiedRecord:nil];
}
- (void)expectCKModifyKeyRecords:(NSUInteger)expectedNumberOfRecords
currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
tlkShareRecords:(NSUInteger)expectedTLKShareRecords
zoneID:(CKRecordZoneID*)zoneID
checkModifiedRecord:(BOOL (^_Nullable)(CKRecord*))checkModifiedRecord
{
NSNumber* nkeys = [NSNumber numberWithUnsignedInteger: expectedNumberOfRecords];
NSNumber* ncurrentkeys = [NSNumber numberWithUnsignedInteger: expectedCurrentKeyRecords];
NSNumber* ntlkshares = [NSNumber numberWithUnsignedInteger: expectedTLKShareRecords];
[self expectCKModifyRecords:@{SecCKRecordIntermediateKeyType: nkeys,
SecCKRecordCurrentKeyType: ncurrentkeys,
SecCKRecordTLKShareType: ntlkshares,
}
deletedRecordTypeCounts:nil
zoneID:zoneID
checkModifiedRecord:checkModifiedRecord
inspectOperationGroup:nil
runAfterModification:nil];
}
- (void)expectCKModifyRecords:(NSDictionary<NSString*, NSNumber*>* _Nullable) expectedRecordTypeCounts
deletedRecordTypeCounts:(NSDictionary<NSString*, NSNumber*>* _Nullable) expectedDeletedRecordTypeCounts
zoneID:(CKRecordZoneID*) zoneID
checkModifiedRecord:(BOOL (^ _Nullable)(CKRecord*)) checkModifiedRecord
runAfterModification:(void (^ _Nullable) (void))afterModification
{
[self expectCKModifyRecords:expectedRecordTypeCounts
deletedRecordTypeCounts:expectedDeletedRecordTypeCounts
zoneID:zoneID
checkModifiedRecord:checkModifiedRecord
inspectOperationGroup:nil
runAfterModification:afterModification];
}
- (void)expectCKModifyRecords:(NSDictionary<NSString*, NSNumber*>* _Nullable)expectedRecordTypeCounts
deletedRecordTypeCounts:(NSDictionary<NSString*, NSNumber*>* _Nullable)expectedDeletedRecordTypeCounts
zoneID:(CKRecordZoneID*) zoneID
checkModifiedRecord:(BOOL (^)(CKRecord*)) checkModifiedRecord
inspectOperationGroup:(void (^ _Nullable)(CKOperationGroup* _Nullable))inspectOperationGroup
runAfterModification:(void (^) (void))afterModification
{
__weak __typeof(self) weakSelf = self;
// Create an object for the block to retain and modify
BoolHolder* runAlready = [[BoolHolder alloc] init];
secnotice("fakecloudkit", "expecting an operation matching modifications: expectedRecordTypeCounts, expectedDeletedRecordTypeCounts);
[[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
secnotice("fakecloudkit", "Received an operation ( __block bool matches = false;
if(runAlready.state) {
secnotice("fakecloudkit", "Run already, skipping");
return NO;
}
if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
XCTAssertNotNil(strongSelf, "self exists");
CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
matches = true;
NSMutableDictionary<NSString*, NSNumber*>* modifiedRecordTypeCounts = [[NSMutableDictionary alloc] init];
NSMutableDictionary<NSString*, NSNumber*>* deletedRecordTypeCounts = [[NSMutableDictionary alloc] init];
// First: check if it matches. If it does, _then_ execute the operation.
// Supports single-zone atomic writes only
if(!op.atomic) {
// We only care about atomic operations
secnotice("fakecloudkit", "Not an atomic operation; quitting: return NO;
}
FakeCKZone* zone = strongSelf.zones[zoneID];
XCTAssertNotNil(zone, "Have a zone for these records");
__block BOOL result = YES;
dispatch_sync(zone.queue, ^{
for(CKRecord* record in op.recordsToSave) {
if(![record.recordID.zoneID isEqual: zoneID]) {
secnotice("fakecloudkit", "Modified record zone ID mismatch: result = NO;
return;
}
NSError* recordError = [zone errorFromSavingRecord: record];
if(recordError) {
secnotice("fakecloudkit", "Record zone rejected record write: XCTFail(@"Record zone rejected record write: result = NO;
return;
}
NSNumber* currentCountNumber = modifiedRecordTypeCounts[record.recordType];
NSUInteger currentCount = currentCountNumber ? [currentCountNumber unsignedIntegerValue] : 0;
modifiedRecordTypeCounts[record.recordType] = [NSNumber numberWithUnsignedInteger: currentCount + 1];
}
for(CKRecordID* recordID in op.recordIDsToDelete) {
if(![recordID.zoneID isEqual: zoneID]) {
matches = false;
secnotice("fakecloudkit", "Deleted record zone ID mismatch: }
// Find the object in CloudKit, and record its type
CKRecord* record = strongSelf.zones[zoneID].currentDatabase[recordID];
if(record) {
NSNumber* currentCountNumber = deletedRecordTypeCounts[record.recordType];
NSUInteger currentCount = currentCountNumber ? [currentCountNumber unsignedIntegerValue] : 0;
deletedRecordTypeCounts[record.recordType] = [NSNumber numberWithUnsignedInteger: currentCount + 1];
}
}
NSMutableDictionary* filteredExpectedRecordTypeCounts = [expectedRecordTypeCounts mutableCopy];
for(NSString* key in filteredExpectedRecordTypeCounts.allKeys) {
if([filteredExpectedRecordTypeCounts[key] isEqual: [NSNumber numberWithInt:0]]) {
filteredExpectedRecordTypeCounts[key] = nil;
}
}
filteredExpectedRecordTypeCounts[SecCKRecordManifestType] = modifiedRecordTypeCounts[SecCKRecordManifestType];
filteredExpectedRecordTypeCounts[SecCKRecordManifestLeafType] = modifiedRecordTypeCounts[SecCKRecordManifestLeafType];
// Inspect that we have exactly the same records as we expect
if(expectedRecordTypeCounts) {
matches &= !![modifiedRecordTypeCounts isEqual: filteredExpectedRecordTypeCounts];
if(!matches) {
secnotice("fakecloudkit", "Record number mismatch: attempted: result = NO;
return;
}
} else {
matches &= op.recordsToSave.count == 0u;
if(!matches) {
secnotice("fakecloudkit", "Record number mismatch: attempted: result = NO;
return;
}
}
if(expectedDeletedRecordTypeCounts) {
matches &= !![deletedRecordTypeCounts isEqual: expectedDeletedRecordTypeCounts];
if(!matches) {
secnotice("fakecloudkit", "Deleted record number mismatch: attempted: result = NO;
return;
}
} else {
matches &= op.recordIDsToDelete.count == 0u;
if(!matches) {
secnotice("fakecloudkit", "Deleted record number mismatch: attempted: result = NO;
return;
}
}
// We have the right number of things, and their etags match. Ensure that they have the right etags
if(matches && checkModifiedRecord) {
// Clearly we have the right number of things. Call checkRecord on them...
for(CKRecord* record in op.recordsToSave) {
matches &= !!(checkModifiedRecord(record));
if(!matches) {
secnotice("fakecloudkit", "Check record reports NO: result = NO;
return;
}
}
}
if(matches) {
if(inspectOperationGroup) {
inspectOperationGroup(op.group);
}
// Emulate cloudkit and schedule the operation for execution. Be sure to wait for this operation
// if you'd like to read the data from this write.
NSBlockOperation* ckop = [NSBlockOperation named:@"cloudkit-write" withBlock: ^{
@synchronized(zone.currentDatabase) {
if(zone.blockBeforeWriteOperation) {
zone.blockBeforeWriteOperation();
}
NSMutableArray* savedRecords = [[NSMutableArray alloc] init];
for(CKRecord* record in op.recordsToSave) {
CKRecord* reflectedRecord = [record copy];
reflectedRecord.modificationDate = [NSDate date];
[zone addToZone: reflectedRecord];
[savedRecords addObject:reflectedRecord];
op.perRecordCompletionBlock(reflectedRecord, nil);
}
for(CKRecordID* recordID in op.recordIDsToDelete) {
// I don't believe CloudKit fails an operation if you delete a record that's not there, so:
[zone deleteCKRecordIDFromZone: recordID];
}
if(afterModification) {
afterModification();
}
op.modifyRecordsCompletionBlock(savedRecords, op.recordIDsToDelete, nil);
op.isFinished = YES;
}
}];
[ckop addNullableDependency:strongSelf.ckModifyHoldOperation];
[strongSelf.operationQueue addOperation: ckop];
}
});
if(result != YES) {
return result;
}
}
if(matches) {
runAlready.state = true;
}
return matches ? YES : NO;
}]];
}
- (void)failNextZoneCreation:(CKRecordZoneID*)zoneID {
XCTAssertNil(self.zones[zoneID], "Zone does not exist yet");
self.zones[zoneID] = [[FakeCKZone alloc] initZone: zoneID];
self.zones[zoneID].creationError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
code:CKErrorNetworkUnavailable
userInfo:@{
CKErrorRetryAfterKey: @(0.5),
}];
}
// Report success, but don't actually create the zone.
// This way, you can find ZoneNotFound errors later on
- (void)failNextZoneCreationSilently:(CKRecordZoneID*)zoneID {
XCTAssertNil(self.zones[zoneID], "Zone does not exist yet");
self.zones[zoneID] = [[FakeCKZone alloc] initZone: zoneID];
self.zones[zoneID].failCreationSilently = true;
}
- (void)failNextZoneSubscription:(CKRecordZoneID*)zoneID {
XCTAssertNotNil(self.zones[zoneID], "Zone exists");
self.zones[zoneID].subscriptionError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
}
- (void)failNextZoneSubscription:(CKRecordZoneID*)zoneID withError:(NSError*)error {
XCTAssertNotNil(self.zones[zoneID], "Zone exists");
self.zones[zoneID].subscriptionError = error;
}
- (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID {
[self failNextCKAtomicModifyItemRecordsUpdateFailure:zoneID blockAfterReject:nil];
}
- (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID blockAfterReject: (void (^)(void))blockAfterReject {
[self failNextCKAtomicModifyItemRecordsUpdateFailure:zoneID blockAfterReject:blockAfterReject withError:nil];
}
- (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID blockAfterReject: (void (^)(void))blockAfterReject withError:(NSError*)error {
__weak __typeof(self) weakSelf = self;
[[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
XCTAssertNotNil(strongSelf, "self exists");
__block bool rejected = false;
if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
if(!op.atomic) {
// We only care about atomic operations
return NO;
}
// We want to only match zone updates pertaining to this zone
for(CKRecord* record in op.recordsToSave) {
if(![record.recordID.zoneID isEqual: zoneID]) {
return NO;
}
}
FakeCKZone* zone = strongSelf.zones[zoneID];
XCTAssertNotNil(zone, "Have a zone for these records");
rejected = true;
if(error) {
[strongSelf rejectWrite: op withError:error];
} else {
NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = [[NSMutableDictionary alloc] init];
[strongSelf rejectWrite: op failedRecords:failedRecords];
}
if(blockAfterReject) {
blockAfterReject();
}
}
return rejected ? YES : NO;
}]];
}
- (void)expectCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID {
__weak __typeof(self) weakSelf = self;
[[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
XCTAssertNotNil(strongSelf, "self exists");
__block bool rejected = false;
if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
secnotice("fakecloudkit", "checking for expectCKAtomicModifyItemRecordsUpdateFailure");
if(!op.atomic) {
// We only care about atomic operations
secnotice("fakecloudkit", "expectCKAtomicModifyItemRecordsUpdateFailure: update not atomic");
return NO;
}
// We want to only match zone updates pertaining to this zone
for(CKRecord* record in op.recordsToSave) {
if(![record.recordID.zoneID isEqual: zoneID]) {
secnotice("fakecloudkit", "expectCKAtomicModifyItemRecordsUpdateFailure: return NO;
}
}
FakeCKZone* zone = strongSelf.zones[zoneID];
XCTAssertNotNil(zone, "Have a zone for these records");
NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = [[NSMutableDictionary alloc] init];
@synchronized(zone.currentDatabase) {
for(CKRecord* record in op.recordsToSave) {
// Check if we should allow this transaction
NSError* recordSaveError = [zone errorFromSavingRecord: record];
if(recordSaveError) {
failedRecords[record.recordID] = recordSaveError;
rejected = true;
}
}
}
if(rejected) {
[strongSelf rejectWrite: op failedRecords:failedRecords];
} else {
secnotice("fakecloudkit", "expectCKAtomicModifyItemRecordsUpdateFailure: doesn't seem like an error to us");
}
}
return rejected ? YES : NO;
}]];
}
-(void)rejectWrite:(CKModifyRecordsOperation*)op withError:(NSError*)error {
// Emulate cloudkit and schedule the operation for execution. Be sure to wait for this operation
// if you'd like to read the data from this write.
NSBlockOperation* ckop = [NSBlockOperation named:@"cloudkit-reject-write-error" withBlock: ^{
op.modifyRecordsCompletionBlock(nil, nil, error);
op.isFinished = YES;
}];
[ckop addNullableDependency: self.ckModifyHoldOperation];
[self.operationQueue addOperation: ckop];
}
-(void)rejectWrite:(CKModifyRecordsOperation*)op failedRecords:(NSMutableDictionary<CKRecordID*, NSError*>*)failedRecords {
// Add the batch request failed errors
for(CKRecord* record in op.recordsToSave) {
NSError* exists = failedRecords[record.recordID];
if(!exists) {
// TODO: might have important userInfo, but we're not mocking that yet
failedRecords[record.recordID] = [[CKPrettyError alloc] initWithDomain: CKErrorDomain code: CKErrorBatchRequestFailed userInfo: @{}];
}
}
NSError* error = [[CKPrettyError alloc] initWithDomain: CKErrorDomain code: CKErrorPartialFailure userInfo: @{CKPartialErrorsByItemIDKey: failedRecords}];
// Emulate cloudkit and schedule the operation for execution. Be sure to wait for this operation
// if you'd like to read the data from this write.
NSBlockOperation* ckop = [NSBlockOperation named:@"cloudkit-reject-write" withBlock: ^{
op.modifyRecordsCompletionBlock(nil, nil, error);
op.isFinished = YES;
}];
[ckop addNullableDependency: self.ckModifyHoldOperation];
[self.operationQueue addOperation: ckop];
}
- (void)expectCKDeleteItemRecords:(NSUInteger)expectedNumberOfRecords
zoneID:(CKRecordZoneID*)zoneID
{
return [self expectCKDeleteItemRecords:expectedNumberOfRecords
zoneID:zoneID
expectedOperationGroupName:nil];
}
- (void)expectCKDeleteItemRecords:(NSUInteger)expectedNumberOfRecords
zoneID:(CKRecordZoneID*)zoneID
expectedOperationGroupName:(NSString* _Nullable)operationGroupName
{
// We're updating the device state type on every update, so add it in here
NSMutableDictionary* expectedRecords = [@{
SecCKRecordDeviceStateType: [NSNumber numberWithUnsignedInteger:expectedNumberOfRecords],
} mutableCopy];
if(SecCKKSSyncManifests()) {
// TODO: this really shouldn't be 2.
expectedRecords[SecCKRecordManifestType] = [NSNumber numberWithInt: 2];
expectedRecords[SecCKRecordManifestLeafType] = [NSNumber numberWithInt: 72];
}
[self expectCKModifyRecords:expectedRecords
deletedRecordTypeCounts:@{SecCKRecordItemType: [NSNumber numberWithUnsignedInteger: expectedNumberOfRecords]}
zoneID:zoneID
checkModifiedRecord:nil
inspectOperationGroup:operationGroupName != nil ? ^(CKOperationGroup* group) {
XCTAssertEqualObjects(group.name, operationGroupName, "Should have expected group name");
} : nil
runAfterModification:nil];
}
-(void)waitForCKModifications {
// CloudKit modifications are put on the local queue.
// This is heavyweight but should suffice.
[self.operationQueue waitUntilAllOperationsAreFinished];
}
- (void)tearDown {
NSString* testName = [self.name componentsSeparatedByString:@" "][1];
testName = [testName stringByReplacingOccurrencesOfString:@"]" withString:@""];
secnotice("ckkstest", "Ending test
if(SecCKKSIsEnabled()) {
self.accountStatus = CKAccountStatusCouldNotDetermine;
// If the test never initialized the account state, don't call status later
bool callStatus = [self.ckaccountHoldOperation isFinished];
[self.ckaccountHoldOperation cancel];
self.ckaccountHoldOperation = nil;
// Ensure we don't have any blocking operations left
[self.operationQueue cancelAllOperations];
[self waitForCKModifications];
XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:20*NSEC_PER_SEC],
"Timeout did not occur waiting for SecCKKSInitialize");
// Ensure that we can fetch zone status for all zones
if(callStatus) {
XCTestExpectation *statusReturned = [self expectationWithDescription:@"status returned"];
[self.injectedManager rpcStatus:nil reply:^(NSArray<NSDictionary *> *result, NSError *error) {
XCTAssertNil(error, "Should be no error fetching status");
[statusReturned fulfill];
}];
[self waitForExpectations: @[statusReturned] timeout:20];
// Make sure this happens before teardown.
XCTAssertEqual(0, [self.accountStateTracker.finishedInitialDispatches wait:20*NSEC_PER_SEC], "Account state tracker initialized itself");
dispatch_group_t accountChangesDelivered = [self.accountStateTracker checkForAllDeliveries];
XCTAssertEqual(0, dispatch_group_wait(accountChangesDelivered, dispatch_time(DISPATCH_TIME_NOW, 10*NSEC_PER_SEC)), "Account state tracker finished delivering everything");
}
}
[self.injectedManager.zoneChangeFetcher halt];
[super tearDown];
[self.injectedManager cancelPendingOperations];
[self.injectedManager clearAllViews];
self.injectedManager = nil;
[self.mockCKKSViewManager stopMocking];
self.mockCKKSViewManager = nil;
self.injectedOTManager.viewManager = nil;
[self.injectedOTManager clearAllContexts];
self.injectedOTManager = nil;
[OTManager resetManager:true to:nil];
[self.mockAccountStateTracker stopMocking];
self.mockAccountStateTracker = nil;
[self.mockFakeCKModifyRecordZonesOperation stopMocking];
self.mockFakeCKModifyRecordZonesOperation = nil;
[self.mockFakeCKModifySubscriptionsOperation stopMocking];
self.mockFakeCKModifySubscriptionsOperation = nil;
[self.mockFakeCKFetchRecordZoneChangesOperation stopMocking];
self.mockFakeCKFetchRecordZoneChangesOperation = nil;
[self.mockFakeCKFetchRecordsOperation stopMocking];
self.mockFakeCKFetchRecordsOperation = nil;
[self.mockFakeCKQueryOperation stopMocking];
self.mockFakeCKQueryOperation = nil;
[self.mockDatabase stopMocking];
self.mockDatabase = nil;
[self.mockDatabaseExceptionCatcher stopMocking];
self.mockDatabaseExceptionCatcher = nil;
[self.mockContainer stopMocking];
self.mockContainer = nil;
[self.mockTTR stopMocking];
self.mockTTR = nil;
self.ttrExpectation = nil;
self.isTTRRatelimited = true;
self.zones = nil;
_mockSOSAdapter = nil;
_mockOctagonAdapter = nil;
// Bring the database down and delete it
NSURL* keychainDir = (NSURL*)CFBridgingRelease(SecCopyHomeURL());
// Force-close the analytics DBs so we can clean out the test directory
[[CKKSAnalytics logger] removeState];
SecItemDataSourceFactoryReleaseAll();
SecKeychainDbForceClose();
SecKeychainDbReset(NULL);
// Only perform the desctructive step if the url matches what we expect!
if([keychainDir.path hasPrefix:[NSString stringWithFormat:@"/tmp/ secnotice("ckkstest", "Removing test-specific keychain directory at
NSError* removeError = nil;
[[NSFileManager defaultManager] removeItemAtURL:keychainDir error:&removeError];
XCTAssertNil(removeError, "Should have been able to remove temporary files");
} else {
XCTFail("Unsure what happened to the keychain directory URL: }
SecCKKSTestResetFlags();
}
- (CKKSKey*) fakeTLK: (CKRecordZoneID*)zoneID {
CKKSKey* key = [[CKKSKey alloc] initSelfWrappedWithAESKey:[[CKKSAESSIVKey alloc] initWithBase64: @"uImdbZ7Zg+6WJXScTnRBfNmoU1UiMkSYxWc+d1Vuq3IFn2RmTRkTdWTe3HmeWo1pAomqy+upK8KHg2PGiRGhqg=="]
uuid:[[NSUUID UUID] UUIDString]
keyclass:SecCKKSKeyClassTLK
state: SecCKKSProcessedStateLocal
zoneID:zoneID
encodedCKRecord: nil
currentkey: true];
[key CKRecordWithZoneID: zoneID];
return key;
}
- (NSError*)ckInternalServerExtensionError:(NSInteger)code description:(NSString*)desc {
return [FakeCKZone internalPluginError:@"CloudkitKeychainService" code:code description:desc];
}
@end
#endif