kt-template-online-api/test/modules/qqbot/message-push/qqbot-message-push-permission.guard.spec.ts

272 lines
7.1 KiB
TypeScript

import type { ExecutionContext } from '@nestjs/common';
import { HttpException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { QqbotMessagePushPermissionGuard } from '../../../../src/modules/qqbot/core/contract/message-push/qqbot-message-push-permission.guard';
import { QqbotMessagePushPermission } from '../../../../src/modules/qqbot/core/contract/message-push/qqbot-message-push-permission.decorator';
type RoleInput = {
isDeleted?: boolean;
menus?: Array<{
authCode?: string;
isDeleted?: boolean;
status?: number;
}>;
roleCode?: string;
status?: number;
};
const contextFor = (
roles: RoleInput[],
handler: () => void = () => undefined,
): ExecutionContext =>
({
getClass: () => class TestController {},
getHandler: () => handler,
switchToHttp: () => ({
getRequest: () => ({ adminUser: { roles } }),
}),
}) as unknown as ExecutionContext;
const handlerWithPermission = (...authCodes: string[]): (() => void) => {
class PermissionFixture {
@QqbotMessagePushPermission(...authCodes)
run(): void {}
}
return PermissionFixture.prototype.run;
};
describe('QqbotMessagePushPermissionGuard', () => {
const guard = new QqbotMessagePushPermissionGuard(new Reflector());
const handler = handlerWithPermission('QqBot:MessageTemplate:Preview');
it('allows only an active non-deleted super assignment to bypass', () => {
expect(
guard.canActivate(
contextFor(
[{ isDeleted: false, roleCode: 'super', status: 1 }],
handler,
),
),
).toBe(true);
expect(() =>
guard.canActivate(
contextFor(
[{ isDeleted: true, roleCode: 'super', status: 1 }],
handler,
),
),
).toThrow(HttpException);
expect(() =>
guard.canActivate(
contextFor(
[{ isDeleted: false, roleCode: 'super', status: 0 }],
handler,
),
),
).toThrow(HttpException);
});
it('uses only active roles and active exact-code menus', () => {
expect(
guard.canActivate(
contextFor(
[
{
isDeleted: false,
menus: [
{
authCode: 'QqBot:MessageSubscription:List',
isDeleted: false,
status: 1,
},
{
authCode: 'QqBot:MessageTemplate:Preview',
isDeleted: false,
status: 1,
},
],
roleCode: 'operator',
status: 1,
},
],
handler,
),
),
).toBe(true);
for (const roles of [
[
{
isDeleted: true,
menus: [
{
authCode: 'QqBot:MessageTemplate:Preview',
isDeleted: false,
status: 1,
},
],
status: 1,
},
],
[
{
isDeleted: false,
menus: [
{
authCode: 'QqBot:MessageTemplate:Preview',
isDeleted: false,
status: 1,
},
],
status: 0,
},
],
[
{
isDeleted: false,
menus: [
{
authCode: 'QqBot:MessageTemplate:Preview',
isDeleted: true,
status: 1,
},
],
status: 1,
},
],
[
{
isDeleted: false,
menus: [
{
authCode: 'QqBot:MessageTemplate:Preview',
isDeleted: false,
status: 0,
},
],
status: 1,
},
],
]) {
expect(() => guard.canActivate(contextFor(roles, handler))).toThrow(
HttpException,
);
}
});
it('fails closed when route metadata is missing or empty', () => {
expect(() => guard.canActivate(contextFor([]))).toThrow(HttpException);
class EmptyPermissionFixture {
@QqbotMessagePushPermission()
run(): void {}
}
expect(() =>
guard.canActivate(contextFor([], EmptyPermissionFixture.prototype.run)),
).toThrow(HttpException);
});
it.each([
[
'source read',
[
'QqBot:MessageSubscription:List',
'QqBot:MessageSubscription:Create',
'QqBot:MessageSubscription:Update',
'QqBot:MessageTemplate:List',
'QqBot:MessageTemplate:Create',
'QqBot:MessageTemplate:Update',
'QqBot:MessageTemplate:Preview',
'QqBot:Account:MessagePush:List',
'QqBot:Account:MessagePush:Create',
'QqBot:Account:MessagePush:Update',
],
],
[
'source options',
[
'QqBot:MessageSubscription:Create',
'QqBot:MessageSubscription:Update',
'QqBot:Account:MessagePush:Create',
'QqBot:Account:MessagePush:Update',
],
],
[
'subscription read',
[
'QqBot:MessageSubscription:List',
'QqBot:Account:MessagePush:List',
'QqBot:Account:MessagePush:Create',
'QqBot:Account:MessagePush:Update',
],
],
[
'template read',
[
'QqBot:MessageTemplate:List',
'QqBot:Account:MessagePush:List',
'QqBot:Account:MessagePush:Create',
'QqBot:Account:MessagePush:Update',
],
],
[
'target read',
['QqBot:Account:MessagePush:Create', 'QqBot:Account:MessagePush:Update'],
],
])('accepts every independent %s OR permission', (_label, authCodes) => {
const routeHandler = handlerWithPermission(...authCodes);
authCodes.forEach((authCode) => {
expect(
guard.canActivate(
contextFor(
[
{
isDeleted: false,
menus: [{ authCode, isDeleted: false, status: 1 }],
roleCode: 'operator',
status: 1,
},
],
routeHandler,
),
),
).toBe(true);
});
});
it.each([
['QqBot:MessageSubscription:Create', 'QqBot:MessageSubscription:List'],
['QqBot:MessageSubscription:Update', 'QqBot:MessageSubscription:Create'],
['QqBot:MessageTemplate:Delete', 'QqBot:MessageTemplate:List'],
['QqBot:Account:MessagePush:Toggle', 'QqBot:Account:MessagePush:Update'],
])(
'does not let neighboring code %s grant mutation %s',
(required, neighboring) => {
expect(() =>
guard.canActivate(
contextFor(
[
{
isDeleted: false,
menus: [{ authCode: neighboring, isDeleted: false, status: 1 }],
roleCode: 'operator',
status: 1,
},
],
handlerWithPermission(required),
),
),
).toThrow(HttpException);
},
);
it('fails closed when JwtAuthGuard did not populate adminUser', () => {
const missingUserContext = {
getClass: () => class TestController {},
getHandler: () => handler,
switchToHttp: () => ({ getRequest: () => ({}) }),
} as unknown as ExecutionContext;
expect(() => guard.canActivate(missingUserContext)).toThrow(HttpException);
});
});