import Database from 'better-sqlite3'; import request from 'supertest'; import { hashPassword } from '../src/accounts'; import { InMemoryAdminUserRepo, seedAdminUsersFromEnv } from '../src/admin-users'; import { buildApp } from '../src/app'; import { SqliteAdminUserRepo } from '../src/db/admin-user-repo'; const ADMIN = { 'x-admin-key': 'admin-dev-key' }; describe('admin user repos', () => { it('seeds from env defaults (in-memory)', () => { const repo = InMemoryAdminUserRepo.seeded(seedAdminUsersFromEnv()); expect(repo.findByUsername('admin')?.active).toBe(true); expect(repo.findByUsername('demo')?.active).toBe(true); expect(repo.findByUsername('admin')?.passwordHash).toMatch(/^scrypt:/); }); it('seeds from env defaults (sqlite) and persists', () => { const db = new Database(':memory:'); const first = SqliteAdminUserRepo.seeded(db, seedAdminUsersFromEnv()); first.save({ id: 'usr_extra', username: 'ops', passwordHash: hashPassword('ops-password-1'), active: true, createdMs: Date.now(), }); const second = SqliteAdminUserRepo.seeded(db, seedAdminUsersFromEnv()); expect(second.findByUsername('ops')?.active).toBe(true); // re-seeding must not duplicate the env accounts expect(second.list().filter((u) => u.username === 'admin')).toHaveLength(1); }); it('deactivation persists in sqlite', () => { const repo = SqliteAdminUserRepo.seeded(new Database(':memory:'), seedAdminUsersFromEnv()); const demo = repo.findByUsername('demo')!; repo.save({ ...demo, active: false }); expect(repo.findByUsername('demo')?.active).toBe(false); }); }); describe('admin users API', () => { it('lists users without password hashes', async () => { const { app } = buildApp(); const res = await request(app).get('/admin/api/users').set(ADMIN); expect(res.status).toBe(200); const usernames = res.body.users.map((u: { username: string }) => u.username); expect(usernames).toEqual(expect.arrayContaining(['admin', 'demo'])); expect(res.body.users[0].passwordHash).toBeUndefined(); }); it('creates a user who can then log in', async () => { const { app } = buildApp(); const created = await request(app) .post('/admin/api/users') .set(ADMIN) .send({ username: 'finance', password: 'finance-pass-1' }); expect(created.status).toBe(201); const login = await request(app) .post('/admin/api/login') .send({ username: 'finance', password: 'finance-pass-1' }); expect(login.status).toBe(200); expect(login.body.token).toMatch(/^[a-f0-9]{48}$/); }); it('rejects duplicates, weak passwords, and bad usernames', async () => { const { app } = buildApp(); const dup = await request(app) .post('/admin/api/users') .set(ADMIN) .send({ username: 'demo', password: 'whatever-pass-1' }); expect(dup.status).toBe(409); const weak = await request(app) .post('/admin/api/users') .set(ADMIN) .send({ username: 'x1', password: 'short' }); expect(weak.status).toBe(400); const bad = await request(app) .post('/admin/api/users') .set(ADMIN) .send({ username: 'bad name!', password: 'fine-password-1' }); expect(bad.status).toBe(400); }); it('deactivation blocks login; reactivation restores it', async () => { const { app } = buildApp(); const off = await request(app).post('/admin/api/users/demo/deactivate').set(ADMIN); expect(off.status).toBe(200); const login = await request(app) .post('/admin/api/login') .send({ username: 'demo', password: '$$$Adm1n###' }); expect(login.status).toBe(401); const on = await request(app).post('/admin/api/users/demo/activate').set(ADMIN); expect(on.status).toBe(200); const again = await request(app) .post('/admin/api/login') .send({ username: 'demo', password: '$$$Adm1n###' }); expect(again.status).toBe(200); }); it('refuses to deactivate the last active admin', async () => { const { app } = buildApp(); await request(app).post('/admin/api/users/demo/deactivate').set(ADMIN); const last = await request(app).post('/admin/api/users/admin/deactivate').set(ADMIN); expect(last.status).toBe(400); expect(last.body.error).toContain('last active'); }); it('404s unknown usernames on activate/deactivate', async () => { const { app } = buildApp(); const res = await request(app).post('/admin/api/users/nobody/deactivate').set(ADMIN); expect(res.status).toBe(404); }); });