diff --git a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift index 2df3bb08..184ed8a0 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift @@ -61,6 +61,7 @@ "lastKnownServerRecord" BLOB, "_lastKnownServerRecordAllFields" BLOB, "share" BLOB, + "hasLastKnownServerRecord" INTEGER NOT NULL AS ("lastKnownServerRecord" IS NOT NULL), "isShared" INTEGER NOT NULL AS ("share" IS NOT NULL), "userModificationDate" TEXT NOT NULL DEFAULT (\($datetime())), "_isDeleted" INTEGER NOT NULL DEFAULT 0, @@ -85,6 +86,13 @@ """ ) .execute(db) + try #sql( + """ + CREATE INDEX IF NOT EXISTS "\(raw: .sqliteDataCloudKitSchemaName)_metadata_hasLastKnownServerRecord" + ON "\(raw: .sqliteDataCloudKitSchemaName)_metadata"("hasLastKnownServerRecord") + """ + ) + .execute(db) try #sql( """ CREATE TABLE "\(raw: .sqliteDataCloudKitSchemaName)_recordTypes" ( diff --git a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift index 82d4819d..65ed1899 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift @@ -205,6 +205,11 @@ package final class MockSyncEngineState: CKSyncEngineStateProtocol, CustomDumpRe _pendingDatabaseChanges.withValue { Array($0) } } + package func removePendingChanges() { + _pendingDatabaseChanges.withValue { $0.removeAll() } + _pendingRecordZoneChanges.withValue { $0.removeAll() } + } + package func add(pendingRecordZoneChanges: [CKSyncEngine.PendingRecordZoneChange]) { self._pendingRecordZoneChanges.withValue { $0.append(contentsOf: pendingRecordZoneChanges) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index b25a633b..2492b2b5 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -437,6 +437,23 @@ previousRecordTypeByTableName: [String: RecordType], currentRecordTypeByTableName: [String: RecordType] ) async throws { + try await enqueueLocallyPendingChanges() + try await userDatabase.write { db in + try PendingRecordZoneChange.delete().execute(db) + + let newTableNames = currentRecordTypeByTableName.keys.filter { tableName in + previousRecordTypeByTableName[tableName] == nil + } + + try $_isSynchronizingChanges.withValue(false) { + for tableName in newTableNames { + try self.uploadRecordsToCloudKit(tableName: tableName, db: db) + } + } + } + } + + private func enqueueLocallyPendingChanges() async throws { let pendingRecordZoneChanges = try await metadatabase.read { db in try PendingRecordZoneChange .select(\.pendingRecordZoneChange) @@ -454,18 +471,15 @@ $0.private?.state.add(pendingRecordZoneChanges: changesByIsPrivate[true] ?? []) $0.shared?.state.add(pendingRecordZoneChanges: changesByIsPrivate[false] ?? []) } + } + private func enqueueUnknownRecordsForCloudKit() async throws { try await userDatabase.write { db in - try PendingRecordZoneChange.delete().execute(db) - - let newTableNames = currentRecordTypeByTableName.keys.filter { tableName in - previousRecordTypeByTableName[tableName] == nil - } - try $_isSynchronizingChanges.withValue(false) { - for tableName in newTableNames { - try self.uploadRecordsToCloudKit(tableName: tableName, db: db) - } + try SyncMetadata + .where { !$0.hasLastKnownServerRecord } + .update { $0.recordPrimaryKey = $0.recordPrimaryKey } + .execute(db) } } } @@ -1011,6 +1025,9 @@ switch changeType { case .signIn: syncEngine.state.add(pendingDatabaseChanges: [.saveZone(defaultZone)]) + await withErrorReporting { + try await enqueueUnknownRecordsForCloudKit() + } case .signOut, .switchAccounts: withErrorReporting(.sqliteDataCloudKitFailure) { try deleteLocalData() @@ -1070,6 +1087,7 @@ func deleteRecords(in zoneID: CKRecordZone.ID, db: Database) throws { let recordTypes = Set( try SyncMetadata + .where(\.hasLastKnownServerRecord) .select(\.lastKnownServerRecord) .fetchAll(db) .compactMap { $0?.recordID.zoneID == zoneID ? $0?.recordType : nil } @@ -1089,6 +1107,7 @@ func uploadRecords(in zoneID: CKRecordZone.ID, db: Database) throws { let recordTypes = Set( try SyncMetadata + .where(\.hasLastKnownServerRecord) .select(\.lastKnownServerRecord) .fetchAll(db) .compactMap { $0?.recordID.zoneID == zoneID ? $0?.recordType : nil } diff --git a/Sources/SQLiteData/CloudKit/SyncMetadata.swift b/Sources/SQLiteData/CloudKit/SyncMetadata.swift index ce79f912..8fca929d 100644 --- a/Sources/SQLiteData/CloudKit/SyncMetadata.swift +++ b/Sources/SQLiteData/CloudKit/SyncMetadata.swift @@ -64,6 +64,14 @@ /// next batch of pending changes is processed. public var _isDeleted = false + @Column(generated: .virtual) + public let hasLastKnownServerRecord: Bool + + /// Determines if the record associated with this metadata is currently shared in CloudKit. + /// + /// This can only return `true` for root records. For example, the metadata associated with a + /// `RemindersList` can have `isShared == true`, but a `Reminder` associated with the list + /// will have `isShared == false`. @Column(generated: .virtual) public let isShared: Bool @@ -125,6 +133,7 @@ self.lastKnownServerRecord = lastKnownServerRecord self._lastKnownServerRecordAllFields = _lastKnownServerRecordAllFields self.share = share + self.hasLastKnownServerRecord = lastKnownServerRecord != nil self.isShared = share != nil self.userModificationDate = userModificationDate } diff --git a/Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift b/Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift index ba1e4bdc..1420b436 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift @@ -57,6 +57,20 @@ try await syncEngine.metadatabase.read { db in try #expect(SyncMetadata.count().fetchOne(db) == 3) } + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } await signIn() @@ -107,6 +121,451 @@ } } + // * Create reminders list + // * Soft log out + // * Create reminder in list + // * Sign in + // * Reminder is sync'd to CloudKit + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func signInUploadsLocalRecordsToCloudKit_SkipExistingCloudKitRecords() async throws { + try await userDatabase.userWrite { db in + try db.seed { + RemindersList(id: 1, title: "Personal") + } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + await softSignOut() + + try await userDatabase.userWrite { db in + try db.seed { + Reminder(id: 1, title: "Get milk", remindersListID: 1) + } + } + + assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) { + """ + ┌────────────────────────────────────────────────────────────────────┐ + │ SyncMetadata( │ + │ recordPrimaryKey: "1", │ + │ recordType: "remindersLists", │ + │ recordName: "1:remindersLists", │ + │ parentRecordPrimaryKey: nil, │ + │ parentRecordType: nil, │ + │ parentRecordName: nil, │ + │ lastKnownServerRecord: CKRecord( │ + │ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │ + │ recordType: "remindersLists", │ + │ parent: nil, │ + │ share: nil │ + │ ), │ + │ _lastKnownServerRecordAllFields: CKRecord( │ + │ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │ + │ recordType: "remindersLists", │ + │ parent: nil, │ + │ share: nil, │ + │ id: 1, │ + │ title: "Personal" │ + │ ), │ + │ share: nil, │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ + │ isShared: false, │ + │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ ) │ + ├────────────────────────────────────────────────────────────────────┤ + │ SyncMetadata( │ + │ recordPrimaryKey: "1", │ + │ recordType: "reminders", │ + │ recordName: "1:reminders", │ + │ parentRecordPrimaryKey: "1", │ + │ parentRecordType: "remindersLists", │ + │ parentRecordName: "1:remindersLists", │ + │ lastKnownServerRecord: nil, │ + │ _lastKnownServerRecordAllFields: nil, │ + │ share: nil, │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: false, │ + │ isShared: false, │ + │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ ) │ + └────────────────────────────────────────────────────────────────────┘ + """ + } + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + title: "Personal" + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + + await signIn() + try await syncEngine.processPendingDatabaseChanges(scope: .private) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) { + """ + ┌─────────────────────────────────────────────────────────────────────────────────────────┐ + │ SyncMetadata( │ + │ recordPrimaryKey: "1", │ + │ recordType: "remindersLists", │ + │ recordName: "1:remindersLists", │ + │ parentRecordPrimaryKey: nil, │ + │ parentRecordType: nil, │ + │ parentRecordName: nil, │ + │ lastKnownServerRecord: CKRecord( │ + │ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │ + │ recordType: "remindersLists", │ + │ parent: nil, │ + │ share: nil │ + │ ), │ + │ _lastKnownServerRecordAllFields: CKRecord( │ + │ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │ + │ recordType: "remindersLists", │ + │ parent: nil, │ + │ share: nil, │ + │ id: 1, │ + │ title: "Personal" │ + │ ), │ + │ share: nil, │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ + │ isShared: false, │ + │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ ) │ + ├─────────────────────────────────────────────────────────────────────────────────────────┤ + │ SyncMetadata( │ + │ recordPrimaryKey: "1", │ + │ recordType: "reminders", │ + │ recordName: "1:reminders", │ + │ parentRecordPrimaryKey: "1", │ + │ parentRecordType: "remindersLists", │ + │ parentRecordName: "1:remindersLists", │ + │ lastKnownServerRecord: CKRecord( │ + │ recordID: CKRecord.ID(1:reminders/zone/__defaultOwner__), │ + │ recordType: "reminders", │ + │ parent: CKReference(recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__)), │ + │ share: nil │ + │ ), │ + │ _lastKnownServerRecordAllFields: CKRecord( │ + │ recordID: CKRecord.ID(1:reminders/zone/__defaultOwner__), │ + │ recordType: "reminders", │ + │ parent: CKReference(recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__)), │ + │ share: nil, │ + │ id: 1, │ + │ isCompleted: 0, │ + │ remindersListID: 1, │ + │ title: "Get milk" │ + │ ), │ + │ share: nil, │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ + │ isShared: false, │ + │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ ) │ + └─────────────────────────────────────────────────────────────────────────────────────────┘ + """ + } + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:reminders/zone/__defaultOwner__), + recordType: "reminders", + parent: CKReference(recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__)), + share: nil, + id: 1, + isCompleted: 0, + remindersListID: 1, + title: "Get milk" + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + title: "Personal" + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + // * Join shared reminders list + // * Soft log out + // * Create reminder in list + // * Sign in + // * Reminder is sync'd to CloudKit with proper metadata + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func createSharedRecordWhileSoftLoggedOut() 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 syncEngine.modifyRecords(scope: .shared, saving: [share, remindersListRecord]) + let freshShare = try syncEngine.shared.database.record(for: share.recordID) as! CKShare + let freshRemindersListRecord = try syncEngine.shared.database.record(for: remindersListRecord.recordID) + + try await syncEngine + .acceptShare( + metadata: ShareMetadata( + containerIdentifier: container.containerIdentifier!, + hierarchicalRootRecordID: freshRemindersListRecord.recordID, + rootRecord: freshRemindersListRecord, + share: freshShare + ) + ) + + await softSignOut() + + try await userDatabase.userWrite { db in + try db.seed { + Reminder(id: 1, title: "Get milk", remindersListID: 1) + } + } + + assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) { + """ + ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ SyncMetadata( │ + │ recordPrimaryKey: "1", │ + │ recordType: "remindersLists", │ + │ recordName: "1:remindersLists", │ + │ parentRecordPrimaryKey: nil, │ + │ parentRecordType: nil, │ + │ parentRecordName: nil, │ + │ lastKnownServerRecord: 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)) │ + │ ), │ + │ _lastKnownServerRecordAllFields: 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" │ + │ ), │ + │ share: CKRecord( │ + │ recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner), │ + │ recordType: "cloudkit.share", │ + │ parent: nil, │ + │ share: nil │ + │ ), │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ + │ isShared: true, │ + │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ ) │ + ├─────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ SyncMetadata( │ + │ recordPrimaryKey: "1", │ + │ recordType: "reminders", │ + │ recordName: "1:reminders", │ + │ parentRecordPrimaryKey: "1", │ + │ parentRecordType: "remindersLists", │ + │ parentRecordName: "1:remindersLists", │ + │ lastKnownServerRecord: nil, │ + │ _lastKnownServerRecordAllFields: nil, │ + │ share: nil, │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: false, │ + │ isShared: false, │ + │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ ) │ + └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ + """ + } + 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" + ) + ] + ) + ) + """ + } + + await signIn() + try await syncEngine.processPendingDatabaseChanges(scope: .private) + try await syncEngine.processPendingRecordZoneChanges(scope: .shared) + + assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) { + """ + ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ SyncMetadata( │ + │ recordPrimaryKey: "1", │ + │ recordType: "remindersLists", │ + │ recordName: "1:remindersLists", │ + │ parentRecordPrimaryKey: nil, │ + │ parentRecordType: nil, │ + │ parentRecordName: nil, │ + │ lastKnownServerRecord: 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)) │ + │ ), │ + │ _lastKnownServerRecordAllFields: 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" │ + │ ), │ + │ share: CKRecord( │ + │ recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner), │ + │ recordType: "cloudkit.share", │ + │ parent: nil, │ + │ share: nil │ + │ ), │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ + │ isShared: true, │ + │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ ) │ + ├─────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ SyncMetadata( │ + │ recordPrimaryKey: "1", │ + │ recordType: "reminders", │ + │ recordName: "1:reminders", │ + │ parentRecordPrimaryKey: "1", │ + │ parentRecordType: "remindersLists", │ + │ parentRecordName: "1:remindersLists", │ + │ lastKnownServerRecord: 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 │ + │ ), │ + │ _lastKnownServerRecordAllFields: 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" │ + │ ), │ + │ share: nil, │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ + │ isShared: false, │ + │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ ) │ + └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ + """ + } + 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" + ) + ] + ) + ) + """ + } + } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test( .accountStatus(.noAccount), diff --git a/Tests/SQLiteDataTests/CloudKitTests/FetchRecordZoneChangesTests.swift b/Tests/SQLiteDataTests/CloudKitTests/FetchRecordZoneChangesTests.swift index 1e6beb65..7bd6a539 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/FetchRecordZoneChangesTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/FetchRecordZoneChangesTests.swift @@ -539,6 +539,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -618,6 +619,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -684,6 +686,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ diff --git a/Tests/SQLiteDataTests/CloudKitTests/MetadataTests.swift b/Tests/SQLiteDataTests/CloudKitTests/MetadataTests.swift index d16b2ba2..0a927d32 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/MetadataTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/MetadataTests.swift @@ -232,6 +232,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -261,6 +262,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -288,6 +290,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -316,6 +319,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -345,6 +349,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -372,6 +377,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -400,6 +406,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -429,6 +436,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -455,6 +463,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -481,6 +490,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -530,6 +540,7 @@ │ │ ), │ │ │ share: nil, │ │ │ _isDeleted: false, │ + │ │ hasLastKnownServerRecord: true, │ │ │ isShared: false, │ │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ │ ) │ @@ -557,6 +568,7 @@ │ │ ), │ │ │ share: nil, │ │ │ _isDeleted: false, │ + │ │ hasLastKnownServerRecord: true, │ │ │ isShared: false, │ │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ │ ) │ diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift index ed2856e5..12ce91a5 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift @@ -316,6 +316,7 @@ │ share: nil │ │ ), │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: true, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -626,14 +627,17 @@ zoneID: remindersListRecord.recordID.zoneID ) ) + _ = try syncEngine.modifyRecords(scope: .shared, saving: [share, remindersListRecord]) + let freshShare = try syncEngine.shared.database.record(for: share.recordID) as! CKShare + let freshRemindersListRecord = try syncEngine.shared.database.record(for: remindersListRecord.recordID) try await syncEngine .acceptShare( metadata: ShareMetadata( containerIdentifier: container.containerIdentifier!, - hierarchicalRootRecordID: remindersListRecord.recordID, - rootRecord: remindersListRecord, - share: share + hierarchicalRootRecordID: freshRemindersListRecord.recordID, + rootRecord: freshRemindersListRecord, + share: freshShare ) ) diff --git a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift index 24d6f633..8ed166d6 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift @@ -45,6 +45,7 @@ │ _lastKnownServerRecordAllFields: nil, │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: false, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -60,6 +61,7 @@ │ _lastKnownServerRecordAllFields: nil, │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: false, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -444,6 +446,7 @@ │ _lastKnownServerRecordAllFields: nil, │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: false, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -459,6 +462,7 @@ │ _lastKnownServerRecordAllFields: nil, │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: false, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -511,6 +515,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ @@ -540,6 +545,7 @@ │ ), │ │ share: nil, │ │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ │ isShared: false, │ │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ │ ) │ diff --git a/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift b/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift index 659acd77..953ef2b3 100644 --- a/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift +++ b/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift @@ -101,9 +101,17 @@ class BaseCloudKitTests: @unchecked Sendable { ) } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + func softSignOut() async { + container._accountStatus.withValue { $0 = .temporarilyUnavailable } + } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) func signIn() async { container._accountStatus.withValue { $0 = .available } + // NB: Emulates what CKSyncEngine does when signing in + syncEngine.private.state.removePendingChanges() + syncEngine.shared.state.removePendingChanges() await syncEngine.handleEvent( .accountChange(changeType: .signIn(currentUser: currentUserRecordID)), syncEngine: syncEngine.private