4 #import <CloudKit/CloudKit.h>
5 #import <XCTest/XCTest.h>
6 #import <OCMock/OCMock.h>
9 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
10 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
11 #import "keychain/ckks/tests/CloudKitKeychainSyncingTestsBase.h"
12 #import "keychain/ckks/CKKS.h"
13 #import "keychain/ckks/CKKSKeychainView.h"
15 #import "keychain/ckks/tests/MockCloudKit.h"
17 @interface CloudKitKeychainFetchTests : CloudKitKeychainSyncingTestsBase
20 @implementation CloudKitKeychainFetchTests
22 - (void)testMoreComing {
23 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
24 [self saveTLKMaterialToKeychain:self.keychainZoneID];
26 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
27 [self startCKKSSubsystem];
28 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
29 OCMVerifyAllWithDelay(self.mockDatabase, 20);
30 [self waitForCKModifications];
32 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
34 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"account0"]];
35 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"account1"]];
36 CKServerChangeToken* ck1 = ckzone.currentChangeToken;
37 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"account2"]];
38 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"account3"]];
40 ckzone.limitFetchTo = ck1;
42 self.silentFetchesAllowed = false;
44 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
45 // Assert that the fetch is happening with the change token we paused at before
46 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
47 if(changeToken && [changeToken isEqual:ck1]) {
52 } runBeforeFinished:^{}];
54 // Trigger a notification (with hilariously fake data)
55 [self.keychainView notifyZoneChange:nil];
57 [self.keychainView waitForFetchAndIncomingQueueProcessing];
59 OCMVerifyAllWithDelay(self.mockDatabase, 20);
61 NSTimeInterval delta = [ckzone.fetchRecordZoneChangesTimestamps[2] timeIntervalSinceDate:ckzone.fetchRecordZoneChangesTimestamps[1]];
62 XCTAssertLessThan(delta, 2, "operation 1 and 2 should be back-to-back");
64 [self findGenericPassword: @"account0" expecting:errSecSuccess];
65 [self findGenericPassword: @"account1" expecting:errSecSuccess];
66 [self findGenericPassword: @"account2" expecting:errSecSuccess];
67 [self findGenericPassword: @"account3" expecting:errSecSuccess];
70 - (void)testMoreComingRepeated {
71 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
72 [self saveTLKMaterialToKeychain:self.keychainZoneID];
74 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
75 [self startCKKSSubsystem];
76 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
77 OCMVerifyAllWithDelay(self.mockDatabase, 20);
78 [self waitForCKModifications];
80 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
82 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"account0"]];
83 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"account1"]];
84 CKServerChangeToken* ck1 = ckzone.currentChangeToken;
85 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"account2"]];
86 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"account3"]];
87 CKServerChangeToken* ck2 = ckzone.currentChangeToken;
88 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D04" withAccount:@"account4"]];
89 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D05" withAccount:@"account5"]];
91 ckzone.limitFetchTo = ck1;
93 self.silentFetchesAllowed = false;
94 // This fetch will return up to ck1.
95 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
97 } runBeforeFinished:^{
98 ckzone.limitFetchTo = ck2;
101 // This fetch will return up to ck2.
102 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
103 // Assert that the fetch is happening with the change token we paused at before
104 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
105 if(changeToken && [changeToken isEqual:ck1]) {
110 } runBeforeFinished:^{}];
112 // This fetch will return the final two items.
113 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
114 // Assert that the fetch is happening with the change token we paused at before
115 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
116 if(changeToken && [changeToken isEqual:ck2]) {
121 } runBeforeFinished:^{}];
123 // Trigger a notification (with hilariously fake data)
124 [self.keychainView notifyZoneChange:nil];
126 [self.keychainView waitForFetchAndIncomingQueueProcessing];
128 OCMVerifyAllWithDelay(self.mockDatabase, 20);
130 NSTimeInterval delta = [ckzone.fetchRecordZoneChangesTimestamps[2] timeIntervalSinceDate:ckzone.fetchRecordZoneChangesTimestamps[1]];
131 XCTAssertLessThan(delta, 2, "operation 1 and 2 should be back-to-back");
133 delta = [ckzone.fetchRecordZoneChangesTimestamps[3] timeIntervalSinceDate:ckzone.fetchRecordZoneChangesTimestamps[2]];
134 XCTAssertLessThan(delta, 2, "operation 2 and 3 should be back-to-back");
136 [self findGenericPassword: @"account0" expecting:errSecSuccess];
137 [self findGenericPassword: @"account1" expecting:errSecSuccess];
138 [self findGenericPassword: @"account2" expecting:errSecSuccess];
139 [self findGenericPassword: @"account3" expecting:errSecSuccess];
140 [self findGenericPassword: @"account4" expecting:errSecSuccess];
141 [self findGenericPassword: @"account5" expecting:errSecSuccess];
144 - (void)testMoreComingDespitePartialTimeout {
145 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
146 [self saveTLKMaterialToKeychain:self.keychainZoneID];
148 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
149 [self startCKKSSubsystem];
150 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
151 OCMVerifyAllWithDelay(self.mockDatabase, 20);
152 [self waitForCKModifications];
154 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
156 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"account0"]];
157 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"account1"]];
158 CKServerChangeToken* ck1 = ckzone.currentChangeToken;
159 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"account2"]];
160 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"account3"]];
162 // The fetch fails with partial results
163 ckzone.limitFetchTo = ck1;
164 ckzone.limitFetchError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkFailure userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}];
166 self.silentFetchesAllowed = false;
167 [self expectCKFetch];
168 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
169 // Assert that the fetch is happening with the change token we paused at before
170 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
171 if(changeToken && [changeToken isEqual:ck1]) {
176 } runBeforeFinished:^{}];
178 // Trigger a notification (with hilariously fake data)
179 [self.keychainView notifyZoneChange:nil];
181 [self.keychainView waitForFetchAndIncomingQueueProcessing];
183 OCMVerifyAllWithDelay(self.mockDatabase, 20);
185 [self findGenericPassword: @"account0" expecting:errSecSuccess];
186 [self findGenericPassword: @"account1" expecting:errSecSuccess];
187 [self findGenericPassword: @"account2" expecting:errSecSuccess];
188 [self findGenericPassword: @"account3" expecting:errSecSuccess];