|
11 | 11 | @MainActor |
12 | 12 | final class ChangeSupersessionTests: BaseCloudKitTests, @unchecked Sendable { |
13 | 13 | @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) |
14 | | - @Test func deleteAndReinsertInSingleWrite() async throws { |
15 | | - try await userDatabase.userWrite { db in |
16 | | - try db.seed { |
17 | | - RemindersList(id: 1, title: "Personal") |
18 | | - } |
19 | | - } |
20 | | - try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
21 | | - |
| 14 | + @Test func insertThenDelete_deletes() async throws { |
22 | 15 | try await userDatabase.userWrite { db in |
| 16 | + try RemindersList.insert { RemindersList(id: 1, title: "Personal") }.execute(db) |
23 | 17 | try RemindersList.find(1).delete().execute(db) |
24 | | - try RemindersList.insert { RemindersList(id: 1, title: "Renamed") }.execute(db) |
25 | 18 | } |
26 | 19 |
|
27 | 20 | let pending = syncEngine.private.state.pendingRecordZoneChanges |
28 | | - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) |
| 21 | + #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) |
29 | 22 |
|
30 | 23 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
31 | 24 |
|
|
34 | 27 | MockCloudContainer( |
35 | 28 | privateCloudDatabase: MockCloudDatabase( |
36 | 29 | databaseScope: .private, |
37 | | - storage: [ |
38 | | - [0]: CKRecord( |
39 | | - recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), |
40 | | - recordType: "remindersLists", |
41 | | - parent: nil, |
42 | | - share: nil, |
43 | | - id: 1, |
44 | | - title: "Renamed" |
45 | | - ) |
46 | | - ] |
| 30 | + storage: [] |
47 | 31 | ), |
48 | 32 | sharedCloudDatabase: MockCloudDatabase( |
49 | 33 | databaseScope: .shared, |
|
55 | 39 | } |
56 | 40 |
|
57 | 41 | @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) |
58 | | - @Test func deleteAndReinsertInSeparateWrites() async throws { |
| 42 | + @Test func updateThenDelete_deletes() async throws { |
59 | 43 | try await userDatabase.userWrite { db in |
60 | | - try db.seed { |
61 | | - RemindersList(id: 1, title: "Personal") |
62 | | - } |
| 44 | + try db.seed { RemindersList(id: 1, title: "Original") } |
63 | 45 | } |
64 | 46 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
65 | 47 |
|
66 | 48 | try await userDatabase.userWrite { db in |
| 49 | + try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) |
67 | 50 | try RemindersList.find(1).delete().execute(db) |
68 | 51 | } |
69 | | - try await userDatabase.userWrite { db in |
70 | | - try RemindersList.insert { RemindersList(id: 1, title: "Restored") }.execute(db) |
71 | | - } |
72 | 52 |
|
73 | 53 | let pending = syncEngine.private.state.pendingRecordZoneChanges |
74 | | - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) |
| 54 | + #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) |
75 | 55 |
|
76 | 56 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
77 | 57 |
|
|
80 | 60 | MockCloudContainer( |
81 | 61 | privateCloudDatabase: MockCloudDatabase( |
82 | 62 | databaseScope: .private, |
83 | | - storage: [ |
84 | | - [0]: CKRecord( |
85 | | - recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), |
86 | | - recordType: "remindersLists", |
87 | | - parent: nil, |
88 | | - share: nil, |
89 | | - id: 1, |
90 | | - title: "Restored" |
91 | | - ) |
92 | | - ] |
| 63 | + storage: [] |
93 | 64 | ), |
94 | 65 | sharedCloudDatabase: MockCloudDatabase( |
95 | 66 | databaseScope: .shared, |
|
101 | 72 | } |
102 | 73 |
|
103 | 74 | @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) |
104 | | - @Test func deleteWithoutReinsert_stillDeletes() async throws { |
| 75 | + @Test func deleteAndReinsertInSingleWrite_saves() async throws { |
105 | 76 | try await userDatabase.userWrite { db in |
106 | 77 | try db.seed { |
107 | | - RemindersList(id: 1, title: "Personal") |
| 78 | + RemindersList(id: 1, title: "Original") |
108 | 79 | } |
109 | 80 | } |
110 | 81 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
111 | 82 |
|
112 | 83 | try await userDatabase.userWrite { db in |
113 | 84 | try RemindersList.find(1).delete().execute(db) |
| 85 | + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) |
114 | 86 | } |
115 | 87 |
|
116 | 88 | let pending = syncEngine.private.state.pendingRecordZoneChanges |
117 | | - #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) |
| 89 | + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) |
118 | 90 |
|
119 | 91 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
120 | 92 |
|
|
123 | 95 | MockCloudContainer( |
124 | 96 | privateCloudDatabase: MockCloudDatabase( |
125 | 97 | databaseScope: .private, |
126 | | - storage: [] |
| 98 | + storage: [ |
| 99 | + [0]: CKRecord( |
| 100 | + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), |
| 101 | + recordType: "remindersLists", |
| 102 | + parent: nil, |
| 103 | + share: nil, |
| 104 | + id: 1, |
| 105 | + title: "Reinserted" |
| 106 | + ) |
| 107 | + ] |
127 | 108 | ), |
128 | 109 | sharedCloudDatabase: MockCloudDatabase( |
129 | 110 | databaseScope: .shared, |
|
135 | 116 | } |
136 | 117 |
|
137 | 118 | @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) |
138 | | - @Test func deleteReinsertThenDeleteAgain_deletes() async throws { |
| 119 | + @Test func deleteAndReinsertInSeparateWrites_saves() async throws { |
139 | 120 | try await userDatabase.userWrite { db in |
140 | | - try db.seed { RemindersList(id: 1, title: "Original") } |
| 121 | + try db.seed { |
| 122 | + RemindersList(id: 1, title: "Original") |
| 123 | + } |
141 | 124 | } |
142 | 125 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
143 | 126 |
|
144 | 127 | try await userDatabase.userWrite { db in |
145 | 128 | try RemindersList.find(1).delete().execute(db) |
146 | | - try RemindersList.insert { RemindersList(id: 1, title: "Middle") }.execute(db) |
147 | | - try RemindersList.find(1).delete().execute(db) |
148 | 129 | } |
| 130 | + try await userDatabase.userWrite { db in |
| 131 | + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) |
| 132 | + } |
| 133 | + |
| 134 | + let pending = syncEngine.private.state.pendingRecordZoneChanges |
| 135 | + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) |
149 | 136 |
|
150 | 137 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
151 | 138 |
|
|
154 | 141 | MockCloudContainer( |
155 | 142 | privateCloudDatabase: MockCloudDatabase( |
156 | 143 | databaseScope: .private, |
157 | | - storage: [] |
| 144 | + storage: [ |
| 145 | + [0]: CKRecord( |
| 146 | + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), |
| 147 | + recordType: "remindersLists", |
| 148 | + parent: nil, |
| 149 | + share: nil, |
| 150 | + id: 1, |
| 151 | + title: "Reinserted" |
| 152 | + ) |
| 153 | + ] |
158 | 154 | ), |
159 | 155 | sharedCloudDatabase: MockCloudDatabase( |
160 | 156 | databaseScope: .shared, |
|
166 | 162 | } |
167 | 163 |
|
168 | 164 | @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) |
169 | | - @Test func balancedDeleteReinsertCycles_savesWithFinalValues() async throws { |
| 165 | + @Test func updateThenDeleteThenReinsert_saves() async throws { |
170 | 166 | try await userDatabase.userWrite { db in |
171 | 167 | try db.seed { RemindersList(id: 1, title: "Original") } |
172 | 168 | } |
173 | 169 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
174 | 170 |
|
175 | 171 | try await userDatabase.userWrite { db in |
| 172 | + try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) |
176 | 173 | try RemindersList.find(1).delete().execute(db) |
177 | | - try RemindersList.insert { RemindersList(id: 1, title: "Middle") }.execute(db) |
178 | | - try RemindersList.find(1).delete().execute(db) |
179 | | - try RemindersList.insert { RemindersList(id: 1, title: "Final") }.execute(db) |
| 174 | + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) |
180 | 175 | } |
181 | 176 |
|
| 177 | + let pending = syncEngine.private.state.pendingRecordZoneChanges |
| 178 | + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) |
| 179 | + |
182 | 180 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
183 | 181 |
|
184 | 182 | assertInlineSnapshot(of: container, as: .customDump) { |
|
193 | 191 | parent: nil, |
194 | 192 | share: nil, |
195 | 193 | id: 1, |
196 | | - title: "Final" |
| 194 | + title: "Reinserted" |
197 | 195 | ) |
198 | 196 | ] |
199 | 197 | ), |
|
207 | 205 | } |
208 | 206 |
|
209 | 207 | @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) |
210 | | - @Test func updateThenDelete_deletes() async throws { |
| 208 | + @Test func deleteReinsertThenDeleteAgain_deletes() async throws { |
211 | 209 | try await userDatabase.userWrite { db in |
212 | 210 | try db.seed { RemindersList(id: 1, title: "Original") } |
213 | 211 | } |
214 | 212 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
215 | 213 |
|
216 | | - try await userDatabase.userWrite { db in |
217 | | - try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) |
218 | | - } |
219 | 214 | try await userDatabase.userWrite { db in |
220 | 215 | try RemindersList.find(1).delete().execute(db) |
| 216 | + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) |
| 217 | + try RemindersList.find(1).delete().execute(db) |
221 | 218 | } |
222 | 219 |
|
| 220 | + let pending = syncEngine.private.state.pendingRecordZoneChanges |
| 221 | + #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) |
| 222 | + |
223 | 223 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
224 | 224 |
|
225 | 225 | assertInlineSnapshot(of: container, as: .customDump) { |
|
239 | 239 | } |
240 | 240 |
|
241 | 241 | @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) |
242 | | - @Test func updateThenDeleteThenReinsert_savesWithReinsertedValues() async throws { |
| 242 | + @Test(.printTimestamps) func twoDeleteReinsertCyclesInSameWrite_propagatesLatestValueAndTimestamp() |
| 243 | + async throws |
| 244 | + { |
243 | 245 | try await userDatabase.userWrite { db in |
244 | 246 | try db.seed { RemindersList(id: 1, title: "Original") } |
245 | 247 | } |
246 | 248 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
247 | 249 |
|
248 | | - try await userDatabase.userWrite { db in |
249 | | - try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) |
250 | | - } |
251 | | - try await userDatabase.userWrite { db in |
252 | | - try RemindersList.find(1).delete().execute(db) |
253 | | - try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) |
| 250 | + try await withDependencies { |
| 251 | + $0.currentTime.now = 1 |
| 252 | + } operation: { |
| 253 | + try await userDatabase.userWrite { db in |
| 254 | + try RemindersList.find(1).delete().execute(db) |
| 255 | + try RemindersList.insert { RemindersList(id: 1, title: "Middle") }.execute(db) |
| 256 | + try RemindersList.find(1).delete().execute(db) |
| 257 | + try RemindersList.insert { RemindersList(id: 1, title: "Final") }.execute(db) |
| 258 | + } |
| 259 | + let pending = syncEngine.private.state.pendingRecordZoneChanges |
| 260 | + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) |
| 261 | + try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
254 | 262 | } |
255 | 263 |
|
256 | | - try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
257 | | - |
258 | 264 | assertInlineSnapshot(of: container, as: .customDump) { |
259 | 265 | """ |
260 | 266 | MockCloudContainer( |
|
267 | 273 | parent: nil, |
268 | 274 | share: nil, |
269 | 275 | id: 1, |
270 | | - title: "Reinserted" |
| 276 | + id🗓️: 0, |
| 277 | + title: "Final", |
| 278 | + title🗓️: 1, |
| 279 | + 🗓️: 1 |
271 | 280 | ) |
272 | 281 | ] |
273 | 282 | ), |
|
280 | 289 | } |
281 | 290 | } |
282 | 291 |
|
283 | | - // A second delete+reinsert cycle should propagate the cycle-2 field values to CloudKit, |
284 | | - // not the stale cycle-1 values. Regression test for stale userModificationTime timestamps. |
285 | 292 | @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) |
286 | | - @Test func secondDeleteAndReinsertPropagatesCycle2Values() async throws { |
287 | | - // Seed and sync initial record. |
| 293 | + @Test(.printTimestamps) func twoDeleteReinsertCyclesInSeparateBatches_propagatesLatestValueAndTimestamp() |
| 294 | + async throws |
| 295 | + { |
288 | 296 | try await userDatabase.userWrite { db in |
289 | 297 | try db.seed { RemindersList(id: 1, title: "Original") } |
290 | 298 | } |
291 | 299 | try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
292 | 300 |
|
293 | | - // Cycle 1: delete + reinsert. |
294 | | - try await userDatabase.userWrite { db in |
295 | | - try RemindersList.find(1).delete().execute(db) |
296 | | - try RemindersList.insert { RemindersList(id: 1, title: "Cycle1") }.execute(db) |
| 301 | + try await withDependencies { |
| 302 | + $0.currentTime.now = 1 |
| 303 | + } operation: { |
| 304 | + try await userDatabase.userWrite { db in |
| 305 | + try RemindersList.find(1).delete().execute(db) |
| 306 | + try RemindersList.insert { RemindersList(id: 1, title: "Cycle1") }.execute(db) |
| 307 | + } |
| 308 | + let pending = syncEngine.private.state.pendingRecordZoneChanges |
| 309 | + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) |
| 310 | + try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
297 | 311 | } |
298 | | - try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
299 | 312 |
|
300 | | - // Cycle 2: delete + reinsert with new value. |
301 | | - try await userDatabase.userWrite { db in |
302 | | - try RemindersList.find(1).delete().execute(db) |
303 | | - try RemindersList.insert { RemindersList(id: 1, title: "Cycle2") }.execute(db) |
| 313 | + try await withDependencies { |
| 314 | + $0.currentTime.now = 2 |
| 315 | + } operation: { |
| 316 | + try await userDatabase.userWrite { db in |
| 317 | + try RemindersList.find(1).delete().execute(db) |
| 318 | + try RemindersList.insert { RemindersList(id: 1, title: "Cycle2") }.execute(db) |
| 319 | + } |
| 320 | + let pending = syncEngine.private.state.pendingRecordZoneChanges |
| 321 | + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) |
| 322 | + try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
304 | 323 | } |
305 | | - try await syncEngine.processPendingRecordZoneChanges(scope: .private) |
306 | 324 |
|
307 | 325 | assertInlineSnapshot(of: container, as: .customDump) { |
308 | 326 | """ |
|
316 | 334 | parent: nil, |
317 | 335 | share: nil, |
318 | 336 | id: 1, |
319 | | - title: "Cycle2" |
| 337 | + id🗓️: 0, |
| 338 | + title: "Cycle2", |
| 339 | + title🗓️: 2, |
| 340 | + 🗓️: 2 |
320 | 341 | ) |
321 | 342 | ] |
322 | 343 | ), |
|
0 commit comments