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