Files
TREK/server/tests/integration/database-lifecycle.test.ts
T
Maurice 0aa5b36661 fix(backup): back up and restore through an injected database port
Backup, restore and the demo reset each handled the SQLite file
themselves: backup.impl closed and reinitialized the connection, built
MaintenanceRepository with new, and assumed the database was
data/travel.db. That tied the whole chain to one local file and hid two
bugs.

They now go through DATABASE_BACKUP, a port declared in the shared
database kernel (snapshot, verify, keep a copy before a restore, replace).
SqliteDatabaseBackup implements it with what the code did before:
VACUUM INTO for a consistent copy, and a close, copy, sidecar drop,
rename and reopen through DatabaseLifecycle for a restore. BackupService
injects it and hands it to backup.impl. The demo reset and the baseline
save use the same port; the first baseline is saved by DemoResetJob once
the app is up, because the boot-time seed runs before anything can be
injected. Another engine can plug in a logical dump behind the port.

Fixes on the way:

- The backup and the restore ignored TREK_DB_FILE. The backup looked for
  data/travel.db and, with the database elsewhere, silently left it out
  of the archive; the restore wrote data/travel.db and never replaced
  the live database. Both now use DatabaseLifecycle.file, the path the
  connection runs on, and the pre-restore copy sits next to it.
- A failed VACUUM INTO fell back to archiving the live WAL-mode file
  without a word, the torn copy the snapshot exists to prevent. The
  backup now fails: nothing is archived or committed, the existing
  backups are untouched, a partial snapshot is removed, and the log says
  why. A missing database file and a failed checkpoint are logged too.
2026-10-08 18:38:28 +02:00

67 lines
2.6 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import type { INestApplication } from '@nestjs/common';
import { buildApp } from '../../src/bootstrap';
import { DatabaseLifecycle } from '../../src/nest/database/database-lifecycle.service';
import { DATABASE_BACKUP, type DatabaseBackupStrategy } from '../../src/nest/database/database-backup.interface';
import { SqliteDatabaseBackup } from '../../src/nest/backup/sqlite-database-backup';
/**
* The core database's lifecycle, owned by the DatabaseLifecycle provider, on a
* real boot.
*
* The connection used to open as a side effect of importing db/database.ts and
* was closed and reopened through module functions the restore and the demo
* reset called directly. buildApp() now opens it through the provider, which
* also binds the ORM to later swaps; the backup port closes and reopens it
* through the same provider. Unmocked on purpose: under NODE_ENV=test the
* module opens a copy of the migrated schema snapshot, and every reopen opens a
* pristine copy again, which is all these cases need.
*/
describe('the database connection lifecycle under buildApp()', () => {
let app: INestApplication;
beforeAll(async () => {
app = await buildApp();
await app.init();
});
afterAll(async () => {
await app?.close();
});
it('LIFECYCLE-001: the boot opened the connection through the provider and the database answers', async () => {
expect(app.get(DatabaseLifecycle).file).toBe(':memory:');
const res = await request(app.getHttpServer()).get('/api/health/ready');
expect(res.status).toBe(200);
expect(res.body).toEqual({ status: 'ready' });
});
it('LIFECYCLE-002: closed, the readiness probe answers 503; reopened, the ORM is bound to the new handle and it answers again', async () => {
const lifecycle = app.get(DatabaseLifecycle);
lifecycle.close();
try {
const down = await request(app.getHttpServer()).get('/api/health/ready');
expect(down.status).toBe(503);
expect(down.body).toEqual({ status: 'unavailable' });
} finally {
await lifecycle.reopen();
}
const up = await request(app.getHttpServer()).get('/api/health/ready');
expect(up.status).toBe(200);
});
it('LIFECYCLE-003: the backup port the container hands out is the SQLite one, on the file the connection runs on', () => {
const port = app.get<DatabaseBackupStrategy>(DATABASE_BACKUP);
expect(port).toBeInstanceOf(SqliteDatabaseBackup);
expect(port.location()).toBe(app.get(DatabaseLifecycle).file);
// The in-memory test database has no file to copy.
expect(port.canSnapshot()).toBe(false);
});
});