ItemStoreEdgeCaseTests.swift (9519B)
1 import Foundation 2 import CoreData 3 import 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 13 struct ItemStoreEdgeCaseTests { 14 15 // MARK: - Title Edge Cases 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 // MARK: - Large Data Sets 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 // MARK: - State Transitions 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 // MARK: - Store State Tests 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 }