/**
* Negative tests for database failure handling
* Tests graceful handling of SQLite errors, timeouts, and connection issues
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
describe('Database Failure Handling', () => {
describe('SQLite error handling', () => {
// Simulate SQLite query function that can fail
const createMockSqlite = (options = {}) => {
const {
shouldFail = false,
errorType = 'BUSY',
errorMessage = 'database is locked',
failAfter = 0,
results = []
} = options
let callCount = 0
return (db, query, queryOptions = {}) => {
callCount++
if (shouldFail && callCount > failAfter) {
const error = new Error(errorMessage)
error.code = errorType
throw error
}
return results
}
}
it('should handle database locked error (SQLITE_BUSY)', () => {
const mockSqlite = createMockSqlite({
shouldFail: true,
errorType: 'SQLITE_BUSY',
errorMessage: 'database is locked'
})
const executeQuery = () => {
try {
return { success: true, data: mockSqlite('test.db', 'SELECT * FROM messages') }
} catch (e) {
if (e.code === 'SQLITE_BUSY') {
return { success: false, error: 'database_locked', retryable: true }
}
throw e
}
}
const result = executeQuery()
expect(result.success).toBe(false)
expect(result.error).toBe('database_locked')
expect(result.retryable).toBe(true)
})
it('should handle permission denied error', () => {
const mockSqlite = createMockSqlite({
shouldFail: true,
errorType: 'SQLITE_CANTOPEN',
errorMessage: 'unable to open database file'
})
const executeQuery = () => {
try {
return { success: true, data: mockSqlite('test.db', 'SELECT * FROM messages') }
} catch (e) {
if (e.code === 'SQLITE_CANTOPEN') {
return { success: false, error: 'permission_denied', retryable: false }
}
throw e
}
}
const result = executeQuery()
expect(result.success).toBe(false)
expect(result.error).toBe('permission_denied')
expect(result.retryable).toBe(false)
})
it('should handle corrupted database error', () => {
const mockSqlite = createMockSqlite({
shouldFail: true,
errorType: 'SQLITE_CORRUPT',
errorMessage: 'database disk image is malformed'
})
const executeQuery = () => {
try {
return { success: true, data: mockSqlite('test.db', 'SELECT * FROM messages') }
} catch (e) {
if (e.code === 'SQLITE_CORRUPT') {
return { success: false, error: 'database_corrupted', retryable: false }
}
throw e
}
}
const result = executeQuery()
expect(result.success).toBe(false)
expect(result.error).toBe('database_corrupted')
})
it('should handle empty result sets gracefully', () => {
const mockSqlite = createMockSqlite({ results: [] })
const getMessages = () => {
const results = mockSqlite('Messages.db', 'SELECT * FROM message')
return results || []
}
const messages = getMessages()
expect(messages).toEqual([])
expect(messages).toHaveLength(0)
})
})
describe('query timeout handling', () => {
it('should respect timeout option', async () => {
const createTimeoutQuery = (timeoutMs) => {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Query timeout'))
}, timeoutMs)
// Simulate slow query
setTimeout(() => {
clearTimeout(timer)
resolve([{ id: 1 }])
}, timeoutMs + 100) // Always slower than timeout
})
}
const executeWithTimeout = async () => {
try {
await createTimeoutQuery(50)
return { success: true }
} catch (e) {
if (e.message === 'Query timeout') {
return { success: false, error: 'timeout' }
}
throw e
}
}
const result = await executeWithTimeout()
expect(result.success).toBe(false)
expect(result.error).toBe('timeout')
})
it('should complete before timeout when query is fast', async () => {
const createFastQuery = (timeoutMs) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([{ id: 1 }])
}, 10) // Much faster than timeout
})
}
const result = await createFastQuery(1000)
expect(result).toEqual([{ id: 1 }])
})
})
describe('mid-indexing failure recovery', () => {
it('should track progress for recovery after failure', async () => {
const BATCH_SIZE = 32
let processedCount = 0
let lastSuccessfulBatch = -1
const items = Array.from({ length: 100 }, (_, i) => ({ id: i }))
const processWithFailure = async (failAtBatch) => {
processedCount = 0
lastSuccessfulBatch = -1
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batchIndex = Math.floor(i / BATCH_SIZE)
if (batchIndex === failAtBatch) {
throw new Error('Simulated failure')
}
// Process batch
processedCount += Math.min(BATCH_SIZE, items.length - i)
lastSuccessfulBatch = batchIndex
}
return { processed: processedCount }
}
// Fail at batch 2 (after processing 64 items)
try {
await processWithFailure(2)
} catch (e) {
expect(e.message).toBe('Simulated failure')
}
expect(processedCount).toBe(64) // 2 batches of 32
expect(lastSuccessfulBatch).toBe(1) // Batches 0 and 1 completed
})
it('should allow resuming from last successful batch', async () => {
const BATCH_SIZE = 32
const items = Array.from({ length: 100 }, (_, i) => ({ id: i }))
const processFromBatch = (startBatch) => {
const startIndex = startBatch * BATCH_SIZE
let processed = 0
for (let i = startIndex; i < items.length; i += BATCH_SIZE) {
processed += Math.min(BATCH_SIZE, items.length - i)
}
return processed
}
// Resume from batch 2
const remaining = processFromBatch(2)
expect(remaining).toBe(36) // 100 - 64 = 36 items remaining
})
})
describe('connection recovery', () => {
it('should retry connection on transient failure', async () => {
let attempts = 0
const maxRetries = 3
const connectWithRetry = async () => {
for (let i = 0; i < maxRetries; i++) {
attempts++
try {
if (attempts < 3) {
throw new Error('Connection failed')
}
return { connected: true }
} catch (e) {
if (i === maxRetries - 1) throw e
await new Promise(r => setTimeout(r, 10))
}
}
}
const result = await connectWithRetry()
expect(result.connected).toBe(true)
expect(attempts).toBe(3)
})
it('should fail after max retries', async () => {
const maxRetries = 3
const connectWithRetry = async () => {
for (let i = 0; i < maxRetries; i++) {
try {
throw new Error('Connection failed')
} catch (e) {
if (i === maxRetries - 1) throw e
await new Promise(r => setTimeout(r, 10))
}
}
}
await expect(connectWithRetry()).rejects.toThrow('Connection failed')
})
})
describe('null and undefined data handling', () => {
it('should handle null database path', () => {
const validateDbPath = (path) => {
if (!path) return { valid: false, error: 'path_required' }
return { valid: true }
}
expect(validateDbPath(null)).toEqual({ valid: false, error: 'path_required' })
expect(validateDbPath(undefined)).toEqual({ valid: false, error: 'path_required' })
expect(validateDbPath('')).toEqual({ valid: false, error: 'path_required' })
})
it('should handle null query results', () => {
const processResults = (results) => {
if (results === null || results === undefined) {
return []
}
return results
}
expect(processResults(null)).toEqual([])
expect(processResults(undefined)).toEqual([])
expect(processResults([{ id: 1 }])).toEqual([{ id: 1 }])
})
it('should handle messages with null fields', () => {
const processMessage = (msg) => {
return {
id: String(msg.id ?? 'unknown'),
text: msg.text ?? '',
sender: msg.sender ?? 'Unknown',
date: msg.date ?? null
}
}
const result = processMessage({ id: 1, text: null, sender: undefined })
expect(result.id).toBe('1')
expect(result.text).toBe('')
expect(result.sender).toBe('Unknown')
expect(result.date).toBeNull()
})
})
})