import { jest } from '@jest/globals'; const redisMock = { getKey: jest.fn(), setKey: jest.fn(), }; const lean = jest.fn(); const select = jest.fn(() => ({ lean })); const findById = jest.fn(() => ({ select })); const userModel = { findById }; jest.unstable_mockModule('../redis.js', () => ({ redisServer: redisMock, })); jest.unstable_mockModule('mongoose', () => ({ default: { model: jest.fn(() => userModel), }, })); const { applyPermissionSettingsList, checkPermissions, hasPermission, getUserPermissionsCacheKey, saveUserPermissionsToRedis, } = await import('../permissions.js'); describe('applyPermissionSettingsList', () => { it('applies permission settings in order and inherits intermediate values', () => { const result = applyPermissionSettingsList([ { permissions: { user: { info: true, edit: false, delete: true }, printer: { info: true }, }, }, { permissions: { user: { edit: true, delete: null }, printer: { info: null, delete: false }, }, }, ]); expect(result).toEqual({ user: { info: true, edit: true, delete: true }, printer: { info: true, delete: false }, }); }); it('returns an empty object when nothing is explicitly set', () => { expect(applyPermissionSettingsList([{ permissions: { user: { info: null } } }])).toEqual({}); }); it('applies group permissions first, then user permission settings', () => { const result = applyPermissionSettingsList([ { permissions: { user: { info: true, edit: false }, printer: { info: true }, }, }, { permissions: { user: { delete: true }, }, }, { permissions: { user: { edit: true }, printer: { info: false }, }, }, ]); expect(result).toEqual({ user: { info: true, edit: true, delete: true }, printer: { info: false }, }); }); }); describe('saveUserPermissionsToRedis', () => { beforeEach(() => { jest.clearAllMocks(); }); it('saves permissions under permissions:userID', async () => { const permissions = { user: { info: true } }; await saveUserPermissionsToRedis('user-1', permissions); expect(redisMock.setKey).toHaveBeenCalledWith('permissions:user-1', permissions); }); }); describe('hasPermission', () => { beforeEach(() => { jest.clearAllMocks(); redisMock.getKey.mockResolvedValue(null); redisMock.setKey.mockResolvedValue(undefined); lean.mockResolvedValue(null); }); it('returns true when the cached permission is allowed', async () => { redisMock.getKey.mockResolvedValue({ printer: { edit: true } }); await expect(hasPermission({ _id: 'user-1' }, 'printer', 'edit')).resolves.toBe(true); expect(redisMock.getKey).toHaveBeenCalledWith(getUserPermissionsCacheKey('user-1')); expect(findById).not.toHaveBeenCalled(); }); it('returns false when the cached permission is denied or missing', async () => { redisMock.getKey.mockResolvedValue({ printer: { edit: false } }); await expect(hasPermission('user-1', 'printer', 'edit')).resolves.toBe(false); await expect(hasPermission('user-1', 'printer', 'delete')).resolves.toBe(false); }); it('loads permissions from mongodb when redis is empty and caches them', async () => { lean.mockResolvedValue({ permissions: { job: { info: true, edit: false } }, }); await expect(hasPermission('user-2', 'job', 'info')).resolves.toBe(true); expect(findById).toHaveBeenCalledWith('user-2'); expect(select).toHaveBeenCalledWith('permissions'); expect(redisMock.setKey).toHaveBeenCalledWith('permissions:user-2', { job: { info: true, edit: false }, }); }); it('returns false when the user or action cannot be resolved', async () => { await expect(hasPermission(null, 'job', 'info')).resolves.toBe(false); await expect(hasPermission('user-1', '', 'info')).resolves.toBe(false); expect(redisMock.getKey).not.toHaveBeenCalled(); }); }); describe('checkPermissions middleware', () => { const json = jest.fn(); const status = jest.fn(() => ({ json })); const next = jest.fn(); const res = { status }; beforeEach(() => { jest.clearAllMocks(); redisMock.getKey.mockResolvedValue(null); redisMock.setKey.mockResolvedValue(undefined); lean.mockResolvedValue(null); }); it('calls next when the user is allowed', async () => { redisMock.getKey.mockResolvedValue({ part: { edit: true } }); const req = { user: { _id: 'user-1' } }; await checkPermissions('part', 'edit')(req, res, next); expect(next).toHaveBeenCalledWith(); expect(status).not.toHaveBeenCalled(); }); it('returns 403 when the user is not allowed', async () => { redisMock.getKey.mockResolvedValue({ part: { edit: false } }); const req = { user: { _id: 'user-1' } }; await checkPermissions('part', 'edit')(req, res, next); expect(status).toHaveBeenCalledWith(403); expect(json).toHaveBeenCalledWith({ error: 'Forbidden', code: 'FORBIDDEN' }); expect(next).not.toHaveBeenCalled(); }); });