如何对 NestJS 中的控制器应用防护进行单元测试?

Rig*_*eek 5 node.js typescript jestjs nestjs

我在 NestJS 中配置了一个控制器,我想检查是否设置了适当的保护 - 有没有人有如何完成的示例?

这个(删节的)示例作为一个应用程序可以正常工作,所以我只是在测试指导之后。

您会注意到在用户测试中有我正在调用的测试Reflect.getMetadata。我在做这样的事情 - 当我在__guards__元数据上检查它时,这是一个函数,我正在努力模拟它,以便我可以检查它AuthGuard('jwt')是否在设置时应用。

用户控制器.ts

@Controller('/api/user')
export class UserController {
  @UseGuards(AuthGuard('jwt'))
  @Get()
  user(@Request() req) {
    return req.user;
  }
}
Run Code Online (Sandbox Code Playgroud)

User.controller.spec.ts

describe('User Controller', () => {
  // beforeEach setup as per the cli generator

  describe('#user', () => {
    beforeEach(() => {
      // This is how I'm checking the @Get() decorator is applied correctly - I'm after something for __guards__
      expect(Reflect.getMetadata('path', controller.user)).toBe('/');
      expect(Reflect.getMetadata('method', controller.user)).toBe(RequestMethod.GET);
    });

    it('should return the user', () => {
      const req = {
        user: 'userObj',
      };

      expect(controller.user(req)).toBe(req.user);
    });
  });
});
Run Code Online (Sandbox Code Playgroud)

myo*_*yol 7

我意识到这并不完全是您正在寻找的答案,但是在 @Jay McDoniel 的答案的基础上,我使用以下内容来测试控制器函数上自定义装饰器的存在(尽管我不能 100% 确定这是否是测试的正确方法)这适用于非定制警卫)

import { Controller } from '@nestjs/common';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './jwtAuthGuard';

@Controller()
export class MyController {

  @UseGuards(JwtAuthGuard)
  user() {
    ...
  }
}
Run Code Online (Sandbox Code Playgroud)
it('should ensure the JwtAuthGuard is applied to the user method', async () => {
  const guards = Reflect.getMetadata('__guards__', MyController.prototype.user)
  const guard = new (guards[0])

  expect(guard).toBeInstanceOf(JwtAuthGuard)
});
Run Code Online (Sandbox Code Playgroud)

对于控制器来说

it('should ensure the JwtAuthGuard is applied to the controller', async () => {
  const guards = Reflect.getMetadata('__guards__', MyController)
  const guard = new (guards[0])

  expect(guard).toBeInstanceOf(JwtAuthGuard)
});
Run Code Online (Sandbox Code Playgroud)


Jay*_*iel 5

对于它的价值,您不应该需要测试框架提供的装饰器是否也设置了您所期望的。这就是该框架开始对它们进行测试的原因。不过,如果您想检查装饰器是否实际设置了预期的元数据,您可以在此处看到已完成的操作

如果你只是想测试守卫,你可以直接实例化 GuardClass 并canActivate通过提供一个ExecutionContext对象来测试它的方法。我这里有一个例子。该示例使用一个为您创建模拟对象(从那时起重命名),但它的想法是您将创建一个对象,如

const mockExecutionContext: Partial<
  Record<
    jest.FunctionPropertyNames<ExecutionContext>,
    jest.MockedFunction<any>
  >
> = {
  switchToHttp: jest.fn().mockReturnValue({
    getRequest: jest.fn(),
    getResponse: jest.fn(),
  }),
};
Run Code Online (Sandbox Code Playgroud)

在哪里getRequestgetResponse返回 HTTP 请求和响应对象(或至少部分)。要仅使用此对象,您还需要使用as any来防止 Typescript 抱怨太多。

  • 那并不是我真正想做的事。我希望确保为该方法设置了防护装饰器。我不关心装饰器在下面做什么(出于你提到的原因),并且我已经对守卫本身进行了测试。如果这是一个“经典”的 Express 应用程序,我可以测试中间件是否应用于路由,这就是我在这里想要实现的目标,但是使用装饰器 (3认同)

Yog*_*ity 5

根据 myol 的回答,我制作了一个实用函数来在一个衬垫中测试这一点。其特点是:

  1. 测试是单行的。

  2. 即使有多个守卫也能工作。

  3. 当测试失败时,它会显示有意义的笑话风格的错误消息。例如:

    Expected: findMe to be protected with JwtAuthGuard

    Received: only AdminGuard,EditorGuard

测试如下所示:

it(`should be protected with JwtAuthGuard.`, async () => {
  expect(isGuarded(UsersController.prototype.findMe, JwtAuthGuard)).toBe(true)
})
Run Code Online (Sandbox Code Playgroud)

为了测试整个控制器上的防护,请调用相同的函数,如下所示:

  expect(isGuarded(UsersController, JwtAuthGuard)).toBe(true)
Run Code Online (Sandbox Code Playgroud)

这是效用函数isGuarded()。您可以将其复制到任何文件,例如test/utils.ts

/**
 * Checks whether a route or a Controller is protected with the specified Guard.
 * @param route is the route or Controller to be checked for the Guard.
 * @param guardType is the type of the Guard, e.g. JwtAuthGuard.
 * @returns true if the specified Guard is applied.
 */
export function isGuarded(
  route: ((...args: any[]) => any) | (new (...args: any[]) => unknown),
  guardType: new (...args: any[]) => CanActivate
) {
  const guards: any[] = Reflect.getMetadata('__guards__', route)

  if (!guards) {
    throw Error(
      `Expected: ${route.name} to be protected with ${guardType.name}\nReceived: No guard`
    )
  }

  let foundGuard = false
  const guardList: string[] = []
  guards.forEach((guard) => {
    guardList.push(guard.name)
    if (guard.name === guardType.name) foundGuard = true
  })

  if (!foundGuard) {
    throw Error(
      `Expected: ${route.name} to be protected with ${guardType.name}\nReceived: only ${guardList}`
    )
  }
  return true
}
Run Code Online (Sandbox Code Playgroud)