2 * Copyright (c) 2017 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@
27 #import "keychain/ckks/CKKSAnalytics.h"
28 #import <Security/SFSQLite.h>
29 #import <Foundation/Foundation.h>
30 #import <CloudKit/CloudKit.h>
31 #import <CloudKit/CloudKit_Private.h>
32 #import <XCTest/XCTest.h>
33 #import <OCMock/OCMock.h>
35 static NSString* tablePath = nil;
37 @interface SQLiteTests : XCTestCase
40 @implementation SQLiteTests
46 tablePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"test_table.db"];
53 [[NSFileManager defaultManager] removeItemAtPath:tablePath error:nil];
58 [[NSFileManager defaultManager] removeItemAtPath:tablePath error:nil];
63 - (void)testCreateLoggingDatabase
65 NSString* schema = @"CREATE table test (test_column INTEGER);";
66 SFSQLite* sqlTable = [[SFSQLite alloc] initWithPath:tablePath schema:schema];
68 XCTAssertTrue([sqlTable openWithError:&error], @"failed to open sql database");
69 XCTAssertNil(error, "encountered error opening database: %@", error);
70 XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:tablePath]);
74 - (void)testInsertAndDelete
76 NSString* schema = @"CREATE table test (test_column INTEGER);";
77 SFSQLite* sqlTable = [[SFSQLite alloc] initWithPath:tablePath schema:schema];
79 XCTAssertTrue([sqlTable openWithError:&error], @"failed to open sql database");
80 XCTAssertNil(error, "encountered error opening database: %@", error);
82 [sqlTable insertOrReplaceInto:@"test" values:@{@"test_column" : @(1)}];
83 [sqlTable insertOrReplaceInto:@"test" values:@{@"test_column" : @(2)}];
84 [sqlTable insertOrReplaceInto:@"test" values:@{@"test_column" : @(3)}];
85 XCTAssertTrue([[sqlTable selectAllFrom:@"test" where:nil bindings:nil] count] == 3);
87 [sqlTable deleteFrom:@"test" where:@"test_column = ?" bindings:@[@3]];
88 XCTAssertTrue([[sqlTable selectAllFrom:@"test" where:nil bindings:nil] count] == 2);
90 [sqlTable executeSQL:@"delete from test"];
91 XCTAssertTrue([[sqlTable selectAllFrom:@"test" where:nil bindings:nil] count] == 0);
94 - (void)testDontCrashWhenThereAreNoWritePermissions
96 NSString* schema = @"CREATE table test (test_column INTEGER);";
97 SFSQLite* sqlTable = [[SFSQLite alloc] initWithPath:tablePath schema:schema];
100 XCTAssertNoThrow([sqlTable openWithError:&error], @"opening database threw an exception");
101 XCTAssertNil(error, "encountered error opening database: %@", error);
102 XCTAssertNoThrow([sqlTable close], @"closing database threw an exception");
104 NSDictionary* originalAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:tablePath error:&error];
105 XCTAssertNil(error, @"encountered error getting database file attributes: %@", error);
107 [[NSFileManager defaultManager] setAttributes:@{NSFilePosixPermissions : @(400), NSFileImmutable : @(YES)} ofItemAtPath:tablePath error:&error];
108 XCTAssertNil(error, @"encountered error setting database file attributes: %@", error);
109 XCTAssertNoThrow([sqlTable openWithError:&error]);
110 XCTAssertNil(error, @"encounterd error when opening file without permissions: %@", error);
112 XCTAssertFalse([sqlTable executeSQL:@"insert or replace into test (test_column) VALUES (1)"],
113 @"writing to read-only database succeeded");
115 [[NSFileManager defaultManager] setAttributes:originalAttributes ofItemAtPath:tablePath error:&error];
116 XCTAssertNil(error, @"encountered error setting database file attributes back to original attributes: %@", error);
119 - (void)testDontCrashFromInternalErrors
121 NSString* schema = @"CREATE table test (test_column INTEGER);";
122 SFSQLite* sqlTable = [[SFSQLite alloc] initWithPath:tablePath schema:schema];
124 NSError* error = nil;
125 XCTAssertTrue([sqlTable openWithError:&error], @"failed to open database");
126 XCTAssertNil(error, "encountered error opening database: %@", error);
128 // delete the table to create havoc
129 XCTAssertTrue([sqlTable executeSQL:@"drop table test;"], @"deleting test table should have worked");
131 XCTAssertNoThrow([sqlTable insertOrReplaceInto:@"test" values:@{@"test_column" : @(1)}], @"inserting into deleted table threw an exception");
136 @interface CKKSAnalyticsTests : CloudKitKeychainSyncingTestsBase
137 @property id mockCKKSAnalytics;
140 @implementation CKKSAnalyticsTests
144 self.mockCKKSAnalytics = OCMClassMock([CKKSAnalytics class]);
145 OCMStub([self.mockCKKSAnalytics databasePath]).andCall(self, @selector(databasePath));
151 [self.mockCKKSAnalytics stopMocking];
152 self.mockCKKSAnalytics = nil;
156 - (NSString*)databasePath
158 return [NSTemporaryDirectory() stringByAppendingPathComponent:@"test_ckks_analytics_v2.db"];
161 - (void)testLastSuccessfulXDate
163 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
164 [self startCKKSSubsystem];
165 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
166 [self.keychainZone addToZone: ckr];
168 // Trigger a notification (with hilariously fake data)
169 [self.keychainView notifyZoneChange:nil];
171 [[[self.keychainView waitForFetchAndIncomingQueueProcessing] completionHandlerDidRunCondition] wait:4 * NSEC_PER_SEC];
173 NSDate* nowDate = [NSDate date];
174 NSTimeInterval timeInterval;
177 * Check last sync date for class A
179 NSDate* syncADate = [[CKKSAnalytics logger] dateOfLastSuccessForEvent:CKKSEventProcessIncomingQueueClassA inView:self.keychainView];
180 XCTAssertNotNil(syncADate, "Failed to get a last successful A sync date");
181 timeInterval = [nowDate timeIntervalSinceDate:syncADate];
182 XCTAssertTrue(timeInterval >= 0.0 && timeInterval <= 15.0, "Last sync date does not look like a reasonable one");
185 * Check last sync date for class C
187 NSDate *syncCDate = [[CKKSAnalytics logger] dateOfLastSuccessForEvent:CKKSEventProcessIncomingQueueClassC inView:self.keychainView];
188 XCTAssertNotNil(syncCDate, "Failed to get a last successful C sync date");
189 timeInterval = [nowDate timeIntervalSinceDate:syncCDate];
190 XCTAssertTrue(timeInterval >= 0.0 && timeInterval <= 15.0, "Last sync date does not look like a reasonable one");
193 * Check last unlock date
195 NSDate* unlockDate = [[CKKSAnalytics logger] datePropertyForKey:CKKSAnalyticsLastUnlock];
196 XCTAssertNotNil(unlockDate, "Failed to get a last unlock date");
197 timeInterval = [nowDate timeIntervalSinceDate:unlockDate];
198 NSLog(@"timeinterval: %f\n", timeInterval);
199 XCTAssertTrue(timeInterval >= 0.0 && timeInterval <= 15.0, "Last unlock date does not look like a reasonable one");
201 sleep(1); // wait to be a differnt second
203 self.aksLockState = true;
204 [self.lockStateTracker recheck];
206 NSDate* newUnlockDate = [[CKKSAnalytics logger] datePropertyForKey:CKKSAnalyticsLastUnlock];
207 XCTAssertNotNil(newUnlockDate, "Failed to get a last unlock date");
208 XCTAssertEqualObjects(newUnlockDate, unlockDate, "unlock date not the same");
210 sleep(1); // wait to be a differnt second
212 self.aksLockState = false;
213 [self.lockStateTracker recheck];
215 sleep(1); // wait for the completion block to have time to fire
217 newUnlockDate = [[CKKSAnalytics logger] datePropertyForKey:CKKSAnalyticsLastUnlock];
218 XCTAssertNotNil(newUnlockDate, "Failed to get a last unlock date");
219 XCTAssertNotEqualObjects(newUnlockDate, unlockDate, "unlock date the same");
222 - (void)testRaceToCreateLoggers
224 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
225 for (NSInteger i = 0; i < 5; i++) {
226 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
227 CKKSAnalytics* logger = [CKKSAnalytics logger];
228 [logger logSuccessForEvent:(CKKSAnalyticsFailableEvent*)@"test_event" inView:self.keychainView];
229 dispatch_semaphore_signal(semaphore);
233 for (NSInteger i = 0; i < 5; i++) {
234 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
238 - (void)testUnderlayingError
240 NSDictionary *errorString = nil;
241 NSError *error = nil;
243 error = [NSError errorWithDomain:CKErrorDomain code:CKErrorPartialFailure userInfo:@{
244 CKPartialErrorsByItemIDKey : @{
245 @"recordid" : [NSError errorWithDomain:CKErrorDomain code:1 userInfo:nil],
249 errorString = [[CKKSAnalytics logger] errorChain:error depth:0];
251 XCTAssertEqualObjects(errorString[@"domain"], CKErrorDomain, "error domain");
252 XCTAssertEqual([errorString[@"code"] intValue], CKErrorPartialFailure, "error code");
254 XCTAssertEqualObjects(errorString[@"oneCloudKitPartialFailure"][@"domain"], CKErrorDomain, "error domain");
255 XCTAssertEqual([errorString[@"oneCloudKitPartialFailure"][@"code"] intValue], 1, "error code");
257 /* interal partial error leaks out of CK */
259 error = [NSError errorWithDomain:CKErrorDomain code:CKErrorPartialFailure userInfo:@{
260 CKPartialErrorsByItemIDKey : @{
261 @"recordid1" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
262 @"recordid2" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
263 @"recordid3" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
264 @"recordid4" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
265 @"recordid5" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
266 @"recordid6" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
267 @"recordid7" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
268 @"recordid8" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
269 @"recordid9" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
270 @"recordid0" : [NSError errorWithDomain:CKErrorDomain code:1 userInfo:nil],
271 @"recordid10" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
272 @"recordid12" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
273 @"recordid13" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
274 @"recordid14" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
275 @"recordid15" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
276 @"recordid16" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
277 @"recordid17" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
278 @"recordid18" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
279 @"recordid19" : [NSError errorWithDomain:CKErrorDomain code:CKErrorBatchRequestFailed userInfo:nil],
283 errorString = [[CKKSAnalytics logger] errorChain:error depth:0];
285 XCTAssertEqualObjects(errorString[@"domain"], CKErrorDomain, "error domain");
286 XCTAssertEqual([errorString[@"code"] intValue], CKErrorPartialFailure, "error code");
288 XCTAssertEqualObjects(errorString[@"oneCloudKitPartialFailure"][@"domain"], CKErrorDomain, "error domain");
289 XCTAssertEqualObjects(errorString[@"oneCloudKitPartialFailure"][@"code"], @1, "error code");
294 error = [NSError errorWithDomain:@"domain" code:1 userInfo:@{
295 NSUnderlyingErrorKey : [NSError errorWithDomain:CKErrorDomain code:1 userInfo:nil],
298 errorString = [[CKKSAnalytics logger] errorChain:error depth:0];
300 XCTAssertEqualObjects(errorString[@"domain"], @"domain", "error domain");
301 XCTAssertEqual([errorString[@"code"] intValue], 1, "error code");
303 XCTAssertEqualObjects(errorString[@"child"][@"domain"], CKErrorDomain, "error domain");
304 XCTAssertEqual([errorString[@"child"][@"code"] intValue], 1, "error code");