fix: address review findings — race safety, validation DRY, type accuracy, test coverage

- Add post-write admin count verification in removeRoleMember to prevent
  zero-admin race condition (TOCTOU → rollback if count hits 0)
- Make IRole.description optional; backfill in initializeRoles for
  pre-existing roles that lack the field (.lean() bypasses defaults)
- Extract parsePagination, validateNameParam, validateRoleName, and
  validateDescription helpers to eliminate duplicated validation
- Add validateNameParam guard to all 7 handlers reading req.params.name
- Catch 11000 in updateRoleByName and surface as 409 via RoleConflictError
- Add idempotent skip in addRoleMember when user already has target role
- Verify updateRolePermissions test asserts response body
- Add data-layer tests: listRoles sort/pagination/projection, countRoles,
  and createRoleByName 11000 duplicate key race
This commit is contained in:
Dustin Healy 2026-03-26 17:45:16 -07:00
parent 2506644d58
commit ad47919ecd
5 changed files with 276 additions and 42 deletions

View file

@ -703,7 +703,7 @@ describe('createAdminRolesHandlers', () => {
});
const handlers = createAdminRolesHandlers(deps);
const perms = { chat: { read: true, write: true } };
const { req, res, status } = createReqRes({
const { req, res, status, json } = createReqRes({
params: { name: 'editor' },
body: { permissions: perms },
});
@ -712,6 +712,7 @@ describe('createAdminRolesHandlers', () => {
expect(deps.updateAccessPermissions).toHaveBeenCalledWith('editor', perms, role);
expect(status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ role: expect.objectContaining({ name: 'editor' }) });
});
it('returns 400 when permissions is missing', async () => {
@ -945,7 +946,7 @@ describe('createAdminRolesHandlers', () => {
it('adds member and returns 200', async () => {
const deps = createDeps({
getRoleByName: jest.fn().mockResolvedValue(mockRole()),
findUser: jest.fn().mockResolvedValue(mockUser()),
findUser: jest.fn().mockResolvedValue(mockUser({ role: 'viewer' })),
});
const handlers = createAdminRolesHandlers(deps);
const { req, res, status, json } = createReqRes({
@ -960,6 +961,24 @@ describe('createAdminRolesHandlers', () => {
expect(json).toHaveBeenCalledWith({ success: true });
});
it('skips DB write when user already has the target role', async () => {
const deps = createDeps({
getRoleByName: jest.fn().mockResolvedValue(mockRole()),
findUser: jest.fn().mockResolvedValue(mockUser({ role: 'editor' })),
});
const handlers = createAdminRolesHandlers(deps);
const { req, res, status, json } = createReqRes({
params: { name: 'editor' },
body: { userId: validUserId },
});
await handlers.addRoleMember(req, res);
expect(status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ success: true });
expect(deps.updateUser).not.toHaveBeenCalled();
});
it('returns 400 when userId is missing', async () => {
const deps = createDeps();
const handlers = createAdminRolesHandlers(deps);
@ -1022,7 +1041,7 @@ describe('createAdminRolesHandlers', () => {
it('returns 500 on unexpected error', async () => {
const deps = createDeps({
getRoleByName: jest.fn().mockResolvedValue(mockRole()),
findUser: jest.fn().mockResolvedValue(mockUser()),
findUser: jest.fn().mockResolvedValue(mockUser({ role: 'viewer' })),
updateUser: jest.fn().mockRejectedValue(new Error('timeout')),
});
const handlers = createAdminRolesHandlers(deps);
@ -1153,6 +1172,30 @@ describe('createAdminRolesHandlers', () => {
expect(deps.updateUser).toHaveBeenCalledWith(validUserId, { role: SystemRoles.USER });
});
it('rolls back removal when post-write check finds zero admins', async () => {
const deps = createDeps({
getRoleByName: jest.fn().mockResolvedValue(mockRole({ name: SystemRoles.ADMIN })),
findUser: jest.fn().mockResolvedValue(mockUser({ role: SystemRoles.ADMIN })),
countUsersByRole: jest.fn().mockResolvedValueOnce(2).mockResolvedValueOnce(0),
});
const handlers = createAdminRolesHandlers(deps);
const { req, res, status, json } = createReqRes({
params: { name: SystemRoles.ADMIN, userId: validUserId },
});
await handlers.removeRoleMember(req, res);
expect(status).toHaveBeenCalledWith(400);
expect(json).toHaveBeenCalledWith({ error: 'Cannot remove the last admin user' });
expect(deps.updateUser).toHaveBeenCalledTimes(2);
expect(deps.updateUser).toHaveBeenNthCalledWith(1, validUserId, {
role: SystemRoles.USER,
});
expect(deps.updateUser).toHaveBeenNthCalledWith(2, validUserId, {
role: SystemRoles.ADMIN,
});
});
it('returns 500 on unexpected error', async () => {
const deps = createDeps({
getRoleByName: jest.fn().mockResolvedValue(mockRole()),