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