Files
TREK/server/tests/unit/demo/demo-seed.test.ts
T
Maurice a7aaa29c16 fix(db): stop losing secrets and notes to the boot and migration paths
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.
2026-08-23 11:59:12 +02:00

65 lines
2.1 KiB
TypeScript

/**
* Demo mode boot seeding.
*
* DEMO_MODE=true creates a role=admin account on first boot. When
* DEMO_ADMIN_PASS is unset that account gets the password published in
* demo-seed itself, so the seeder has to say so out loud.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type Database from 'better-sqlite3';
import { createTestDb } from '../../helpers/test-db';
// Baseline handling is demo-reset's job and touches the file system.
vi.mock('../../../src/demo/demo-reset', () => ({
saveBaseline: vi.fn(),
hasBaseline: vi.fn(() => true),
resetDemoUser: vi.fn(),
}));
import { seedDemoData } from '../../../src/demo/demo-seed';
describe('demo seeding', () => {
let db: Database.Database;
const realPass = process.env.DEMO_ADMIN_PASS;
beforeEach(() => {
db = createTestDb();
vi.spyOn(console, 'log').mockImplementation(() => undefined);
});
afterEach(() => {
vi.restoreAllMocks();
db.close();
if (realPass === undefined) delete process.env.DEMO_ADMIN_PASS;
else process.env.DEMO_ADMIN_PASS = realPass;
});
it('DEMOSEED-001: warns when the admin account is created with the default password', () => {
delete process.env.DEMO_ADMIN_PASS;
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
seedDemoData(db);
expect(warn).toHaveBeenCalledWith(expect.stringContaining('DEMO_ADMIN_PASS is not set'));
});
it('DEMOSEED-002: stays quiet when the operator set DEMO_ADMIN_PASS', () => {
process.env.DEMO_ADMIN_PASS = 'a-real-password';
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
seedDemoData(db);
expect(warn).not.toHaveBeenCalledWith(expect.stringContaining('DEMO_ADMIN_PASS is not set'));
});
it('DEMOSEED-003: does not warn again once the admin account exists', () => {
delete process.env.DEMO_ADMIN_PASS;
seedDemoData(db);
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
seedDemoData(db);
expect(warn).not.toHaveBeenCalledWith(expect.stringContaining('DEMO_ADMIN_PASS is not set'));
});
});