829 lines
29 KiB
Swift
829 lines
29 KiB
Swift
import Testing
|
|
@testable import Kordant
|
|
import BackgroundTasks
|
|
import SwiftUI
|
|
import Network
|
|
|
|
// 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)
|
|
#expect(status.syncProgress == 0.0)
|
|
#expect(status.syncStageDescription == "")
|
|
}
|
|
|
|
@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)
|
|
}
|
|
|
|
@Test("SyncStatus progress fields do not affect equality")
|
|
func equalityIgnoresProgress() {
|
|
var status1 = SyncStatus()
|
|
var status2 = SyncStatus()
|
|
status1.syncProgress = 0.5
|
|
status1.syncStageDescription = "Fetching..."
|
|
status2.syncProgress = 0.8
|
|
status2.syncStageDescription = "Saving..."
|
|
// Progress fields are transient and should not affect equality
|
|
#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)
|
|
#expect(manager.status.syncProgress == 0.0)
|
|
#expect(manager.status.syncStageDescription == "")
|
|
}
|
|
|
|
@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)
|
|
#expect(manager.status.syncProgress == 0.0)
|
|
}
|
|
|
|
@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)
|
|
#expect(manager.status.syncProgress == 1.0)
|
|
}
|
|
|
|
@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")
|
|
#expect(manager.status.syncProgress == 0.0)
|
|
}
|
|
|
|
@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)
|
|
}
|
|
|
|
@Test("SyncStatusManager resets progress on launch")
|
|
func resetProgressOnLaunch() {
|
|
let defaults = UserDefaults(suiteName: UUID().uuidString)!
|
|
let manager1 = SyncStatusManager(defaults: defaults)
|
|
manager1.startSync(.appRefresh)
|
|
manager1.updateProgress(0.5, stage: "Fetching alerts...")
|
|
|
|
let manager2 = SyncStatusManager(defaults: defaults)
|
|
// Progress should be reset on launch
|
|
#expect(manager2.status.syncProgress == 0.0)
|
|
#expect(manager2.status.syncStageDescription == "")
|
|
}
|
|
|
|
@Test("SyncStatusManager updateProgress updates progress fields")
|
|
func updateProgress() {
|
|
let manager = SyncStatusManager(defaults: UserDefaults(suiteName: UUID().uuidString)!)
|
|
manager.startSync(.appRefresh)
|
|
manager.updateProgress(0.3, stage: "Fetching alerts...")
|
|
#expect(manager.status.syncProgress == 0.3)
|
|
#expect(manager.status.syncStageDescription == "Fetching alerts...")
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
@Test("BackgroundTaskID isProcessingTask returns correct values")
|
|
func isProcessingTask() {
|
|
#expect(BackgroundTaskID.appRefresh.isProcessingTask == false)
|
|
#expect(BackgroundTaskID.darkWebScan.isProcessingTask == true)
|
|
#expect(BackgroundTaskID.spamDatabaseUpdate.isProcessingTask == true)
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
@Test("SyncProgressStage fetching has correct description")
|
|
func progressStageFetching() {
|
|
let progress = SyncProgress(
|
|
operation: .appRefresh,
|
|
stage: .fetching(label: "alerts"),
|
|
fractionCompleted: 0.3,
|
|
estimatedRemaining: nil
|
|
)
|
|
#expect(progress.description == "Fetching alerts...")
|
|
}
|
|
|
|
@Test("SyncProgressStage processing has correct description")
|
|
func progressStageProcessing() {
|
|
let progress = SyncProgress(
|
|
operation: .appRefresh,
|
|
stage: .processing,
|
|
fractionCompleted: 0.7,
|
|
estimatedRemaining: nil
|
|
)
|
|
#expect(progress.description == "Processing data...")
|
|
}
|
|
|
|
@Test("SyncProgressStage saving has correct description")
|
|
func progressStageSaving() {
|
|
let progress = SyncProgress(
|
|
operation: .appRefresh,
|
|
stage: .saving,
|
|
fractionCompleted: 0.9,
|
|
estimatedRemaining: nil
|
|
)
|
|
#expect(progress.description == "Saving data...")
|
|
}
|
|
|
|
@Test("SyncProgressStage completed has correct description")
|
|
func progressStageCompleted() {
|
|
let progress = SyncProgress(
|
|
operation: .appRefresh,
|
|
stage: .completed,
|
|
fractionCompleted: 1.0,
|
|
estimatedRemaining: nil
|
|
)
|
|
#expect(progress.description == "Sync completed")
|
|
}
|
|
|
|
@Test("SyncProgressStage failed has correct description")
|
|
func progressStageFailed() {
|
|
let progress = SyncProgress(
|
|
operation: .appRefresh,
|
|
stage: .failed(message: "Network error"),
|
|
fractionCompleted: 0.0,
|
|
estimatedRemaining: nil
|
|
)
|
|
#expect(progress.description == "Sync failed: Network error")
|
|
}
|
|
|
|
@Test("DeltaFetchResult is generic and works with any type")
|
|
func deltaFetchResultGeneric() {
|
|
let result: DeltaFetchResult<String> = DeltaFetchResult(
|
|
changed: true,
|
|
data: "test",
|
|
bytes: 1024,
|
|
savings: 512
|
|
)
|
|
#expect(result.changed == true)
|
|
#expect(result.data == "test")
|
|
#expect(result.bytes == 1024)
|
|
#expect(result.savings == 512)
|
|
}
|
|
}
|
|
|
|
// MARK: - BackgroundTaskScheduler Tests
|
|
|
|
struct BackgroundTaskSchedulerTests {
|
|
@Test("BackgroundTaskScheduler minimumRefreshInterval is 15 minutes")
|
|
func minimumRefreshInterval() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.minimumRefreshInterval == 15 * 60)
|
|
}
|
|
|
|
@Test("BackgroundTaskScheduler lowPowerRefreshInterval is 30 minutes")
|
|
func lowPowerRefreshInterval() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.lowPowerRefreshInterval == 30 * 60)
|
|
}
|
|
|
|
@Test("BackgroundTaskScheduler darkWebScanInterval is 6 hours")
|
|
func darkWebScanInterval() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.darkWebScanInterval == 6 * 60 * 60)
|
|
}
|
|
|
|
@Test("BackgroundTaskScheduler spamUpdateInterval is 24 hours")
|
|
func spamUpdateInterval() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.spamUpdateInterval == 24 * 60 * 60)
|
|
}
|
|
|
|
@Test("BackgroundTaskScheduler lowPowerDarkWebScanInterval is 12 hours")
|
|
func lowPowerDarkWebScanInterval() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.lowPowerDarkWebScanInterval == 12 * 60 * 60)
|
|
}
|
|
|
|
@Test("BackgroundTaskScheduler lowPowerSpamUpdateInterval is 48 hours")
|
|
func lowPowerSpamUpdateInterval() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.lowPowerSpamUpdateInterval == 48 * 60 * 60)
|
|
}
|
|
|
|
@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()
|
|
}
|
|
|
|
@Test("BackgroundTaskScheduler currentlyHasRunningTask starts as false")
|
|
func currentlyHasRunningTask() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.currentlyHasRunningTask == false)
|
|
}
|
|
|
|
@Test("BackgroundTaskScheduler task counters start at zero")
|
|
func taskCountersStartAtZero() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.tasksCompleted == 0)
|
|
#expect(scheduler.tasksExpired == 0)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
@Test("All SyncOperation values encode and decode correctly")
|
|
func allOperationsEncodeDecode() throws {
|
|
let operations: [SyncOperation] = [
|
|
.appRefresh, .darkWebScan, .spamDatabaseUpdate,
|
|
.pushNotificationSync, .manual
|
|
]
|
|
for op in operations {
|
|
let data = try JSONEncoder().encode(op)
|
|
let decoded = try JSONDecoder().decode(SyncOperation.self, from: data)
|
|
#expect(decoded == op)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
@Test("All SyncState values encode and decode correctly")
|
|
func allStatesEncodeDecode() throws {
|
|
let states: [SyncState] = [.idle, .syncing, .completed, .failed, .offline]
|
|
for state in states {
|
|
let data = try JSONEncoder().encode(state)
|
|
let decoded = try JSONDecoder().decode(SyncState.self, from: data)
|
|
#expect(decoded == state)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SyncProgress Tests
|
|
|
|
struct SyncProgressTests {
|
|
@Test("SyncProgress is Equatable on stage")
|
|
func progressStageEquatable() {
|
|
#expect(SyncProgressStage.fetching(label: "alerts") == SyncProgressStage.fetching(label: "alerts"))
|
|
#expect(SyncProgressStage.fetching(label: "alerts") != SyncProgressStage.fetching(label: "exposures"))
|
|
#expect(SyncProgressStage.processing == SyncProgressStage.processing)
|
|
#expect(SyncProgressStage.completed == SyncProgressStage.completed)
|
|
#expect(SyncProgressStage.processing != SyncProgressStage.saving)
|
|
}
|
|
}
|
|
|
|
// MARK: - Background Fetch Timing Tests
|
|
|
|
struct BackgroundFetchTimingTests {
|
|
@Test("App refresh interval meets 15-minute minimum")
|
|
func appRefreshMeetsMinimum() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.minimumRefreshInterval >= 15 * 60)
|
|
}
|
|
|
|
@Test("Low power mode doubles the refresh interval")
|
|
func lowPowerDoublesInterval() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.lowPowerRefreshInterval == scheduler.minimumRefreshInterval * 2)
|
|
}
|
|
|
|
@Test("Dark web scan interval in low power mode is doubled")
|
|
func lowPowerDarkWebDoublesInterval() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.lowPowerDarkWebScanInterval == scheduler.darkWebScanInterval * 2)
|
|
}
|
|
|
|
@Test("Spam update interval in low power mode is doubled")
|
|
func lowPowerSpamDoublesInterval() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
#expect(scheduler.lowPowerSpamUpdateInterval == scheduler.spamUpdateInterval * 2)
|
|
}
|
|
}
|
|
|
|
// MARK: - Delta Sync Savings Tests
|
|
|
|
struct DeltaSyncSavingsTests {
|
|
@Test("Delta sync savings percent is 0 when no savings")
|
|
func noSavings() {
|
|
var status = SyncStatus()
|
|
status.totalBytesTransferred = 1000
|
|
status.deltaSyncSavings = 0
|
|
#expect(status.deltaSyncSavingsPercent == 0.0)
|
|
}
|
|
|
|
@Test("Delta sync savings percent is 100 when all data was cached")
|
|
func allCached() {
|
|
var status = SyncStatus()
|
|
status.totalBytesTransferred = 0
|
|
status.deltaSyncSavings = 1000
|
|
#expect(status.deltaSyncSavingsPercent == 100.0)
|
|
}
|
|
|
|
@Test("Delta sync savings percent is 75 when 75% was saved")
|
|
func seventyFivePercentSaved() {
|
|
var status = SyncStatus()
|
|
status.totalBytesTransferred = 250
|
|
status.deltaSyncSavings = 750
|
|
// 750 / (250 + 750) * 100 = 75%
|
|
#expect(status.deltaSyncSavingsPercent == 75.0)
|
|
}
|
|
}
|
|
|
|
// MARK: - DeltaSyncResult Tests
|
|
|
|
struct DeltaSyncResultTests {
|
|
@Test("DeltaSyncResult deltaSavingsRatio is 0 when no data")
|
|
func zeroRatio() {
|
|
let result = DeltaSyncResult(
|
|
alertsChanged: false,
|
|
exposuresChanged: false,
|
|
watchlistChanged: false,
|
|
bytesTransferred: 0,
|
|
deltaSavings: 0,
|
|
newAlerts: [],
|
|
newExposures: []
|
|
)
|
|
#expect(result.deltaSavingsRatio == 0)
|
|
}
|
|
|
|
@Test("DeltaSyncResult deltaSavingsRatio calculates correctly")
|
|
func ratioCalculation() {
|
|
let result = DeltaSyncResult(
|
|
alertsChanged: true,
|
|
exposuresChanged: false,
|
|
watchlistChanged: false,
|
|
bytesTransferred: 250,
|
|
deltaSavings: 750,
|
|
newAlerts: [],
|
|
newExposures: []
|
|
)
|
|
// 750 / (250 + 750) = 0.75
|
|
#expect(result.deltaSavingsRatio == 0.75)
|
|
}
|
|
|
|
@Test("DeltaSyncResult deltaSavingsRatio is 1.0 when nothing transferred")
|
|
func allSaved() {
|
|
let result = DeltaSyncResult(
|
|
alertsChanged: false,
|
|
exposuresChanged: false,
|
|
watchlistChanged: false,
|
|
bytesTransferred: 0,
|
|
deltaSavings: 1000,
|
|
newAlerts: [],
|
|
newExposures: []
|
|
)
|
|
#expect(result.deltaSavingsRatio == 1.0)
|
|
}
|
|
}
|
|
|
|
// MARK: - BatteryUsageMetrics Tests
|
|
|
|
struct BatteryUsageMetricsTests {
|
|
@Test("BatteryUsageMetrics defaults are correct")
|
|
func defaults() {
|
|
let metrics = BatteryUsageMetrics()
|
|
#expect(metrics.totalSyncTime == 0)
|
|
#expect(metrics.syncCount == 0)
|
|
#expect(metrics.totalBytesTransferred == 0)
|
|
#expect(metrics.estimatedBatteryDrain == 0)
|
|
#expect(metrics.averageSyncDuration == 0)
|
|
#expect(metrics.batteryDrainPerSync == 0)
|
|
}
|
|
|
|
@Test("BatteryUsageMetrics records sync correctly")
|
|
func recordSync() {
|
|
var metrics = BatteryUsageMetrics()
|
|
metrics.recordSync(duration: 2.5, bytesTransferred: 10240)
|
|
#expect(metrics.syncCount == 1)
|
|
#expect(metrics.totalSyncTime == 2.5)
|
|
#expect(metrics.totalBytesTransferred == 10240)
|
|
#expect(metrics.averageSyncDuration == 2.5)
|
|
#expect(metrics.estimatedBatteryDrain > 0)
|
|
}
|
|
|
|
@Test("BatteryUsageMetrics accumulates across multiple syncs")
|
|
func accumulate() {
|
|
var metrics = BatteryUsageMetrics()
|
|
metrics.recordSync(duration: 2.0, bytesTransferred: 5000)
|
|
metrics.recordSync(duration: 3.0, bytesTransferred: 10000)
|
|
#expect(metrics.syncCount == 2)
|
|
#expect(metrics.totalSyncTime == 5.0)
|
|
#expect(metrics.totalBytesTransferred == 15000)
|
|
#expect(metrics.averageSyncDuration == 2.5)
|
|
}
|
|
|
|
@Test("BatteryUsageMetrics calculates drain per sync")
|
|
func batteryDrainPerSync() {
|
|
var metrics = BatteryUsageMetrics()
|
|
metrics.recordSync(duration: 2.0, bytesTransferred: 10240)
|
|
metrics.recordSync(duration: 3.0, bytesTransferred: 20480)
|
|
#expect(metrics.batteryDrainPerSync > 0)
|
|
#expect(metrics.batteryDrainPerSync == metrics.estimatedBatteryDrain / 2.0)
|
|
}
|
|
|
|
@Test("BatteryUsageMetrics reset clears all values")
|
|
func reset() {
|
|
var metrics = BatteryUsageMetrics()
|
|
metrics.recordSync(duration: 2.0, bytesTransferred: 10240)
|
|
metrics.reset()
|
|
#expect(metrics.syncCount == 0)
|
|
#expect(metrics.totalSyncTime == 0)
|
|
#expect(metrics.totalBytesTransferred == 0)
|
|
#expect(metrics.estimatedBatteryDrain == 0)
|
|
}
|
|
|
|
@Test("BatteryUsageMetrics is Codable")
|
|
func codable() throws {
|
|
var metrics = BatteryUsageMetrics()
|
|
metrics.recordSync(duration: 2.5, bytesTransferred: 10240)
|
|
let data = try JSONEncoder().encode(metrics)
|
|
let decoded = try JSONDecoder().decode(BatteryUsageMetrics.self, from: data)
|
|
#expect(decoded.syncCount == 1)
|
|
#expect(decoded.totalSyncTime == 2.5)
|
|
#expect(decoded.totalBytesTransferred == 10240)
|
|
}
|
|
}
|
|
|
|
// MARK: - SyncStatus Extended Tests
|
|
|
|
struct SyncStatusExtendedTests {
|
|
@Test("SyncStatus totalSyncCount starts at 0")
|
|
func defaultSyncCount() {
|
|
let status = SyncStatus()
|
|
#expect(status.totalSyncCount == 0)
|
|
}
|
|
|
|
@Test("SyncStatus syncSummary shows never when no syncs")
|
|
func syncSummaryNoSyncs() {
|
|
let status = SyncStatus()
|
|
#expect(status.syncSummary.contains("No syncs"))
|
|
}
|
|
|
|
@Test("SyncStatus syncSummary shows count and stats")
|
|
func syncSummaryWithStats() {
|
|
var status = SyncStatus()
|
|
status.totalSyncCount = 5
|
|
status.totalBytesTransferred = 2048
|
|
status.deltaSyncSavings = 1024
|
|
let summary = status.syncSummary
|
|
#expect(summary.contains("5 syncs"))
|
|
#expect(summary.contains("33%"))
|
|
}
|
|
|
|
@Test("SyncStatus equality includes totalSyncCount")
|
|
func equalityIncludesSyncCount() {
|
|
var status1 = SyncStatus()
|
|
var status2 = SyncStatus()
|
|
status1.totalSyncCount = 5
|
|
status2.totalSyncCount = 10
|
|
#expect(status1 != status2)
|
|
}
|
|
}
|
|
|
|
// MARK: - SyncStatusManager Extended Tests
|
|
|
|
@MainActor
|
|
struct SyncStatusManagerExtendedTests {
|
|
@Test("SyncStatusManager completeSync increments totalSyncCount")
|
|
func incrementSyncCount() {
|
|
let manager = SyncStatusManager(defaults: UserDefaults(suiteName: UUID().uuidString)!)
|
|
#expect(manager.status.totalSyncCount == 0)
|
|
manager.completeSync(bytesTransferred: 100, deltaSavings: 50)
|
|
#expect(manager.status.totalSyncCount == 1)
|
|
manager.completeSync(bytesTransferred: 200, deltaSavings: 100)
|
|
#expect(manager.status.totalSyncCount == 2)
|
|
}
|
|
|
|
@Test("SyncStatusManager persists totalSyncCount")
|
|
func persistSyncCount() {
|
|
let defaults = UserDefaults(suiteName: UUID().uuidString)!
|
|
let manager1 = SyncStatusManager(defaults: defaults)
|
|
manager1.completeSync(bytesTransferred: 100, deltaSavings: 50)
|
|
manager1.completeSync(bytesTransferred: 200, deltaSavings: 100)
|
|
|
|
let manager2 = SyncStatusManager(defaults: defaults)
|
|
#expect(manager2.status.totalSyncCount == 2)
|
|
}
|
|
}
|
|
|
|
// MARK: - BackgroundTaskScheduler Extended Tests
|
|
|
|
struct BackgroundTaskSchedulerExtendedTests {
|
|
@Test("BackgroundTaskScheduler rescheduleAllTasks does not crash")
|
|
func rescheduleAllTasks() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
// Should not throw
|
|
scheduler.rescheduleAllTasks()
|
|
}
|
|
|
|
@Test("BackgroundTaskScheduler rescheduleAllTasks skips when task is running")
|
|
func rescheduleSkipsWhenRunning() {
|
|
let scheduler = BackgroundTaskScheduler()
|
|
// Initially no task running, reschedule should work
|
|
scheduler.rescheduleAllTasks()
|
|
#expect(scheduler.currentlyHasRunningTask == false)
|
|
}
|
|
}
|
|
|
|
// MARK: - BackgroundSyncService Extended Tests
|
|
|
|
struct BackgroundSyncServiceExtendedTests {
|
|
@Test("BackgroundSyncService battery metrics start at zero")
|
|
func batteryMetricsStartAtZero() {
|
|
let metrics = BackgroundSyncService.shared.currentBatteryMetrics
|
|
#expect(metrics.syncCount == 0)
|
|
#expect(metrics.totalSyncTime == 0)
|
|
#expect(metrics.estimatedBatteryDrain == 0)
|
|
}
|
|
|
|
@Test("BackgroundSyncService resetBatteryMetrics clears values")
|
|
func resetBatteryMetrics() {
|
|
BackgroundSyncService.shared.resetBatteryMetrics()
|
|
let metrics = BackgroundSyncService.shared.currentBatteryMetrics
|
|
#expect(metrics.syncCount == 0)
|
|
#expect(metrics.totalSyncTime == 0)
|
|
#expect(metrics.estimatedBatteryDrain == 0)
|
|
}
|
|
}
|
|
|
|
// MARK: - ConditionalResponseMetadata Tests
|
|
|
|
struct ConditionalResponseMetadataTests {
|
|
@Test("ConditionalResponseMetadata defaults are correct")
|
|
func defaults() {
|
|
let metadata = ConditionalResponseMetadata(
|
|
etag: nil,
|
|
lastModified: nil,
|
|
notModified: false
|
|
)
|
|
#expect(metadata.etag == nil)
|
|
#expect(metadata.lastModified == nil)
|
|
#expect(metadata.notModified == false)
|
|
}
|
|
|
|
@Test("ConditionalResponseMetadata stores ETag and lastModified")
|
|
func storesHeaders() {
|
|
let metadata = ConditionalResponseMetadata(
|
|
etag: "\"abc123\"",
|
|
lastModified: "Wed, 21 Oct 2015 07:28:00 GMT",
|
|
notModified: false
|
|
)
|
|
#expect(metadata.etag == "\"abc123\"")
|
|
#expect(metadata.lastModified == "Wed, 21 Oct 2015 07:28:00 GMT")
|
|
#expect(metadata.notModified == false)
|
|
}
|
|
|
|
@Test("ConditionalResponseMetadata marks 304 response")
|
|
func notModified() {
|
|
let metadata = ConditionalResponseMetadata(
|
|
etag: "\"abc123\"",
|
|
lastModified: nil,
|
|
notModified: true
|
|
)
|
|
#expect(metadata.notModified == true)
|
|
}
|
|
}
|