From c3a071f3221d183fb7451300b60bcdcd3a2268c9 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 16 Aug 2025 10:26:49 -0500 Subject: [PATCH 1/8] Fixing more sharing edge cases with tests. --- .../CloudKit/CloudKitSharing.swift | 28 +- .../SharingGRDBCore/CloudKit/Triggers.swift | 3 +- .../CloudKitTests/AccountLifecycleTests.swift | 2 +- .../CloudKitTests/AssetsTests.swift | 4 +- .../CloudKitTests/CloudKitTests.swift | 26 +- .../FetchRecordZoneChangesTests.swift | 8 +- .../FetchedDatabaseChangesTests.swift | 2 +- .../ForeignKeyConstraintTests.swift | 18 +- .../CloudKitTests/MergeConflictTests.swift | 22 +- .../CloudKitTests/MetadataTests.swift | 6 +- .../MockCloudDatabaseTests.swift | 14 +- .../CloudKitTests/NewTableSyncTests.swift | 2 +- .../NextRecordZoneChangeBatchTests.swift | 12 +- .../ReferenceViolationTests.swift | 10 +- .../CloudKitTests/SharingTests.swift | 360 +++++++++++++++++- 15 files changed, 443 insertions(+), 74 deletions(-) diff --git a/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift b/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift index 556b9b34..9d91c8ca 100644 --- a/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift +++ b/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift @@ -32,6 +32,7 @@ extension SyncEngine { case recordNotRoot([ForeignKey]) case recordTableNotSynchronized case recordTablePrivate + case shareNotFound } let recordTableName: String @@ -40,7 +41,7 @@ extension SyncEngine { let debugDescription: String var errorDescription: String? { - "The record could not be shared." + "An error occured when editing sharing." } } @@ -147,6 +148,31 @@ extension SyncEngine { return SharedRecord(container: container, share: sharedRecord) } + public func unshare(record: T) async throws + where T.TableColumns.PrimaryKey.QueryOutput: IdentifierStringConvertible + { + let metadata = try await userDatabase.read { [recordName = record.recordName] db in + try SyncMetadata + .where { $0.recordName.eq(recordName) } + .fetchOne(db) + } + guard let share = metadata?.share + else { + throw SharingError( + recordTableName: T.tableName, + recordPrimaryKey: record.primaryKey.rawIdentifier, + reason: .shareNotFound, + debugDescription: "No share found associated with record." + ) + } + + let result = try await syncEngines.private?.database.modifyRecords( + saving: [], + deleting: [share.recordID] + ) + try result?.deleteResults.values.forEach { _ = try $0.get() } + } + public func acceptShare(metadata: CKShare.Metadata) async throws { try await acceptShare(metadata: ShareMetadata(rawValue: metadata)) } diff --git a/Sources/SharingGRDBCore/CloudKit/Triggers.swift b/Sources/SharingGRDBCore/CloudKit/Triggers.swift index 65f11912..099275d4 100644 --- a/Sources/SharingGRDBCore/CloudKit/Triggers.swift +++ b/Sources/SharingGRDBCore/CloudKit/Triggers.swift @@ -238,7 +238,8 @@ ) } .where { - $0.parentRecordName.is(nil) + !SyncEngine.isSynchronizingChanges() + && $0.parentRecordName.is(nil) && !SQLQueryExpression( "\(raw: String.sqliteDataCloudKitSchemaName)_hasPermission(\($0.share))" ) diff --git a/Tests/SharingGRDBTests/CloudKitTests/AccountLifecycleTests.swift b/Tests/SharingGRDBTests/CloudKitTests/AccountLifecycleTests.swift index 885b1e0e..ebe46262 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/AccountLifecycleTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/AccountLifecycleTests.swift @@ -59,7 +59,7 @@ extension BaseCloudKitTests { try await syncEngine.processPendingDatabaseChanges(scope: .private) try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( diff --git a/Tests/SharingGRDBTests/CloudKitTests/AssetsTests.swift b/Tests/SharingGRDBTests/CloudKitTests/AssetsTests.swift index 0fd6da2c..41fefae0 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/AssetsTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/AssetsTests.swift @@ -26,7 +26,7 @@ extension BaseCloudKitTests { try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -82,7 +82,7 @@ extension BaseCloudKitTests { try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( diff --git a/Tests/SharingGRDBTests/CloudKitTests/CloudKitTests.swift b/Tests/SharingGRDBTests/CloudKitTests/CloudKitTests.swift index c68dc153..13559069 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/CloudKitTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/CloudKitTests.swift @@ -460,7 +460,7 @@ extension BaseCloudKitTests { } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -506,7 +506,7 @@ extension BaseCloudKitTests { } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -587,7 +587,7 @@ extension BaseCloudKitTests { .execute(db) } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -622,7 +622,7 @@ extension BaseCloudKitTests { } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -653,7 +653,7 @@ extension BaseCloudKitTests { .execute(db) } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -677,7 +677,7 @@ extension BaseCloudKitTests { } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -728,7 +728,7 @@ extension BaseCloudKitTests { } ) #expect(metadata.userModificationDate == serverModificationDate) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -773,7 +773,7 @@ extension BaseCloudKitTests { let record = try syncEngine.private.database.record(for: RemindersList.recordID(for: 1)) try await syncEngine.modifyRecords(scope: .private, saving: [record]).notify() try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -806,7 +806,7 @@ extension BaseCloudKitTests { } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -856,7 +856,7 @@ extension BaseCloudKitTests { } ) #expect(metadata.userModificationDate == userModificationDate) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -889,7 +889,7 @@ extension BaseCloudKitTests { } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -926,7 +926,7 @@ extension BaseCloudKitTests { .fetchOne(db) } #expect(metadata == nil) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -976,7 +976,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( diff --git a/Tests/SharingGRDBTests/CloudKitTests/FetchRecordZoneChangesTests.swift b/Tests/SharingGRDBTests/CloudKitTests/FetchRecordZoneChangesTests.swift index c3051a28..929d7037 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/FetchRecordZoneChangesTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/FetchRecordZoneChangesTests.swift @@ -211,7 +211,7 @@ extension BaseCloudKitTests { try await syncEngine.modifyRecords(scope: .private, saving: [remindersListRecord]).notify() - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -254,7 +254,7 @@ extension BaseCloudKitTests { try await syncEngine.processPendingRecordZoneChanges(scope: .private) } - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -310,7 +310,7 @@ extension BaseCloudKitTests { ) try await syncEngine.modifyRecords(scope: .private, saving: [reminderRecord]).notify() - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -375,7 +375,7 @@ extension BaseCloudKitTests { try await syncEngine.processPendingRecordZoneChanges(scope: .private) } - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( diff --git a/Tests/SharingGRDBTests/CloudKitTests/FetchedDatabaseChangesTests.swift b/Tests/SharingGRDBTests/CloudKitTests/FetchedDatabaseChangesTests.swift index 2e9fd4af..e13f3781 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/FetchedDatabaseChangesTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/FetchedDatabaseChangesTests.swift @@ -74,7 +74,7 @@ extension BaseCloudKitTests { try #expect(UnsyncedModel.count().fetchOne(db) == 2) } - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( diff --git a/Tests/SharingGRDBTests/CloudKitTests/ForeignKeyConstraintTests.swift b/Tests/SharingGRDBTests/CloudKitTests/ForeignKeyConstraintTests.swift index 2d679e4c..bf09dd5b 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/ForeignKeyConstraintTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/ForeignKeyConstraintTests.swift @@ -35,7 +35,7 @@ extension BaseCloudKitTests { ) try await syncEngine.modifyRecords(scope: .private, saving: [reminderRecord]).notify() - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -107,7 +107,7 @@ extension BaseCloudKitTests { try await syncEngine.processPendingRecordZoneChanges(scope: .private) } - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -184,7 +184,7 @@ extension BaseCloudKitTests { ) } - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -246,7 +246,7 @@ extension BaseCloudKitTests { ) .notify() - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -316,7 +316,7 @@ extension BaseCloudKitTests { _ = try { try syncEngine.modifyRecords(scope: .private, saving: [remindersListRecord]) }() try await syncEngine.modifyRecords(scope: .private, saving: [reminderRecord]).notify() - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -397,7 +397,7 @@ extension BaseCloudKitTests { try await relaunchedSyncEngine.processPendingRecordZoneChanges(scope: .private) } - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -474,7 +474,7 @@ extension BaseCloudKitTests { saving: [reminderRecord, personalListRecord] ).notify() - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -526,7 +526,7 @@ extension BaseCloudKitTests { await modifications.notify() - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -779,7 +779,7 @@ extension BaseCloudKitTests { try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( diff --git a/Tests/SharingGRDBTests/CloudKitTests/MergeConflictTests.swift b/Tests/SharingGRDBTests/CloudKitTests/MergeConflictTests.swift index 3416b803..36495d03 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/MergeConflictTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/MergeConflictTests.swift @@ -18,7 +18,7 @@ extension BaseCloudKitTests { } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -76,7 +76,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -121,7 +121,7 @@ extension BaseCloudKitTests { try await syncEngine.processPendingRecordZoneChanges(scope: .private) await modificationCallback.notify() - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -172,7 +172,7 @@ extension BaseCloudKitTests { } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -230,7 +230,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -275,7 +275,7 @@ extension BaseCloudKitTests { try await syncEngine.processPendingRecordZoneChanges(scope: .private) await modificationCallback.notify() - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -344,7 +344,7 @@ extension BaseCloudKitTests { await modificationCallback.notify() try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -420,7 +420,7 @@ extension BaseCloudKitTests { ) } - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -489,7 +489,7 @@ extension BaseCloudKitTests { await modificationCallback.notify() try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -559,7 +559,7 @@ extension BaseCloudKitTests { await modificationCallback.notify() try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -636,7 +636,7 @@ extension BaseCloudKitTests { await modificationsFinished.notify() try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( diff --git a/Tests/SharingGRDBTests/CloudKitTests/MetadataTests.swift b/Tests/SharingGRDBTests/CloudKitTests/MetadataTests.swift index 9470bf6f..aebbc811 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/MetadataTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/MetadataTests.swift @@ -21,7 +21,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -91,7 +91,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -145,7 +145,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( diff --git a/Tests/SharingGRDBTests/CloudKitTests/MockCloudDatabaseTests.swift b/Tests/SharingGRDBTests/CloudKitTests/MockCloudDatabaseTests.swift index 5b94aa00..7751074d 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/MockCloudDatabaseTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/MockCloudDatabaseTests.swift @@ -56,7 +56,7 @@ extension BaseCloudKitTests { ) #expect(saveRecordResults.allSatisfy({ (try? $1.get()) != nil })) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -102,7 +102,7 @@ extension BaseCloudKitTests { try await syncEngine.modifyRecords(scope: .private, saving: [child]).notify() - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -134,7 +134,7 @@ extension BaseCloudKitTests { } #expect(error == CKError(.zoneNotFound)) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -162,7 +162,7 @@ extension BaseCloudKitTests { ) #expect(deleteResults.allSatisfy({ (try? $1.get()) != nil })) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -187,7 +187,7 @@ extension BaseCloudKitTests { ) #expect(deleteResults.allSatisfy({ (try? $1.get()) != nil })) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -218,7 +218,7 @@ extension BaseCloudKitTests { } #expect(error == CKError(.zoneNotFound)) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -249,7 +249,7 @@ extension BaseCloudKitTests { } #expect(error == CKError(.referenceViolation)) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( diff --git a/Tests/SharingGRDBTests/CloudKitTests/NewTableSyncTests.swift b/Tests/SharingGRDBTests/CloudKitTests/NewTableSyncTests.swift index b580e573..387cac19 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/NewTableSyncTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/NewTableSyncTests.swift @@ -25,7 +25,7 @@ extension BaseCloudKitTests { @Test func initialSync() async throws { try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( diff --git a/Tests/SharingGRDBTests/CloudKitTests/NextRecordZoneChangeBatchTests.swift b/Tests/SharingGRDBTests/CloudKitTests/NextRecordZoneChangeBatchTests.swift index 8b688a13..b8b23725 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/NextRecordZoneChangeBatchTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/NextRecordZoneChangeBatchTests.swift @@ -15,7 +15,7 @@ extension BaseCloudKitTests { ) try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -44,7 +44,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -73,7 +73,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -97,7 +97,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -132,7 +132,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -176,7 +176,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( diff --git a/Tests/SharingGRDBTests/CloudKitTests/ReferenceViolationTests.swift b/Tests/SharingGRDBTests/CloudKitTests/ReferenceViolationTests.swift index 47aeff38..23770546 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/ReferenceViolationTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/ReferenceViolationTests.swift @@ -41,7 +41,7 @@ extension BaseCloudKitTests { await modifications.notify() try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -115,7 +115,7 @@ extension BaseCloudKitTests { try await syncEngine.processPendingRecordZoneChanges(scope: .private) await modifications.notify() - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -199,7 +199,7 @@ extension BaseCloudKitTests { await modifications.notify() try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -277,7 +277,7 @@ extension BaseCloudKitTests { await modifications.notify() try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -357,7 +357,7 @@ extension BaseCloudKitTests { await modifications.notify() try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( diff --git a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift index 0d599b0f..718c8538 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift @@ -27,7 +27,7 @@ extension BaseCloudKitTests { } assertInlineSnapshot(of: error?.localizedDescription, as: .customDump) { """ - "The record could not be shared." + "An error occured when editing sharing." """ } assertInlineSnapshot(of: error, as: .customDump) { @@ -66,7 +66,7 @@ extension BaseCloudKitTests { as: .customDump ) { """ - "The record could not be shared." + "An error occured when editing sharing." """ } assertInlineSnapshot(of: error, as: .customDump) { @@ -94,7 +94,7 @@ extension BaseCloudKitTests { as: .customDump ) { """ - "The record could not be shared." + "An error occured when editing sharing." """ } assertInlineSnapshot(of: error, as: .customDump) { @@ -133,7 +133,7 @@ extension BaseCloudKitTests { as: .customDump ) { """ - "The record could not be shared." + "An error occured when editing sharing." """ } assertInlineSnapshot(of: error, as: .customDump) { @@ -178,7 +178,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .shared) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -249,7 +249,7 @@ extension BaseCloudKitTests { try await syncEngine.modifyRecords(scope: .shared, saving: [newShare]).notify() try await syncEngine.modifyRecords(scope: .shared, saving: [newRemindersListRecord]).notify() - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -353,7 +353,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .shared) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -436,7 +436,7 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .shared) - assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( @@ -511,6 +511,74 @@ extension BaseCloudKitTests { } } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func shareUnshareShareAgain() async throws { + let remindersList = RemindersList(id: 1, title: "Personal") + try await userDatabase.userWrite { db in + try db.seed { + remindersList + } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + let _ = try await syncEngine.share(record: remindersList, configure: { _ in }) + + try await syncEngine.unshare(record: remindersList) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/co.pointfree.SQLiteData.defaultZone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/co.pointfree.SQLiteData.defaultZone/__defaultOwner__)) + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + + let _ = try await syncEngine.share(record: remindersList, configure: { _ in }) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(share-1:remindersLists/co.pointfree.SQLiteData.defaultZone/__defaultOwner__), + recordType: "cloudkit.share", + parent: nil, + share: nil + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/co.pointfree.SQLiteData.defaultZone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/co.pointfree.SQLiteData.defaultZone/__defaultOwner__)) + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + + } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func acceptShare() async throws { let externalZone = CKRecordZone( @@ -589,7 +657,6 @@ extension BaseCloudKitTests { } } - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func acceptShareCreateReminder() async throws { let externalZone = CKRecordZone( @@ -681,6 +748,281 @@ extension BaseCloudKitTests { """ } } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deleteRootSharedRecord_CurrentUserOwnsRecord() async throws { + let remindersList = RemindersList(id: 1, title: "Personal") + try await userDatabase.userWrite { db in + try db.seed { + remindersList + } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + let _ = try await syncEngine.share(record: remindersList, configure: { _ in }) + + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + /// Deleting a root shared record that is not owned by current user should only delete + /// the share but not the actual records. + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deleteRootSharedRecord_CurrentUserNotOwner() async throws { + let externalZone = CKRecordZone( + zoneID: CKRecordZone.ID( + zoneName: "external.zone", + ownerName: "external.owner" + ) + ) + try await syncEngine.modifyRecordZones(scope: .shared, saving: [externalZone]).notify() + + let remindersListRecord = CKRecord( + recordType: RemindersList.tableName, + recordID: RemindersList.recordID(for: 1, zoneID: externalZone.zoneID) + ) + remindersListRecord.setValue(1, forKey: "id", at: now) + remindersListRecord.setValue("Personal", forKey: "title", at: now) + let share = CKShare( + rootRecord: remindersListRecord, + shareID: CKRecord.ID( + recordName: "share-\(remindersListRecord.recordID.recordName)", + zoneID: remindersListRecord.recordID.zoneID + ) + ) + + try await syncEngine + .acceptShare( + metadata: ShareMetadata( + containerIdentifier: container.containerIdentifier!, + hierarchicalRootRecordID: remindersListRecord.recordID, + rootRecord: remindersListRecord, + share: share + ) + ) + + try await userDatabase.userWrite { db in + try db.seed { + Reminder(id: 1, title: "Get milk", remindersListID: 1) + } + } + + try await syncEngine.processPendingRecordZoneChanges(scope: .shared) + + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + } + + try await syncEngine.processPendingRecordZoneChanges(scope: .shared) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:reminders/external.zone/external.owner), + recordType: "reminders", + parent: CKReference(recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner)), + share: nil, + id: 1, + isCompleted: 0, + remindersListID: 1, + title: "Get milk" + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), + recordType: "remindersLists", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), + id: 1, + title: "Personal" + ) + ] + ) + ) + """ + } + } + + /// Inserting record into shared record when user does not have permission. + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func insertRecordInReadOnlyRemindersList() async throws { + let externalZone = CKRecordZone( + zoneID: CKRecordZone.ID( + zoneName: "external.zone", + ownerName: "external.owner" + ) + ) + try await syncEngine.modifyRecordZones(scope: .shared, saving: [externalZone]).notify() + + let remindersListRecord = CKRecord( + recordType: RemindersList.tableName, + recordID: RemindersList.recordID(for: 1, zoneID: externalZone.zoneID) + ) + remindersListRecord.setValue(1, forKey: "id", at: now) + remindersListRecord.setValue("Personal", forKey: "title", at: now) + let share = CKShare( + rootRecord: remindersListRecord, + shareID: CKRecord.ID( + recordName: "share-\(remindersListRecord.recordID.recordName)", + zoneID: remindersListRecord.recordID.zoneID + ) + ) + share.publicPermission = .readOnly + share.currentUserParticipant?.permission = .readOnly + + try await syncEngine + .acceptShare( + metadata: ShareMetadata( + containerIdentifier: container.containerIdentifier!, + hierarchicalRootRecordID: remindersListRecord.recordID, + rootRecord: remindersListRecord, + share: share + ) + ) + + let error = await #expect(throws: DatabaseError.self) { + try await self.userDatabase.userWrite { db in + try db.seed { + Reminder(id: 1, title: "Get milk", remindersListID: 1) + } + } + } + #expect(error?.message == SyncEngine.writePermissionError) + } + + /// Delete record in shared record when user does not have permission. + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deleteReminderInReadOnlyRemindersList() async throws { + let externalZone = CKRecordZone( + zoneID: CKRecordZone.ID( + zoneName: "external.zone", + ownerName: "external.owner" + ) + ) + try await syncEngine.modifyRecordZones(scope: .shared, saving: [externalZone]).notify() + + let remindersListRecord = CKRecord( + recordType: RemindersList.tableName, + recordID: RemindersList.recordID(for: 1, zoneID: externalZone.zoneID) + ) + remindersListRecord.setValue(1, forKey: "id", at: now) + remindersListRecord.setValue("Personal", forKey: "title", at: now) + let share = CKShare( + rootRecord: remindersListRecord, + shareID: CKRecord.ID( + recordName: "share-\(remindersListRecord.recordID.recordName)", + zoneID: remindersListRecord.recordID.zoneID + ) + ) + share.publicPermission = .readOnly + share.currentUserParticipant?.permission = .readOnly + + try await syncEngine + .acceptShare( + metadata: ShareMetadata( + containerIdentifier: container.containerIdentifier!, + hierarchicalRootRecordID: remindersListRecord.recordID, + rootRecord: remindersListRecord, + share: share + ) + ) + let reminderRecord = CKRecord( + recordType: Reminder.tableName, + recordID: Reminder.recordID(for: 1, zoneID: externalZone.zoneID) + ) + reminderRecord.setValue(1, forKey: "id", at: now) + reminderRecord.setValue("Get milk", forKey: "title", at: now) + reminderRecord.setValue(1, forKey: "remindersListID", at: now) + reminderRecord.parent = CKRecord.Reference(record: remindersListRecord, action: .none) + try await syncEngine.modifyRecords(scope: .shared, saving: [reminderRecord]).notify() + + try await self.userDatabase.userWrite { db in + let error = #expect(throws: DatabaseError.self) { + try Reminder.find(1).delete().execute(db) + } + #expect(error?.message == SyncEngine.writePermissionError) + try #expect(Reminder.count().fetchOne(db) == 1) + } + } + + /// Editing record in shared record when user does not have permission. + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func editReminderInReadOnlyRemindersList() async throws { + let externalZone = CKRecordZone( + zoneID: CKRecordZone.ID( + zoneName: "external.zone", + ownerName: "external.owner" + ) + ) + try await syncEngine.modifyRecordZones(scope: .shared, saving: [externalZone]).notify() + + let remindersListRecord = CKRecord( + recordType: RemindersList.tableName, + recordID: RemindersList.recordID(for: 1, zoneID: externalZone.zoneID) + ) + remindersListRecord.setValue(1, forKey: "id", at: now) + remindersListRecord.setValue("Personal", forKey: "title", at: now) + let share = CKShare( + rootRecord: remindersListRecord, + shareID: CKRecord.ID( + recordName: "share-\(remindersListRecord.recordID.recordName)", + zoneID: remindersListRecord.recordID.zoneID + ) + ) + share.publicPermission = .readOnly + share.currentUserParticipant?.permission = .readOnly + + try await syncEngine + .acceptShare( + metadata: ShareMetadata( + containerIdentifier: container.containerIdentifier!, + hierarchicalRootRecordID: remindersListRecord.recordID, + rootRecord: remindersListRecord, + share: share + ) + ) + let reminderRecord = CKRecord( + recordType: Reminder.tableName, + recordID: Reminder.recordID(for: 1, zoneID: externalZone.zoneID) + ) + reminderRecord.setValue(1, forKey: "id", at: now) + reminderRecord.setValue("Get milk", forKey: "title", at: now) + reminderRecord.setValue(1, forKey: "remindersListID", at: now) + reminderRecord.parent = CKRecord.Reference(record: remindersListRecord, action: .none) + try await syncEngine.modifyRecords(scope: .shared, saving: [reminderRecord]).notify() + + try await self.userDatabase.userWrite { db in + let error = #expect(throws: DatabaseError.self) { + try Reminder.find(1).delete().execute(db) + } + #expect(error?.message == SyncEngine.writePermissionError) + try #expect(Reminder.count().fetchOne(db) == 1) + } + } } } From 6c4680d0b372281461e02b072d974bbf3b4cf341 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 17 Aug 2025 07:29:29 -0500 Subject: [PATCH 2/8] wip --- Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift index 718c8538..4f296c28 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift @@ -1017,10 +1017,10 @@ extension BaseCloudKitTests { try await self.userDatabase.userWrite { db in let error = #expect(throws: DatabaseError.self) { - try Reminder.find(1).delete().execute(db) + try Reminder.update { $0.isCompleted = true }.execute(db) } #expect(error?.message == SyncEngine.writePermissionError) - try #expect(Reminder.count().fetchOne(db) == 1) + try #expect(Reminder.where(\.isCompleted).fetchCount(db) == 0) } } } From e721a9113eb716712ede0bc0e65929ce2ce0f02d Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 17 Aug 2025 07:31:06 -0500 Subject: [PATCH 3/8] wip --- .../CloudKitTests/TriggerTests.swift | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/Tests/SharingGRDBTests/CloudKitTests/TriggerTests.swift b/Tests/SharingGRDBTests/CloudKitTests/TriggerTests.swift index 2118cd01..a7baa5a1 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/TriggerTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/TriggerTests.swift @@ -100,7 +100,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); UPDATE "sqlitedata_icloud_metadata" SET "_isDeleted" = 1 WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'childWithOnDeleteSetDefaults')); @@ -129,7 +129,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); UPDATE "sqlitedata_icloud_metadata" SET "_isDeleted" = 1 WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'childWithOnDeleteSetNulls')); @@ -158,7 +158,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); UPDATE "sqlitedata_icloud_metadata" SET "_isDeleted" = 1 WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'modelAs')); @@ -187,7 +187,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); UPDATE "sqlitedata_icloud_metadata" SET "_isDeleted" = 1 WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'modelBs')); @@ -216,7 +216,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); UPDATE "sqlitedata_icloud_metadata" SET "_isDeleted" = 1 WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'modelCs')); @@ -245,7 +245,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); UPDATE "sqlitedata_icloud_metadata" SET "_isDeleted" = 1 WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'parents')); @@ -274,7 +274,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); UPDATE "sqlitedata_icloud_metadata" SET "_isDeleted" = 1 WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'reminderTags')); @@ -303,7 +303,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); UPDATE "sqlitedata_icloud_metadata" SET "_isDeleted" = 1 WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'remindersListAssets')); @@ -332,7 +332,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); UPDATE "sqlitedata_icloud_metadata" SET "_isDeleted" = 1 WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'remindersListPrivates')); @@ -361,7 +361,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); UPDATE "sqlitedata_icloud_metadata" SET "_isDeleted" = 1 WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'remindersLists')); @@ -390,7 +390,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); UPDATE "sqlitedata_icloud_metadata" SET "_isDeleted" = 1 WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'reminders')); @@ -419,7 +419,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); UPDATE "sqlitedata_icloud_metadata" SET "_isDeleted" = 1 WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."title") AND ("sqlitedata_icloud_metadata"."recordType" = 'tags')); @@ -440,7 +440,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'childWithOnDeleteSetDefaults', "new"."parentID", 'parents' @@ -463,7 +463,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'childWithOnDeleteSetNulls', "new"."parentID", 'parents' @@ -486,7 +486,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelAs', NULL, NULL @@ -509,7 +509,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelBs', "new"."modelAID", 'modelAs' @@ -532,7 +532,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelCs', "new"."modelBID", 'modelBs' @@ -555,7 +555,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'parents', NULL, NULL @@ -578,7 +578,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'reminderTags', NULL, NULL @@ -601,7 +601,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'reminders', "new"."remindersListID", 'remindersLists' @@ -624,7 +624,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersListAssets', "new"."remindersListID", 'remindersLists' @@ -647,7 +647,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersListPrivates', "new"."remindersListID", 'remindersLists' @@ -670,7 +670,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersLists', NULL, NULL @@ -693,7 +693,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."title", 'tags', NULL, NULL @@ -716,7 +716,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'childWithOnDeleteSetDefaults', "new"."parentID", 'parents' @@ -739,7 +739,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'childWithOnDeleteSetNulls', "new"."parentID", 'parents' @@ -762,7 +762,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelAs', NULL, NULL @@ -785,7 +785,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelBs', "new"."modelAID", 'modelAs' @@ -808,7 +808,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelCs', "new"."modelBID", 'modelBs' @@ -831,7 +831,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'parents', NULL, NULL @@ -854,7 +854,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'reminderTags', NULL, NULL @@ -877,7 +877,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'reminders', "new"."remindersListID", 'remindersLists' @@ -900,7 +900,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersListAssets', "new"."remindersListID", 'remindersLists' @@ -923,7 +923,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersListPrivates', "new"."remindersListID", 'remindersLists' @@ -946,7 +946,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersLists', NULL, NULL @@ -969,7 +969,7 @@ extension BaseCloudKitTests { ) SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') FROM "rootShares" - WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + WHERE ((NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) AND ("rootShares"."parentRecordName" IS NULL)) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."title", 'tags', NULL, NULL From 4f467d50f37c7fd7fe59121c1835a765bb6c7de4 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 17 Aug 2025 09:05:54 -0500 Subject: [PATCH 4/8] wip --- Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift | 4 ++++ Tests/SharingGRDBTests/Internal/Schema.swift | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift index 4f296c28..d42f4125 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift @@ -766,6 +766,10 @@ extension BaseCloudKitTests { } try await syncEngine.processPendingRecordZoneChanges(scope: .private) + try await userDatabase.userWrite { db in + #expect(try RemindersList.all.fetchCount(db) == 0) + } + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( diff --git a/Tests/SharingGRDBTests/Internal/Schema.swift b/Tests/SharingGRDBTests/Internal/Schema.swift index b499eced..093c7aa9 100644 --- a/Tests/SharingGRDBTests/Internal/Schema.swift +++ b/Tests/SharingGRDBTests/Internal/Schema.swift @@ -75,9 +75,6 @@ func database(containerIdentifier: String) throws -> DatabasePool { var configuration = Configuration() configuration.prepareDatabase { db in try db.attachMetadatabase(containerIdentifier: containerIdentifier) - db.trace { - print($0.expandedDescription) - } } let url = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).sqlite") let database = try DatabasePool(path: url.path(), configuration: configuration) From 44aca1cfa852f4b8f3fd22ecd4de8eaf2a619459 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 17 Aug 2025 09:11:18 -0500 Subject: [PATCH 5/8] wip --- .../CloudKit/CloudKitSharing.swift | 473 +++++++++--------- 1 file changed, 242 insertions(+), 231 deletions(-) diff --git a/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift b/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift index 9d91c8ca..9a3c0f58 100644 --- a/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift +++ b/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift @@ -1,271 +1,282 @@ #if canImport(CloudKit) -import CloudKit -import Dependencies -import SwiftUI + import CloudKit + import Dependencies + import SwiftUI -#if canImport(UIKit) - import UIKit -#endif + #if canImport(UIKit) + import UIKit + #endif -@available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) -public struct SharedRecord: Hashable, Identifiable, Sendable { - let container: any CloudContainer - public let share: CKShare + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + public struct SharedRecord: Hashable, Identifiable, Sendable { + let container: any CloudContainer + public let share: CKShare - public var id: CKRecord.ID { share.recordID } + public var id: CKRecord.ID { share.recordID } - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.container === rhs.container && lhs.share == rhs.share - } + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.container === rhs.container && lhs.share == rhs.share + } - public func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(container)) - hasher.combine(share) + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(container)) + hasher.combine(share) + } } -} -@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -extension SyncEngine { - private struct SharingError: LocalizedError { - enum Reason { - case recordMetadataNotFound - case recordNotRoot([ForeignKey]) - case recordTableNotSynchronized - case recordTablePrivate - case shareNotFound - } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + extension SyncEngine { + private struct SharingError: LocalizedError { + enum Reason { + case recordMetadataNotFound + case recordNotRoot([ForeignKey]) + case recordTableNotSynchronized + case recordTablePrivate + case shareNotFound + } - let recordTableName: String - let recordPrimaryKey: String - let reason: Reason - let debugDescription: String + let recordTableName: String + let recordPrimaryKey: String + let reason: Reason + let debugDescription: String - var errorDescription: String? { - "An error occured when editing sharing." + var errorDescription: String? { + "An error occured when editing sharing." + } } - } - public func share( - record: T, - configure: @Sendable (CKShare) -> Void - ) async throws -> SharedRecord - where T.TableColumns.PrimaryKey.QueryOutput: IdentifierStringConvertible { - guard tablesByName[T.tableName] != nil - else { - throw SharingError( - recordTableName: T.tableName, - recordPrimaryKey: record.primaryKey.rawIdentifier, - reason: .recordTableNotSynchronized, - debugDescription: """ - Table is not shareable: table type not passed to 'tables' parameter of 'SyncEngine.init'. - """ - ) - } - if let foreignKeys = foreignKeysByTableName[T.tableName], !foreignKeys.isEmpty { - throw SharingError( - recordTableName: T.tableName, - recordPrimaryKey: record.primaryKey.rawIdentifier, - reason: .recordNotRoot(foreignKeys), - debugDescription: """ - Only root records are shareable, but parent record(s) detected via foreign key(s). - """ - ) - } - guard !privateTables.contains(where: { T.self == $0 }) - else { - throw SharingError( - recordTableName: T.tableName, - recordPrimaryKey: record.primaryKey.rawIdentifier, - reason: .recordTablePrivate, - debugDescription: """ - Private tables are not shareable: table type passed to 'privateTables' parameter of \ - 'SyncEngine.init'. - """ - ) - } - let recordName = record.recordName - let metadata = - try await metadatabase.read { db in - try SyncMetadata - .where { $0.recordName.eq(recordName) } - .fetchOne(db) - } ?? nil - guard let metadata - else { - throw SharingError( - recordTableName: T.tableName, - recordPrimaryKey: record.primaryKey.rawIdentifier, - reason: .recordMetadataNotFound, - debugDescription: """ - No sync metadata found for record. Has the record been saved to the database? - """ - ) - } + public func share( + record: T, + configure: @Sendable (CKShare) -> Void + ) async throws -> SharedRecord + where T.TableColumns.PrimaryKey.QueryOutput: IdentifierStringConvertible { + guard tablesByName[T.tableName] != nil + else { + throw SharingError( + recordTableName: T.tableName, + recordPrimaryKey: record.primaryKey.rawIdentifier, + reason: .recordTableNotSynchronized, + debugDescription: """ + Table is not shareable: table type not passed to 'tables' parameter of 'SyncEngine.init'. + """ + ) + } + if let foreignKeys = foreignKeysByTableName[T.tableName], !foreignKeys.isEmpty { + throw SharingError( + recordTableName: T.tableName, + recordPrimaryKey: record.primaryKey.rawIdentifier, + reason: .recordNotRoot(foreignKeys), + debugDescription: """ + Only root records are shareable, but parent record(s) detected via foreign key(s). + """ + ) + } + guard !privateTables.contains(where: { T.self == $0 }) + else { + throw SharingError( + recordTableName: T.tableName, + recordPrimaryKey: record.primaryKey.rawIdentifier, + reason: .recordTablePrivate, + debugDescription: """ + Private tables are not shareable: table type passed to 'privateTables' parameter of \ + 'SyncEngine.init'. + """ + ) + } + let recordName = record.recordName + let metadata = + try await metadatabase.read { db in + try SyncMetadata + .where { $0.recordName.eq(recordName) } + .fetchOne(db) + } ?? nil + guard let metadata + else { + throw SharingError( + recordTableName: T.tableName, + recordPrimaryKey: record.primaryKey.rawIdentifier, + reason: .recordMetadataNotFound, + debugDescription: """ + No sync metadata found for record. Has the record been saved to the database? + """ + ) + } - let rootRecord = - metadata.lastKnownServerRecord - ?? CKRecord( - recordType: metadata.recordType, - recordID: CKRecord.ID(recordName: metadata.recordName, zoneID: defaultZone.zoneID) - ) + let rootRecord = + metadata.lastKnownServerRecord + ?? CKRecord( + recordType: metadata.recordType, + recordID: CKRecord.ID(recordName: metadata.recordName, zoneID: defaultZone.zoneID) + ) - var existingShare: CKShare? { - get async throws { - guard let shareRecordID = rootRecord.share?.recordID - else { return nil } - do { - return try await container.database(for: rootRecord.recordID) - .record(for: shareRecordID) as? CKShare - } catch let error as CKError where error.code == .unknownItem { - reportIssue("This would have been a problem before") - return nil + var existingShare: CKShare? { + get async throws { + guard let shareRecordID = rootRecord.share?.recordID + else { return nil } + do { + return try await container.database(for: rootRecord.recordID) + .record(for: shareRecordID) as? CKShare + } catch let error as CKError where error.code == .unknownItem { + reportIssue("This would have been a problem before") + return nil + } } } - } - let sharedRecord = try await existingShare ?? CKShare( - rootRecord: rootRecord, - shareID: CKRecord.ID( - recordName: "share-\(recordName)", - zoneID: rootRecord.recordID.zoneID + let sharedRecord = + try await existingShare + ?? CKShare( + rootRecord: rootRecord, + shareID: CKRecord.ID( + recordName: "share-\(recordName)", + zoneID: rootRecord.recordID.zoneID + ) + ) + + configure(sharedRecord) + // TODO: We are getting an "client oplock error updating record" error in the logs when + // creating new shares / editing existing shares. + _ = try await container.privateCloudDatabase.modifyRecords( + saving: [sharedRecord, rootRecord], + deleting: [] ) - ) + try await userDatabase.write { db in + try SyncMetadata + .where { $0.recordName.eq(recordName) } + .update { $0.share = sharedRecord } + .execute(db) + } - configure(sharedRecord) - // TODO: We are getting an "client oplock error updating record" error in the logs when - // creating new shares / editing existing shares. - _ = try await container.privateCloudDatabase.modifyRecords( - saving: [sharedRecord, rootRecord], - deleting: [] - ) - try await userDatabase.write { db in - try SyncMetadata - .where { $0.recordName.eq(recordName) } - .update { $0.share = sharedRecord } - .execute(db) + return SharedRecord(container: container, share: sharedRecord) } - return SharedRecord(container: container, share: sharedRecord) - } + public func unshare(record: T) async throws + where T.TableColumns.PrimaryKey.QueryOutput: IdentifierStringConvertible { + let share = try await userDatabase.read { [recordName = record.recordName] db in + try SyncMetadata + .where { $0.recordName.eq(recordName) } + .select(\.share) + .fetchOne(db) + ?? nil + } + guard let share + else { + throw SharingError( + recordTableName: T.tableName, + recordPrimaryKey: record.primaryKey.rawIdentifier, + reason: .shareNotFound, + debugDescription: "No share found associated with record." + ) + } - public func unshare(record: T) async throws - where T.TableColumns.PrimaryKey.QueryOutput: IdentifierStringConvertible - { - let metadata = try await userDatabase.read { [recordName = record.recordName] db in - try SyncMetadata - .where { $0.recordName.eq(recordName) } - .fetchOne(db) - } - guard let share = metadata?.share - else { - throw SharingError( - recordTableName: T.tableName, - recordPrimaryKey: record.primaryKey.rawIdentifier, - reason: .shareNotFound, - debugDescription: "No share found associated with record." + let result = try await syncEngines.private?.database.modifyRecords( + saving: [], + deleting: [share.recordID] ) + try result?.deleteResults.values.forEach { _ = try $0.get() } } - let result = try await syncEngines.private?.database.modifyRecords( - saving: [], - deleting: [share.recordID] - ) - try result?.deleteResults.values.forEach { _ = try $0.get() } - } - - public func acceptShare(metadata: CKShare.Metadata) async throws { - try await acceptShare(metadata: ShareMetadata(rawValue: metadata)) + public func acceptShare(metadata: CKShare.Metadata) async throws { + try await acceptShare(metadata: ShareMetadata(rawValue: metadata)) + } } -} -#if canImport(UIKit) && !os(watchOS) - @available(iOS 17, macOS 14, tvOS 17, *) - public struct CloudSharingView: UIViewControllerRepresentable { - let sharedRecord: SharedRecord - let availablePermissions: UICloudSharingController.PermissionOptions - let didFinish: (Result) -> Void - let didStopSharing: () -> Void - public init(sharedRecord: SharedRecord, availablePermissions: UICloudSharingController.PermissionOptions = []) { - self.init(sharedRecord: sharedRecord, availablePermissions: availablePermissions, didFinish: { _ in }, didStopSharing: {}) - } - public init( - sharedRecord: SharedRecord, - availablePermissions: UICloudSharingController.PermissionOptions = [], - didFinish: @escaping (Result) -> Void, - didStopSharing: @escaping () -> Void - ) { - self.sharedRecord = sharedRecord - self.didFinish = didFinish - self.didStopSharing = didStopSharing - self.availablePermissions = availablePermissions - } + #if canImport(UIKit) && !os(watchOS) + @available(iOS 17, macOS 14, tvOS 17, *) + public struct CloudSharingView: UIViewControllerRepresentable { + let sharedRecord: SharedRecord + let availablePermissions: UICloudSharingController.PermissionOptions + let didFinish: (Result) -> Void + let didStopSharing: () -> Void + public init( + sharedRecord: SharedRecord, + availablePermissions: UICloudSharingController.PermissionOptions = [] + ) { + self.init( + sharedRecord: sharedRecord, + availablePermissions: availablePermissions, + didFinish: { _ in }, + didStopSharing: {} + ) + } + public init( + sharedRecord: SharedRecord, + availablePermissions: UICloudSharingController.PermissionOptions = [], + didFinish: @escaping (Result) -> Void, + didStopSharing: @escaping () -> Void + ) { + self.sharedRecord = sharedRecord + self.didFinish = didFinish + self.didStopSharing = didStopSharing + self.availablePermissions = availablePermissions + } - public func makeCoordinator() -> CloudSharingDelegate { - CloudSharingDelegate( - share: sharedRecord.share, - didFinish: didFinish, - didStopSharing: didStopSharing - ) - } + public func makeCoordinator() -> CloudSharingDelegate { + CloudSharingDelegate( + share: sharedRecord.share, + didFinish: didFinish, + didStopSharing: didStopSharing + ) + } - public func makeUIViewController(context: Context) -> UICloudSharingController { - let controller = UICloudSharingController( - share: sharedRecord.share, - container: sharedRecord.container.rawValue - ) - controller.delegate = context.coordinator - controller.availablePermissions = availablePermissions - return controller - } + public func makeUIViewController(context: Context) -> UICloudSharingController { + let controller = UICloudSharingController( + share: sharedRecord.share, + container: sharedRecord.container.rawValue + ) + controller.delegate = context.coordinator + controller.availablePermissions = availablePermissions + return controller + } - public func updateUIViewController( - _ uiViewController: UICloudSharingController, - context: Context - ) { + public func updateUIViewController( + _ uiViewController: UICloudSharingController, + context: Context + ) { + } } - } - @available(iOS 17, macOS 14, tvOS 17, *) - public final class CloudSharingDelegate: NSObject, UICloudSharingControllerDelegate { - let share: CKShare - let didFinish: (Result) -> Void - let didStopSharing: () -> Void - init( - share: CKShare, - didFinish: @escaping (Result) -> Void, - didStopSharing: @escaping () -> Void - ) { - self.share = share - self.didFinish = didFinish - self.didStopSharing = didStopSharing - } + @available(iOS 17, macOS 14, tvOS 17, *) + public final class CloudSharingDelegate: NSObject, UICloudSharingControllerDelegate { + let share: CKShare + let didFinish: (Result) -> Void + let didStopSharing: () -> Void + init( + share: CKShare, + didFinish: @escaping (Result) -> Void, + didStopSharing: @escaping () -> Void + ) { + self.share = share + self.didFinish = didFinish + self.didStopSharing = didStopSharing + } - public func itemThumbnailData(for csc: UICloudSharingController) -> Data? { - share[CKShare.SystemFieldKey.thumbnailImageData] as? Data - } + public func itemThumbnailData(for csc: UICloudSharingController) -> Data? { + share[CKShare.SystemFieldKey.thumbnailImageData] as? Data + } - public func itemTitle(for csc: UICloudSharingController) -> String? { - share[CKShare.SystemFieldKey.title] as? String - } + public func itemTitle(for csc: UICloudSharingController) -> String? { + share[CKShare.SystemFieldKey.title] as? String + } - public func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) { - didFinish(.success(())) - } + public func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) { + didFinish(.success(())) + } - public func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) { - @Dependency(\.defaultSyncEngine) var syncEngine - withErrorReporting { - try syncEngine.deleteShare(recordID: share.recordID) + public func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) { + @Dependency(\.defaultSyncEngine) var syncEngine + withErrorReporting { + try syncEngine.deleteShare(recordID: share.recordID) + } + didStopSharing() } - didStopSharing() - } - public func cloudSharingController( - _ csc: UICloudSharingController, - failedToSaveShareWithError error: any Error - ) { - didFinish(.failure(error)) + public func cloudSharingController( + _ csc: UICloudSharingController, + failedToSaveShareWithError error: any Error + ) { + didFinish(.failure(error)) + } } - } -#endif + #endif #endif From bb3f597b24faeae117381035ac2be70c221f5474 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 18 Aug 2025 12:30:10 -0500 Subject: [PATCH 6/8] dont emit error when unsharing unshared record. --- .../CloudKit/CloudKitSharing.swift | 13 +- .../SharingGRDBCore/CloudKit/SyncEngine.swift | 4 +- .../CloudKitTests/SharingTests.swift | 148 +++++++++++++++++- 3 files changed, 149 insertions(+), 16 deletions(-) diff --git a/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift b/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift index 9a3c0f58..50dd1928 100644 --- a/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift +++ b/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift @@ -32,7 +32,6 @@ case recordNotRoot([ForeignKey]) case recordTableNotSynchronized case recordTablePrivate - case shareNotFound } let recordTableName: String @@ -41,7 +40,7 @@ let debugDescription: String var errorDescription: String? { - "An error occured when editing sharing." + "The record could not be shared." } } @@ -161,12 +160,10 @@ } guard let share else { - throw SharingError( - recordTableName: T.tableName, - recordPrimaryKey: record.primaryKey.rawIdentifier, - reason: .shareNotFound, - debugDescription: "No share found associated with record." - ) + reportIssue(""" + No share found associated with record. + """) + return } let result = try await syncEngines.private?.database.modifyRecords( diff --git a/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift b/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift index 92072d43..b0d8ea73 100644 --- a/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift +++ b/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift @@ -1006,8 +1006,8 @@ } open(table) } else if recordType == CKRecord.SystemType.share { - withErrorReporting { - for recordID in recordIDs { + for recordID in recordIDs { + withErrorReporting { try deleteShare(recordID: recordID) } } diff --git a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift index d42f4125..470be16e 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift @@ -511,6 +511,25 @@ extension BaseCloudKitTests { } } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func unshareNonSharedRecord() async throws { + let remindersList = RemindersList(id: 1, title: "Personal") + try await userDatabase.userWrite { db in + try db.seed { + remindersList + } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await withKnownIssue { + try await syncEngine.unshare(record: remindersList) + } matching: { issue in + issue.description == """ + No share found associated with record. + """ + } + } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func shareUnshareShareAgain() async throws { let remindersList = RemindersList(id: 1, title: "Personal") @@ -767,7 +786,7 @@ extension BaseCloudKitTests { try await syncEngine.processPendingRecordZoneChanges(scope: .private) try await userDatabase.userWrite { db in - #expect(try RemindersList.all.fetchCount(db) == 0) + try #expect(RemindersList.all.fetchCount(db) == 0) } assertInlineSnapshot(of: container, as: .customDump) { @@ -787,7 +806,7 @@ extension BaseCloudKitTests { } /// Deleting a root shared record that is not owned by current user should only delete - /// the share but not the actual records. + /// the CKShare but not the actual records. @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func deleteRootSharedRecord_CurrentUserNotOwner() async throws { let externalZone = CKRecordZone( @@ -836,6 +855,14 @@ extension BaseCloudKitTests { try await syncEngine.processPendingRecordZoneChanges(scope: .shared) + try await userDatabase.read { db in + let share = try SyncMetadata + .where { $0.recordName.eq(remindersListRecord.recordID.recordName) } + .select(\.share) + .fetchOne(db) + #expect(share == .none) + } + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( @@ -871,7 +898,7 @@ extension BaseCloudKitTests { } } - /// Inserting record into shared record when user does not have permission. + /// Inserting record into shared record when user does not have permission should be rejected. @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func insertRecordInReadOnlyRemindersList() async throws { let externalZone = CKRecordZone( @@ -908,14 +935,45 @@ extension BaseCloudKitTests { ) ) - let error = await #expect(throws: DatabaseError.self) { - try await self.userDatabase.userWrite { db in + + try await self.userDatabase.userWrite { db in + let error = #expect(throws: DatabaseError.self) { try db.seed { Reminder(id: 1, title: "Get milk", remindersListID: 1) } } + #expect(error?.message == SyncEngine.writePermissionError) + try #expect(Reminder.all.fetchCount(db) == 0) + } + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner), + recordType: "cloudkit.share", + parent: nil, + share: nil + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), + recordType: "remindersLists", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), + id: 1, + title: "Personal" + ) + ] + ) + ) + """ } - #expect(error?.message == SyncEngine.writePermissionError) } /// Delete record in shared record when user does not have permission. @@ -971,6 +1029,44 @@ extension BaseCloudKitTests { #expect(error?.message == SyncEngine.writePermissionError) try #expect(Reminder.count().fetchOne(db) == 1) } + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner), + recordType: "cloudkit.share", + parent: nil, + share: nil + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:reminders/external.zone/external.owner), + recordType: "reminders", + parent: CKReference(recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner)), + share: nil, + id: 1, + remindersListID: 1, + title: "Get milk" + ), + [2]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), + recordType: "remindersLists", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), + id: 1, + title: "Personal" + ) + ] + ) + ) + """ + } } /// Editing record in shared record when user does not have permission. @@ -1016,6 +1112,7 @@ extension BaseCloudKitTests { reminderRecord.setValue(1, forKey: "id", at: now) reminderRecord.setValue("Get milk", forKey: "title", at: now) reminderRecord.setValue(1, forKey: "remindersListID", at: now) + reminderRecord.setValue(false, forKey: "isCompleted", at: now) reminderRecord.parent = CKRecord.Reference(record: remindersListRecord, action: .none) try await syncEngine.modifyRecords(scope: .shared, saving: [reminderRecord]).notify() @@ -1026,6 +1123,45 @@ extension BaseCloudKitTests { #expect(error?.message == SyncEngine.writePermissionError) try #expect(Reminder.where(\.isCompleted).fetchCount(db) == 0) } + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner), + recordType: "cloudkit.share", + parent: nil, + share: nil + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:reminders/external.zone/external.owner), + recordType: "reminders", + parent: CKReference(recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner)), + share: nil, + id: 1, + isCompleted: 0, + remindersListID: 1, + title: "Get milk" + ), + [2]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), + recordType: "remindersLists", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), + id: 1, + title: "Personal" + ) + ] + ) + ) + """ + } } } } From b23ab736fb38b5cd67997f1c1dc58a7ccdc38cfb Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 18 Aug 2025 12:30:36 -0500 Subject: [PATCH 7/8] wip --- Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift index 470be16e..b2179eaf 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift @@ -525,7 +525,7 @@ extension BaseCloudKitTests { try await syncEngine.unshare(record: remindersList) } matching: { issue in issue.description == """ - No share found associated with record. + Issue recorded: No share found associated with record. """ } } From b626c552fdaa4bb15d7c31f79fefaf21368c2835 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 18 Aug 2025 12:45:46 -0700 Subject: [PATCH 8/8] fix --- Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift index b2179eaf..58c7e2a1 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift @@ -27,7 +27,7 @@ extension BaseCloudKitTests { } assertInlineSnapshot(of: error?.localizedDescription, as: .customDump) { """ - "An error occured when editing sharing." + "The record could not be shared." """ } assertInlineSnapshot(of: error, as: .customDump) { @@ -66,7 +66,7 @@ extension BaseCloudKitTests { as: .customDump ) { """ - "An error occured when editing sharing." + "The record could not be shared." """ } assertInlineSnapshot(of: error, as: .customDump) { @@ -94,7 +94,7 @@ extension BaseCloudKitTests { as: .customDump ) { """ - "An error occured when editing sharing." + "The record could not be shared." """ } assertInlineSnapshot(of: error, as: .customDump) { @@ -133,7 +133,7 @@ extension BaseCloudKitTests { as: .customDump ) { """ - "An error occured when editing sharing." + "The record could not be shared." """ } assertInlineSnapshot(of: error, as: .customDump) {