zappier-edge/tests/admin-users.test.ts

115 lines
4.5 KiB
TypeScript

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);
});
});