Tests/Unit/ItemStoreEdgeCaseTests.swift (9519B)
1import Foundation
2import CoreData
3import Testing
4
5#if os(macOS)
6@testable import Listless
7#else
8@testable import Listless_iOS
9#endif
10
11@Suite("ItemStore Edge Cases", .serialized)
12@MainActor
13struct ItemStoreEdgeCaseTests {
14
15
16
17 @Test("Task with empty title")
18 func itemWithEmptyTitle() async throws {
19 let store = makeTestStore()
20
21 let item = try store.createItem(title: "")
22
23 let items = try store.fetchItems()
24 #expect(items.first?.title == "")
25 }
26
27 @Test("Task with very long title")
28 func itemWithVeryLongTitle() async throws {
29 let store = makeTestStore()
30 let longTitle = String(repeating: "A", count: 10_000)
31
32 let item = try store.createItem(title: longTitle)
33
34 let items = try store.fetchItems()
35 #expect(items.first?.title.count == 10_000)
36 }
37
38 @Test("Task with special characters")
39 func itemWithSpecialCharacters() async throws {
40 let store = makeTestStore()
41 let specialTitle = "Test 🎉 with émojis & spëcial çharacters! @#$%^&*()"
42
43 let item = try store.createItem(title: specialTitle)
44
45 let items = try store.fetchItems()
46 #expect(items.first?.title == specialTitle)
47 }
48
49 @Test("Task with newlines and tabs")
50 func itemWithNewlinesAndTabs() async throws {
51 let store = makeTestStore()
52 let multilineTitle = "Line 1\nLine 2\tTabbed"
53
54 let item = try store.createItem(title: multilineTitle)
55
56 let items = try store.fetchItems()
57 #expect(items.first?.title == multilineTitle)
58 }
59
60
61
62 @Test("Create many items")
63 func createManyItems() async throws {
64 let store = makeTestStore()
65 let count = 100
66
67 for i in 0..<count {
68 _ = try store.createItem(title: "Task \(i)")
69 }
70
71 let items = try store.fetchItems()
72 #expect(items.count == count)
73 }
74
75 @Test("Delete all items from large set")
76 func deleteAllItemsFromLargeSet() async throws {
77 let store = makeTestStore()
78 var itemIDs: [UUID] = []
79
80 for i in 0..<50 {
81 let item = try store.createItem(title: "Task \(i)")
82 itemIDs.append(item.id)
83 }
84
85 for id in itemIDs {
86 try store.delete(itemID: id)
87 }
88
89 let items = try store.fetchItems()
90 #expect(items.isEmpty)
91 }
92
93
94
95 @Test("Create item after completing all items")
96 func createItemAfterCompletingAllItems() async throws {
97 let (store, itemIDs) = try makeTestStoreWithItems(count: 3)
98
99 for id in itemIDs {
100 try store.complete(itemID: id)
101 }
102
103 let newItem = try store.createItem(title: "New item")
104
105 let items = try store.fetchItems()
106 let activeItems = items.filter { !$0.isCompleted }
107 #expect(activeItems.count == 1)
108 #expect(activeItems[0].id == newItem.id)
109 }
110
111 @Test("Rapid updates to same item")
112 func rapidUpdatesToSameItem() async throws {
113 let store = makeTestStore()
114 let item = try store.createItem(title: "Original")
115
116 for i in 0..<10 {
117 try store.update(itemID: item.id, title: "Update \(i)")
118 }
119
120 let items = try store.fetchItems()
121 #expect(items.first?.title == "Update 9")
122 }
123
124
125
126 @Test("Store with only completed items")
127 func storeWithOnlyCompletedItems() async throws {
128 let (store, itemIDs) = try makeTestStoreWithItems(count: 5)
129
130 for id in itemIDs {
131 try store.complete(itemID: id)
132 }
133
134 let items = try store.fetchItems()
135 #expect(items.allSatisfy { $0.isCompleted })
136 #expect(items.count == 5)
137 }
138
139 @Test("SortOrder after completing all items")
140 func sortOrderAfterCompletingAllItems() async throws {
141 let (store, itemIDs) = try makeTestStoreWithItems(count: 3)
142
143 for id in itemIDs {
144 try store.complete(itemID: id)
145 }
146
147 let newItem1 = try store.createItem(title: "New 1")
148 let newItem2 = try store.createItem(title: "New 2")
149
150 let activeItems = try store.fetchItems().filter { !$0.isCompleted }
151 #expect(activeItems[0].id == newItem1.id)
152 #expect(activeItems[1].id == newItem2.id)
153 #expect(activeItems[1].sortOrder > activeItems[0].sortOrder)
154 }
155
156 @Test("Uncompleting item moves it back to active")
157 func uncompletingItemMovesItBackToActive() async throws {
158 let (store, itemIDs) = try makeTestStoreWithItems(count: 3)
159 try store.complete(itemID: itemIDs[1])
160
161 try store.uncomplete(itemID: itemIDs[1])
162
163 let items = try store.fetchItems()
164 let activeItems = items.filter { !$0.isCompleted }
165 #expect(activeItems.count == 3)
166 #expect(activeItems.contains { $0.id == itemIDs[1] })
167 }
168
169 @Test("Uncomplete legacy sortOrder zero conflict appends to end")
170 func uncompleteLegacyZeroConflictAppendsToEnd() async throws {
171 let store = makeTestStore()
172 let activeItem = try store.createItem(title: "Active")
173 let completedItem = try store.createItem(title: "Completed")
174
175 activeItem.sortOrder = 0
176 try store.complete(itemID: completedItem.id)
177 completedItem.sortOrder = 0
178 try store.save()
179
180 try store.uncomplete(itemID: completedItem.id)
181
182 let activeItems = try store.fetchItems().filter { !$0.isCompleted }
183 .sorted { $0.sortOrder < $1.sortOrder }
184 #expect(activeItems.count == 2)
185 #expect(activeItems[0].id == activeItem.id)
186 #expect(activeItems[1].id == completedItem.id)
187 #expect(activeItems[1].sortOrder > activeItems[0].sortOrder)
188 }
189
190 @Test("Merge policy prefers store when store updatedAt is newer")
191 func mergePolicyPrefersStoreWhenStoreIsNewer() async throws {
192 let controller = PersistenceController(inMemory: true)
193 let viewContext = controller.viewContext
194 viewContext.automaticallyMergesChangesFromParent = false
195
196 let item = ItemEntity(context: viewContext)
197 item.title = "Original"
198 item.updatedAt = Date(timeIntervalSince1970: 100)
199 try viewContext.save()
200
201 let itemID = item.id
202
203 let backgroundContext = controller.container.newBackgroundContext()
204 try await backgroundContext.perform {
205 let request = ItemEntity.fetchRequest()
206 request.predicate = NSPredicate(format: "id == %@", itemID as CVarArg)
207 request.fetchLimit = 1
208
209 guard let remote = try backgroundContext.fetch(request).first else {
210 Issue.record("Expected item to exist in background context")
211 return
212 }
213
214 remote.title = "Remote newer"
215 remote.updatedAt = Date(timeIntervalSince1970: 200)
216 try backgroundContext.save()
217 }
218
219 item.updatedAt = Date(timeIntervalSince1970: 150)
220 try viewContext.save()
221
222 let verifyContext = controller.container.newBackgroundContext()
223 try await verifyContext.perform {
224 let request = ItemEntity.fetchRequest()
225 request.predicate = NSPredicate(format: "id == %@", itemID as CVarArg)
226 request.fetchLimit = 1
227
228 guard let resolved = try verifyContext.fetch(request).first else {
229 Issue.record("Expected item to exist in verify context")
230 return
231 }
232
233 #expect(resolved.title == "Remote newer")
234 #expect(resolved.updatedAt == Date(timeIntervalSince1970: 200))
235 }
236 }
237
238 @Test("Merge policy prefers local when local updatedAt is newer")
239 func mergePolicyPrefersLocalWhenLocalIsNewer() async throws {
240 let controller = PersistenceController(inMemory: true)
241 let viewContext = controller.viewContext
242 viewContext.automaticallyMergesChangesFromParent = false
243
244 let item = ItemEntity(context: viewContext)
245 item.title = "Original"
246 item.updatedAt = Date(timeIntervalSince1970: 100)
247 try viewContext.save()
248
249 let itemID = item.id
250
251 let backgroundContext = controller.container.newBackgroundContext()
252 try await backgroundContext.perform {
253 let request = ItemEntity.fetchRequest()
254 request.predicate = NSPredicate(format: "id == %@", itemID as CVarArg)
255 request.fetchLimit = 1
256
257 guard let remote = try backgroundContext.fetch(request).first else {
258 Issue.record("Expected item to exist in background context")
259 return
260 }
261
262 remote.title = "Remote older"
263 remote.updatedAt = Date(timeIntervalSince1970: 200)
264 try backgroundContext.save()
265 }
266
267 item.updatedAt = Date(timeIntervalSince1970: 300)
268 try viewContext.save()
269
270 let verifyContext = controller.container.newBackgroundContext()
271 try await verifyContext.perform {
272 let request = ItemEntity.fetchRequest()
273 request.predicate = NSPredicate(format: "id == %@", itemID as CVarArg)
274 request.fetchLimit = 1
275
276 guard let resolved = try verifyContext.fetch(request).first else {
277 Issue.record("Expected item to exist in verify context")
278 return
279 }
280
281 #expect(resolved.title == "Original")
282 #expect(resolved.updatedAt == Date(timeIntervalSince1970: 300))
283 }
284 }
285}