feat(ios): add error handling and integration tests

Implement comprehensive error handling and integration test suite:

Error Handling (Section 8):
- Add iOS-specific error codes to DailyNotificationErrorCodes:
  - NOTIFICATION_PERMISSION_DENIED
  - PENDING_NOTIFICATION_LIMIT_EXCEEDED
  - BG_TASK_NOT_REGISTERED
  - BG_TASK_EXECUTION_FAILED
  - BACKGROUND_REFRESH_DISABLED
- Add helper methods for iOS-specific error responses
- Enhance error handling in ReactivationManager:
  - Database errors handled gracefully (non-fatal)
  - Notification center errors handled gracefully (non-fatal)
  - Scheduling errors handled gracefully (non-fatal)
  - All errors logged, app continues normally
  - Partial results returned when operations fail
- Update plugin methods to use iOS-specific error codes:
  - getNotificationPermissionStatus uses NOTIFICATION_PERMISSION_DENIED

Integration Tests (Section 9.2):
- Add DailyNotificationRecoveryIntegrationTests:
  - Full recovery flow tests (cold start, termination)
  - Error handling tests (database, notification center, scheduling)
  - App stability tests (no crashes, concurrent operations)
  - Partial recovery tests
  - Timeout handling tests
- Test coverage:
  - 10 integration tests covering recovery scenarios
  - Error handling verification
  - App stability verification
  - Concurrent operation safety

Completes sections 8.1, 8.2, and 9.2 of iOS implementation checklist.
This commit is contained in:
Matthew
2025-12-09 02:46:13 -08:00
parent 12d8536588
commit 3649e76c49
5 changed files with 650 additions and 29 deletions

View File

@@ -189,14 +189,32 @@ class DailyNotificationReactivationManager {
*/
internal func detectScenario() async throws -> RecoveryScenario {
// Step 1: Check if database has notifications
let allNotifications = storage.getAllNotifications()
// Handle storage errors gracefully (non-fatal)
let allNotifications: [NotificationContent]
do {
allNotifications = storage.getAllNotifications()
} catch {
// Non-fatal: Log error and assume empty storage
NSLog("\(Self.TAG): Error getting notifications from storage (non-fatal): \(error.localizedDescription)")
return .none
}
if allNotifications.isEmpty {
return .none // First launch
}
// Step 2: Get pending notifications from UNUserNotificationCenter
let pendingRequests = try await notificationCenter.pendingNotificationRequests()
// Handle notification center errors gracefully (non-fatal)
let pendingRequests: [UNNotificationRequest]
do {
pendingRequests = try await notificationCenter.pendingNotificationRequests()
} catch {
// Non-fatal: Log error and assume no pending notifications
NSLog("\(Self.TAG): Error getting pending notifications (non-fatal): \(error.localizedDescription)")
// Return cold start as safe default - will trigger recovery
return .coldStart
}
let pendingIds = Set(pendingRequests.map { $0.identifier })
// Step 3: Get notification IDs from storage
@@ -317,7 +335,15 @@ class DailyNotificationReactivationManager {
*/
internal func detectMissedNotifications(currentTime: Date) async throws -> [NotificationContent] {
// Get all notifications from storage
let allNotifications = storage.getAllNotifications()
// Handle database/storage errors gracefully (non-fatal)
let allNotifications: [NotificationContent]
do {
allNotifications = storage.getAllNotifications()
} catch {
// Non-fatal: Log error and return empty array
NSLog("\(Self.TAG): Error getting notifications from storage (non-fatal): \(error.localizedDescription)")
return []
}
// Convert currentTime to milliseconds (Int64) for comparison
let currentTimeMs = Int64(currentTime.timeIntervalSince1970 * 1000)
@@ -368,12 +394,46 @@ class DailyNotificationReactivationManager {
*/
internal func verifyFutureNotifications() async throws -> VerificationResult {
// Get pending notifications from UNUserNotificationCenter
let pendingRequests = try await notificationCenter.pendingNotificationRequests()
// Handle notification center errors gracefully (non-fatal)
let pendingRequests: [UNNotificationRequest]
do {
pendingRequests = try await notificationCenter.pendingNotificationRequests()
} catch {
// Non-fatal: Log error and assume no pending notifications
NSLog("\(Self.TAG): Error getting pending notifications (non-fatal): \(error.localizedDescription)")
// Return verification result indicating all are missing
let currentTimeMs = Int64(Date().timeIntervalSince1970 * 1000)
let allNotifications = storage.getAllNotifications()
let futureNotifications = allNotifications.filter { $0.scheduledTime >= currentTimeMs }
let futureIds = Set(futureNotifications.map { $0.id })
return VerificationResult(
totalSchedules: futureNotifications.count,
notificationsFound: 0,
notificationsMissing: futureIds.count,
missingIds: Array(futureIds)
)
}
let pendingIds = Set(pendingRequests.map { $0.identifier })
// Get all notifications from storage that are scheduled for future
// Handle storage errors gracefully (non-fatal)
let currentTimeMs = Int64(Date().timeIntervalSince1970 * 1000)
let allNotifications = storage.getAllNotifications()
let allNotifications: [NotificationContent]
do {
allNotifications = storage.getAllNotifications()
} catch {
// Non-fatal: Log error and return empty verification result
NSLog("\(Self.TAG): Error getting notifications from storage (non-fatal): \(error.localizedDescription)")
return VerificationResult(
totalSchedules: 0,
notificationsFound: pendingIds.count,
notificationsMissing: 0,
missingIds: []
)
}
let futureNotifications = allNotifications.filter { $0.scheduledTime >= currentTimeMs }
let futureIds = Set(futureNotifications.map { $0.id })
@@ -397,14 +457,27 @@ class DailyNotificationReactivationManager {
*/
private func rescheduleMissingNotification(id: String) async throws {
// Get notification content from storage
guard let notification = storage.getNotificationContent(id: id) else {
// Handle storage errors gracefully (non-fatal)
let notification: NotificationContent?
do {
notification = storage.getNotificationContent(id: id)
} catch {
// Non-fatal: Log error and throw to be caught by caller
NSLog("\(Self.TAG): Error getting notification from storage (non-fatal): \(error.localizedDescription)")
throw ReactivationError.notificationNotFound(id: id)
}
guard let notification = notification else {
throw ReactivationError.notificationNotFound(id: id)
}
// Reschedule using scheduler
// Handle scheduling errors gracefully (non-fatal)
let success = await scheduler.scheduleNotification(notification)
if !success {
// Non-fatal: Log error and throw to be caught by caller
NSLog("\(Self.TAG): Failed to reschedule notification \(id) (non-fatal)")
throw ReactivationError.rescheduleFailed(id: id)
}
}