// // OfflineCacheRepositoryTests.swift // readeckTests // // Created by Claude on 21.11.25. // import Testing import Foundation import CoreData @testable import readeck @Suite("OfflineCacheRepository Tests") struct OfflineCacheRepositoryTests { // MARK: - Test Setup private func createInMemoryCoreDataStack() -> NSManagedObjectContext { let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main])! let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel) try! persistentStoreCoordinator.addPersistentStore( ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil ) let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) context.persistentStoreCoordinator = persistentStoreCoordinator context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return context } private func createTestBookmark(id: String = "test-123", title: String = "Test Article") -> Bookmark { return Bookmark( id: id, title: title, url: "https://example.com/article", href: "/api/bookmarks/\(id)", description: "Test description", authors: [], created: ISO8601DateFormatter().string(from: Date()), published: nil, updated: ISO8601DateFormatter().string(from: Date()), siteName: "Example Site", site: "example.com", readingTime: 5, wordCount: 1000, hasArticle: true, isArchived: false, isDeleted: false, isMarked: false, labels: [], lang: "en", loaded: true, readProgress: 0, documentType: "article", state: 0, textDirection: "ltr", type: "bookmark", resources: BookmarkResources( article: Resource(src: "/api/bookmarks/\(id)/article"), icon: nil, image: nil, log: nil, props: nil, thumbnail: nil ) ) } // MARK: - HTML Extraction Tests @Test("Extract image URLs from HTML correctly") func testExtractImageURLsFromHTML() { let html = """ Image 1 Relative """ // We need to test the private method indirectly via cacheBookmarkWithMetadata // For now, we'll test the regex pattern separately let pattern = #"]+src=\"([^\"]+)\""# let regex = try! NSRegularExpression(pattern: pattern, options: []) let nsString = html as NSString let results = regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsString.length)) var imageURLs: [String] = [] for result in results { if result.numberOfRanges >= 2 { let urlRange = result.range(at: 1) if let url = nsString.substring(with: urlRange) as String?, url.hasPrefix("http") { imageURLs.append(url) } } } #expect(imageURLs.count == 3) #expect(imageURLs.contains("https://example.com/image1.jpg")) #expect(imageURLs.contains("https://example.com/image2.png")) #expect(imageURLs.contains("https://example.com/image3.gif")) #expect(!imageURLs.contains("/relative/image.jpg")) } @Test("Extract image URLs handles empty HTML") func testExtractImageURLsFromEmptyHTML() { let html = "

No images here

" let pattern = #"]+src=\"([^\"]+)\""# let regex = try! NSRegularExpression(pattern: pattern, options: []) let results = regex.matches(in: html, options: [], range: NSRange(location: 0, length: html.count)) #expect(results.count == 0) } @Test("Extract image URLs handles malformed HTML") func testExtractImageURLsFromMalformedHTML() { let html = """ """ let pattern = #"]+src=\"([^\"]+)\""# let regex = try! NSRegularExpression(pattern: pattern, options: []) let results = regex.matches(in: html, options: [], range: NSRange(location: 0, length: html.count)) // Should only match double-quoted URLs #expect(results.count == 1) } // MARK: - Cache Size Calculation Tests @Test("Cache size calculation is accurate") func testCacheSizeCalculation() { let html = "Test HTML content" let expectedSize = Int64(html.utf8.count) #expect(expectedSize == 17) } @Test("Cache size handles empty content") func testCacheSizeWithEmptyContent() { let html = "" let expectedSize = Int64(html.utf8.count) #expect(expectedSize == 0) } @Test("Cache size handles UTF-8 characters correctly") func testCacheSizeWithUTF8Characters() { let html = "Hello δΈ–η•Œ 🌍" let expectedSize = Int64(html.utf8.count) // UTF-8: "Hello " (6) + "δΈ–η•Œ" (6) + " " (1) + "🌍" (4) = 17 bytes #expect(expectedSize > html.count) // More bytes than characters } // MARK: - Image URL Storage Tests @Test("Image URLs are joined correctly with comma separator") func testImageURLsJoining() { let imageURLs = [ "https://example.com/image1.jpg", "https://example.com/image2.png", "https://example.com/image3.gif" ] let joined = imageURLs.joined(separator: ",") #expect(joined == "https://example.com/image1.jpg,https://example.com/image2.png,https://example.com/image3.gif") // Test splitting let split = joined.split(separator: ",").map(String.init) #expect(split.count == 3) #expect(split == imageURLs) } @Test("Image URLs splitting handles empty string") func testImageURLsSplittingEmptyString() { let imageURLsString = "" let split = imageURLsString.split(separator: ",").map(String.init) #expect(split.isEmpty) } @Test("Image URLs splitting handles single URL") func testImageURLsSplittingSingleURL() { let imageURLsString = "https://example.com/single.jpg" let split = imageURLsString.split(separator: ",").map(String.init) #expect(split.count == 1) #expect(split.first == "https://example.com/single.jpg") } // MARK: - Bookmark Domain Model Tests @Test("Bookmark creation has correct defaults") func testBookmarkCreation() { let bookmark = createTestBookmark() #expect(bookmark.id == "test-123") #expect(bookmark.title == "Test Article") #expect(bookmark.url == "https://example.com/article") #expect(bookmark.readProgress == 0) #expect(bookmark.isMarked == false) } // MARK: - FIFO Cleanup Logic Tests @Test("FIFO cleanup calculates correct number of items to delete") func testFIFOCleanupCalculation() { let totalCount = 30 let keepCount = 20 let expectedDeleteCount = totalCount - keepCount #expect(expectedDeleteCount == 10) } @Test("FIFO cleanup does not delete when under limit") func testFIFOCleanupUnderLimit() { let totalCount = 15 let keepCount = 20 if totalCount > keepCount { #expect(Bool(false), "Should not trigger cleanup") } else { #expect(Bool(true), "Cleanup should be skipped") } } @Test("FIFO cleanup deletes all items when keepCount is zero") func testFIFOCleanupKeepZero() { let totalCount = 10 let keepCount = 0 let expectedDeleteCount = totalCount - keepCount #expect(expectedDeleteCount == 10) } // MARK: - Date Handling Tests @Test("Cache date and access date are set correctly") func testCacheDates() { let now = Date() let cachedDate = now let lastAccessDate = now #expect(cachedDate.timeIntervalSince1970 == now.timeIntervalSince1970) #expect(lastAccessDate.timeIntervalSince1970 == now.timeIntervalSince1970) } @Test("Last access date updates on read") func testLastAccessDateUpdate() { let initialDate = Date(timeIntervalSince1970: 1000) let updatedDate = Date(timeIntervalSince1970: 2000) #expect(updatedDate > initialDate) #expect(updatedDate.timeIntervalSince1970 - initialDate.timeIntervalSince1970 == 1000) } // MARK: - ByteCountFormatter Tests @Test("ByteCountFormatter formats small sizes correctly") func testByteCountFormatterSmallSizes() { let formatter = ByteCountFormatter() formatter.countStyle = .file let bytes1KB = Int64(1024) let formatted1KB = formatter.string(fromByteCount: bytes1KB) #expect(formatted1KB.contains("KB") || formatted1KB.contains("kB")) let bytes10KB = Int64(10 * 1024) let formatted10KB = formatter.string(fromByteCount: bytes10KB) #expect(formatted10KB.contains("KB") || formatted10KB.contains("kB")) } @Test("ByteCountFormatter formats large sizes correctly") func testByteCountFormatterLargeSizes() { let formatter = ByteCountFormatter() formatter.countStyle = .file let bytes1MB = Int64(1024 * 1024) let formatted1MB = formatter.string(fromByteCount: bytes1MB) #expect(formatted1MB.contains("MB")) let bytes100MB = Int64(100 * 1024 * 1024) let formatted100MB = formatter.string(fromByteCount: bytes100MB) #expect(formatted100MB.contains("MB")) } @Test("ByteCountFormatter handles zero bytes") func testByteCountFormatterZero() { let formatter = ByteCountFormatter() formatter.countStyle = .file let formatted = formatter.string(fromByteCount: 0) #expect(formatted.contains("0") || formatted.contains("Zero")) } // MARK: - NSPredicate Tests @Test("Cache filter predicate syntax is correct") func testCacheFilterPredicate() { let predicate = NSPredicate(format: "htmlContent != nil") // Test with mock data let testData = ["htmlContent": "Some HTML"] let result = predicate.evaluate(with: testData) #expect(result == true) } @Test("ID filter predicate syntax is correct") func testIDFilterPredicate() { let testID = "test-123" let predicate = NSPredicate(format: "id == %@", testID) let testData = ["id": "test-123"] let result = predicate.evaluate(with: testData) #expect(result == true) let wrongData = ["id": "wrong-id"] let wrongResult = predicate.evaluate(with: wrongData) #expect(wrongResult == false) } @Test("Combined cache and ID predicate is correct") func testCombinedPredicate() { let testID = "test-123" let predicate = NSPredicate(format: "id == %@ AND htmlContent != nil", testID) let validData = ["id": "test-123", "htmlContent": "HTML"] let validResult = predicate.evaluate(with: validData) #expect(validResult == true) let missingHTML = ["id": "test-123", "htmlContent": nil as String?] let missingHTMLResult = predicate.evaluate(with: missingHTML) #expect(missingHTMLResult == false) } }