Skip to content

Commit 1670fbb

Browse files
mbrandonwstephencelisRobbieClarken
authored
Improvements to the mock cloud database (#312)
* Private tables should always be saved in private db. * snapshots * Handle shared database more correctly. * wip * fix test * add another test * Update Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift * Fix id column type in documentation example schema to prevent error (#322) When the `id` column is defined as `INT NOT NULL PRIMARY KEY AUTOINCREMENT` sqlite will throw an error: > AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY * fixes * format --------- Co-authored-by: Stephen Celis <stephen@stephencelis.com> Co-authored-by: Robbie Clarken <last.gem5797@invariance.io>
1 parent 7fb8907 commit 1670fbb

File tree

5 files changed

+632
-195
lines changed

5 files changed

+632
-195
lines changed

Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,17 @@
9797
saveResults[recordToSave.recordID] = .failure(CKError(.invalidArguments))
9898
continue
9999
}
100+
} else if databaseScope == .shared,
101+
recordToSave.parent == nil,
102+
recordToSave.share == nil
103+
{
104+
// NB: Emit 'permissionFailure' if saving to shared database with no parent reference
105+
// or share reference.
106+
saveResults[recordToSave.recordID] = .failure(CKError(.permissionFailure))
107+
continue
100108
}
101109

110+
// NB: Emit 'zoneNotFound' error if saving record with a zone not found in database.
102111
guard storage[recordToSave.recordID.zoneID] != nil
103112
else {
104113
saveResults[recordToSave.recordID] = .failure(CKError(.zoneNotFound))
@@ -231,8 +240,30 @@
231240
deleteResults[recordIDToDelete] = .failure(CKError(.referenceViolation))
232241
continue
233242
}
243+
let recordToDelete = storage[recordIDToDelete.zoneID]?[recordIDToDelete]
234244
storage[recordIDToDelete.zoneID]?[recordIDToDelete] = nil
235245
deleteResults[recordIDToDelete] = .success(())
246+
247+
// NB: If deleting a share that the current user owns, delete the shared records and all
248+
// associated records.
249+
if databaseScope == .shared,
250+
let shareToDelete = recordToDelete as? CKShare,
251+
shareToDelete.recordID.zoneID.ownerName == CKCurrentUserDefaultName
252+
{
253+
func deleteRecords(referencing recordID: CKRecord.ID) {
254+
for recordToDelete in (storage[recordIDToDelete.zoneID] ?? [:]).values {
255+
guard
256+
recordToDelete.share?.recordID == recordID
257+
|| recordToDelete.parent?.recordID == recordID
258+
else {
259+
continue
260+
}
261+
storage[recordIDToDelete.zoneID]?[recordToDelete.recordID] = nil
262+
deleteRecords(referencing: recordToDelete.recordID)
263+
}
264+
}
265+
deleteRecords(referencing: shareToDelete.recordID)
266+
}
236267
}
237268

238269
return (saveResults: saveResults, deleteResults: deleteResults)

Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,9 @@
406406

407407
let newShare = try syncEngine.private.database.record(for: CKRecord.ID(recordName: "share"))
408408
let (saveResults, _) = try syncEngine.private.database.modifyRecords(saving: [newShare])
409-
_ = try saveResults.values.first?.get()
409+
#expect(throws: Never.self) {
410+
_ = try saveResults.values.first?.get()
411+
}
410412
}
411413

412414
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
@@ -421,6 +423,188 @@
421423
}
422424
#expect(error?.code == .unknownItem)
423425
}
426+
427+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
428+
@Test func saveSharedRecordWithoutParent() async throws {
429+
let record = CKRecord(recordType: "A", recordID: CKRecord.ID(recordName: "1"))
430+
let (saveResults, _) = try syncEngine.shared.database.modifyRecords(saving: [record])
431+
let error = #expect(throws: CKError.self) {
432+
_ = try saveResults.values.first?.get()
433+
}
434+
#expect(error?.code == .permissionFailure)
435+
}
436+
437+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
438+
@Test func deletingShareOwnedByCurrentUserDeletesShareAndDoesNotDeleteAssociatedData() async throws {
439+
let zone = syncEngine.defaultZone
440+
_ = try syncEngine.private.database.modifyRecordZones(saving: [zone])
441+
442+
let recordA = CKRecord(
443+
recordType: "A",
444+
recordID: CKRecord.ID(recordName: "A1", zoneID: zone.zoneID)
445+
)
446+
let recordB = CKRecord(
447+
recordType: "B",
448+
recordID: CKRecord.ID(recordName: "B1", zoneID: zone.zoneID)
449+
)
450+
recordB.parent = CKRecord.Reference(recordID: recordA.recordID, action: .none)
451+
let share = CKShare(
452+
rootRecord: recordA,
453+
shareID: CKRecord.ID(recordName: "share", zoneID: zone.zoneID)
454+
)
455+
_ = try syncEngine.private.database.modifyRecords(saving: [share, recordA, recordB])
456+
457+
assertInlineSnapshot(of: container, as: .customDump) {
458+
"""
459+
MockCloudContainer(
460+
privateCloudDatabase: MockCloudDatabase(
461+
databaseScope: .private,
462+
storage: [
463+
[0]: CKRecord(
464+
recordID: CKRecord.ID(A1/zone/__defaultOwner__),
465+
recordType: "A",
466+
parent: nil,
467+
share: CKReference(recordID: CKRecord.ID(share/zone/__defaultOwner__))
468+
),
469+
[1]: CKRecord(
470+
recordID: CKRecord.ID(B1/zone/__defaultOwner__),
471+
recordType: "B",
472+
parent: CKReference(recordID: CKRecord.ID(A1/zone/__defaultOwner__)),
473+
share: nil
474+
),
475+
[2]: CKRecord(
476+
recordID: CKRecord.ID(share/zone/__defaultOwner__),
477+
recordType: "cloudkit.share",
478+
parent: nil,
479+
share: nil
480+
)
481+
]
482+
),
483+
sharedCloudDatabase: MockCloudDatabase(
484+
databaseScope: .shared,
485+
storage: []
486+
)
487+
)
488+
"""
489+
}
490+
491+
_ = try syncEngine.private.database.modifyRecords(deleting: [share.recordID])
492+
493+
assertInlineSnapshot(of: container, as: .customDump) {
494+
"""
495+
MockCloudContainer(
496+
privateCloudDatabase: MockCloudDatabase(
497+
databaseScope: .private,
498+
storage: [
499+
[0]: CKRecord(
500+
recordID: CKRecord.ID(A1/zone/__defaultOwner__),
501+
recordType: "A",
502+
parent: nil,
503+
share: CKReference(recordID: CKRecord.ID(share/zone/__defaultOwner__))
504+
),
505+
[1]: CKRecord(
506+
recordID: CKRecord.ID(B1/zone/__defaultOwner__),
507+
recordType: "B",
508+
parent: CKReference(recordID: CKRecord.ID(A1/zone/__defaultOwner__)),
509+
share: nil
510+
)
511+
]
512+
),
513+
sharedCloudDatabase: MockCloudDatabase(
514+
databaseScope: .shared,
515+
storage: []
516+
)
517+
)
518+
"""
519+
}
520+
}
521+
522+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
523+
@Test func deletingShareNotOwnedByCurrentUserDeletesOnlyShareAndNotAssociatedRecords() async throws {
524+
let externalZone = CKRecordZone(
525+
zoneID: CKRecordZone.ID(zoneName: "external.zone", ownerName: "external.owner")
526+
)
527+
_ = try syncEngine.shared.database.modifyRecordZones(saving: [externalZone])
528+
529+
let recordA = CKRecord(
530+
recordType: "A",
531+
recordID: CKRecord.ID(recordName: "A1", zoneID: externalZone.zoneID)
532+
)
533+
let recordB = CKRecord(
534+
recordType: "B",
535+
recordID: CKRecord.ID(recordName: "B1", zoneID: externalZone.zoneID)
536+
)
537+
recordB.parent = CKRecord.Reference(recordID: recordA.recordID, action: .none)
538+
let share = CKShare(
539+
rootRecord: recordA,
540+
shareID: CKRecord.ID(recordName: "share", zoneID: externalZone.zoneID)
541+
)
542+
_ = try syncEngine.shared.database.modifyRecords(saving: [share, recordA, recordB])
543+
544+
assertInlineSnapshot(of: container, as: .customDump) {
545+
"""
546+
MockCloudContainer(
547+
privateCloudDatabase: MockCloudDatabase(
548+
databaseScope: .private,
549+
storage: []
550+
),
551+
sharedCloudDatabase: MockCloudDatabase(
552+
databaseScope: .shared,
553+
storage: [
554+
[0]: CKRecord(
555+
recordID: CKRecord.ID(A1/external.zone/external.owner),
556+
recordType: "A",
557+
parent: nil,
558+
share: CKReference(recordID: CKRecord.ID(share/external.zone/external.owner))
559+
),
560+
[1]: CKRecord(
561+
recordID: CKRecord.ID(B1/external.zone/external.owner),
562+
recordType: "B",
563+
parent: CKReference(recordID: CKRecord.ID(A1/external.zone/external.owner)),
564+
share: nil
565+
),
566+
[2]: CKRecord(
567+
recordID: CKRecord.ID(share/external.zone/external.owner),
568+
recordType: "cloudkit.share",
569+
parent: nil,
570+
share: nil
571+
)
572+
]
573+
)
574+
)
575+
"""
576+
}
577+
578+
_ = try syncEngine.shared.database.modifyRecords(deleting: [share.recordID])
579+
580+
assertInlineSnapshot(of: container, as: .customDump) {
581+
"""
582+
MockCloudContainer(
583+
privateCloudDatabase: MockCloudDatabase(
584+
databaseScope: .private,
585+
storage: []
586+
),
587+
sharedCloudDatabase: MockCloudDatabase(
588+
databaseScope: .shared,
589+
storage: [
590+
[0]: CKRecord(
591+
recordID: CKRecord.ID(A1/external.zone/external.owner),
592+
recordType: "A",
593+
parent: nil,
594+
share: CKReference(recordID: CKRecord.ID(share/external.zone/external.owner))
595+
),
596+
[1]: CKRecord(
597+
recordID: CKRecord.ID(B1/external.zone/external.owner),
598+
recordType: "B",
599+
parent: CKReference(recordID: CKRecord.ID(A1/external.zone/external.owner)),
600+
share: nil
601+
)
602+
]
603+
)
604+
)
605+
"""
606+
}
607+
}
424608
}
425609
}
426610
#endif

Tests/SQLiteDataTests/CloudKitTests/SharingPermissionsTests.swift

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -113,33 +113,35 @@
113113
reminderRecord.setValue("Get milk", forKey: "title", at: now)
114114
reminderRecord.setValue(1, forKey: "remindersListID", at: now)
115115
reminderRecord.parent = CKRecord.Reference(record: remindersListRecord, action: .none)
116+
let share = CKShare(
117+
rootRecord: remindersListRecord,
118+
shareID: CKRecord.ID(
119+
recordName: "share-\(remindersListRecord.recordID.recordName)",
120+
zoneID: remindersListRecord.recordID.zoneID
121+
)
122+
)
123+
share.publicPermission = .readOnly
124+
share.currentUserParticipant?.permission = .readOnly
116125

117126
_ = try syncEngine.modifyRecords(
118127
scope: .shared,
119-
saving: [reminderRecord, remindersListRecord]
128+
saving: [reminderRecord, remindersListRecord, share]
120129
)
121130

122131
let freshRemindersListRecord = try syncEngine.shared.database.record(
123132
for: remindersListRecord.recordID
124133
)
125-
126-
let share = CKShare(
127-
rootRecord: freshRemindersListRecord,
128-
shareID: CKRecord.ID(
129-
recordName: "share-\(freshRemindersListRecord.recordID.recordName)",
130-
zoneID: freshRemindersListRecord.recordID.zoneID
131-
)
134+
let freshShare = try #require(
135+
syncEngine.shared.database.record(for: share.recordID) as? CKShare
132136
)
133-
share.publicPermission = .readOnly
134-
share.currentUserParticipant?.permission = .readOnly
135137

136138
try await syncEngine
137139
.acceptShare(
138140
metadata: ShareMetadata(
139141
containerIdentifier: container.containerIdentifier!,
140142
hierarchicalRootRecordID: freshRemindersListRecord.recordID,
141143
rootRecord: freshRemindersListRecord,
142-
share: share
144+
share: freshShare
143145
)
144146
)
145147

@@ -216,31 +218,34 @@
216218
reminderRecord.setValue(1, forKey: "remindersListID", at: now)
217219
reminderRecord.setValue(false, forKey: "isCompleted", at: now)
218220
reminderRecord.parent = CKRecord.Reference(record: remindersListRecord, action: .none)
221+
let share = CKShare(
222+
rootRecord: remindersListRecord,
223+
shareID: CKRecord.ID(
224+
recordName: "share-\(remindersListRecord.recordID.recordName)",
225+
zoneID: remindersListRecord.recordID.zoneID
226+
)
227+
)
228+
share.publicPermission = .readOnly
229+
share.currentUserParticipant?.permission = .readOnly
219230
_ = try syncEngine.modifyRecords(
220231
scope: .shared,
221-
saving: [remindersListRecord, reminderRecord]
232+
saving: [remindersListRecord, reminderRecord, share]
222233
)
223234

224235
let freshRemindersListRecord = try syncEngine.shared.database.record(
225236
for: remindersListRecord.recordID
226237
)
227-
let share = CKShare(
228-
rootRecord: freshRemindersListRecord,
229-
shareID: CKRecord.ID(
230-
recordName: "share-\(freshRemindersListRecord.recordID.recordName)",
231-
zoneID: freshRemindersListRecord.recordID.zoneID
232-
)
238+
let freshShare = try #require(
239+
syncEngine.shared.database.record(for: share.recordID) as? CKShare
233240
)
234-
share.publicPermission = .readOnly
235-
share.currentUserParticipant?.permission = .readOnly
236241

237242
try await syncEngine
238243
.acceptShare(
239244
metadata: ShareMetadata(
240245
containerIdentifier: container.containerIdentifier!,
241246
hierarchicalRootRecordID: freshRemindersListRecord.recordID,
242247
rootRecord: freshRemindersListRecord,
243-
share: share
248+
share: freshShare
244249
)
245250
)
246251

0 commit comments

Comments
 (0)