test(phase1): automate TEST 4 invalid data handling verification
Implements automated testing for TEST 4 (Invalid Data Handling) to verify
recovery gracefully handles invalid database entries without crashing.
Changes:
- Add injectInvalidTestData plugin method for injecting invalid test data
(empty schedule IDs, null nextRunAt, empty notification IDs)
- Make test app debuggable to enable direct database access
- Enhance test-phase1.sh with automated database injection and verification:
* Detect debuggable app status (check for DEBUGGABLE flag)
* Inject invalid data via direct SQL (schedules and notifications)
* Handle WAL mode with checkpoint
* Verify data injection success
* Trigger recovery and check logs for "Skipping invalid" messages
* Report pass/fail/inconclusive results
Fixes database constraint issues discovered during testing:
- Include jitterMs and backoffPolicy in schedule inserts
- Include priority, vibration_enabled, sound_enabled in notification inserts
Test results: ✅ PASSED
- Invalid data successfully injected
- Cold start recovery correctly skips invalid entries
- Recovery completes without crashing
- Boot recovery processes invalid data (follow-up improvement needed)
This enables automated verification that recovery handles corrupted or
invalid database entries gracefully, preventing crashes in production.
This commit is contained in:
@@ -1487,6 +1487,112 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method: Inject invalid data into database for recovery testing
|
||||
*
|
||||
* This method is used by TEST 4 to verify that recovery handles invalid
|
||||
* data gracefully (empty IDs, null nextRunAt, etc.) without crashing.
|
||||
*
|
||||
* @param call Plugin call with optional parameters:
|
||||
* - injectEmptyScheduleId: boolean (default: true) - inject schedule with empty ID
|
||||
* - injectNullNextRunAt: boolean (default: true) - inject schedule with null nextRunAt
|
||||
* - injectEmptyNotificationId: boolean (default: true) - inject notification with empty ID
|
||||
*/
|
||||
@PluginMethod
|
||||
fun injectInvalidTestData(call: PluginCall) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val options = call.data
|
||||
val injectEmptyScheduleId = options?.getBoolean("injectEmptyScheduleId") ?: true
|
||||
val injectNullNextRunAt = options?.getBoolean("injectNullNextRunAt") ?: true
|
||||
val injectEmptyNotificationId = options?.getBoolean("injectEmptyNotificationId") ?: true
|
||||
|
||||
val db = getDatabase()
|
||||
val injected = mutableListOf<String>()
|
||||
|
||||
// Inject schedule with empty ID
|
||||
if (injectEmptyScheduleId) {
|
||||
try {
|
||||
val invalidSchedule = Schedule(
|
||||
id = "", // Empty ID - should be skipped by recovery
|
||||
kind = "notify",
|
||||
cron = "0 9 * * *",
|
||||
clockTime = "09:00",
|
||||
enabled = true,
|
||||
nextRunAt = System.currentTimeMillis() + 86400000L
|
||||
)
|
||||
db.scheduleDao().upsert(invalidSchedule)
|
||||
injected.add("empty_schedule_id")
|
||||
Log.i(TAG, "TEST: Injected schedule with empty ID")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "TEST: Failed to inject empty schedule ID", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Inject schedule with null nextRunAt
|
||||
if (injectNullNextRunAt) {
|
||||
try {
|
||||
val invalidSchedule = Schedule(
|
||||
id = "test_null_nextrunat",
|
||||
kind = "notify",
|
||||
cron = "0 9 * * *",
|
||||
clockTime = "09:00",
|
||||
enabled = true,
|
||||
nextRunAt = null // Null nextRunAt - should be skipped by recovery
|
||||
)
|
||||
db.scheduleDao().upsert(invalidSchedule)
|
||||
injected.add("null_nextrunat")
|
||||
Log.i(TAG, "TEST: Injected schedule with null nextRunAt")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "TEST: Failed to inject null nextRunAt", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Inject notification with empty ID
|
||||
// Note: Room's @NonNull constraint may prevent this, but we try anyway
|
||||
// If it fails, the other invalid data types (null nextRunAt) will still test recovery
|
||||
if (injectEmptyNotificationId) {
|
||||
try {
|
||||
val invalidNotification =
|
||||
com.timesafari.dailynotification.entities.NotificationContentEntity()
|
||||
invalidNotification.id = "" // Empty ID - should be skipped by recovery
|
||||
invalidNotification.title = "Test Invalid Notification"
|
||||
invalidNotification.body = "This has an empty ID"
|
||||
invalidNotification.scheduledTime = System.currentTimeMillis() - 3600000L // 1 hour ago
|
||||
invalidNotification.deliveryStatus = "pending"
|
||||
invalidNotification.deliveryAttempts = 0
|
||||
invalidNotification.lastDeliveryAttempt = 0
|
||||
invalidNotification.userInteractionCount = 0
|
||||
invalidNotification.lastUserInteraction = 0
|
||||
invalidNotification.ttlSeconds = 86400L
|
||||
invalidNotification.createdAt = System.currentTimeMillis()
|
||||
invalidNotification.updatedAt = System.currentTimeMillis()
|
||||
|
||||
db.notificationContentDao().insertNotification(invalidNotification)
|
||||
injected.add("empty_notification_id")
|
||||
Log.i(TAG, "TEST: Injected notification with empty ID")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "TEST: Failed to inject empty notification ID (Room @NonNull constraint may prevent this): ${e.message}")
|
||||
Log.i(TAG, "TEST: Other invalid data types (null nextRunAt, empty schedule ID) will still test recovery")
|
||||
// This is expected - Room may reject empty primary keys
|
||||
// The other invalid data types will still test recovery handling
|
||||
}
|
||||
}
|
||||
|
||||
val result = JSObject().apply {
|
||||
put("success", true)
|
||||
put("injected", JSONArray(injected))
|
||||
put("message", "Invalid test data injected: ${injected.joinToString(", ")}")
|
||||
}
|
||||
|
||||
call.resolve(result)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to inject invalid test data", e)
|
||||
call.reject("Failed to inject invalid test data: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun scheduleUserNotification(call: PluginCall) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user