Files
TREK/server/tests/unit/db/seeds-admin.test.ts
T
Maurice 41c541828f fix(setup): warn when ADMIN_EMAIL/ADMIN_PASSWORD are ignored, ship reset-admin
The first-run seeder only applies ADMIN_EMAIL/ADMIN_PASSWORD on an empty
database and then silently ignores them. People add the vars after the first
boot, or pull a fresh image without clearing ./data, restart, and cannot log
in with no hint why (#1339). The default is a generated password (not the
.env.example placeholder), printed once in the first-run box. Now: warn loudly
when the vars are set but a user already exists, and warn on a partial
(one-of-two) config instead of quietly falling back.

Also ship the reset-admin recovery script in the image -- it was never COPYed in
despite the wiki referencing it. node server/reset-admin.js resets/creates
admin@trek.local with a generated password (RESET_ADMIN_EMAIL/RESET_ADMIN_PASSWORD
overridable), picks a free username so it cannot trip UNIQUE(username), and sets
must_change_password.
2026-06-28 11:10:40 +02:00

102 lines
3.7 KiB
TypeScript

/**
* First-run admin seeding (seedAdminAccount).
*
* Covers the #1339 fix: ADMIN_EMAIL/ADMIN_PASSWORD only take effect on first run
* (empty database). Setting them once a user exists must no longer be silent — it
* has to warn — and a partial config (only one of the two) must warn too instead
* of quietly falling back to a generated password.
*/
import { seedAdminAccount } from '../../../src/db/seeds';
import { createTestDb } from '../../helpers/test-db';
import type Database from 'better-sqlite3';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
const ENV_KEYS = ['ADMIN_EMAIL', 'ADMIN_PASSWORD', 'DEMO_MODE', 'OIDC_ONLY', 'OIDC_ISSUER', 'OIDC_CLIENT_ID'];
function countUsers(db: Database.Database): number {
return (db.prepare('SELECT COUNT(*) as c FROM users').get() as { c: number }).c;
}
function insertExistingUser(db: Database.Database): void {
db.prepare(
"INSERT INTO users (username, email, password_hash, role) VALUES ('admin', 'admin@trek.local', 'x', 'admin')",
).run();
}
describe('seedAdminAccount — first-run admin', () => {
let db: Database.Database;
let saved: Record<string, string | undefined>;
beforeEach(() => {
db = createTestDb();
saved = {};
for (const k of ENV_KEYS) {
saved[k] = process.env[k];
delete process.env[k];
}
});
afterEach(() => {
db.close();
for (const k of ENV_KEYS) {
if (saved[k] === undefined) delete process.env[k];
else process.env[k] = saved[k];
}
vi.restoreAllMocks();
});
it('creates the admin from ADMIN_EMAIL/ADMIN_PASSWORD on an empty database', () => {
process.env.ADMIN_EMAIL = 'me@example.com';
process.env.ADMIN_PASSWORD = 'S3cret-pw';
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
seedAdminAccount(db);
const user = db
.prepare('SELECT email, role, must_change_password FROM users WHERE email = ?')
.get('me@example.com') as { email: string; role: string; must_change_password: number } | undefined;
expect(user).toBeDefined();
expect(user!.role).toBe('admin');
expect(user!.must_change_password).toBe(1);
expect(warn).not.toHaveBeenCalled();
});
it('warns and creates nothing when ADMIN_* is set but a user already exists', () => {
insertExistingUser(db);
process.env.ADMIN_EMAIL = 'new@example.com';
process.env.ADMIN_PASSWORD = 'whatever';
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
seedAdminAccount(db);
expect(countUsers(db)).toBe(1);
expect(db.prepare('SELECT 1 FROM users WHERE email = ?').get('new@example.com')).toBeUndefined();
const msg = warn.mock.calls.map((c) => c.join(' ')).join('\n');
expect(msg).toContain('only apply on first run');
});
it('stays silent when no admin env is set and a user already exists', () => {
insertExistingUser(db);
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
seedAdminAccount(db);
expect(countUsers(db)).toBe(1);
expect(warn).not.toHaveBeenCalled();
});
it('warns about a partial config and falls back to a generated password', () => {
process.env.ADMIN_EMAIL = 'me@example.com'; // ADMIN_PASSWORD intentionally missing
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
seedAdminAccount(db);
// Falls back to the default local admin, NOT the provided email.
expect(db.prepare('SELECT 1 FROM users WHERE email = ?').get('admin@trek.local')).toBeDefined();
expect(db.prepare('SELECT 1 FROM users WHERE email = ?').get('me@example.com')).toBeUndefined();
const msg = warn.mock.calls.map((c) => c.join(' ')).join('\n');
expect(msg).toContain('Only one of ADMIN_EMAIL/ADMIN_PASSWORD');
});
});