- Add Apple Sign-In backend (JWKS verification, account linking, session management) - Implement push notification deep linking with NotificationDeepLinkRouter - Add jailbreak detection, runtime integrity monitoring, secure enclave service - Implement OAuth social login, token refresh, and secure logout flows - Add image caching (memory/disk), optimizer, upload queue, async semaphore - Implement notification analytics, type preferences, and category setup - Expand UI test suite with UITestBase, accessibility, auth flow, performance tests - Add CI pipeline for iOS UI tests (3 device sizes) and performance benchmarks - Restructure Xcode project to manual groups with KordantWidgets target - Add SwiftLint, Swift Collections/Algorithms/GoogleSignIn dependencies - Update project.yml for XcodeGen with new targets and configurations
322 lines
12 KiB
Swift
322 lines
12 KiB
Swift
import Testing
|
|
@testable import Kordant
|
|
import BackgroundTasks
|
|
import SwiftUI
|
|
|
|
// MARK: - SyncStatus Tests
|
|
|
|
struct SyncStatusTests {
|
|
@Test("SyncStatus defaults are correct")
|
|
func defaults() {
|
|
let status = SyncStatus()
|
|
#expect(status.lastSuccessfulSync == nil)
|
|
#expect(status.lastSyncAttempt == nil)
|
|
#expect(status.currentSyncState == .idle)
|
|
#expect(status.syncError == nil)
|
|
#expect(status.totalBytesTransferred == 0)
|
|
#expect(status.deltaSyncSavings == 0)
|
|
#expect(status.isLowPowerMode == false)
|
|
#expect(status.isOffline == false)
|
|
}
|
|
|
|
@Test("SyncStatus lastSyncDescription shows Never when no sync")
|
|
func lastSyncDescriptionNever() {
|
|
let status = SyncStatus()
|
|
#expect(status.lastSyncDescription == "Never")
|
|
}
|
|
|
|
@Test("SyncStatus lastSyncDescription shows relative time")
|
|
func lastSyncDescriptionRelative() {
|
|
var status = SyncStatus()
|
|
status.lastSuccessfulSync = Date()
|
|
// Should show something like "now" or "0s"
|
|
#expect(!status.lastSyncDescription.isEmpty)
|
|
#expect(status.lastSyncDescription != "Never")
|
|
}
|
|
|
|
@Test("SyncStatus bytesTransferredString formats bytes")
|
|
func bytesTransferredString() {
|
|
var status = SyncStatus()
|
|
status.totalBytesTransferred = 1024
|
|
#expect(status.bytesTransferredString == "1 KB")
|
|
}
|
|
|
|
@Test("SyncStatus deltaSyncSavingsPercent is 0 when no data")
|
|
func deltaSyncSavingsZero() {
|
|
let status = SyncStatus()
|
|
#expect(status.deltaSyncSavingsPercent == 0)
|
|
}
|
|
|
|
@Test("SyncStatus deltaSyncSavingsPercent calculates correctly")
|
|
func deltaSyncSavingsCalculation() {
|
|
var status = SyncStatus()
|
|
status.totalBytesTransferred = 500
|
|
status.deltaSyncSavings = 500
|
|
// 500 / (500 + 500) * 100 = 50%
|
|
#expect(status.deltaSyncSavingsPercent == 50.0)
|
|
}
|
|
|
|
@Test("SyncStatus equality compares key fields")
|
|
func equality() {
|
|
var status1 = SyncStatus()
|
|
var status2 = SyncStatus()
|
|
status1.lastSuccessfulSync = Date()
|
|
status2.lastSuccessfulSync = status1.lastSuccessfulSync
|
|
status1.currentSyncState = .completed
|
|
status2.currentSyncState = .completed
|
|
#expect(status1 == status2)
|
|
}
|
|
}
|
|
|
|
// MARK: - SyncStatusManager Tests
|
|
|
|
@MainActor
|
|
struct SyncStatusManagerTests {
|
|
@Test("SyncStatusManager starts with idle state")
|
|
func initialState() {
|
|
let manager = SyncStatusManager(defaults: UserDefaults(suiteName: UUID().uuidString)!)
|
|
#expect(manager.status.currentSyncState == .idle)
|
|
}
|
|
|
|
@Test("SyncStatusManager startSync updates state")
|
|
func startSync() {
|
|
let manager = SyncStatusManager(defaults: UserDefaults(suiteName: UUID().uuidString)!)
|
|
manager.startSync(.appRefresh)
|
|
#expect(manager.status.currentSyncState == .syncing)
|
|
#expect(manager.status.lastSyncAttempt != nil)
|
|
#expect(manager.status.lastSyncOperation == .appRefresh)
|
|
#expect(manager.status.syncError == nil)
|
|
}
|
|
|
|
@Test("SyncStatusManager completeSync updates state")
|
|
func completeSync() {
|
|
let manager = SyncStatusManager(defaults: UserDefaults(suiteName: UUID().uuidString)!)
|
|
manager.startSync(.appRefresh)
|
|
manager.completeSync(bytesTransferred: 1024, deltaSavings: 512)
|
|
#expect(manager.status.currentSyncState == .completed)
|
|
#expect(manager.status.lastSuccessfulSync != nil)
|
|
#expect(manager.status.syncError == nil)
|
|
#expect(manager.status.totalBytesTransferred == 1024)
|
|
#expect(manager.status.deltaSyncSavings == 512)
|
|
}
|
|
|
|
@Test("SyncStatusManager failSync updates state")
|
|
func failSync() {
|
|
let manager = SyncStatusManager(defaults: UserDefaults(suiteName: UUID().uuidString)!)
|
|
manager.startSync(.appRefresh)
|
|
manager.failSync(with: "Network error")
|
|
#expect(manager.status.currentSyncState == .failed)
|
|
#expect(manager.status.syncError == "Network error")
|
|
}
|
|
|
|
@Test("SyncStatusManager setOffline updates state")
|
|
func setOffline() {
|
|
let manager = SyncStatusManager(defaults: UserDefaults(suiteName: UUID().uuidString)!)
|
|
manager.setOffline(true)
|
|
#expect(manager.status.isOffline == true)
|
|
#expect(manager.status.currentSyncState == .offline)
|
|
|
|
manager.setOffline(false)
|
|
#expect(manager.status.isOffline == false)
|
|
#expect(manager.status.currentSyncState == .idle)
|
|
}
|
|
|
|
@Test("SyncStatusManager accumulates bytes across syncs")
|
|
func accumulateBytes() {
|
|
let manager = SyncStatusManager(defaults: UserDefaults(suiteName: UUID().uuidString)!)
|
|
manager.completeSync(bytesTransferred: 100, deltaSavings: 50)
|
|
manager.completeSync(bytesTransferred: 200, deltaSavings: 100)
|
|
#expect(manager.status.totalBytesTransferred == 300)
|
|
#expect(manager.status.deltaSyncSavings == 150)
|
|
}
|
|
|
|
@Test("SyncStatusManager persists and restores state")
|
|
func persistence() {
|
|
let defaults = UserDefaults(suiteName: UUID().uuidString)!
|
|
let manager1 = SyncStatusManager(defaults: defaults)
|
|
manager1.completeSync(bytesTransferred: 500, deltaSavings: 200)
|
|
|
|
let manager2 = SyncStatusManager(defaults: defaults)
|
|
#expect(manager2.status.totalBytesTransferred == 500)
|
|
#expect(manager2.status.deltaSyncSavings == 200)
|
|
}
|
|
|
|
@Test("SyncStatusManager resets syncing state on launch")
|
|
func resetSyncingStateOnLaunch() {
|
|
let defaults = UserDefaults(suiteName: UUID().uuidString)!
|
|
let manager1 = SyncStatusManager(defaults: defaults)
|
|
manager1.startSync(.appRefresh)
|
|
// Simulate crash — state is syncing
|
|
|
|
let manager2 = SyncStatusManager(defaults: defaults)
|
|
// Should reset to idle
|
|
#expect(manager2.status.currentSyncState == .idle)
|
|
}
|
|
}
|
|
|
|
// MARK: - BackgroundTaskID Tests
|
|
|
|
struct BackgroundTaskIDTests {
|
|
@Test("BackgroundTaskID has correct raw values")
|
|
func rawValues() {
|
|
#expect(BackgroundTaskID.appRefresh.rawValue == "com.frenocorp.kordant.refresh")
|
|
#expect(BackgroundTaskID.darkWebScan.rawValue == "com.frenocorp.kordant.darkWebScan")
|
|
#expect(BackgroundTaskID.spamDatabaseUpdate.rawValue == "com.frenocorp.kordant.spamDatabaseUpdate")
|
|
}
|
|
}
|
|
|
|
// MARK: - BackgroundSyncService Tests
|
|
|
|
struct BackgroundSyncServiceTests {
|
|
@Test("BackgroundSyncService minimumFetchInterval is 15 minutes")
|
|
func minimumFetchInterval() {
|
|
#expect(BackgroundSyncService.minimumFetchInterval == 15 * 60)
|
|
}
|
|
|
|
@Test("BackgroundSyncService lowPowerFetchInterval is 30 minutes")
|
|
func lowPowerFetchInterval() {
|
|
#expect(BackgroundSyncService.lowPowerFetchInterval == 30 * 60)
|
|
}
|
|
|
|
@Test("DeltaSyncResult hasChanges is true when any data changed")
|
|
func deltaSyncResultHasChanges() {
|
|
let result = DeltaSyncResult(
|
|
alertsChanged: true,
|
|
exposuresChanged: false,
|
|
watchlistChanged: false,
|
|
bytesTransferred: 100,
|
|
deltaSavings: 50,
|
|
newAlerts: [],
|
|
newExposures: []
|
|
)
|
|
#expect(result.hasChanges == true)
|
|
}
|
|
|
|
@Test("DeltaSyncResult hasChanges is false when nothing changed")
|
|
func deltaSyncResultNoChanges() {
|
|
let result = DeltaSyncResult(
|
|
alertsChanged: false,
|
|
exposuresChanged: false,
|
|
watchlistChanged: false,
|
|
bytesTransferred: 0,
|
|
deltaSavings: 0,
|
|
newAlerts: [],
|
|
newExposures: []
|
|
)
|
|
#expect(result.hasChanges == false)
|
|
}
|
|
|
|
@Test("SyncOperation raw values are correct")
|
|
func syncOperationRawValues() {
|
|
#expect(SyncOperation.appRefresh.rawValue == "app_refresh")
|
|
#expect(SyncOperation.darkWebScan.rawValue == "dark_web_scan")
|
|
#expect(SyncOperation.spamDatabaseUpdate.rawValue == "spam_database_update")
|
|
#expect(SyncOperation.pushNotificationSync.rawValue == "push_notification_sync")
|
|
#expect(SyncOperation.manual.rawValue == "manual")
|
|
}
|
|
|
|
@Test("SyncState raw values are correct")
|
|
func syncStateRawValues() {
|
|
#expect(SyncState.idle.rawValue == "idle")
|
|
#expect(SyncState.syncing.rawValue == "syncing")
|
|
#expect(SyncState.completed.rawValue == "completed")
|
|
#expect(SyncState.failed.rawValue == "failed")
|
|
#expect(SyncState.offline.rawValue == "offline")
|
|
}
|
|
}
|
|
|
|
// MARK: - BackgroundTaskScheduler Tests
|
|
|
|
struct BackgroundTaskSchedulerTests {
|
|
@Test("BackgroundTaskScheduler minimumRefreshInterval is 15 minutes")
|
|
func minimumRefreshInterval() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
// We can't directly access private properties, but we can verify
|
|
// the scheduling logic works through the public API
|
|
#expect(scheduler.shouldDeferBackgroundTasks() == false)
|
|
}
|
|
|
|
@Test("BackgroundTaskScheduler should not defer when recently synced in normal mode")
|
|
func shouldNotDeferNormalMode() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.shouldDeferBackgroundTasks() == false)
|
|
}
|
|
|
|
@Test("BackgroundTaskScheduler registerAllTasks does not crash")
|
|
func registerAllTasks() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
// Should not throw
|
|
scheduler.registerAllTasks()
|
|
}
|
|
|
|
@Test("BackgroundTaskScheduler scheduleAllTasks does not crash")
|
|
func scheduleAllTasks() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
// Should not throw
|
|
scheduler.scheduleAllTasks()
|
|
}
|
|
}
|
|
|
|
// MARK: - ETagCacheEntry Tests
|
|
|
|
struct ETagCacheEntryTests {
|
|
@Test("ETagCacheEntry is not stale when fresh")
|
|
func freshEntry() {
|
|
let entry = ETagCacheEntry(etag: "abc123", lastModified: "2024-01-01", timestamp: Date())
|
|
#expect(entry.isStale == false)
|
|
}
|
|
|
|
@Test("ETagCacheEntry is stale after 10 minutes")
|
|
func staleEntry() {
|
|
let entry = ETagCacheEntry(
|
|
etag: "abc123",
|
|
lastModified: "2024-01-01",
|
|
timestamp: Date().addingTimeInterval(-601) // Just over 10 minutes
|
|
)
|
|
#expect(entry.isStale == true)
|
|
}
|
|
|
|
@Test("ETagCacheEntry is stale at exactly 10 minutes")
|
|
func exactlyStale() {
|
|
let entry = ETagCacheEntry(
|
|
etag: "abc123",
|
|
lastModified: "2024-01-01",
|
|
timestamp: Date().addingTimeInterval(-600) // Exactly 10 minutes
|
|
)
|
|
#expect(entry.isStale == true)
|
|
}
|
|
|
|
@Test("ETagCacheEntry is Codable")
|
|
func codable() throws {
|
|
let entry = ETagCacheEntry(etag: "abc123", lastModified: "2024-01-01", timestamp: Date())
|
|
let data = try JSONEncoder().encode(entry)
|
|
let decoded = try JSONDecoder().decode(ETagCacheEntry.self, from: data)
|
|
#expect(decoded.etag == "abc123")
|
|
#expect(decoded.lastModified == "2024-01-01")
|
|
}
|
|
}
|
|
|
|
// MARK: - SyncOperation Codable Tests
|
|
|
|
struct SyncOperationCodableTests {
|
|
@Test("SyncOperation encodes and decodes correctly")
|
|
func encodeDecode() throws {
|
|
let operation = SyncOperation.appRefresh
|
|
let data = try JSONEncoder().encode(operation)
|
|
let decoded = try JSONDecoder().decode(SyncOperation.self, from: data)
|
|
#expect(decoded == .appRefresh)
|
|
}
|
|
}
|
|
|
|
// MARK: - SyncState Codable Tests
|
|
|
|
struct SyncStateCodableTests {
|
|
@Test("SyncState encodes and decodes correctly")
|
|
func encodeDecode() throws {
|
|
let state = SyncState.syncing
|
|
let data = try JSONEncoder().encode(state)
|
|
let decoded = try JSONDecoder().decode(SyncState.self, from: data)
|
|
#expect(decoded == .syncing)
|
|
}
|
|
}
|