import {vi, describe, it, expect, beforeEach, type Mock} from 'vitest'; import express from 'express'; import request from 'supertest'; vi.mock('../../src/models/admin/users/users.admin.service.js', () => ({ listUsers: vi.fn(), getUserDetail: vi.fn(), loadAccess: vi.fn(), setPermissions: vi.fn(), disableUser: vi.fn(), enableUser: vi.fn(), revokeSession: vi.fn(), countActiveAdmins: vi.fn(), userExists: vi.fn() })); import * as UsersService from '../../src/models/admin/users/users.admin.service.js'; import {usersAdminRouter} from '../../src/models/admin/users/users.admin.router.js'; const service = UsersService as unknown as Record; // The router always runs behind requireAppAccess('admin'), which is what puts // res.locals.admin there; this stands in for it. const makeApp = (callerId = 'me') => { const app = express(); app.use(express.json()); app.use((req, res, next) => { res.locals.admin = {id: callerId, email: 'me@nachklang.art', displayName: 'Me', apps: ['admin']}; next(); }); app.use('/admin/users', usersAdminRouter); return app; }; beforeEach(() => { for (const fn of Object.values(service)) { if (typeof fn?.mockReset === 'function') { fn.mockReset(); } } service.getUserDetail.mockResolvedValue({id: 'other', apps: []}); service.userExists.mockResolvedValue(true); }); describe('PUT /admin/users/:id/permissions', () => { it('rejects an unknown app name', async () => { const res = await request(makeApp()).put('/admin/users/other/permissions').send({apps: ['calendar', 'nope']}); expect(res.status).toBe(400); expect(service.setPermissions).not.toHaveBeenCalled(); }); it('rejects a non-array body', async () => { const res = await request(makeApp()).put('/admin/users/other/permissions').send({apps: 'admin'}); expect(res.status).toBe(400); }); it('404s for an unknown user', async () => { service.userExists.mockResolvedValue(false); const res = await request(makeApp()).put('/admin/users/ghost/permissions').send({apps: []}); expect(res.status).toBe(404); expect(service.setPermissions).not.toHaveBeenCalled(); }); it('refuses to remove the caller\'s own admin permission', async () => { service.loadAccess.mockResolvedValue({id: 'me', disabled: false, apps: ['admin']}); service.countActiveAdmins.mockResolvedValue(5); const res = await request(makeApp('me')).put('/admin/users/me/permissions').send({apps: ['feedback']}); expect(res.status).toBe(409); expect(service.setPermissions).not.toHaveBeenCalled(); }); // Defence in depth: with the caller themselves being an active admin this // count cannot actually reach 1 in production, but the guard is what makes // that safe to rely on rather than to reason about. it('refuses to remove the last remaining active admin', async () => { service.loadAccess.mockResolvedValue({id: 'other', disabled: false, apps: ['admin']}); service.countActiveAdmins.mockResolvedValue(1); const res = await request(makeApp('me')).put('/admin/users/other/permissions').send({apps: []}); expect(res.status).toBe(409); expect(service.setPermissions).not.toHaveBeenCalled(); }); it('allows removing an admin while another active admin remains', async () => { service.loadAccess.mockResolvedValue({id: 'other', disabled: false, apps: ['admin']}); service.countActiveAdmins.mockResolvedValue(2); const res = await request(makeApp('me')).put('/admin/users/other/permissions').send({apps: ['tickets']}); expect(res.status).toBe(200); expect(service.setPermissions).toHaveBeenCalledWith('other', ['tickets'], 'me'); }); it('allows granting permissions to someone who has none', async () => { service.loadAccess.mockResolvedValue({id: 'other', disabled: false, apps: []}); const res = await request(makeApp('me')).put('/admin/users/other/permissions').send({apps: ['feedback', 'tickets']}); expect(res.status).toBe(200); // Nothing is being taken away, so the last-admin count is not consulted. expect(service.countActiveAdmins).not.toHaveBeenCalled(); }); }); describe('POST /admin/users/:id/disable', () => { it('refuses to disable the caller', async () => { const res = await request(makeApp('me')).post('/admin/users/me/disable'); expect(res.status).toBe(409); expect(service.disableUser).not.toHaveBeenCalled(); }); it('refuses to disable the last active admin', async () => { service.loadAccess.mockResolvedValue({id: 'other', disabled: false, apps: ['admin']}); service.countActiveAdmins.mockResolvedValue(1); const res = await request(makeApp('me')).post('/admin/users/other/disable'); expect(res.status).toBe(409); expect(service.disableUser).not.toHaveBeenCalled(); }); it('disables a non-admin user', async () => { service.loadAccess.mockResolvedValue({id: 'other', disabled: false, apps: ['feedback']}); const res = await request(makeApp('me')).post('/admin/users/other/disable'); expect(res.status).toBe(200); expect(service.disableUser).toHaveBeenCalledWith('other'); }); it('404s for an unknown user', async () => { service.loadAccess.mockResolvedValue(null); const res = await request(makeApp('me')).post('/admin/users/ghost/disable'); expect(res.status).toBe(404); }); }); describe('DELETE /admin/users/:id/sessions/:sid', () => { it('404s when the session does not belong to that user', async () => { service.revokeSession.mockResolvedValue(false); const res = await request(makeApp()).delete('/admin/users/other/sessions/s1'); expect(res.status).toBe(404); }); it('204s on a successful revoke', async () => { service.revokeSession.mockResolvedValue(true); const res = await request(makeApp()).delete('/admin/users/other/sessions/s1'); expect(res.status).toBe(204); expect(service.revokeSession).toHaveBeenCalledWith('other', 's1'); }); });