21

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);
    });
  });
});
2
  • 1
    I used to make this test using e2e but definitely they are completelly valid regression testing in my opinion. Commented Jan 6, 2022 at 14:36
  • @RuslanGonzalez yeah, e2e tests are important. I'd argue that the e2e tests check that they're applying the correct functionality and the unit tests check that they're being applied - both are important and crucial that they're working in concert. Unit tests tend to be faster though Commented Jan 6, 2022 at 17:08

3 Answers 3

17

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.

Sign up to request clarification or add additional context in comments.

6 Comments

That's not really what I'm after doing. I'm looking to ensure that the guard decorator is set for the method. I don't care what the decorator does underneath (for the reasons you mention) and I've got tests around the guard itself. If this were a "classic" Express app, I could be able to test that the middleware is applied for routing which is what I'm trying to achieve here, but with the decorators
I'm not quite sure I understand what it is you are trying to achieve then. The first link shows tests that show the metadata being set correctly on both classes and class methods which is how the guards are "set". If you're wanting to test that when you call the route the guard is executed, then you need to set up supertest to make the call to the route. Maybe I'm not understanding what you're looking to do
Yeah, an e2e test is one option of achieving that. It may well be that's a more appropriate way of doing it. The example I gave does check that the correct metadata is set, but that's because I couldn't work out a better way of testing that the @Get decorator is applied (I'm open to suggestions). What I'm trying to achieve is a test to ensure that the appropriate guards are set - I'd prefer to do it through unit tests rather than e2e, because it'll appear in coverage reports but that's not a deal-breaker
It seems that reflection of metadata is still going to be your best bet here. In your above example you could have a test like expect(Reflect.getMetadata('__guards__', UserController.user)).toEqual(MixinAuthGuard). MixinAuthGuard is the class that the mixin AuthGuard('jwt') produces (or should be). This would assert that the guard applied to the UserController.user method (i.e. the GET /api/user route) would be the correct guard
Also see a related question (stackoverflow.com/questions/62595603/…), there's an up-to-date example for mocking an ExecutionContext there. Also worth mentioning -- the package @golevelup/nestjs-testing has been renamed to @golevelup/ts-jest, see github.com/golevelup/nestjs/issues/265
|
10

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

1 Comment

Great, thanks. As I said in the OP, I'm not after testing what the decorators do, just that they're applied with the appropriate configuration. As we're relying upon it in the code, I'm of the opinion that this should be part of the unit tests
6

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
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.