2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
26 #import "keychain/ckks/CKKS.h"
27 #import "keychain/ckks/tests/CKKSTests.h"
28 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
31 @interface CKKSKeychainView(test)
32 @property NSOperationQueue* operationQueue;
35 @implementation CloudKitKeychainSyncingTests (CoalesceTests)
36 // These tests check that, if CKKS doesn't start processing an item before a new update comes in,
37 // each case is properly handled.
39 - (void)testCoalesceAddModifyItem {
40 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
42 NSString* account = @"account-delete-me";
44 [self addGenericPassword: @"data" account: account];
45 [self updateGenericPassword: @"otherdata" account:account];
47 // We expect a single record to be uploaded.
48 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
50 [self startCKKSSubsystem];
51 OCMVerifyAllWithDelay(self.mockDatabase, 20);
54 - (void)testCoalesceAddModifyModifyItem {
55 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
57 NSString* account = @"account-delete-me";
59 [self addGenericPassword: @"data" account: account];
60 [self updateGenericPassword: @"otherdata" account:account];
61 [self updateGenericPassword: @"again" account:account];
63 // We expect a single record to be uploaded.
64 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
66 [self startCKKSSubsystem];
67 OCMVerifyAllWithDelay(self.mockDatabase, 20);
70 - (void)testCoalesceAddModifyDeleteItem {
71 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
73 NSString* account = @"account-delete-me";
75 [self addGenericPassword: @"data" account: account];
76 [self updateGenericPassword: @"otherdata" account:account];
77 [self deleteGenericPassword: account];
79 // We expect no uploads.
80 [self startCKKSSubsystem];
81 [self.keychainView waitUntilAllOperationsAreFinished];
82 OCMVerifyAllWithDelay(self.mockDatabase, 20);
85 - (void)testCoalesceDeleteAddItem {
86 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
88 NSString* account = @"account-delete-me";
90 [self addGenericPassword: @"data" account: account];
92 // We expect a single record to be uploaded.
93 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
94 [self startCKKSSubsystem];
95 OCMVerifyAllWithDelay(self.mockDatabase, 20);
96 [self waitForCKModifications];
98 // Okay, now the delete/add. Note that this is not a coalescing operation, since the new item
99 // has different contents. (This test used to upload the item to a different UUID, but no longer).
101 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"hold-outgoing-queue"
104 [self deleteGenericPassword: account];
105 [self addGenericPassword: @"data_new_contents" account: account];
107 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
108 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data_new_contents"]];
110 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
111 OCMVerifyAllWithDelay(self.mockDatabase, 20);
114 - (void)testCoalesceReceiveModifyWhileDeletingItem {
115 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
117 NSString* account = @"account-delete-me";
119 [self addGenericPassword:@"data" account:account];
121 // We expect a single record to be uploaded.
122 __block CKRecord* itemRecord = nil;
123 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
124 checkItem:^BOOL(CKRecord * _Nonnull record) {
129 [self startCKKSSubsystem];
130 OCMVerifyAllWithDelay(self.mockDatabase, 20);
131 [self waitForCKModifications];
133 // Now, we receive a modification from CK, but then delete the item locally before processing the IQE.
135 XCTAssertNotNil(itemRecord, "Should have a record for the uploaded item");
136 NSMutableDictionary* contents = [[self decryptRecord:itemRecord] mutableCopy];
137 contents[@"v_Data"] = [@"updated" dataUsingEncoding:NSUTF8StringEncoding];
139 CKRecord* recordUpdate = [self newRecord:itemRecord.recordID withNewItemData:contents];
140 [self.keychainZone addCKRecordToZone:recordUpdate];
142 self.keychainView.holdIncomingQueueOperation = [NSBlockOperation blockOperationWithBlock:^{}];
144 // Ensure we wait for the whole fetch
145 NSOperation* fetchOp = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
146 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
148 [fetchOp waitUntilFinished];
150 // now, delete the item
151 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
152 [self deleteGenericPassword:account];
154 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
156 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
157 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
158 OCMVerifyAllWithDelay(self.mockDatabase, 20);
160 // And the item shouldn't be present, since it was deleted via API after the item was fetched
161 [self findGenericPassword:account expecting:errSecItemNotFound];
164 - (void)testCoalesceReceiveDeleteWhileModifyingItem {
165 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
167 NSString* account = @"account-delete-me";
169 [self startCKKSSubsystem];
170 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
172 __block CKRecord* itemRecord = nil;
173 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
174 checkItem:^BOOL(CKRecord * _Nonnull record) {
179 [self addGenericPassword:@"data" account:account];
181 OCMVerifyAllWithDelay(self.mockDatabase, 20);
182 [self waitForCKModifications];
184 // Ensure we fetch again, to prime the delete (due to insufficient mock CK)
185 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
186 [self.keychainView waitForFetchAndIncomingQueueProcessing];
188 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
189 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
191 // Now, we receive a delete from CK, but after we modify the item locally
192 self.keychainView.holdOutgoingQueueOperation = [NSBlockOperation blockOperationWithBlock:^{}];
194 XCTAssertNotNil(itemRecord, "Should have an item record from the upload");
195 [self.keychainZone deleteCKRecordIDFromZone:itemRecord.recordID];
196 [self updateGenericPassword:@"new-password" account:account];
198 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
199 [self.keychainView waitForFetchAndIncomingQueueProcessing];
200 [self findGenericPassword:account expecting:errSecItemNotFound];
202 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
204 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
205 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
206 OCMVerifyAllWithDelay(self.mockDatabase, 20);
208 // And the item shouldn't be present, since it was deleted via CK after the API change
209 [self findGenericPassword:account expecting:errSecItemNotFound];