Files
Kazier/mobile/tests/dbClient.test.ts
T
2026-09-10 22:08:27 +02:00

117 lines
3.4 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
isBrokenConnectionError,
withDatabaseRetry,
type DatabaseRetryDeps,
} from '../services/db/client';
import type { SQLiteDatabase } from 'expo-sqlite';
function brokenConnectionError(): Error {
return new Error(
"Call to function 'NativeDatabase.prepareAsync' has been rejected.\n→ Caused by: java.lang.NullPointerException: java.lang.NullPointerException",
);
}
test('isBrokenConnectionError recognizes the expo-sqlite Android NPE signature', () => {
assert.equal(isBrokenConnectionError(brokenConnectionError()), true);
assert.equal(
isBrokenConnectionError(
new Error(
"Call to function 'NativeDatabase.execAsync' has been rejected.\n→ Caused by: java.lang.NullPointerException: java.lang.NullPointerException",
),
),
true,
);
});
test('isBrokenConnectionError ignores regular and non-Error values', () => {
assert.equal(isBrokenConnectionError(new Error('disk I/O error')), false);
assert.equal(isBrokenConnectionError(null), false);
assert.equal(isBrokenConnectionError('NativeDatabase.prepareAsync'), false);
});
test('withDatabaseRetry reconnects once on a dead connection and retries', async () => {
let current: SQLiteDatabase = {
runAsync: async () => {
throw brokenConnectionError();
},
} as unknown as SQLiteDatabase;
let getCalls = 0;
let recoverCalls = 0;
const deps: DatabaseRetryDeps = {
get: async () => {
getCalls++;
return current;
},
recover: async () => {
recoverCalls++;
current = {
runAsync: async () => 42 as never,
} as unknown as SQLiteDatabase;
return current;
},
};
const result = await withDatabaseRetry(async (db) => db.runAsync('SELECT 1'), deps);
assert.equal(result, 42);
assert.equal(recoverCalls, 1);
assert.equal(getCalls, 1);
});
test('withDatabaseRetry recovers when the open itself rejects with the NPE signature', async () => {
let recoverCalls = 0;
const deps: DatabaseRetryDeps = {
get: async () => {
throw brokenConnectionError();
},
recover: async () => {
recoverCalls++;
return {
runAsync: async () => 42 as never,
} as unknown as SQLiteDatabase;
},
};
const result = await withDatabaseRetry(async (db) => db.runAsync('SELECT 1'), deps);
assert.equal(result, 42);
assert.equal(recoverCalls, 1);
});
test('withDatabaseRetry does not recover on a regular database error', async () => {
let recoverCalls = 0;
const deps: DatabaseRetryDeps = {
get: async () =>
({
runAsync: async () => {
throw new Error('disk I/O error');
},
}) as unknown as SQLiteDatabase,
recover: async () => {
recoverCalls++;
return {} as SQLiteDatabase;
},
};
await assert.rejects(() => withDatabaseRetry(async (db) => db.runAsync('SELECT 1'), deps), {
message: 'disk I/O error',
});
assert.equal(recoverCalls, 0);
});
test('withDatabaseRetry converges on a broken retry (recovery does not loop forever)', async () => {
const deps: DatabaseRetryDeps = {
get: async () => ({} as SQLiteDatabase),
recover: async () => ({} as SQLiteDatabase),
};
await assert.rejects(
() =>
withDatabaseRetry(async () => {
throw brokenConnectionError();
}, deps),
(error: unknown) =>
error instanceof Error && isBrokenConnectionError(error),
);
});