Skip to content

Commit 58470ff

Browse files
authored
Close #487 improve backup exception handler & test (#489)
1 parent f196444 commit 58470ff

29 files changed

+2730
-232
lines changed

docs/backup_exception_handling.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# Backup Exception Handling Implementation Summary
2+
3+
## Overview
4+
5+
This implementation provides comprehensive exception handling for the StoryPad backup sync system, following MVVM architecture principles and ensuring robust error management across all layers.
6+
7+
## Architecture
8+
9+
### Exception Flow
10+
11+
```
12+
Google Drive Client → Backup Sync Service → Repository → Backup Provider
13+
(Throws) → (Catches/Wraps) → (Safe Types) → (UI States)
14+
```
15+
16+
## Key Components
17+
18+
### 1. Exception Types (`lib/core/exceptions/backup_exceptions.dart`)
19+
20+
**Base Exception:**
21+
22+
- `BackupException` - Abstract base class with message, context, and retry information
23+
24+
**Specific Exception Types:**
25+
26+
- `NetworkException` - Network connectivity issues (retryable by default)
27+
- `AuthException` - Authentication/authorization failures with specific types:
28+
- `tokenExpired` - Requires reauthentication
29+
- `tokenRevoked` - Requires sign out and new login
30+
- `insufficientScopes` - Requires scope request
31+
- `signInRequired` - User needs to sign in
32+
- `signInFailed` - Sign-in process failed
33+
- `QuotaException` - Google Drive quota and rate limiting
34+
- `FileOperationException` - File upload/download/delete failures (retryable)
35+
- `ServiceException` - Business logic and data processing errors
36+
- `ConfigurationException` - Setup and configuration errors
37+
38+
### 2. Result Types (`lib/core/types/backup_result.dart`)
39+
40+
**BackupResult<T>** - Sealed class with three variants:
41+
42+
- `BackupSuccess<T>` - Operation completed successfully
43+
- `BackupFailure<T>` - Operation failed completely
44+
- `BackupPartialSuccess<T>` - Some operations succeeded, some failed
45+
46+
**BackupError** - Standardized error representation:
47+
48+
- Type classification (network, auth, quota, etc.)
49+
- User-friendly messages
50+
- Retry capability flags
51+
- Contextual metadata
52+
53+
### 3. Retry Policy (`lib/core/utils/retry_policy.dart`)
54+
55+
**RetryPolicy:**
56+
57+
- Configurable retry attempts, delays, and backoff strategies
58+
- Exception type filtering
59+
- Predefined policies for common scenarios:
60+
- `RetryPolicy.network` - For network operations
61+
- `RetryPolicy.quota` - For quota-limited operations
62+
- `RetryPolicy.none` - No retry
63+
64+
**RetryExecutor:**
65+
66+
- Automatic retry logic with exponential backoff
67+
- Timeout support
68+
- Safe result wrapping
69+
70+
### 4. Enhanced Google Drive Client (`lib/core/services/google_drive_client.dart`)
71+
72+
**Authentication Enhancements:**
73+
74+
- Proper token expiration detection and handling
75+
- Token revocation detection
76+
- Scope insufficiency handling
77+
- User-friendly error messages
78+
79+
**API Operation Safety:**
80+
81+
- HTTP status code to exception mapping
82+
- Network error detection and classification
83+
- File operation context preservation
84+
- Automatic retry for transient failures
85+
86+
### 5. Updated Backup Services
87+
88+
**Service Layer Changes:**
89+
90+
- `BackupImagesUploaderService` - Enhanced with retry logic and auth handling
91+
- `BackupUploaderService` - Improved error reporting and file handling
92+
- `BackupLatestCheckerService` - Better content validation and download retry
93+
- All services now throw specific exceptions instead of generic errors
94+
95+
### 6. Repository Layer (`lib/core/repositories/backup_repository.dart`)
96+
97+
**Safe Result Wrapping:**
98+
99+
- All public methods return `BackupResult<T>` instead of throwing
100+
- Authentication error detection and user sign-out handling
101+
- Connection status mapping from exceptions
102+
- Comprehensive error context preservation
103+
104+
### 7. Provider Layer (`lib/providers/backup_provider.dart`)
105+
106+
**UI State Management:**
107+
108+
- Proper handling of `BackupResult` types
109+
- User-friendly error message display
110+
- Automatic sign-out on token revocation
111+
- Graceful fallback for partial failures
112+
113+
## Error Handling Scenarios
114+
115+
### 1. Token Expiration
116+
117+
- **Detection:** 401 HTTP responses, token validation failures
118+
- **Handling:** Automatic reauthentication attempt
119+
- **UI:** "Your session has expired. Please sign in again."
120+
121+
### 2. Token Revocation
122+
123+
- **Detection:** 403 responses, scope validation failures
124+
- **Handling:** Automatic sign-out, clear stored credentials
125+
- **UI:** "Access has been revoked. Please sign in again to continue using backup."
126+
127+
### 3. Network Issues
128+
129+
- **Detection:** SocketException, TimeoutException
130+
- **Handling:** Automatic retry with exponential backoff
131+
- **UI:** "Network connection error. Please check your internet connection and try again."
132+
133+
### 4. Storage Quota Exceeded
134+
135+
- **Detection:** Specific Google Drive API error messages
136+
- **Handling:** No retry, immediate user notification
137+
- **UI:** "Google Drive storage is full. Please free up space or upgrade your storage plan."
138+
139+
### 5. Rate Limiting
140+
141+
- **Detection:** 429 HTTP responses
142+
- **Handling:** Retry with rate limit aware delays
143+
- **UI:** "Too many requests. Please wait a moment before trying again."
144+
145+
## Testing
146+
147+
### Test Coverage
148+
149+
- **Exception Types:** Comprehensive unit tests for all exception scenarios
150+
- **Result Types:** Complete testing of success, failure, and partial success cases
151+
- **Retry Logic:** Various retry scenarios and policy configurations
152+
- **Integration:** End-to-end error handling scenarios
153+
154+
### Test Files
155+
156+
- `test/core/exceptions/backup_exceptions_test.dart` - Exception type tests
157+
- `test/core/types/backup_result_test.dart` - Result type functionality
158+
- `test/core/utils/retry_policy_test.dart` - Retry logic and policies
159+
- `test/core/services/google_drive_client_test.dart` - Client error scenarios
160+
161+
## Benefits
162+
163+
### For Users
164+
165+
- Clear, actionable error messages
166+
- Automatic recovery from transient issues
167+
- Graceful handling of authentication problems
168+
- Continued functionality during partial failures
169+
170+
### For Developers
171+
172+
- Type-safe error handling
173+
- Centralized error classification
174+
- Comprehensive logging and debugging context
175+
- Easy testing of error scenarios
176+
177+
### For Maintenance
178+
179+
- Consistent error handling patterns
180+
- Clear separation of concerns
181+
- Extensible exception hierarchy
182+
- Robust retry and recovery mechanisms
183+
184+
## Key Features
185+
186+
### 🔐 Authentication Handling
187+
188+
- Automatic token refresh
189+
- Graceful handling of revoked access
190+
- Smart sign-out on permanent auth failures
191+
192+
### 🔄 Retry Logic
193+
194+
- Exponential backoff with jitter
195+
- Configurable retry policies
196+
- Rate limit aware delays
197+
198+
### 📱 User Experience
199+
200+
- User-friendly error messages
201+
- Progress indication during retries
202+
- Offline mode support
203+
204+
### 🧪 Testing
205+
206+
- Comprehensive test coverage
207+
- Mock-friendly architecture
208+
- Error scenario validation
209+
210+
This implementation ensures robust, user-friendly backup functionality that gracefully handles all common failure scenarios while maintaining the UI's functional integrity.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
part of 'backup_exception.dart';
2+
3+
enum AuthExceptionType {
4+
tokenExpired,
5+
tokenRevoked,
6+
insufficientScopes,
7+
signInRequired,
8+
signInFailed,
9+
}
10+
11+
/// Google authentication and authorization exceptions
12+
class AuthException extends BackupException {
13+
final AuthExceptionType type;
14+
15+
const AuthException(
16+
super.message,
17+
this.type, {
18+
super.context,
19+
super.isRetryable = false,
20+
});
21+
22+
@override
23+
String get userFriendlyMessage {
24+
switch (type) {
25+
case AuthExceptionType.tokenExpired:
26+
return 'Your session has expired. Please sign in again.';
27+
case AuthExceptionType.tokenRevoked:
28+
return 'Access has been revoked. Please sign in again to continue using backup.';
29+
case AuthExceptionType.insufficientScopes:
30+
return 'Additional permissions are required for backup functionality.';
31+
case AuthExceptionType.signInRequired:
32+
return 'Please sign in to Google Drive to use backup features.';
33+
case AuthExceptionType.signInFailed:
34+
return 'Failed to sign in to Google Drive. Please try again.';
35+
}
36+
}
37+
38+
bool get requiresSignOut => type == AuthExceptionType.tokenRevoked;
39+
bool get requiresReauth => type == AuthExceptionType.tokenExpired || type == AuthExceptionType.signInRequired;
40+
bool get requiresScopeRequest => type == AuthExceptionType.insufficientScopes;
41+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
part 'auth_exception.dart';
2+
part 'configuration_exception.dart';
3+
part 'file_operation_exception.dart';
4+
part 'network_exception.dart';
5+
part 'quota_exception.dart';
6+
part 'service_exception.dart';
7+
8+
/// Base exception for all backup-related errors
9+
abstract class BackupException implements Exception {
10+
final String message;
11+
final String? context;
12+
final bool isRetryable;
13+
14+
const BackupException(
15+
this.message, {
16+
this.context,
17+
this.isRetryable = false,
18+
});
19+
20+
@override
21+
String toString() => 'BackupException: $message${context != null ? ' ($context)' : ''}';
22+
23+
/// Creates user-friendly error message for UI display
24+
String get userFriendlyMessage => message;
25+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
part of 'backup_exception.dart';
2+
3+
/// Configuration or setup related exceptions
4+
class ConfigurationException extends BackupException {
5+
const ConfigurationException(super.message, {super.context, super.isRetryable = false});
6+
7+
@override
8+
String get userFriendlyMessage => 'Configuration error. Please restart the app or contact support.';
9+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
part of 'backup_exception.dart';
2+
3+
enum FileOperationType {
4+
upload,
5+
download,
6+
delete,
7+
list,
8+
}
9+
10+
/// File operation exceptions
11+
class FileOperationException extends BackupException {
12+
final FileOperationType operation;
13+
14+
const FileOperationException(
15+
super.message,
16+
this.operation, {
17+
super.context,
18+
super.isRetryable = true,
19+
});
20+
21+
@override
22+
String get userFriendlyMessage {
23+
switch (operation) {
24+
case FileOperationType.upload:
25+
return 'Failed to upload backup. Please try again.';
26+
case FileOperationType.download:
27+
return 'Failed to download backup. Please check your connection and try again.';
28+
case FileOperationType.delete:
29+
return 'Failed to delete backup file. Please try again.';
30+
case FileOperationType.list:
31+
return 'Failed to load backup files. Please check your connection and try again.';
32+
}
33+
}
34+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
part of 'backup_exception.dart';
2+
3+
/// Network-related exceptions
4+
class NetworkException extends BackupException {
5+
const NetworkException(super.message, {super.context, super.isRetryable = true});
6+
7+
@override
8+
String get userFriendlyMessage => 'Network connection error. Please check your internet connection and try again.';
9+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
part of 'backup_exception.dart';
2+
3+
enum QuotaExceptionType {
4+
storageQuotaExceeded,
5+
rateLimitExceeded,
6+
dailyLimitExceeded,
7+
}
8+
9+
/// Google Drive API quota and storage exceptions
10+
class QuotaException extends BackupException {
11+
final QuotaExceptionType type;
12+
13+
const QuotaException(
14+
super.message,
15+
this.type, {
16+
super.context,
17+
super.isRetryable = false,
18+
});
19+
20+
@override
21+
String get userFriendlyMessage {
22+
switch (type) {
23+
case QuotaExceptionType.storageQuotaExceeded:
24+
return 'Google Drive storage is full. Please free up space or upgrade your storage plan.';
25+
case QuotaExceptionType.rateLimitExceeded:
26+
return 'Too many requests. Please wait a moment before trying again.';
27+
case QuotaExceptionType.dailyLimitExceeded:
28+
return 'Daily API limit reached. Please try again tomorrow.';
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)