fix: rollback on rename throw, description validation, delete/DRY cleanup

- Hoist isRename/trimmedName above try block so catch can roll back user
  migration when updateRoleByName throws (not just returns null)
- Add description type + max-length (2000) validation in create and update,
  consistent with groups handler
- Remove redundant getRoleByName existence check in deleteRoleHandler —
  use deleteRoleByName return value directly
- Skip no-op name write when body.name equals current name (use isRename)
- Extract getUserModel() accessor to DRY repeated Model<IUser> casts
- Use name.trim() consistently in createRoleByName error messages
- Add tests: rename-throw rollback, description validation (create+update),
  update delete test mocks to match simplified handler
This commit is contained in:
Dustin Healy 2026-03-26 16:25:14 -07:00
parent 16bb113614
commit ce526ed51a
3 changed files with 98 additions and 22 deletions

View file

@ -191,6 +191,22 @@ describe('createAdminRolesHandlers', () => {
expect(deps.createRoleByName).not.toHaveBeenCalled();
});
it('returns 400 when description exceeds max length', async () => {
const deps = createDeps();
const handlers = createAdminRolesHandlers(deps);
const { req, res, status, json } = createReqRes({
body: { name: 'editor', description: 'a'.repeat(2001) },
});
await handlers.createRole(req, res);
expect(status).toHaveBeenCalledWith(400);
expect(json).toHaveBeenCalledWith({
error: 'description must not exceed 2000 characters',
});
expect(deps.createRoleByName).not.toHaveBeenCalled();
});
it('returns 409 when role already exists', async () => {
const deps = createDeps({
createRoleByName: jest.fn().mockRejectedValue(new Error('Role "editor" already exists')),
@ -487,6 +503,44 @@ describe('createAdminRolesHandlers', () => {
expect(deps.updateUsersByRole).toHaveBeenNthCalledWith(2, 'new-name', 'editor');
});
it('rolls back user migration when rename throws', async () => {
const deps = createDeps({
getRoleByName: jest.fn().mockResolvedValueOnce(mockRole()).mockResolvedValueOnce(null),
updateRoleByName: jest.fn().mockRejectedValue(new Error('db crash')),
});
const handlers = createAdminRolesHandlers(deps);
const { req, res, status } = createReqRes({
params: { name: 'editor' },
body: { name: 'new-name' },
});
await handlers.updateRole(req, res);
expect(status).toHaveBeenCalledWith(500);
expect(deps.updateUsersByRole).toHaveBeenCalledTimes(2);
expect(deps.updateUsersByRole).toHaveBeenNthCalledWith(1, 'editor', 'new-name');
expect(deps.updateUsersByRole).toHaveBeenNthCalledWith(2, 'new-name', 'editor');
});
it('returns 400 when description exceeds max length', async () => {
const deps = createDeps({
getRoleByName: jest.fn().mockResolvedValue(mockRole()),
});
const handlers = createAdminRolesHandlers(deps);
const { req, res, status, json } = createReqRes({
params: { name: 'editor' },
body: { description: 'a'.repeat(2001) },
});
await handlers.updateRole(req, res);
expect(status).toHaveBeenCalledWith(400);
expect(json).toHaveBeenCalledWith({
error: 'description must not exceed 2000 characters',
});
expect(deps.updateRoleByName).not.toHaveBeenCalled();
});
it('returns 500 on unexpected error', async () => {
const deps = createDeps({
getRoleByName: jest.fn().mockResolvedValue(mockRole()),
@ -586,7 +640,7 @@ describe('createAdminRolesHandlers', () => {
describe('deleteRole', () => {
it('deletes role and returns 200', async () => {
const deps = createDeps({ getRoleByName: jest.fn().mockResolvedValue(mockRole()) });
const deps = createDeps();
const handlers = createAdminRolesHandlers(deps);
const { req, res, status, json } = createReqRes({ params: { name: 'editor' } });
@ -610,7 +664,7 @@ describe('createAdminRolesHandlers', () => {
});
it('returns 404 when role not found', async () => {
const deps = createDeps({ getRoleByName: jest.fn().mockResolvedValue(null) });
const deps = createDeps({ deleteRoleByName: jest.fn().mockResolvedValue(null) });
const handlers = createAdminRolesHandlers(deps);
const { req, res, status, json } = createReqRes({ params: { name: 'nonexistent' } });
@ -622,7 +676,6 @@ describe('createAdminRolesHandlers', () => {
it('returns 500 on error', async () => {
const deps = createDeps({
getRoleByName: jest.fn().mockResolvedValue(mockRole()),
deleteRoleByName: jest.fn().mockRejectedValue(new Error('db down')),
});
const handlers = createAdminRolesHandlers(deps);