348 lines
12 KiB
Swift
348 lines
12 KiB
Swift
//
|
|
// OfflineCacheRepositoryTests.swift
|
|
// readeckTests
|
|
//
|
|
// Created by Ilyas Hallak 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 = """
|
|
<html>
|
|
<body>
|
|
<img src="https://example.com/image1.jpg" alt="Image 1">
|
|
<img src="https://example.com/image2.png" />
|
|
<img src="/relative/image.jpg" alt="Relative">
|
|
<img src="https://example.com/image3.gif">
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
// We need to test the private method indirectly via cacheBookmarkWithMetadata
|
|
// For now, we'll test the regex pattern separately
|
|
let pattern = #"<img[^>]+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 = "<html><body><p>No images here</p></body></html>"
|
|
|
|
let pattern = #"<img[^>]+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 = """
|
|
<img src='single-quotes.jpg'>
|
|
<img src=no-quotes.jpg>
|
|
<img src="https://valid.com/image.jpg">
|
|
"""
|
|
|
|
let pattern = #"<img[^>]+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)
|
|
}
|
|
}
|