]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/MockCloudKit.m
Security-59306.11.20.tar.gz
[apple/security.git] / keychain / ckks / tests / MockCloudKit.m
1 /*
2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #import "keychain/ckks/tests/MockCloudKit.h"
27 #import "keychain/ckks/CKKS.h"
28 #import "keychain/ckks/CKKSRecordHolder.h"
29 #import "keychain/ckks/CKKSReachabilityTracker.h"
30
31 #import <CloudKit/CloudKit.h>
32 #import <CloudKit/CloudKit_Private.h>
33 #include <security_utilities/debugging.h>
34 #import <Foundation/Foundation.h>
35 #import <Foundation/NSDistributedNotificationCenter.h>
36
37 @implementation FakeCKOperation
38
39 - (BOOL)isFinishingOnCallbackQueue
40 {
41 return NO;
42 }
43 @end
44
45 @implementation FakeCKModifyRecordZonesOperation
46 @synthesize database = _database;
47 @synthesize recordZonesToSave = _recordZonesToSave;
48 @synthesize recordZoneIDsToDelete = _recordZoneIDsToDelete;
49 @synthesize modifyRecordZonesCompletionBlock = _modifyRecordZonesCompletionBlock;
50 @synthesize group = _group;
51
52 - (CKOperationConfiguration*)configuration {
53 return _configuration;
54 }
55
56 - (void)setConfiguration:(CKOperationConfiguration*)configuration {
57 if(configuration) {
58 _configuration = configuration;
59 } else {
60 _configuration = [[CKOperationConfiguration alloc] init];
61 }
62 }
63
64 - (instancetype)initWithRecordZonesToSave:(nullable NSArray<CKRecordZone *> *)recordZonesToSave recordZoneIDsToDelete:(nullable NSArray<CKRecordZoneID *> *)recordZoneIDsToDelete {
65 if(self = [super init]) {
66 _recordZonesToSave = recordZonesToSave;
67 _recordZoneIDsToDelete = recordZoneIDsToDelete;
68 _modifyRecordZonesCompletionBlock = nil;
69
70 _recordZonesSaved = nil;
71 _recordZoneIDsDeleted = nil;
72 _creationError = nil;
73
74 __weak __typeof(self) weakSelf = self;
75 self.completionBlock = ^{
76 __strong __typeof(weakSelf) strongSelf = weakSelf;
77 if(!strongSelf) {
78 secerror("ckks: received callback for released object");
79 return;
80 }
81
82 strongSelf.modifyRecordZonesCompletionBlock(strongSelf.recordZonesSaved, strongSelf.recordZoneIDsDeleted, strongSelf.creationError);
83 };
84 }
85 return self;
86 }
87
88 -(void)main {
89 // Create the zones we want; delete the ones we don't
90 // No error handling whatsoever
91 FakeCKDatabase* ckdb = [FakeCKModifyRecordZonesOperation ckdb];
92
93 NSError* possibleError = [FakeCKModifyRecordZonesOperation shouldFailModifyRecordZonesOperation];
94 if(possibleError) {
95 self.creationError = possibleError;
96 return;
97 }
98
99 for(CKRecordZone* zone in self.recordZonesToSave) {
100 bool skipCreation = false;
101 FakeCKZone* fakezone = ckdb[zone.zoneID];
102 if(fakezone.failCreationSilently) {
103 // Don't report an error, but do delete the zone
104 ckdb[zone.zoneID] = nil;
105 skipCreation = true;
106
107 } else if(fakezone.creationError) {
108
109 // Not the best way to do this, but it's an error
110 // Needs fixing if you want to support multiple zone failures
111 self.creationError = fakezone.creationError;
112
113 // 'clear' the error
114 ckdb[zone.zoneID] = nil;
115 skipCreation = true;
116
117 } else if(fakezone) {
118 // Don't remake the zone, but report to the client that it was created
119 skipCreation = true;
120 }
121
122 if(!skipCreation) {
123 // Create the zone:
124 secnotice("ckks", "Creating zone %@", zone);
125 ckdb[zone.zoneID] = [[FakeCKZone alloc] initZone: zone.zoneID];
126 }
127
128 if(!self.recordZonesSaved) {
129 self.recordZonesSaved = [[NSMutableArray alloc] init];
130 }
131 [self.recordZonesSaved addObject:zone];
132 }
133
134 for(CKRecordZoneID* zoneID in self.recordZoneIDsToDelete) {
135 FakeCKZone* zone = ckdb[zoneID];
136
137 if(zone) {
138 // The zone exists.
139 [FakeCKModifyRecordZonesOperation ensureZoneDeletionAllowed:zone];
140
141 ckdb[zoneID] = nil;
142
143 if(!self.recordZoneIDsDeleted) {
144 self.recordZoneIDsDeleted = [[NSMutableArray alloc] init];
145 }
146 [self.recordZoneIDsDeleted addObject:zoneID];
147
148 } else {
149 // The zone does not exist! CloudKit will tell us that the deletion failed.
150 if(!self.creationError) {
151 self.creationError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorPartialFailure userInfo:@{
152 CKErrorRetryAfterKey: @(0.2),
153 }];
154 }
155
156 // There really should be a better way to do this...
157 NSMutableDictionary* newDictionary = [self.creationError.userInfo mutableCopy] ?: [NSMutableDictionary dictionary];
158 NSMutableDictionary* newPartials = newDictionary[CKPartialErrorsByItemIDKey] ?: [NSMutableDictionary dictionary];
159 newPartials[zoneID] = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorZoneNotFound
160 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Mock CloudKit: zone '%@' not found", zoneID.zoneName]}];
161 newDictionary[CKPartialErrorsByItemIDKey] = newPartials;
162
163 self.creationError = [[CKPrettyError alloc] initWithDomain:self.creationError.domain code:self.creationError.code userInfo:newDictionary];
164 }
165 }
166 }
167
168 +(FakeCKDatabase*) ckdb {
169 // Shouldn't ever be called: must be mocked out.
170 @throw [NSException exceptionWithName:NSInternalInconsistencyException
171 reason:[NSString stringWithFormat:@"+ckdb[] must be mocked out for use"]
172 userInfo:nil];
173 }
174
175 + (NSError* _Nullable)shouldFailModifyRecordZonesOperation
176 {
177 // Should be mocked out!
178 return nil;
179 }
180
181 +(void)ensureZoneDeletionAllowed:(FakeCKZone*)zone {
182 // Shouldn't ever be called; will be mocked out
183 (void)zone;
184 }
185 @end
186
187 @implementation FakeCKModifySubscriptionsOperation
188 @synthesize database = _database;
189 @synthesize group = _group;
190 @synthesize subscriptionsToSave = _subscriptionsToSave;
191 @synthesize subscriptionIDsToDelete = _subscriptionIDsToDelete;
192 @synthesize modifySubscriptionsCompletionBlock = _modifySubscriptionsCompletionBlock;
193
194 - (CKOperationConfiguration*)configuration {
195 return _configuration;
196 }
197
198 - (void)setConfiguration:(CKOperationConfiguration*)configuration {
199 if(configuration) {
200 _configuration = configuration;
201 } else {
202 _configuration = [[CKOperationConfiguration alloc] init];
203 }
204 }
205
206 - (instancetype)initWithSubscriptionsToSave:(nullable NSArray<CKSubscription *> *)subscriptionsToSave subscriptionIDsToDelete:(nullable NSArray<NSString *> *)subscriptionIDsToDelete {
207 if(self = [super init]) {
208 _subscriptionsToSave = subscriptionsToSave;
209 _subscriptionIDsToDelete = subscriptionIDsToDelete;
210 _modifySubscriptionsCompletionBlock = nil;
211
212 __weak __typeof(self) weakSelf = self;
213 self.completionBlock = ^{
214 __strong __typeof(weakSelf) strongSelf = weakSelf;
215 if(!strongSelf) {
216 secerror("ckks: received callback for released object");
217 return;
218 }
219
220 strongSelf.modifySubscriptionsCompletionBlock(strongSelf.subscriptionsSaved, strongSelf.subscriptionIDsDeleted, strongSelf.subscriptionError);
221 };
222 }
223 return self;
224 }
225
226 -(void)main {
227 FakeCKDatabase* ckdb = [FakeCKModifySubscriptionsOperation ckdb];
228
229 // Are these CKRecordZoneSubscription? Who knows!
230 for(CKRecordZoneSubscription* subscription in self.subscriptionsToSave) {
231 FakeCKZone* fakezone = ckdb[subscription.zoneID];
232
233 if(!fakezone) {
234 // This is an error: the zone doesn't exist
235 self.subscriptionError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
236 code:CKErrorPartialFailure
237 userInfo:@{
238 CKErrorRetryAfterKey: @(0.2),
239 CKPartialErrorsByItemIDKey:
240 @{subscription.zoneID:[[CKPrettyError alloc] initWithDomain:CKErrorDomain
241 code:CKErrorZoneNotFound
242 userInfo:@{}]}
243 }];
244
245 } else if(fakezone.subscriptionError) {
246 // Not the best way to do this, but it's an error
247 // Needs fixing if you want to support multiple zone failures
248 self.subscriptionError = fakezone.subscriptionError;
249
250 // 'clear' the error
251 fakezone.subscriptionError = nil;
252 } else {
253 if(!self.subscriptionsSaved) {
254 self.subscriptionsSaved = [[NSMutableArray alloc] init];
255 }
256 [self.subscriptionsSaved addObject:subscription];
257 }
258 }
259
260 for(NSString* subscriptionID in self.subscriptionIDsToDelete) {
261 if(!self.subscriptionIDsDeleted) {
262 self.subscriptionIDsDeleted = [[NSMutableArray alloc] init];
263 }
264
265 [self.subscriptionIDsDeleted addObject:subscriptionID];
266 }
267 }
268
269 +(FakeCKDatabase*) ckdb {
270 // Shouldn't ever be called: must be mocked out.
271 @throw [NSException exceptionWithName:NSInternalInconsistencyException
272 reason:[NSString stringWithFormat:@"+ckdb[] must be mocked out for use"]
273 userInfo:nil];
274 }
275 @end
276
277 @implementation FakeCKFetchRecordZoneChangesOperation
278 @synthesize database = _database;
279 @synthesize recordZoneIDs = _recordZoneIDs;
280 @synthesize configurationsByRecordZoneID = _configurationsByRecordZoneID;
281
282 @synthesize fetchAllChanges = _fetchAllChanges;
283 @synthesize recordChangedBlock = _recordChangedBlock;
284
285 @synthesize recordWithIDWasDeletedBlock = _recordWithIDWasDeletedBlock;
286 @synthesize recordZoneChangeTokensUpdatedBlock = _recordZoneChangeTokensUpdatedBlock;
287 @synthesize recordZoneFetchCompletionBlock = _recordZoneFetchCompletionBlock;
288 @synthesize fetchRecordZoneChangesCompletionBlock = _fetchRecordZoneChangesCompletionBlock;
289
290 @synthesize operationID = _operationID;
291 @synthesize resolvedConfiguration = _resolvedConfiguration;
292 @synthesize group = _group;
293
294 - (CKOperationConfiguration*)configuration {
295 return _configuration;
296 }
297
298 - (void)setConfiguration:(CKOperationConfiguration*)configuration {
299 if(configuration) {
300 _configuration = configuration;
301 } else {
302 _configuration = [[CKOperationConfiguration alloc] init];
303 }
304 }
305
306 - (instancetype)initWithRecordZoneIDs:(NSArray<CKRecordZoneID *> *)recordZoneIDs configurationsByRecordZoneID:(nullable NSDictionary<CKRecordZoneID *, CKFetchRecordZoneChangesConfiguration *> *)configurationsByRecordZoneID {
307 if(self = [super init]) {
308 _recordZoneIDs = recordZoneIDs;
309 _configurationsByRecordZoneID = configurationsByRecordZoneID;
310
311 _operationID = @"fake-operation-ID";
312 }
313 return self;
314 }
315
316 + (bool)isNetworkReachable
317 {
318 // For mocking purposes.
319 return true;
320 }
321
322 - (void)main {
323 // iterate through database, and return items that aren't in lastDatabase
324 FakeCKDatabase* ckdb = [FakeCKFetchRecordZoneChangesOperation ckdb];
325 if(self.recordZoneIDs.count == 0) {
326 secerror("fakeck: No zones to fetch. Likely a bug?");
327 }
328
329 for(CKRecordZoneID* zoneID in self.recordZoneIDs) {
330 FakeCKZone* zone = ckdb[zoneID];
331 if(!zone) {
332 // Only really supports a single zone failure
333 ckksnotice("fakeck", zoneID, "Fetched for a missing zone %@", zoneID);
334 NSError* zoneNotFoundError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
335 code:CKErrorZoneNotFound
336 userInfo:@{}];
337 NSError* error = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
338 code:CKErrorPartialFailure
339 userInfo:@{CKPartialErrorsByItemIDKey: @{zoneID:zoneNotFoundError}}];
340
341 self.fetchRecordZoneChangesCompletionBlock(error);
342 return;
343 }
344
345 ++zone.fetchRecordZoneChangesOperationCount;
346 [zone.fetchRecordZoneChangesTimestamps addObject: [NSDate date]];
347
348 bool networkReachable = [FakeCKFetchRecordZoneChangesOperation isNetworkReachable];
349 if (!networkReachable) {
350 NSError *networkError = [NSError errorWithDomain:CKErrorDomain code:CKErrorNetworkFailure userInfo:NULL];
351 self.fetchRecordZoneChangesCompletionBlock(networkError);
352 return;
353 }
354
355 // Not precisely correct in the case of multiple zone fetches. However, we don't currently do that, so it'll work for now.
356 NSError* mockError = [zone popFetchChangesError];
357 if(mockError) {
358 self.fetchRecordZoneChangesCompletionBlock(mockError);
359 return;
360 }
361
362 // Extract the database at the last time they asked
363 CKServerChangeToken* fetchToken = self.configurationsByRecordZoneID[zoneID].previousServerChangeToken;
364 __block NSMutableDictionary<CKRecordID*, CKRecord*>* lastDatabase = nil;
365 __block NSDictionary<CKRecordID*, CKRecord*>* currentDatabase = nil;
366 __block CKServerChangeToken* currentChangeToken = nil;
367 __block bool moreComing = false;
368
369 __block NSError* opError = nil;
370
371 dispatch_sync(zone.queue, ^{
372 lastDatabase = fetchToken ? zone.pastDatabases[fetchToken] : nil;
373
374 // You can fetch with the current change token; that's fine
375 if([fetchToken isEqual:zone.currentChangeToken]) {
376 lastDatabase = zone.currentDatabase;
377 }
378
379 currentDatabase = zone.currentDatabase;
380 currentChangeToken = zone.currentChangeToken;
381
382 if (zone.limitFetchTo != nil) {
383 currentDatabase = zone.pastDatabases[zone.limitFetchTo];
384 currentChangeToken = zone.limitFetchTo;
385 zone.limitFetchTo = nil;
386 opError = zone.limitFetchError;
387 moreComing = true;
388 }
389 });
390
391 ckksnotice("fakeck", zone.zoneID, "FakeCKFetchRecordZoneChangesOperation(%@): database is currently %@ change token %@ database then: %@", zone.zoneID, currentDatabase, fetchToken, lastDatabase);
392
393 if(!lastDatabase && fetchToken) {
394 ckksnotice("fakeck", zone.zoneID, "no database for this change token: failing fetch with 'CKErrorChangeTokenExpired'");
395 self.fetchRecordZoneChangesCompletionBlock([[CKPrettyError alloc]
396 initWithDomain:CKErrorDomain
397 code:CKErrorPartialFailure userInfo:@{CKPartialErrorsByItemIDKey:
398 @{zoneID:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorChangeTokenExpired userInfo:@{}]}
399 }]);
400 return;
401 }
402
403 [currentDatabase enumerateKeysAndObjectsUsingBlock:^(CKRecordID * _Nonnull recordID, CKRecord * _Nonnull record, BOOL * _Nonnull stop) {
404 id last = [lastDatabase objectForKey: recordID];
405 if(!last || ![record isEqual:last]) {
406 self.recordChangedBlock(record);
407 }
408 }];
409
410 // iterate through lastDatabase, and delete items that aren't in database
411 [lastDatabase enumerateKeysAndObjectsUsingBlock:^(CKRecordID * _Nonnull recordID, CKRecord * _Nonnull record, BOOL * _Nonnull stop) {
412
413 id current = [currentDatabase objectForKey: recordID];
414 if(current == nil) {
415 self.recordWithIDWasDeletedBlock(recordID, [record recordType]);
416 }
417 }];
418
419 self.recordZoneChangeTokensUpdatedBlock(zoneID, currentChangeToken, nil);
420 self.recordZoneFetchCompletionBlock(zoneID, currentChangeToken, nil, moreComing, opError);
421
422 if(self.blockAfterFetch) {
423 self.blockAfterFetch();
424 }
425
426 }
427
428 self.fetchRecordZoneChangesCompletionBlock(nil);
429 }
430
431 +(FakeCKDatabase*) ckdb {
432 // Shouldn't ever be called: must be mocked out.
433 @throw [NSException exceptionWithName:NSInternalInconsistencyException
434 reason:[NSString stringWithFormat:@"+ckdb[] must be mocked out for use"]
435 userInfo:nil];
436 }
437 @end
438
439 @implementation FakeCKFetchRecordsOperation
440 @synthesize database = _database;
441 @synthesize recordIDs = _recordIDs;
442 @synthesize desiredKeys = _desiredKeys;
443 @synthesize configuration = _configuration;
444
445 @synthesize perRecordProgressBlock = _perRecordProgressBlock;
446 @synthesize perRecordCompletionBlock = _perRecordCompletionBlock;
447
448 @synthesize fetchRecordsCompletionBlock = _fetchRecordsCompletionBlock;
449
450 - (instancetype)init {
451 if((self = [super init])) {
452
453 }
454 return self;
455 }
456 - (instancetype)initWithRecordIDs:(NSArray<CKRecordID *> *)recordIDs {
457 if((self = [super init])) {
458 _recordIDs = recordIDs;
459 }
460 return self;
461 }
462
463 - (void)main {
464 FakeCKDatabase* ckdb = [FakeCKFetchRecordsOperation ckdb];
465
466 // Doesn't call the per-record progress block
467 NSMutableDictionary<CKRecordID*, CKRecord*>* records = [NSMutableDictionary dictionary];
468 NSError* operror = nil;
469
470 for(CKRecordID* recordID in self.recordIDs) {
471 CKRecordZoneID* zoneID = recordID.zoneID;
472 FakeCKZone* zone = ckdb[zoneID];
473
474 if(!zone) {
475 ckksnotice("fakeck", zoneID, "Fetched for a missing zone %@", zoneID);
476 NSError* zoneNotFoundError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
477 code:CKErrorZoneNotFound
478 userInfo:@{}];
479 NSError* error = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
480 code:CKErrorPartialFailure
481 userInfo:@{CKPartialErrorsByItemIDKey: @{zoneID:zoneNotFoundError}}];
482
483 // Not strictly right, but good enough for now
484 self.fetchRecordsCompletionBlock(nil, error);
485 return;
486 }
487
488 CKRecord* record = zone.currentDatabase[recordID];
489 if(record) {
490 if(self.perRecordCompletionBlock) {
491 self.perRecordCompletionBlock(record, recordID, nil);
492 }
493 records[recordID] = record;
494 } else {
495 secerror("fakeck: Should be an error fetching %@", recordID);
496
497 if(!operror) {
498 operror = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorPartialFailure userInfo:nil];
499 }
500
501 // There really should be a better way to do this...
502 NSMutableDictionary* newDictionary = [operror.userInfo mutableCopy] ?: [NSMutableDictionary dictionary];
503 NSMutableDictionary* newPartials = newDictionary[CKPartialErrorsByItemIDKey] ?: [NSMutableDictionary dictionary];
504 newPartials[recordID] = [[CKPrettyError alloc] initWithDomain:operror.domain code:CKErrorUnknownItem
505 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Mock CloudKit: no record of %@", recordID]}];
506 newDictionary[CKPartialErrorsByItemIDKey] = newPartials;
507
508 operror = [[CKPrettyError alloc] initWithDomain:operror.domain code:operror.code userInfo:newDictionary];
509
510 /// TODO: do this better
511 if(self.perRecordCompletionBlock) {
512 self.perRecordCompletionBlock(nil, recordID, newPartials[zoneID]);
513 }
514 }
515 }
516
517 if(self.fetchRecordsCompletionBlock) {
518 self.fetchRecordsCompletionBlock(records, operror);
519 }
520 }
521
522 +(FakeCKDatabase*) ckdb {
523 // Shouldn't ever be called: must be mocked out.
524 @throw [NSException exceptionWithName:NSInternalInconsistencyException
525 reason:[NSString stringWithFormat:@"+ckdb[] must be mocked out for use"]
526 userInfo:nil];
527 }
528 @end
529
530
531 @implementation FakeCKQueryOperation
532 @synthesize query = _query;
533 @synthesize cursor = _cursor;
534 @synthesize zoneID = _zoneID;
535 @synthesize resultsLimit = _resultsLimit;
536 @synthesize desiredKeys = _desiredKeys;
537 @synthesize recordFetchedBlock = _recordFetchedBlock;
538 @synthesize queryCompletionBlock = _queryCompletionBlock;
539
540 - (instancetype)initWithQuery:(CKQuery *)query {
541 if((self = [super init])) {
542 _query = query;
543 }
544 return self;
545 }
546
547 - (void)main {
548 FakeCKDatabase* ckdb = [FakeCKFetchRecordsOperation ckdb];
549
550 FakeCKZone* zone = ckdb[self.zoneID];
551 if(!zone) {
552 ckksnotice("fakeck", self.zoneID, "Queried a missing zone %@", self.zoneID);
553
554 // I'm really not sure if this is right, but...
555 NSError* zoneNotFoundError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
556 code:CKErrorZoneNotFound
557 userInfo:@{
558 CKErrorRetryAfterKey: @(0.2),
559 }];
560 self.queryCompletionBlock(nil, zoneNotFoundError);
561 return;
562 }
563
564 NSMutableArray<CKRecord*>* matches = [NSMutableArray array];
565 for(CKRecordID* recordID in zone.currentDatabase.keyEnumerator) {
566 CKRecord* record = zone.currentDatabase[recordID];
567
568 if([self.query.recordType isEqualToString: record.recordType] &&
569 [self.query.predicate evaluateWithObject:record]) {
570
571 [matches addObject:record];
572 self.recordFetchedBlock(record);
573 }
574 }
575
576 if(self.queryCompletionBlock) {
577 // The query cursor will be non-null if there are more than self.resultsLimit classes. Don't implement this.
578 self.queryCompletionBlock(nil, nil);
579 }
580 }
581
582
583 +(FakeCKDatabase*) ckdb {
584 // Shouldn't ever be called: must be mocked out.
585 @throw [NSException exceptionWithName:NSInternalInconsistencyException
586 reason:[NSString stringWithFormat:@"+ckdb[] must be mocked out for use"]
587 userInfo:nil];
588 }
589 @end
590
591
592
593 // Do literally nothing
594 @implementation FakeAPSConnection
595 @synthesize delegate;
596
597 - (id)initWithEnvironmentName:(NSString *)environmentName namedDelegatePort:(NSString*)namedDelegatePort queue:(dispatch_queue_t)queue {
598 if(self = [super init]) {
599 }
600 return self;
601 }
602
603 - (void)setEnabledTopics:(NSArray<NSString *> *)enabledTopics {
604 }
605
606 - (void)setDarkWakeTopics:(NSArray<NSString *> *)darkWakeTopics {
607 }
608
609 @end
610
611 // Do literally nothing
612
613 @implementation FakeNSNotificationCenter
614 + (instancetype)defaultCenter {
615 return [[FakeNSNotificationCenter alloc] init];
616 }
617 - (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject {
618 }
619 - (void)removeObserver:(id)observer {
620 }
621 @end
622
623 @implementation FakeNSDistributedNotificationCenter
624 + (instancetype)defaultCenter
625 {
626 return [[FakeNSDistributedNotificationCenter alloc] init];
627 }
628 - (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject {
629 }
630 - (void)removeObserver:(id)observer {
631 }
632 - (void)postNotificationName:(NSNotificationName)name object:(nullable NSString *)object userInfo:(nullable NSDictionary *)userInfo options:(NSDistributedNotificationOptions)options
633 {
634 }
635 @end
636
637
638 @interface FakeCKZone ()
639 @property NSMutableArray<NSError*>* fetchErrors;
640 @end
641
642 @implementation FakeCKZone
643 - (instancetype)initZone: (CKRecordZoneID*) zoneID {
644 if(self = [super init]) {
645
646 _zoneID = zoneID;
647 _currentDatabase = [[NSMutableDictionary alloc] init];
648 _pastDatabases = [[NSMutableDictionary alloc] init];
649
650 _fetchErrors = [[NSMutableArray alloc] init];
651
652 _queue = dispatch_queue_create("fake-ckzone", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
653
654 _limitFetchTo = nil;
655 _fetchRecordZoneChangesOperationCount = 0;
656 _fetchRecordZoneChangesTimestamps = [[NSMutableArray alloc] init];
657 dispatch_sync(_queue, ^{
658 [self _onqueueRollChangeToken];
659 });
660 }
661 return self;
662 }
663
664 - (void)_onqueueRollChangeToken {
665 dispatch_assert_queue(self.queue);
666
667 NSData* changeToken = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
668 self.currentChangeToken = [[CKServerChangeToken alloc] initWithData: changeToken];
669 }
670
671 - (void)addToZone: (CKKSCKRecordHolder*) item zoneID: (CKRecordZoneID*) zoneID {
672 dispatch_sync(self.queue, ^{
673 [self _onqueueAddToZone:item zoneID:zoneID];
674 });
675 }
676
677 - (CKRecord*)_onqueueAddToZone:(CKKSCKRecordHolder*)item zoneID:(CKRecordZoneID*)zoneID {
678 dispatch_assert_queue(self.queue);
679
680 CKRecord* record = [item CKRecordWithZoneID: zoneID];
681 [self _onqueueAddToZone: record];
682
683 // Save off the etag
684 item.storedCKRecord = record;
685 return record;
686 }
687
688 - (void)addToZone: (CKRecord*) record {
689 dispatch_sync(self.queue, ^{
690 [self _onqueueAddToZone:record];
691 });
692 }
693
694 - (CKRecord*)_onqueueAddToZone:(CKRecord*)record {
695 dispatch_assert_queue(self.queue);
696
697 // Save off this current databse
698 self.pastDatabases[self.currentChangeToken] = [self.currentDatabase mutableCopy];
699
700 [self _onqueueRollChangeToken];
701
702 record.etag = [self.currentChangeToken description];
703 ckksnotice("fakeck", self.zoneID, "change tag: %@ %@", record.recordChangeTag, record.recordID);
704 record.modificationDate = [NSDate date];
705 self.currentDatabase[record.recordID] = record;
706 return record;
707 }
708
709 - (NSError * _Nullable)errorFromSavingRecord:(CKRecord*) record {
710 CKRecord* existingRecord = self.currentDatabase[record.recordID];
711 if(existingRecord && ![existingRecord.recordChangeTag isEqualToString: record.recordChangeTag]) {
712 ckksnotice("fakeck", self.zoneID, "change tag mismatch! Fail the write: %@ %@", record, existingRecord);
713
714 // TODO: doesn't yet support CKRecordChangedErrorAncestorRecordKey, since I don't understand it
715 return [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorServerRecordChanged
716 userInfo:@{CKRecordChangedErrorClientRecordKey:record,
717 CKRecordChangedErrorServerRecordKey:existingRecord}];
718 }
719
720 if(!existingRecord && record.etag != nil) {
721 ckksnotice("fakeck", self.zoneID, "update to a record that doesn't exist! Fail the write: %@", record);
722 return [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorUnknownItem
723 userInfo:nil];
724 }
725 return nil;
726 }
727
728 - (void)addCKRecordToZone:(CKRecord*) record {
729 if([self errorFromSavingRecord: record]) {
730 ckksnotice("fakeck", self.zoneID, "change tag mismatch! Fail the write!");
731 }
732
733 [self addToZone: record];
734 }
735
736 - (void)deleteFromHistory:(CKRecordID*)recordID {
737 for(NSMutableDictionary* pastDatabase in self.pastDatabases.objectEnumerator) {
738 [pastDatabase removeObjectForKey:recordID];
739 }
740 [self.currentDatabase removeObjectForKey:recordID];
741 }
742
743
744 - (NSError*)deleteCKRecordIDFromZone:(CKRecordID*) recordID {
745 // todo: fail somehow
746 dispatch_sync(self.queue, ^{
747 self.pastDatabases[self.currentChangeToken] = [self.currentDatabase mutableCopy];
748 [self _onqueueRollChangeToken];
749
750 [self.currentDatabase removeObjectForKey: recordID];
751 });
752 return nil;
753 }
754
755 - (void)failNextFetchWith: (NSError*) fetchChangesError {
756 @synchronized(self.fetchErrors) {
757 [self.fetchErrors addObject: fetchChangesError];
758 }
759 }
760
761 - (NSError * _Nullable)popFetchChangesError {
762 NSError* error = nil;
763 @synchronized(self.fetchErrors) {
764 if(self.fetchErrors.count > 0) {
765 error = self.fetchErrors[0];
766 [self.fetchErrors removeObjectAtIndex:0];
767 }
768 }
769 return error;
770 }
771 @end
772
773 @implementation FakeCKKSNotifier
774 +(void)post:(NSString*)notification {
775 if(notification) {
776 // This isn't actually fake, but XCTest likes NSNotificationCenter a whole lot.
777 // These notifications shouldn't escape this process, so it's perfect.
778 secnotice("ckks", "sending fake NSNotification %@", notification);
779 [[NSNotificationCenter defaultCenter] postNotificationName:notification object:nil];
780 }
781 }
782 @end
783
784 #endif // OCTAGON
785