- Add 10 new fonts (Literata, Merriweather, Source Serif, Lato, Montserrat, Source Sans) - Support Apple system fonts and Google Fonts (OFL 1.1 licensed) - Extend FontFamily enum with new fonts and categories - Update FontSettingsViewModel and WebView with font support - Force WebView reload when font settings change - Refactor OfflineSyncManager with protocol and improved error handling - Add test mocks and OfflineSyncManagerTests with 9 test cases - Add OpenSourceLicensesView and FontDebugView - Bump build number - Update localization strings
166 lines
6.0 KiB
Swift
166 lines
6.0 KiB
Swift
//
|
|
// OfflineSyncManagerTests.swift
|
|
// readeckTests
|
|
//
|
|
// Created by Ilyas Hallak
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
@testable import readeck
|
|
|
|
@Suite("OfflineSyncManager Tests")
|
|
@MainActor
|
|
struct OfflineSyncManagerTests {
|
|
|
|
// MARK: - Test: Empty Queue
|
|
|
|
@Test("Should handle empty bookmark queue")
|
|
func testEmptyQueue() async throws {
|
|
let (syncManager, mockAPI, _) = createTestEnvironment()
|
|
|
|
await syncManager.syncOfflineBookmarks()
|
|
|
|
#expect(syncManager.isSyncing == false)
|
|
#expect(syncManager.syncStatus == "No bookmarks to sync")
|
|
#expect(mockAPI.createBookmarkCalls.isEmpty)
|
|
}
|
|
|
|
// MARK: - Test: Successful Sync
|
|
|
|
@Test("Should successfully sync all bookmarks")
|
|
func testSuccessfulSync() async throws {
|
|
let (syncManager, mockAPI, mockCoreData) = createTestEnvironment()
|
|
|
|
_ = mockCoreData.createTestBookmark(url: "https://example.com/1", title: "Article 1", tags: "tag1,tag2")
|
|
_ = mockCoreData.createTestBookmark(url: "https://example.com/2", title: "Article 2")
|
|
_ = mockCoreData.createTestBookmark(url: "https://example.com/3", title: "Article 3", tags: "tag3")
|
|
|
|
mockAPI.createBookmarkResults = Array(repeating: .success(mockSuccessResponse()), count: 3)
|
|
|
|
await syncManager.syncOfflineBookmarks()
|
|
try await Task.sleep(for: .milliseconds(100))
|
|
|
|
#expect(syncManager.isSyncing == false)
|
|
#expect(syncManager.syncStatus?.contains("Successfully synced 3 bookmarks") == true)
|
|
#expect(mockAPI.createBookmarkCalls.count == 3)
|
|
#expect(mockCoreData.fetchAllBookmarks().isEmpty)
|
|
}
|
|
|
|
// MARK: - Test: Server Unreachable
|
|
|
|
@Test("Should abort on first failure (server unreachable)")
|
|
func testServerUnreachable() async throws {
|
|
let (syncManager, mockAPI, mockCoreData) = createTestEnvironment()
|
|
|
|
_ = mockCoreData.createTestBookmark(url: "https://example.com/1", title: "Article 1")
|
|
_ = mockCoreData.createTestBookmark(url: "https://example.com/2", title: "Article 2")
|
|
_ = mockCoreData.createTestBookmark(url: "https://example.com/3", title: "Article 3")
|
|
|
|
mockAPI.createBookmarkResults = [.failure(APIError.serverError(503))]
|
|
|
|
await syncManager.syncOfflineBookmarks()
|
|
try await Task.sleep(for: .milliseconds(100))
|
|
|
|
#expect(syncManager.isSyncing == false)
|
|
#expect(syncManager.syncStatus == "Server not reachable. Cannot sync.")
|
|
#expect(mockAPI.createBookmarkCalls.count == 1)
|
|
#expect(mockCoreData.fetchAllBookmarks().count == 3)
|
|
}
|
|
|
|
// MARK: - Test: Partial Success
|
|
|
|
@Test("Should handle partial sync success")
|
|
func testPartialSuccess() async throws {
|
|
let (syncManager, mockAPI, mockCoreData) = createTestEnvironment()
|
|
|
|
for i in 1...4 {
|
|
_ = mockCoreData.createTestBookmark(url: "https://example.com/\(i)", title: "Article \(i)")
|
|
}
|
|
|
|
mockAPI.createBookmarkResults = [
|
|
.success(mockSuccessResponse()),
|
|
.failure(APIError.serverError(400)),
|
|
.success(mockSuccessResponse()),
|
|
.failure(APIError.serverError(400))
|
|
]
|
|
|
|
await syncManager.syncOfflineBookmarks()
|
|
try await Task.sleep(for: .milliseconds(100))
|
|
|
|
#expect(syncManager.isSyncing == false)
|
|
#expect(syncManager.syncStatus?.contains("Synced 2, failed 2") == true)
|
|
#expect(mockAPI.createBookmarkCalls.count == 4)
|
|
#expect(mockCoreData.fetchAllBookmarks().count == 2)
|
|
}
|
|
|
|
// MARK: - Test: Bookmark Without URL
|
|
|
|
@Test("Should skip bookmarks without URL")
|
|
func testBookmarkWithoutURL() async throws {
|
|
let (syncManager, mockAPI, mockCoreData) = createTestEnvironment()
|
|
|
|
let invalidEntity = ArticleURLEntity(context: mockCoreData.context)
|
|
invalidEntity.url = nil
|
|
invalidEntity.title = "Invalid Bookmark"
|
|
try! mockCoreData.context.save()
|
|
|
|
_ = mockCoreData.createTestBookmark(url: "https://example.com/1", title: "Valid Article")
|
|
|
|
mockAPI.createBookmarkResults = [.success(mockSuccessResponse())]
|
|
|
|
await syncManager.syncOfflineBookmarks()
|
|
try await Task.sleep(for: .milliseconds(100))
|
|
|
|
#expect(syncManager.isSyncing == false)
|
|
#expect(mockAPI.createBookmarkCalls.count == 1)
|
|
}
|
|
|
|
// MARK: - Test: Tags Parsing
|
|
|
|
@Test("Should correctly parse and send tags")
|
|
func testTagsParsing() async throws {
|
|
let (syncManager, mockAPI, mockCoreData) = createTestEnvironment()
|
|
|
|
_ = mockCoreData.createTestBookmark(url: "https://example.com/1", title: "Article", tags: "swift,ios,testing")
|
|
|
|
mockAPI.createBookmarkResults = [.success(mockSuccessResponse())]
|
|
|
|
await syncManager.syncOfflineBookmarks()
|
|
try await Task.sleep(for: .milliseconds(100))
|
|
|
|
#expect(mockAPI.createBookmarkCalls.count == 1)
|
|
#expect(mockAPI.createBookmarkCalls[0].0.labels == ["swift", "ios", "testing"])
|
|
}
|
|
|
|
// MARK: - Test: Empty Tags
|
|
|
|
@Test("Should handle bookmarks without tags")
|
|
func testEmptyTags() async throws {
|
|
let (syncManager, mockAPI, mockCoreData) = createTestEnvironment()
|
|
|
|
_ = mockCoreData.createTestBookmark(url: "https://example.com/1", title: "Article")
|
|
|
|
mockAPI.createBookmarkResults = [.success(mockSuccessResponse())]
|
|
|
|
await syncManager.syncOfflineBookmarks()
|
|
try await Task.sleep(for: .milliseconds(100))
|
|
|
|
#expect(mockAPI.createBookmarkCalls.count == 1)
|
|
#expect(mockAPI.createBookmarkCalls[0].0.labels == nil)
|
|
}
|
|
|
|
// MARK: - Test Helpers
|
|
|
|
private func createTestEnvironment() -> (TestableOfflineSyncManager, TestMockAPI, TestCoreDataManager) {
|
|
let mockAPI = TestMockAPI()
|
|
let mockCoreData = TestCoreDataManager()
|
|
let syncManager = TestableOfflineSyncManager(api: mockAPI, coreDataManager: mockCoreData)
|
|
return (syncManager, mockAPI, mockCoreData)
|
|
}
|
|
|
|
private func mockSuccessResponse() -> CreateBookmarkResponseDto {
|
|
CreateBookmarkResponseDto(message: "Bookmark created", status: 200)
|
|
}
|
|
}
|