Files
dh-inspeccion-v2/api-v3/test/unit/permissions.guard.test.ts
T

141 lines
3.6 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
ExecutionContext,
ForbiddenException,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { RequirePermissions } from '../../src/authorization/decorators/require-permissions.decorator';
import { PermissionsGuard } from '../../src/authorization/guards/permissions.guard';
import { Public } from '../../src/auth/decorators/public.decorator';
import type { AuthPrincipal } from '../../src/common/http/request-context';
@RequirePermissions('dashboard.read')
class ProtectedController {
@RequirePermissions('users.create')
createUser() {}
openToAuthenticatedUsers() {}
}
class UndecoratedController {
handle() {}
}
@RequirePermissions('dashboard.read')
class PublicController {
@Public()
handle() {}
}
function principal(permissions: string[]): AuthPrincipal {
return {
userId: 'b6f6b10e-434a-4d72-9ec9-3520212dd290',
username: 'admin',
sessionId: '1bab61ac-65d1-40e6-816b-63d90131a795',
firstName: 'Admin',
lastName: 'DH',
email: null,
mustChangePassword: false,
roles: ['admin'],
permissions,
transport: 'cookie',
};
}
function context(
controller: new () => object,
handlerName: string,
auth?: AuthPrincipal,
): ExecutionContext {
const handler = controller.prototype[
handlerName as keyof typeof controller.prototype
];
return {
getHandler: () => handler,
getClass: () => controller,
switchToHttp: () => ({
getRequest: () => ({ auth }),
}),
} as unknown as ExecutionContext;
}
const guard = new PermissionsGuard(new Reflector());
test('PermissionsGuard leaves handlers without requirements available', () => {
assert.equal(
guard.canActivate(context(UndecoratedController, 'handle')),
true,
);
});
test('PermissionsGuard gives explicit public metadata priority', () => {
assert.equal(guard.canActivate(context(PublicController, 'handle')), true);
});
test('PermissionsGuard returns 401 when required permissions have no principal', () => {
assert.throws(
() => guard.canActivate(context(ProtectedController, 'createUser')),
(error: unknown) =>
error instanceof UnauthorizedException && error.getStatus() === 401,
);
});
test('PermissionsGuard returns 403 when any required permission is missing', () => {
assert.throws(
() =>
guard.canActivate(
context(
ProtectedController,
'createUser',
principal(['dashboard.read']),
),
),
(error: unknown) =>
error instanceof ForbiddenException && error.getStatus() === 403,
);
});
test('PermissionsGuard requires changing a temporary password first', () => {
const auth = principal(['dashboard.read', 'users.create']);
auth.mustChangePassword = true;
assert.throws(
() =>
guard.canActivate(context(ProtectedController, 'createUser', auth)),
(error: unknown) =>
error instanceof ForbiddenException &&
error.getStatus() === 403 &&
(error.getResponse() as { code?: string }).code ===
'PASSWORD_CHANGE_REQUIRED',
);
});
test('PermissionsGuard authorizes when class and handler permissions are granted', () => {
assert.equal(
guard.canActivate(
context(
ProtectedController,
'createUser',
principal(['dashboard.read', 'users.create']),
),
),
true,
);
});
test('PermissionsGuard applies class requirements to every handler', () => {
assert.equal(
guard.canActivate(
context(
ProtectedController,
'openToAuthenticatedUsers',
principal(['dashboard.read']),
),
),
true,
);
});