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