This commit is contained in:
2026-06-02 17:38:21 -04:00
parent 1511a844a7
commit 1408d0cd1d
4 changed files with 261 additions and 10 deletions

1
iOS/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build

View File

@@ -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 = "<group>"; };
BC0631E3D41BDAF51CF2AAF5 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
BF48DCC2B7B0F0CAC8BBAE74 /* CallRecorderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRecorderService.swift; sourceTree = "<group>"; };
C089AC1DCAE26FF815925D76 /* BGTaskRegistration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BGTaskRegistration.swift; sourceTree = "<group>"; };
C22A2495F0B7162D77898D43 /* AuthFlowUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthFlowUITests.swift; sourceTree = "<group>"; };
C23E16CD2648BB923A600486 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
C654597D61C877BCEC6033A1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -426,6 +428,7 @@
62763E6E8E89624887F90E47 /* TRPCBridge.swift */,
099FBF47526E8BF21E966CE7 /* WidgetDataService.swift */,
F9567672F4E0FFF633E651CE /* Security */,
C089AC1DCAE26FF815925D76 /* BGTaskRegistration.swift */,
);
path = Services;
sourceTree = "<group>";
@@ -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;
};

View File

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

View File

@@ -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