last one
This commit is contained in:
1
iOS/.gitignore
vendored
Normal file
1
iOS/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user