]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+MultiZone.m
Security-59754.41.1.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSTests+MultiZone.m
1
2 #if OCTAGON
3
4 #import <CloudKit/CloudKit.h>
5 #import <XCTest/XCTest.h>
6 #import <OCMock/OCMock.h>
7 #import <notify.h>
8
9 #include <Security/SecItemPriv.h>
10 #import <TrustedPeers/TrustedPeers.h>
11 #import <TrustedPeers/TPPBPolicyKeyViewMapping.h>
12 #import <TrustedPeers/TPDictionaryMatchingRules.h>
13
14 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
15 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
16 #import "keychain/ckks/CKKS.h"
17 #import "keychain/ckks/CKKSKeychainView.h"
18 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
19 #import "keychain/ckks/CKKSItemEncrypter.h"
20 #import "keychain/ckks/CKKSKey.h"
21 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
22 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
23 #import "keychain/ckks/CKKSStates.h"
24 #import "keychain/ckks/CKKSSynchronizeOperation.h"
25 #import "keychain/ckks/CKKSViewManager.h"
26 #import "keychain/ckks/CKKSZoneStateEntry.h"
27 #import "keychain/ckks/CKKSManifest.h"
28
29 #import "keychain/ckks/tests/CKKSTests+MultiZone.h"
30 #import "keychain/ckks/tests/MockCloudKit.h"
31
32 #pragma clang diagnostic push
33 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
34 #include <Security/SecureObjectSync/SOSCloudCircle.h>
35 #include "keychain/SecureObjectSync/SOSAccountPriv.h"
36 #include "keychain/SecureObjectSync/SOSAccount.h"
37 #include "keychain/SecureObjectSync/SOSInternal.h"
38 #include "keychain/SecureObjectSync/SOSFullPeerInfo.h"
39 #pragma clang diagnostic pop
40
41 #include <Security/SecKey.h>
42 #include <Security/SecKeyPriv.h>
43 #pragma clang diagnostic pop
44
45 @interface CloudKitKeychainSyncingMultiZoneTestsBase ()
46 @end
47
48 @implementation CloudKitKeychainSyncingMultiZoneTestsBase
49 + (void)setUp {
50 SecCKKSEnable();
51 SecCKKSResetSyncing();
52 [super setUp];
53 }
54
55 - (NSSet*)managedViewList {
56 NSMutableSet* parentSet = [[super managedViewList] mutableCopy];
57 [parentSet addObject:@"Passwords"];
58 return parentSet;
59 }
60
61 // Make a policy as normal for most views, but Passwords is special
62 - (TPSyncingPolicy*)viewSortingPolicyForManagedViewList
63 {
64 NSMutableArray<TPPBPolicyKeyViewMapping*>* rules = [NSMutableArray array];
65
66 for(NSString* viewName in self.managedViewList) {
67 TPPBPolicyKeyViewMapping* mapping = [[TPPBPolicyKeyViewMapping alloc] init];
68 mapping.view = viewName;
69
70 // The real passwords view is on com.appple.cfnetwork, but for these tests, let's just use the sbd agrp (because of how the entitlements are specified)
71 if([viewName isEqualToString:@"Passwords"]) {
72 mapping.matchingRule = [TPDictionaryMatchingRule fieldMatch:@"agrp"
73 fieldRegex:[NSString stringWithFormat:@"^com\\.apple\\.sbd$"]];
74 } else {
75 mapping.matchingRule = [TPDictionaryMatchingRule fieldMatch:@"vwht"
76 fieldRegex:[NSString stringWithFormat:@"^%@$", viewName]];
77 }
78
79 [rules addObject:mapping];
80 }
81
82 NSSet<NSString*>* viewList = [self managedViewList];
83 TPSyncingPolicy* policy = [[TPSyncingPolicy alloc] initWithModel:@"test-policy"
84 version:[[TPPolicyVersion alloc] initWithVersion:1 hash:@"fake-policy-for-views"]
85 viewList:viewList
86 userControllableViews:[NSSet set]
87 syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_ENABLED
88 viewsToPiggybackTLKs:[viewList containsObject:@"Passwords"] ? [NSSet setWithObject:@"Passwords"] : [NSSet set]
89 keyViewMapping:rules];
90
91 return policy;
92 }
93
94 - (void)setUp {
95 SecCKKSSetSyncManifests(false);
96 SecCKKSSetEnforceManifests(false);
97
98 [super setUp];
99 SecCKKSTestSetDisableSOS(false);
100
101 // Wait for the ViewManager to be brought up
102 XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:20*NSEC_PER_SEC], "No timeout waiting for SecCKKSInitialize");
103
104 self.engramZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Engram" ownerName:CKCurrentUserDefaultName];
105 self.engramZone = [[FakeCKZone alloc] initZone: self.engramZoneID];
106 self.zones[self.engramZoneID] = self.engramZone;
107 self.engramView = [[CKKSViewManager manager] findOrCreateView:@"Engram"];
108 XCTAssertNotNil(self.engramView, "CKKSViewManager created the Engram view");
109 [self.ckksViews addObject:self.engramView];
110 [self.ckksZones addObject:self.engramZoneID];
111
112 self.manateeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Manatee" ownerName:CKCurrentUserDefaultName];
113 self.manateeZone = [[FakeCKZone alloc] initZone: self.manateeZoneID];
114 self.zones[self.manateeZoneID] = self.manateeZone;
115 self.manateeView = [[CKKSViewManager manager] findOrCreateView:@"Manatee"];
116 XCTAssertNotNil(self.manateeView, "CKKSViewManager created the Manatee view");
117 [self.ckksViews addObject:self.manateeView];
118 [self.ckksZones addObject:self.manateeZoneID];
119
120 self.autoUnlockZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"AutoUnlock" ownerName:CKCurrentUserDefaultName];
121 self.autoUnlockZone = [[FakeCKZone alloc] initZone: self.autoUnlockZoneID];
122 self.zones[self.autoUnlockZoneID] = self.autoUnlockZone;
123 self.autoUnlockView = [[CKKSViewManager manager] findOrCreateView:@"AutoUnlock"];
124 XCTAssertNotNil(self.autoUnlockView, "CKKSViewManager created the AutoUnlock view");
125 [self.ckksViews addObject:self.autoUnlockView];
126 [self.ckksZones addObject:self.autoUnlockZoneID];
127
128 self.healthZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Health" ownerName:CKCurrentUserDefaultName];
129 self.healthZone = [[FakeCKZone alloc] initZone: self.healthZoneID];
130 self.zones[self.healthZoneID] = self.healthZone;
131 self.healthView = [[CKKSViewManager manager] findOrCreateView:@"Health"];
132 XCTAssertNotNil(self.healthView, "CKKSViewManager created the Health view");
133 [self.ckksViews addObject:self.healthView];
134 [self.ckksZones addObject:self.healthZoneID];
135
136 self.applepayZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"ApplePay" ownerName:CKCurrentUserDefaultName];
137 self.applepayZone = [[FakeCKZone alloc] initZone: self.applepayZoneID];
138 self.zones[self.applepayZoneID] = self.applepayZone;
139 self.applepayView = [[CKKSViewManager manager] findOrCreateView:@"ApplePay"];
140 XCTAssertNotNil(self.applepayView, "CKKSViewManager created the ApplePay view");
141 [self.ckksViews addObject:self.applepayView];
142 [self.ckksZones addObject:self.applepayZoneID];
143
144 self.homeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Home" ownerName:CKCurrentUserDefaultName];
145 self.homeZone = [[FakeCKZone alloc] initZone: self.homeZoneID];
146 self.zones[self.homeZoneID] = self.homeZone;
147 self.homeView = [[CKKSViewManager manager] findOrCreateView:@"Home"];
148 XCTAssertNotNil(self.homeView, "CKKSViewManager created the Home view");
149 [self.ckksViews addObject:self.homeView];
150 [self.ckksZones addObject:self.homeZoneID];
151
152 self.limitedZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"LimitedPeersAllowed" ownerName:CKCurrentUserDefaultName];
153 self.limitedZone = [[FakeCKZone alloc] initZone: self.limitedZoneID];
154 self.zones[self.limitedZoneID] = self.limitedZone;
155 self.limitedView = [[CKKSViewManager manager] findOrCreateView:@"LimitedPeersAllowed"];
156 XCTAssertNotNil(self.limitedView, "should have a limited ckks view");
157 XCTAssertNotNil(self.limitedView, "CKKSViewManager created the LimitedPeersAllowed view");
158 [self.ckksViews addObject:self.limitedView];
159 [self.ckksZones addObject:self.limitedZoneID];
160
161 self.passwordsZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Passwords" ownerName:CKCurrentUserDefaultName];
162 self.passwordsZone = [[FakeCKZone alloc] initZone: self.passwordsZoneID];
163 self.zones[self.passwordsZoneID] = self.passwordsZone;
164 self.passwordsView = [[CKKSViewManager manager] findOrCreateView:@"Passwords"];
165 XCTAssertNotNil(self.passwordsView, "should have a passwords ckks view");
166 XCTAssertNotNil(self.passwordsView, "CKKSViewManager created the Passwords view");
167 [self.ckksViews addObject:self.passwordsView];
168 [self.ckksZones addObject:self.passwordsZoneID];
169
170 // These tests, at least, will use the policy codepaths!
171 [self.injectedManager setOverrideCKKSViewsFromPolicy:YES];
172 [self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
173 }
174
175 + (void)tearDown {
176 SecCKKSTestSetDisableSOS(true);
177 [super tearDown];
178 SecCKKSResetSyncing();
179 }
180
181 - (void)tearDown {
182 // If the test didn't already do this, allow each zone to spin up
183 self.accountStatus = CKAccountStatusNoAccount;
184 [self startCKKSSubsystem];
185
186 [self.engramView halt];
187 [self.engramView waitUntilAllOperationsAreFinished];
188 self.engramView = nil;
189
190 [self.manateeView halt];
191 [self.manateeView waitUntilAllOperationsAreFinished];
192 self.manateeView = nil;
193
194 [self.autoUnlockView halt];
195 [self.autoUnlockView waitUntilAllOperationsAreFinished];
196 self.autoUnlockView = nil;
197
198 [self.healthView halt];
199 [self.healthView waitUntilAllOperationsAreFinished];
200 self.healthView = nil;
201
202 [self.applepayView halt];
203 [self.applepayView waitUntilAllOperationsAreFinished];
204 self.applepayView = nil;
205
206 [self.homeView halt];
207 [self.homeView waitUntilAllOperationsAreFinished];
208 self.homeView = nil;
209
210 [self.limitedView halt];
211 [self.limitedView waitUntilAllOperationsAreFinished];
212 self.limitedView = nil;
213
214 [self.passwordsView halt];
215 [self.passwordsView waitUntilAllOperationsAreFinished];
216 self.passwordsView = nil;
217
218 [super tearDown];
219 }
220
221 - (ZoneKeys*)engramZoneKeys {
222 return self.keys[self.engramZoneID];
223 }
224
225 - (ZoneKeys*)manateeZoneKeys {
226 return self.keys[self.manateeZoneID];
227 }
228
229 - (void)saveFakeKeyHierarchiesToLocalDatabase {
230 for(CKRecordZoneID* zoneID in self.ckksZones) {
231 [self createAndSaveFakeKeyHierarchy: zoneID];
232 }
233 }
234
235 - (void)putFakeDeviceStatusesInCloudKit {
236 [self putFakeDeviceStatusInCloudKit: self.engramZoneID];
237 [self putFakeDeviceStatusInCloudKit: self.manateeZoneID];
238 [self putFakeDeviceStatusInCloudKit: self.autoUnlockZoneID];
239 [self putFakeDeviceStatusInCloudKit: self.healthZoneID];
240 [self putFakeDeviceStatusInCloudKit: self.applepayZoneID];
241 [self putFakeDeviceStatusInCloudKit: self.homeZoneID];
242 [self putFakeDeviceStatusInCloudKit: self.limitedZoneID];
243 [self putFakeDeviceStatusInCloudKit: self.passwordsZoneID];
244 }
245
246 - (void)putFakeKeyHierachiesInCloudKit{
247 [self putFakeKeyHierarchyInCloudKit: self.engramZoneID];
248 [self putFakeKeyHierarchyInCloudKit: self.manateeZoneID];
249 [self putFakeKeyHierarchyInCloudKit: self.autoUnlockZoneID];
250 [self putFakeKeyHierarchyInCloudKit: self.healthZoneID];
251 [self putFakeKeyHierarchyInCloudKit: self.applepayZoneID];
252 [self putFakeKeyHierarchyInCloudKit: self.homeZoneID];
253 [self putFakeKeyHierarchyInCloudKit: self.limitedZoneID];
254 [self putFakeKeyHierarchyInCloudKit: self.passwordsZoneID];
255 }
256
257 - (void)saveTLKsToKeychain{
258 [self saveTLKMaterialToKeychain:self.engramZoneID];
259 [self saveTLKMaterialToKeychain:self.manateeZoneID];
260 [self saveTLKMaterialToKeychain:self.autoUnlockZoneID];
261 [self saveTLKMaterialToKeychain:self.healthZoneID];
262 [self saveTLKMaterialToKeychain:self.applepayZoneID];
263 [self saveTLKMaterialToKeychain:self.homeZoneID];
264 [self saveTLKMaterialToKeychain:self.limitedZoneID];
265 [self saveTLKMaterialToKeychain:self.passwordsZoneID];
266 }
267
268 - (void)deleteTLKMaterialsFromKeychain{
269 [self deleteTLKMaterialFromKeychain: self.engramZoneID];
270 [self deleteTLKMaterialFromKeychain: self.manateeZoneID];
271 [self deleteTLKMaterialFromKeychain: self.autoUnlockZoneID];
272 [self deleteTLKMaterialFromKeychain: self.healthZoneID];
273 [self deleteTLKMaterialFromKeychain: self.applepayZoneID];
274 [self deleteTLKMaterialFromKeychain: self.homeZoneID];
275 [self deleteTLKMaterialFromKeychain:self.limitedZoneID];
276 [self deleteTLKMaterialFromKeychain:self.passwordsZoneID];
277 }
278
279 - (void)waitForKeyHierarchyReadinesses {
280 [self.manateeView waitForKeyHierarchyReadiness];
281 [self.engramView waitForKeyHierarchyReadiness];
282 [self.autoUnlockView waitForKeyHierarchyReadiness];
283 [self.healthView waitForKeyHierarchyReadiness];
284 [self.applepayView waitForKeyHierarchyReadiness];
285 [self.homeView waitForKeyHierarchyReadiness];
286 [self.limitedView waitForKeyHierarchyReadiness];
287 [self.passwordsView waitForKeyHierarchyReadiness];
288 }
289
290 - (void)expectCKKSTLKSelfShareUploads {
291 for(CKRecordZoneID* zoneID in self.ckksZones) {
292 [self expectCKKSTLKSelfShareUpload:zoneID];
293 }
294
295 }
296
297 @end
298
299 @interface CloudKitKeychainSyncingMultiZoneTests : CloudKitKeychainSyncingMultiZoneTestsBase
300 @end
301
302 @implementation CloudKitKeychainSyncingMultiZoneTests
303
304 - (void)testAllViewsMakeNewKeyHierarchies {
305 // Test starts with nothing anywhere
306
307 // Due to our new cross-zone fetch system, CKKS should only issue one fetch for all zones
308 // Since the tests can sometimes be slow, slow down the fetcher to normal speed
309 [self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
310 self.silentFetchesAllowed = false;
311 [self expectCKFetch];
312
313 [self startCKKSSubsystem];
314 [self performOctagonTLKUpload:self.ckksViews];
315
316 OCMVerifyAllWithDelay(self.mockDatabase, 20);
317
318 for(CKKSKeychainView* view in self.ckksViews) {
319 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
320 }
321 }
322
323 - (void)testAllViewsAcceptExistingKeyHierarchies {
324 for(CKRecordZoneID* zoneID in self.ckksZones) {
325 [self putFakeKeyHierarchyInCloudKit:zoneID];
326 [self saveTLKMaterialToKeychain:zoneID];
327 [self expectCKKSTLKSelfShareUpload:zoneID];
328 }
329
330 [self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
331 self.silentFetchesAllowed = false;
332 [self expectCKFetch];
333
334 [self startCKKSSubsystem];
335 OCMVerifyAllWithDelay(self.mockDatabase, 20);
336
337 for(CKKSKeychainView* view in self.ckksViews) {
338 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
339 }
340 }
341
342 - (void)testAddEngramManateeItems {
343 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
344
345 [self startCKKSSubsystem];
346
347 XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
348 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
349 XCTestExpectation* manateeChanged = [self expectChangeForView:self.manateeZoneID.zoneName];
350
351 // We expect a single record to be uploaded to the engram view.
352 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.engramZoneID];
353 [self addGenericPassword: @"data" account: @"account-delete-me-engram" viewHint:(NSString*) kSecAttrViewHintEngram];
354
355 OCMVerifyAllWithDelay(self.mockDatabase, 20);
356 [self waitForExpectations:@[engramChanged] timeout:1];
357 [self waitForExpectations:@[pcsChanged] timeout:1];
358
359 pcsChanged = [self expectChangeForView:@"PCS"];
360
361 // We expect a single record to be uploaded to the manatee view.
362 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID];
363 [self addGenericPassword: @"data" account: @"account-delete-me-manatee" viewHint:(NSString*) kSecAttrViewHintManatee];
364
365 OCMVerifyAllWithDelay(self.mockDatabase, 20);
366 [self waitForExpectations:@[manateeChanged] timeout:1];
367 [self waitForExpectations:@[pcsChanged] timeout:1];
368 }
369
370 - (void)testAddAutoUnlockItems {
371 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
372
373 [self startCKKSSubsystem];
374
375 XCTestExpectation* autoUnlockChanged = [self expectChangeForView:self.autoUnlockZoneID.zoneName];
376 // AutoUnlock is NOT is PCS view, so it should not send the fake 'PCS' view notification
377 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
378 pcsChanged.inverted = YES;
379
380 // We expect a single record to be uploaded to the AutoUnlock view.
381 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.autoUnlockZoneID];
382 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintAutoUnlock];
383
384 OCMVerifyAllWithDelay(self.mockDatabase, 20);
385 [self waitForExpectations:@[autoUnlockChanged] timeout:1];
386 [self waitForExpectations:@[pcsChanged] timeout:0.2];
387 }
388
389 - (void)testAddHealthItems {
390 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
391
392 [self startCKKSSubsystem];
393
394 XCTestExpectation* healthChanged = [self expectChangeForView:self.healthZoneID.zoneName];
395 // Health is NOT is PCS view, so it should not send the fake 'PCS' view notification
396 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
397 pcsChanged.inverted = YES;
398
399 // We expect a single record to be uploaded to the Health view.
400 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.healthZoneID];
401 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintHealth];
402
403 OCMVerifyAllWithDelay(self.mockDatabase, 20);
404 [self waitForExpectations:@[healthChanged] timeout:1];
405 [self waitForExpectations:@[pcsChanged] timeout:0.2];
406 }
407
408 - (void)testAddApplePayItems {
409 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
410
411 [self startCKKSSubsystem];
412
413 XCTestExpectation* applepayChanged = [self expectChangeForView:self.applepayZoneID.zoneName];
414 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
415
416 // We expect a single record to be uploaded to the ApplePay view.
417 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.applepayZoneID];
418 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintApplePay];
419
420 OCMVerifyAllWithDelay(self.mockDatabase, 20);
421 [self waitForExpectations:@[applepayChanged] timeout:1];
422 [self waitForExpectations:@[pcsChanged] timeout:0.2];
423 }
424
425 - (void)testAddHomeItems {
426 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
427
428 [self startCKKSSubsystem];
429
430 XCTestExpectation* homeChanged = [self expectChangeForView:self.homeZoneID.zoneName];
431 // Home is a now PCS view, so it should send the fake 'PCS' view notification
432 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
433
434 // We expect a single record to be uploaded to the ApplePay view.
435 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.homeZoneID];
436 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintHome];
437
438 OCMVerifyAllWithDelay(self.mockDatabase, 20);
439 [self waitForExpectations:@[homeChanged] timeout:1];
440 [self waitForExpectations:@[pcsChanged] timeout:0.2];
441 }
442
443 - (void)testAddLimitedPeersAllowedItems {
444 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
445
446 [self startCKKSSubsystem];
447
448 XCTestExpectation* limitedChanged = [self expectChangeForView:self.limitedZoneID.zoneName];
449 // LimitedPeersAllowed is a PCS view, so it should send the fake 'PCS' view notification
450 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
451
452 // We expect a single record to be uploaded to the LimitedPeersOkay view.
453 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.limitedZoneID];
454 [self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*) kSecAttrViewHintLimitedPeersAllowed];
455
456 OCMVerifyAllWithDelay(self.mockDatabase, 20);
457 [self waitForExpectations:@[limitedChanged] timeout:1];
458 [self waitForExpectations:@[pcsChanged] timeout:0.2];
459 }
460
461 - (void)testMultipleZoneAdd {
462 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
463
464 // Let the horses loose
465 [self startCKKSSubsystem];
466
467 // We expect a single record to be uploaded to the 'LimitedPeersAllowed' view
468 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.limitedZoneID];
469 [self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*)kSecAttrViewHintLimitedPeersAllowed];
470 OCMVerifyAllWithDelay(self.mockDatabase, 20);
471
472 // We expect a single record to be uploaded to the 'atv' home
473 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.homeZoneID];
474 [self addGenericPassword: @"data" account: @"account-delete-me-home" viewHint:(NSString*)kSecAttrViewHintHome];
475
476 OCMVerifyAllWithDelay(self.mockDatabase, 20);
477 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
478 }
479
480 - (void)testMultipleZoneDelete {
481 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
482
483 [self startCKKSSubsystem];
484
485 // We expect a single record to be uploaded.
486 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.limitedZoneID];
487 [self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*)kSecAttrViewHintLimitedPeersAllowed];
488 OCMVerifyAllWithDelay(self.mockDatabase, 20);
489
490 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.homeZoneID];
491 [self addGenericPassword: @"data" account: @"account-delete-me-home" viewHint:(NSString*)kSecAttrViewHintHome];
492 OCMVerifyAllWithDelay(self.mockDatabase, 20);
493 [self waitForCKModifications];
494
495 // We expect a single record to be deleted from the ATV zone
496 [self expectCKDeleteItemRecords:1 zoneID:self.homeZoneID];
497 [self deleteGenericPassword:@"account-delete-me-home"];
498 OCMVerifyAllWithDelay(self.mockDatabase, 20);
499
500 // Now we expect a single record to be deleted from the test zone
501 [self expectCKDeleteItemRecords:1 zoneID:self.limitedZoneID];
502 [self deleteGenericPassword:@"account-delete-me-limited-peers"];
503 OCMVerifyAllWithDelay(self.mockDatabase, 20);
504 }
505
506 - (void)testAddAndReceiveDeleteForSafariPasswordItem {
507 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
508
509 [self startCKKSSubsystem];
510
511 XCTestExpectation* passwordChanged = [self expectChangeForView:self.passwordsView.zoneName];
512
513 // We expect a single record to be uploaded to the Passwords view.
514 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.passwordsZoneID];
515
516 [self addGenericPassword:@"data"
517 account:@"account-delete-me"
518 access:(id)kSecAttrAccessibleWhenUnlocked
519 viewHint:nil
520 accessGroup:@"com.apple.sbd"
521 expecting:errSecSuccess
522 message:@"Item for Password view"];
523
524 OCMVerifyAllWithDelay(self.mockDatabase, 20);
525 [self waitForExpectations:@[passwordChanged] timeout:1];
526 [self waitForCKModifications];
527
528 [self waitForKeyHierarchyReadinesses];
529 [self.passwordsView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
530
531 // Ensure that we catch up to the newest CK change token so our fake cloudkit will notice the delete at fetch time
532 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
533 [self.passwordsView waitForFetchAndIncomingQueueProcessing];
534
535 // Now, the item is deleted. Do we properly remove it?
536 CKRecord* itemRecord = nil;
537 for(CKRecord* record in [self.passwordsZone.currentDatabase allValues]) {
538 if([record.recordType isEqualToString:SecCKRecordItemType]) {
539 itemRecord = record;
540 break;
541 }
542 }
543 XCTAssertNotNil(itemRecord, "Should have found the item in the password zone");
544
545 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
546 (id)kSecAttrAccessGroup : @"com.apple.sbd",
547 (id)kSecAttrAccount : @"account-delete-me",
548 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
549 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
550 (id)kSecReturnData : (id)kCFBooleanTrue,
551 };
552
553 CFTypeRef item = NULL;
554 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
555 XCTAssertNotNil((__bridge id)item, "An item should have been found");
556 CFReleaseNull(item);
557
558 // Now, the item is deleted. The passwords view should delete the local item, even though it has the wrong 'vwht' on disk.
559 XCTAssertNotNil(self.passwordsZone.currentDatabase[itemRecord.recordID], "Record should exist in fake CK");
560 [self.passwordsZone deleteCKRecordIDFromZone:itemRecord.recordID];
561
562 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
563 [self.passwordsView waitForFetchAndIncomingQueueProcessing];
564
565 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
566 XCTAssertNil((__bridge id)item, "No item should have been found");
567 }
568
569 - (void)testAddOtherViewHintItem {
570 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
571
572 [self startCKKSSubsystem];
573
574 // We expect no uploads to CKKS.
575 [self addGenericPassword: @"data" account: @"account-delete-me-no-viewhint"];
576 [self addGenericPassword: @"data" account: @"account-delete-me-password" viewHint:(NSString*) kSOSViewBackupBagV0];
577
578 sleep(1);
579 OCMVerifyAllWithDelay(self.mockDatabase, 20);
580 }
581
582 - (void)testUploadItemsAddedBeforeStart {
583 [self addGenericPassword:@"data"
584 account:@"account-delete-me"
585 access:(id)kSecAttrAccessibleAfterFirstUnlock
586 viewHint:nil
587 accessGroup:@"com.apple.sbd"
588 expecting:errSecSuccess
589 message:@"Item for Password view"];
590
591 [self addGenericPassword:@"data"
592 account:@"account-delete-me-2"
593 access:(id)kSecAttrAccessibleAfterFirstUnlock
594 viewHint:nil
595 accessGroup:@"com.apple.sbd"
596 expecting:errSecSuccess
597 message:@"Item for Password view"];
598
599 [self addGenericPassword:@"data" account:@"account-delete-me-limited-peers" viewHint:(NSString*)kSecAttrViewHintLimitedPeersAllowed];
600
601 NSError* error = nil;
602 NSDictionary* currentOQEs = [CKKSOutgoingQueueEntry countsByStateInZone:self.passwordsZoneID error:&error];
603 XCTAssertNil(error, "Should be no error counting OQEs");
604 XCTAssertEqual(0, currentOQEs.count, "Should be no OQEs");
605
606 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
607
608 // Now CKKS starts up
609 // Upon sign in, these items should be uploaded
610 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.passwordsZoneID
611 checkItem:[self checkClassCBlock:self.passwordsZoneID message:@"Object was encrypted under class C key in hierarchy"]];
612 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.limitedZoneID];
613 [self startCKKSSubsystem];
614
615 XCTAssertEqual(0, [self.passwordsView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
616 OCMVerifyAllWithDelay(self.mockDatabase, 20);
617 }
618
619 - (void)testReceiveItemInView {
620 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
621 [self startCKKSSubsystem];
622
623 [self waitForKeyHierarchyReadinesses];
624 [self.engramView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
625
626 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
627
628 CKRecord* ckr = [self createFakeRecord:self.engramZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
629 [self.engramZone addToZone: ckr];
630
631 XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
632 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
633
634 self.silentFetchesAllowed = false;
635 [self expectCKFetch];
636
637 // Trigger a notification (with hilariously fake data)
638 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
639
640 OCMVerifyAllWithDelay(self.mockDatabase, 20);
641 [self.engramView waitForFetchAndIncomingQueueProcessing];
642
643 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
644
645 [self waitForExpectations:@[engramChanged] timeout:1];
646 [self waitForExpectations:@[pcsChanged] timeout:1];
647 }
648
649 - (void)testRecoverFromCloudKitOldChangeTokenInKeyHierarchyFetch {
650 [self putFakeKeyHierachiesInCloudKit];
651 [self saveTLKsToKeychain];
652
653
654 [self expectCKKSTLKSelfShareUploads];
655
656 // Spin up CKKS subsystem.
657 [self startCKKSSubsystem];
658
659 [self waitForKeyHierarchyReadinesses];
660
661 // We expect a single record to be uploaded
662 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
663 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
664 OCMVerifyAllWithDelay(self.mockDatabase, 20);
665
666 // Delete all old database states, to destroy the change tag validity
667 [self.manateeZone.pastDatabases removeAllObjects];
668
669 // We expect a total local flush and refetch
670 self.silentFetchesAllowed = false;
671 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
672 [self expectCKFetch]; // and one to succeed
673
674 [self.manateeView.stateMachine handleFlag:CKKSFlagFetchRequested];
675
676 XCTAssertEqual(0, [self.manateeView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
677
678 // Don't cause another fetch, because the machinery might not be ready
679 [self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
680
681 OCMVerifyAllWithDelay(self.mockDatabase, 20);
682
683 // And check that a new upload happens just fine.
684 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
685 [self addGenericPassword:@"asdf"
686 account:@"account-class-A"
687 viewHint:(id)kSecAttrViewHintManatee
688 access:(id)kSecAttrAccessibleWhenUnlocked
689 expecting:errSecSuccess
690 message:@"Adding class A item"];
691 OCMVerifyAllWithDelay(self.mockDatabase, 20);
692 }
693
694 - (void)testResetAllCloudKitZones {
695 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
696 OCMExpect([self.suggestTLKUpload trigger]);
697
698 [self putFakeKeyHierachiesInCloudKit];
699 [self saveTLKsToKeychain];
700 [self expectCKKSTLKSelfShareUploads];
701
702 // Spin up CKKS subsystem.
703 [self startCKKSSubsystem];
704 [self waitForKeyHierarchyReadinesses];
705
706 // We expect a single record to be uploaded
707 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
708 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
709 OCMVerifyAllWithDelay(self.mockDatabase, 20);
710 [self waitForCKModifications];
711
712 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
713
714 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
715
716 // CKKS should issue exactly one deletion for all of these
717 self.silentZoneDeletesAllowed = true;
718
719 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
720 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-all-test" reply:^(NSError* result) {
721 XCTAssertNil(result, "no error resetting cloudkit");
722 ckksnotice_global("ckks", "Received a resetCloudKit callback");
723 [resetExpectation fulfill];
724 }];
725
726 // Sneak in and perform Octagon's duties
727 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
728 [self performOctagonTLKUpload:self.ckksViews];
729
730 [self waitForExpectations:@[resetExpectation] timeout:20];
731
732 OCMVerifyAllWithDelay(self.mockDatabase, 20);
733
734 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
735 [self addGenericPassword:@"asdf"
736 account:@"account-class-A"
737 viewHint:(id)kSecAttrViewHintManatee
738 access:(id)kSecAttrAccessibleWhenUnlocked
739 expecting:errSecSuccess
740 message:@"Adding class A item"];
741 OCMVerifyAllWithDelay(self.mockDatabase, 20);
742 }
743
744 - (void)testResetAllCloudKitZonesWithPartialZonesMissing {
745 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
746 OCMExpect([self.suggestTLKUpload trigger]);
747
748 [self putFakeKeyHierachiesInCloudKit];
749 [self saveTLKsToKeychain];
750 [self expectCKKSTLKSelfShareUploads];
751
752 // Spin up CKKS subsystem.
753 [self startCKKSSubsystem];
754 [self waitForKeyHierarchyReadinesses];
755
756 // We expect a single record to be uploaded
757 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
758 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
759 OCMVerifyAllWithDelay(self.mockDatabase, 20);
760 [self waitForCKModifications];
761
762 self.zones[self.manateeZoneID] = nil;
763 self.keys[self.manateeZoneID] = nil;
764 self.zones[self.applepayZoneID] = nil;
765 self.keys[self.applepayZoneID] = nil;
766
767 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
768 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
769
770 // CKKS should issue exactly one deletion for all of these
771 self.silentZoneDeletesAllowed = true;
772
773 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
774 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-all-test" reply:^(NSError* result) {
775 XCTAssertNil(result, "no error resetting cloudkit");
776 ckksnotice_global("ckks", "Received a resetCloudKit callback");
777 [resetExpectation fulfill];
778 }];
779
780 // Sneak in and perform Octagon's duties
781 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
782 [self performOctagonTLKUpload:self.ckksViews];
783
784 [self waitForExpectations:@[resetExpectation] timeout:20];
785
786 OCMVerifyAllWithDelay(self.mockDatabase, 20);
787
788 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
789 [self addGenericPassword:@"asdf"
790 account:@"account-class-A"
791 viewHint:(id)kSecAttrViewHintManatee
792 access:(id)kSecAttrAccessibleWhenUnlocked
793 expecting:errSecSuccess
794 message:@"Adding class A item"];
795 OCMVerifyAllWithDelay(self.mockDatabase, 20);
796 }
797
798 - (void)testResetMultiCloudKitZoneCloudKitRejects {
799 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
800 OCMExpect([self.suggestTLKUpload trigger]);
801
802 [self putFakeKeyHierachiesInCloudKit];
803 [self saveTLKsToKeychain];
804 [self expectCKKSTLKSelfShareUploads];
805
806 // Spin up CKKS subsystem.
807 [self startCKKSSubsystem];
808 [self waitForKeyHierarchyReadinesses];
809
810 // We expect a single record to be uploaded
811 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
812 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
813 OCMVerifyAllWithDelay(self.mockDatabase, 20);
814 [self waitForCKModifications];
815
816 self.nextModifyRecordZonesError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
817 code:CKErrorZoneBusy
818 userInfo:@{
819 CKErrorRetryAfterKey: @(0.2),
820 NSUnderlyingErrorKey: [[CKPrettyError alloc] initWithDomain:CKErrorDomain
821 code:2029
822 userInfo:nil],
823 }];
824 self.silentZoneDeletesAllowed = true;
825
826 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
827 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
828
829 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
830 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
831 XCTAssertNil(result, "no error resetting cloudkit");
832 ckksnotice_global("ckks", "Received a resetCloudKit callback");
833 [resetExpectation fulfill];
834 }];
835
836 // Sneak in and perform Octagon's duties
837 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
838 [self performOctagonTLKUpload:self.ckksViews];
839
840 [self waitForExpectations:@[resetExpectation] timeout:20];
841
842 OCMVerifyAllWithDelay(self.mockDatabase, 20);
843
844 XCTAssertNil(self.nextModifyRecordZonesError, "zone modification error should have been cleared");
845 }
846
847 - (void)testMultiZoneDeviceStateUploadGood {
848 [self putFakeKeyHierachiesInCloudKit];
849 [self saveTLKsToKeychain];
850 [self expectCKKSTLKSelfShareUploads];
851
852 // Spin up CKKS subsystem.
853 [self startCKKSSubsystem];
854 [self waitForKeyHierarchyReadinesses];
855
856 for(CKKSKeychainView* view in self.ckksViews) {
857 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
858 deletedRecordTypeCounts:nil
859 zoneID:view.zoneID
860 checkModifiedRecord:nil
861 runAfterModification:nil];
862 }
863
864 [self.injectedManager xpc24HrNotification];
865
866 OCMVerifyAllWithDelay(self.mockDatabase, 20);
867 }
868
869 - (void)testMultiZoneResync {
870 // Set up
871 [self putFakeKeyHierachiesInCloudKit];
872 [self saveTLKsToKeychain];
873 [self expectCKKSTLKSelfShareUploads];
874
875 // Put sample data in zones, and save off a change token for later fetch shenanigans
876 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"manatee0"]];
877 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"manatee1"]];
878 CKServerChangeToken* manateeChangeToken1 = self.manateeZone.currentChangeToken;
879 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"manatee2"]];
880 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"manatee3"]];
881
882 [self.healthZone addToZone:[self createFakeRecord:self.healthZoneID recordName:@"7B598D31-0000-0000-FFFF-5A507ACB2D00" withAccount:@"health0"]];
883 [self.healthZone addToZone:[self createFakeRecord:self.healthZoneID recordName:@"7B598D31-0000-0000-FFFF-5A507ACB2D01" withAccount:@"health1"]];
884
885 [self startCKKSSubsystem];
886 [self waitForKeyHierarchyReadinesses];
887
888 OCMVerifyAllWithDelay(self.mockDatabase, 20);
889
890 [self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
891 [self.healthView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
892
893 [self findGenericPassword:@"manatee0" expecting:errSecSuccess];
894 [self findGenericPassword:@"manatee1" expecting:errSecSuccess];
895 [self findGenericPassword:@"manatee2" expecting:errSecSuccess];
896 [self findGenericPassword:@"manatee3" expecting:errSecSuccess];
897 [self findGenericPassword:@"health0" expecting:errSecSuccess];
898 [self findGenericPassword:@"health1" expecting:errSecSuccess];
899
900 // Now, we resync. But, the manatee zone comes down in two fetches
901 self.silentFetchesAllowed = false;
902 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
903 // Assert that the fetch is a refetch
904 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.manateeZoneID].previousServerChangeToken;
905 if(changeToken == nil) {
906 return YES;
907 } else {
908 return NO;
909 }
910 } runBeforeFinished:^{}];
911 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
912 // Assert that the fetch is happening with the change token we paused at before
913 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.manateeZoneID].previousServerChangeToken;
914 if(changeToken && [changeToken isEqual:manateeChangeToken1]) {
915 return YES;
916 } else {
917 return NO;
918 }
919 } runBeforeFinished:^{}];
920
921 self.manateeZone.limitFetchTo = manateeChangeToken1;
922
923 // Attempt to trigger simultaneous key state resyncs. This is a horrible hack...
924 [self.manateeView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
925 self.manateeView.keyStateFullRefetchRequested = YES;
926 [self.manateeView _onqueuePokeKeyStateMachine];
927 return CKKSDatabaseTransactionCommit;
928 }];
929 [self.healthView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
930 self.healthView.keyStateFullRefetchRequested = YES;
931 [self.healthView _onqueuePokeKeyStateMachine];
932 return CKKSDatabaseTransactionCommit;
933 }];
934
935 OCMVerifyAllWithDelay(self.mockDatabase, 20);
936
937 [self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
938 [self.healthView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
939
940 // And all items should still exist
941 [self findGenericPassword:@"manatee0" expecting:errSecSuccess];
942 [self findGenericPassword:@"manatee1" expecting:errSecSuccess];
943 [self findGenericPassword:@"manatee2" expecting:errSecSuccess];
944 [self findGenericPassword:@"manatee3" expecting:errSecSuccess];
945 [self findGenericPassword:@"health0" expecting:errSecSuccess];
946 [self findGenericPassword:@"health1" expecting:errSecSuccess];
947 }
948
949 @end
950
951 #endif // OCTAGON
952