From 1408d0cd1dc902b5fa4d627aa3b8f24a3ed2744c Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Tue, 2 Jun 2026 17:38:21 -0400 Subject: [PATCH] last one --- iOS/.gitignore | 1 + iOS/Kordant.xcodeproj/project.pbxproj | 13 +- iOS/KordantTests/BackgroundSyncTests.swift | 255 +++++++++++++++++++++ tasks/ios-production/README.md | 2 +- 4 files changed, 261 insertions(+), 10 deletions(-) create mode 100644 iOS/.gitignore diff --git a/iOS/.gitignore b/iOS/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/iOS/.gitignore @@ -0,0 +1 @@ +build diff --git a/iOS/Kordant.xcodeproj/project.pbxproj b/iOS/Kordant.xcodeproj/project.pbxproj index 580791d..5f00083 100644 --- a/iOS/Kordant.xcodeproj/project.pbxproj +++ b/iOS/Kordant.xcodeproj/project.pbxproj @@ -90,6 +90,7 @@ 9F1BBA09F99BFA122CE4F25B /* SiriIntentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79C899D0CE53C5AD2CC34B72 /* SiriIntentsTests.swift */; }; A1D77AB578439B70433604BC /* ShieldToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 856911533BDD3778A4B73846 /* ShieldToast.swift */; }; A2A7B622BE4A8E8D70F46DB0 /* BackupExclusionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81B2029FDD789080FB8940E /* BackupExclusionHelper.swift */; }; + A4307EED5A69FD9876587EA2 /* BGTaskRegistration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C089AC1DCAE26FF815925D76 /* BGTaskRegistration.swift */; }; A4693DD9CE09C6CA0FCB1256 /* PermissionRationaleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F31D8ECA2B62ADBCCC615 /* PermissionRationaleView.swift */; }; A4A544DDAA6F3FF2251C8E80 /* NotificationAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A155702BEE39B630103DB5 /* NotificationAnalytics.swift */; }; A501AFBADC2B566D0AAE1F97 /* HomeTitleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B71A19E97A49C6426A2BFE5 /* HomeTitleViewModel.swift */; }; @@ -332,6 +333,7 @@ B92B0397F4DBE1F2F79DCF96 /* AsyncSemaphore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSemaphore.swift; sourceTree = ""; }; BC0631E3D41BDAF51CF2AAF5 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; BF48DCC2B7B0F0CAC8BBAE74 /* CallRecorderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRecorderService.swift; sourceTree = ""; }; + C089AC1DCAE26FF815925D76 /* BGTaskRegistration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BGTaskRegistration.swift; sourceTree = ""; }; C22A2495F0B7162D77898D43 /* AuthFlowUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthFlowUITests.swift; sourceTree = ""; }; C23E16CD2648BB923A600486 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; C654597D61C877BCEC6033A1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -426,6 +428,7 @@ 62763E6E8E89624887F90E47 /* TRPCBridge.swift */, 099FBF47526E8BF21E966CE7 /* WidgetDataService.swift */, F9567672F4E0FFF633E651CE /* Security */, + C089AC1DCAE26FF815925D76 /* BGTaskRegistration.swift */, ); path = Services; sourceTree = ""; @@ -746,8 +749,6 @@ dependencies = ( ); name = KordantWidgets; - packageProductDependencies = ( - ); productName = KordantWidgets; productReference = 1550C2D8DAC3644E57EE2293 /* KordantWidgets.appex */; productType = "com.apple.product-type.app-extension"; @@ -764,8 +765,6 @@ dependencies = ( ); name = KordantSpamShieldExtension; - packageProductDependencies = ( - ); productName = KordantSpamShieldExtension; productReference = A94EF21C88A991CB44E369C6 /* KordantSpamShieldExtension.appex */; productType = "com.apple.product-type.app-extension"; @@ -782,8 +781,6 @@ 27D92BC556A061277B7E39B6 /* PBXTargetDependency */, ); name = KordantUITests; - packageProductDependencies = ( - ); productName = KordantUITests; productReference = 140490443DB9EB9F7D363E53 /* KordantUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; @@ -800,8 +797,6 @@ 59D903BA1449A52AA4740FB6 /* PBXTargetDependency */, ); name = KordantTests; - packageProductDependencies = ( - ); productName = KordantTests; productReference = 478A94508A02D7E028AAAAED /* KordantTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -862,7 +857,6 @@ 56BFCA7A2585E9E10D0679FA /* XCRemoteSwiftPackageReference "swift-collections" */, 76B498AC6A201A7B03687F68 /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, ); - preferredProjectObjectVersion = 77; productRefGroup = C03F0C3C0F49ED169EEE5E4B /* Products */; projectDirPath = ""; projectRoot = ""; @@ -1106,6 +1100,7 @@ 065699225925ACA0A6EAB6A3 /* WidgetData.swift in Sources */, B7FBE8EDE4B42C44A6FBF93B /* WidgetDataManager.swift in Sources */, 0B8C5B12B08FCC49DDFF04BF /* WidgetDataService.swift in Sources */, + A4307EED5A69FD9876587EA2 /* BGTaskRegistration.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/iOS/KordantTests/BackgroundSyncTests.swift b/iOS/KordantTests/BackgroundSyncTests.swift index 068fd1b..5e9cce6 100644 --- a/iOS/KordantTests/BackgroundSyncTests.swift +++ b/iOS/KordantTests/BackgroundSyncTests.swift @@ -571,3 +571,258 @@ struct DeltaSyncSavingsTests { #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) + } +} diff --git a/tasks/ios-production/README.md b/tasks/ios-production/README.md index e7acd90..53b6202 100644 --- a/tasks/ios-production/README.md +++ b/tasks/ios-production/README.md @@ -39,7 +39,7 @@ Status legend: [ ] todo, [~] in-progress, [x] done ### Backend Integration - [x] 21 — Real API Client Wiring (Replace StubAPIClient) → `21-real-api-client.md` - [x] 22 — Token Refresh & Session Management → `22-token-refresh.md` -- [~] 23 — Offline Mode & Sync Conflict Resolution → `23-offline-sync.md` +- [x] 23 — Offline Mode & Sync Conflict Resolution → `23-offline-sync.md` - [x] 24 — Push Notification Deep Linking → `24-push-deep-links.md` ### App Store Compliance