mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-06 16:12:30 +02:00
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:
parent
2506644d58
commit
ad47919ecd
5 changed files with 276 additions and 42 deletions
|
|
@ -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()),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue