Files
Kordant/iOS/KordantTests/BackgroundSyncTests.swift
Michael Freno e33ddf3002 feat: complete Tasks 21-28 — backend integration, security hardening, UI tests & CI
- 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
2026-06-02 15:01:38 -04:00

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)
}
}