tududi/backend/tests/unit/middleware/authorize.test.js
Chris 3486541272
Add comprehensive LLM development documentation (#939)
* Increase coverage

* Add comprehensive LLM development documentation

- Add CLAUDE.md as main documentation index
- Create 8 detailed documentation files in docs/:
  - architecture.md: Tech stack, data models, auth system
  - directory-structure.md: Complete file tree with paths
  - backend-patterns.md: Module architecture and patterns
  - database.md: Models, migrations, and workflows
  - development-workflow.md: Setup and daily development
  - code-conventions.md: Style guide and best practices
  - testing.md: Test organization and patterns
  - common-tasks.md: How-to guides for frequent tasks
- Update .gitignore to allow project-level CLAUDE.md
- 4,285 lines of comprehensive documentation
- Organized for easy navigation with cross-links
- LLM-optimized with absolute paths and code examples

* fixup! Add comprehensive LLM development documentation
2026-03-14 02:54:59 +02:00

265 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { hasAccess } = require('../../../middleware/authorize');
const permissionsService = require('../../../services/permissionsService');
jest.mock('../../../services/permissionsService');
describe('authorize middleware hasAccess', () => {
let req, res, next;
beforeEach(() => {
req = {
currentUser: { id: 42 },
session: { userId: 42 },
params: { uid: 'abc123' },
};
res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
next = jest.fn();
jest.clearAllMocks();
});
// --- UID resolution ---
it('should resolve uid from a function', async () => {
permissionsService.getAccess.mockResolvedValue('rw');
const mw = hasAccess('ro', 'project', (r) => r.params.uid);
await mw(req, res, next);
expect(permissionsService.getAccess).toHaveBeenCalledWith(
42,
'project',
'abc123'
);
expect(next).toHaveBeenCalled();
});
it('should resolve uid from a static string', async () => {
permissionsService.getAccess.mockResolvedValue('rw');
const mw = hasAccess('ro', 'project', 'static-uid');
await mw(req, res, next);
expect(permissionsService.getAccess).toHaveBeenCalledWith(
42,
'project',
'static-uid'
);
expect(next).toHaveBeenCalled();
});
it('should resolve uid from an async function', async () => {
permissionsService.getAccess.mockResolvedValue('rw');
const mw = hasAccess('ro', 'task', async (r) => r.params.uid);
await mw(req, res, next);
expect(permissionsService.getAccess).toHaveBeenCalledWith(
42,
'task',
'abc123'
);
expect(next).toHaveBeenCalled();
});
it('should return 404 when uid resolves to null', async () => {
const mw = hasAccess('ro', 'project', () => null);
await mw(req, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ error: 'Not found' });
expect(next).not.toHaveBeenCalled();
});
it('should return 404 when uid resolves to undefined', async () => {
const mw = hasAccess('ro', 'project', () => undefined);
await mw(req, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(next).not.toHaveBeenCalled();
});
it('should return 404 when uid resolves to empty string', async () => {
const mw = hasAccess('ro', 'project', () => '');
await mw(req, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(next).not.toHaveBeenCalled();
});
// --- Permission hierarchy ---
it('should allow access when user has exact required level (ro)', async () => {
permissionsService.getAccess.mockResolvedValue('ro');
const mw = hasAccess('ro', 'project', () => 'uid1');
await mw(req, res, next);
expect(next).toHaveBeenCalled();
});
it('should allow access when user has higher than required (rw > ro)', async () => {
permissionsService.getAccess.mockResolvedValue('rw');
const mw = hasAccess('ro', 'project', () => 'uid1');
await mw(req, res, next);
expect(next).toHaveBeenCalled();
});
it('should allow access when user has admin and rw is required', async () => {
permissionsService.getAccess.mockResolvedValue('admin');
const mw = hasAccess('rw', 'project', () => 'uid1');
await mw(req, res, next);
expect(next).toHaveBeenCalled();
});
it('should deny access when user has ro but rw is required', async () => {
permissionsService.getAccess.mockResolvedValue('ro');
const mw = hasAccess('rw', 'project', () => 'uid1');
await mw(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({ error: 'Forbidden' });
expect(next).not.toHaveBeenCalled();
});
it('should deny access when user has none', async () => {
permissionsService.getAccess.mockResolvedValue('none');
const mw = hasAccess('ro', 'project', () => 'uid1');
await mw(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({ error: 'Forbidden' });
expect(next).not.toHaveBeenCalled();
});
// --- User ID extraction ---
it('should prefer currentUser.id over session.userId', async () => {
req.currentUser = { id: 100 };
req.session = { userId: 200 };
permissionsService.getAccess.mockResolvedValue('rw');
const mw = hasAccess('ro', 'project', () => 'uid1');
await mw(req, res, next);
expect(permissionsService.getAccess).toHaveBeenCalledWith(
100,
'project',
'uid1'
);
});
it('should fall back to session.userId when currentUser is missing', async () => {
req.currentUser = null;
req.session = { userId: 200 };
permissionsService.getAccess.mockResolvedValue('rw');
const mw = hasAccess('ro', 'project', () => 'uid1');
await mw(req, res, next);
expect(permissionsService.getAccess).toHaveBeenCalledWith(
200,
'project',
'uid1'
);
});
// --- Options ---
it('should use custom notFoundMessage', async () => {
const mw = hasAccess('ro', 'project', () => null, {
notFoundMessage: 'Project not found',
});
await mw(req, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ error: 'Project not found' });
});
it('should return 404 instead of 403 when forbiddenStatus is 404', async () => {
permissionsService.getAccess.mockResolvedValue('none');
const mw = hasAccess('rw', 'project', () => 'uid1', {
forbiddenStatus: 404,
});
await mw(req, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ error: 'Not found' });
});
it('should use custom notFoundMessage with forbiddenStatus 404', async () => {
permissionsService.getAccess.mockResolvedValue('none');
const mw = hasAccess('rw', 'project', () => 'uid1', {
forbiddenStatus: 404,
notFoundMessage: 'Task not found',
});
await mw(req, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ error: 'Task not found' });
});
// --- Resource types ---
it('should pass correct resource type for tasks', async () => {
permissionsService.getAccess.mockResolvedValue('rw');
const mw = hasAccess('rw', 'task', () => 'task-uid');
await mw(req, res, next);
expect(permissionsService.getAccess).toHaveBeenCalledWith(
42,
'task',
'task-uid'
);
});
it('should pass correct resource type for notes', async () => {
permissionsService.getAccess.mockResolvedValue('rw');
const mw = hasAccess('rw', 'note', () => 'note-uid');
await mw(req, res, next);
expect(permissionsService.getAccess).toHaveBeenCalledWith(
42,
'note',
'note-uid'
);
});
// --- Error handling ---
it('should forward errors from getAccess to next()', async () => {
const error = new Error('Database failure');
permissionsService.getAccess.mockRejectedValue(error);
const mw = hasAccess('ro', 'project', () => 'uid1');
await mw(req, res, next);
expect(next).toHaveBeenCalledWith(error);
expect(res.status).not.toHaveBeenCalled();
});
it('should forward errors from getResourceUid to next()', async () => {
const error = new Error('UID lookup failed');
const mw = hasAccess('ro', 'project', () => {
throw error;
});
await mw(req, res, next);
expect(next).toHaveBeenCalledWith(error);
expect(res.status).not.toHaveBeenCalled();
});
});