Fill the remaining coverage gaps in src/routes/admin-users.ts and add the domain lifecycle test, bringing the file to 100% stmts/branch/funcs/lines. New tests (apps/api/test/admin-users.test.ts): - password route: 404 unknown id; 400 when better-auth setUserPassword throws (driven via a spy so the catch path is deterministic). - toListItem: a null DB role lists as 'worker' (nullish-coalescing fallback). - role route: 400 on an invalid role body; 404 on an unknown id. - reactivate route: 404 on an unknown id. Lifecycle (apps/api/test/integration/user-lifecycle.test.ts): admin creates a worker -> worker signs in and the bearer authorizes /api/me -> admin deactivates (session rows deleted, so the pre-issued token 401s and fresh sign-in is blocked) -> admin reactivates -> sign-in works again. The two last-admin guards (role + deactivate routes) are unreachable over HTTP (the self-demote / self-deactivate guards above already handle caller===target, so any caller reaching them is a distinct active admin and activeAdminsExcluding(target) >= 1). Annotated with /* c8 ignore */ + reasons rather than left as silent gaps; the guard logic is unit-tested via activeAdminsExcluding. No production behavior changed; no bugs found.
73 lines
2.9 KiB
TypeScript
73 lines
2.9 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { eq } from 'drizzle-orm';
|
|
import { createApp } from '../../src/app';
|
|
import { db } from '../../src/db/client';
|
|
import { session, user } from '../../src/db/schema';
|
|
import { authToken, bearer, resetDb } from '../helpers';
|
|
|
|
const PASSWORD = 'sterk-wachtwoord-123';
|
|
|
|
async function signIn(app: ReturnType<typeof createApp>, email: string) {
|
|
return app.request('/api/auth/sign-in/email', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ email, password: PASSWORD }),
|
|
});
|
|
}
|
|
|
|
describe('admin user lifecycle (create → sign in → deactivate → reactivate)', () => {
|
|
beforeEach(resetDb);
|
|
|
|
it('deactivate revokes the live token and blocks sign-in; reactivate restores both', async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'lifecycle-admin@example.com', 'admin');
|
|
|
|
// admin creates a worker
|
|
const createRes = await app.request('/api/admin/users', {
|
|
method: 'POST',
|
|
headers: bearer(adminTok),
|
|
body: JSON.stringify({
|
|
email: 'lifecycle-worker@example.com',
|
|
name: 'Lifecycle Worker',
|
|
password: PASSWORD,
|
|
role: 'worker',
|
|
}),
|
|
});
|
|
expect(createRes.status).toBe(200);
|
|
const created = await createRes.json();
|
|
expect(created.status).toBe('active');
|
|
|
|
// worker signs in → the issued bearer authorizes /api/me
|
|
const firstSignIn = await signIn(app, 'lifecycle-worker@example.com');
|
|
expect(firstSignIn.status).toBe(200);
|
|
const workerTok = firstSignIn.headers.get('set-auth-token');
|
|
if (!workerTok) throw new Error('no worker token');
|
|
expect((await app.request('/api/me', { headers: bearer(workerTok) })).status).toBe(200);
|
|
|
|
// admin deactivates the worker
|
|
const off = await app.request(`/api/admin/users/${created.id}/deactivate`, {
|
|
method: 'POST',
|
|
headers: bearer(adminTok),
|
|
});
|
|
expect(off.status).toBe(200);
|
|
expect((await off.json()).status).toBe('inactive');
|
|
|
|
// deactivate deleted the session rows → the pre-issued token no longer authorizes /api/me
|
|
expect(await db.select().from(session).where(eq(session.userId, created.id))).toHaveLength(0);
|
|
expect((await app.request('/api/me', { headers: bearer(workerTok) })).status).toBe(401);
|
|
|
|
// and a fresh sign-in is blocked while banned
|
|
expect((await signIn(app, 'lifecycle-worker@example.com')).status).not.toBe(200);
|
|
|
|
// admin reactivates → ban cleared, sign-in works again
|
|
const on = await app.request(`/api/admin/users/${created.id}/reactivate`, {
|
|
method: 'POST',
|
|
headers: bearer(adminTok),
|
|
});
|
|
expect((await on.json()).status).toBe('active');
|
|
const [row] = await db.select().from(user).where(eq(user.id, created.id));
|
|
expect(row.banned).toBeFalsy();
|
|
expect((await signIn(app, 'lifecycle-worker@example.com')).status).toBe(200);
|
|
});
|
|
});
|