mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
a7aaa29c16
An unreadable or empty encryption-key file was treated the same as a missing one: regenerate and overwrite. That is the one case where regenerating is the worst possible move - every secret already at rest becomes undecryptable, permanently. Both now refuse to start and say which file to fix. The key-rotation script advertised re-encrypting all stored secrets while its hardcoded column lists had drifted: the AirTrail key, the per-user LLM key and the plugin OAuth tokens were never migrated, so they silently stopped decrypting after a rotation. The parity test now pins the full set against what the code actually encrypts, rather than two tables of it. The schema_version bump ran outside the migration's transaction, so a crash in between left the schema advanced and the version stale - the next boot replayed a non-idempotent step and exited, turning one crash into a boot loop. And the TICKETJSON receipt migration matched with LIKE, which SQLite treats case-insensitively, so a note somebody had typed starting 'ticketjson:' was cut up and its text dropped. Also: APP_VERSION was interpolated into migration SQL instead of bound, the capture-metadata backfill passed (photoId, userId) to a (userId, photoId) signature and its mock keyed on the wrong argument, TRUST_PROXY=0 was accepted and silently coerced to 1, demo-reset hardcoded the db path instead of honouring TREK_DB_FILE, and a demo instance with no DEMO_ADMIN_PASS set now says out loud that its admin password is the published default.
56 lines
2.0 KiB
TypeScript
56 lines
2.0 KiB
TypeScript
/**
|
|
* The schema_version bump must commit with the migration that earned it.
|
|
*
|
|
* If it lands in a statement of its own, a crash between the two leaves the
|
|
* schema advanced and the version stale, and the next boot replays a step that
|
|
* is not idempotent (an INSERT INTO app_settings, say) and exits 1 forever.
|
|
*/
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import Database from 'better-sqlite3';
|
|
import { createTables } from '../../../src/db/schema';
|
|
import { runMigrations } from '../../../src/db/migrations';
|
|
|
|
function migratedDb(): Database.Database {
|
|
const db = new Database(':memory:');
|
|
db.exec('PRAGMA journal_mode = WAL');
|
|
db.exec('PRAGMA busy_timeout = 5000');
|
|
db.exec('PRAGMA foreign_keys = ON');
|
|
createTables(db);
|
|
runMigrations(db);
|
|
return db;
|
|
}
|
|
|
|
describe('migration version bump atomicity', () => {
|
|
it('MIGRATE-ATOMIC-001: writes schema_version inside the migration transaction', () => {
|
|
const db = migratedDb();
|
|
try {
|
|
const { version } = db.prepare('SELECT version FROM schema_version LIMIT 1').get() as { version: number };
|
|
// Rewind one slot so exactly the last migration replays.
|
|
db.prepare('UPDATE schema_version SET version = ?').run(version - 1);
|
|
|
|
const realPrepare = db.prepare.bind(db);
|
|
const inTransaction: boolean[] = [];
|
|
vi.spyOn(db, 'prepare').mockImplementation(((sql: string) => {
|
|
const stmt = realPrepare(sql);
|
|
if (sql.includes('UPDATE schema_version SET version')) {
|
|
const realRun = stmt.run.bind(stmt);
|
|
stmt.run = ((...args: unknown[]) => {
|
|
inTransaction.push(db.inTransaction);
|
|
return realRun(...(args as never[]));
|
|
}) as typeof stmt.run;
|
|
}
|
|
return stmt;
|
|
}) as typeof db.prepare);
|
|
|
|
runMigrations(db);
|
|
|
|
expect(inTransaction).toEqual([true]);
|
|
const after = db.prepare('SELECT version FROM schema_version LIMIT 1').get() as { version: number };
|
|
expect(after.version).toBe(version);
|
|
} finally {
|
|
vi.restoreAllMocks();
|
|
db.close();
|
|
}
|
|
});
|
|
});
|