简体   繁体   中英

How can I unit test that a guard is applied on a controller in NestJS?

I've got a controller configured in NestJS and I want to check that the appropriate guards are set - does anyone have an example of how it could be done?

This (abridged) example works correctly as an application so I'm only after guidance on testing.

You'll notice in the user test there are tests where I'm calling Reflect.getMetadata . I'm after something like this - when I check it on the __guards__ metadata, this is a function and I'm struggling to mock it out so I can check that it's applied with AuthGuard('jwt') as it's setting.

User.controller.ts

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

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);
    });
  });
});

For what it's worth, you shouldn't need to test that the decorators provided by the framework set what you expect them too. That's why the framework has tests on them to begin with. Nevertheless, if you want to check that the decorator actually sets the expected metadata you can see that done here .

If you are just looking to test the guard, you can instantiate the GuardClass directly and test its canActivate method by providing an ExecutionContext object. I've got an example here . The example uses a library that creates mock objects for you (since then renamed ), but the idea of it is that you'd create an object like

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

Where getRequest and getResponse return HTTP Request and Response objects (or at least partials of them). To just use this object, you'll need to also use as any to keep Typescript from complaining too much.

Based on myol's answer, I made a utility function to test this in one liner. It's features are:

  1. The tests are one liners.

  2. Works even when there are multiple guards.

  3. It shows meaningful jest style error messages when the tests fail. For example:

    Expected: findMe to be protected with JwtAuthGuard

    Received: only AdminGuard,EditorGuard

Here's how the test looks like:

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

And for testing the guard on the entire controller, call the same function as follows:

  expect(isGuarded(UsersController, JwtAuthGuard)).toBe(true)

Here's the utility function isGuarded() . You can copy this to any file like 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
}

I realize its not quite the answer you are looking for, but building on @Jay McDoniel's answer I used the following to test a custom decorator's existence on a controller function (although i'm not 100% sure if this is the correct way to test this for non-custom guards)

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

@Controller()
export class MyController {

  @UseGuards(JwtAuthGuard)
  user() {
    ...
  }
}
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)
});

And for controllers

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)
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM