17

I have started to work with NestJS and have a question about mocking guards for unit-test. I'm trying to test a basic HTTP controller that has a method Guard attach to it.

My issue started when I injected a service to the Guard (I needed the ConfigService for the Guard).

When running the test the DI is unable to resolve the Guard

  ● AppController › root › should return "Hello World!"

    Nest can't resolve dependencies of the ForceFailGuard (?). Please make sure that the argument at index [0] is available in the _RootTestModule context.

My force fail Guard:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { ConfigService } from './config.service';

@Injectable()
export class ForceFailGuard implements CanActivate {

  constructor(
    private configService: ConfigService,
  ) {}

  canActivate(context: ExecutionContext) {
    return !this.configService.get().shouldFail;
  }
}

Spec file:

import { CanActivate } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ForceFailGuard } from './force-fail.guard';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {

    const mock_ForceFailGuard = { CanActivate: jest.fn(() => true) };

    const app: TestingModule = await Test
      .createTestingModule({
        controllers: [AppController],
        providers: [
          AppService,
          ForceFailGuard,
        ],
      })
      .overrideProvider(ForceFailGuard).useValue(mock_ForceFailGuard)
      .overrideGuard(ForceFailGuard).useValue(mock_ForceFailGuard)
      .compile();

    appController = app.get<AppController>(AppController);
  });

  describe('root', () => {

    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });

  });
});

I wasn't able to find examples or documentation on this issues. Am i missing something or is this a real issue ?

Appreciate any help, Thanks.

Esron Silva
  • 65
  • 1
  • 8
Daniel
  • 2,288
  • 1
  • 14
  • 22
  • Have you found a solution for this? I'm facing the same issue. – eol Aug 21 '19 at 11:31
  • 2
    In the future, please paste the relevant code into your question. This makes the question future proof, for when you change or delete the repository you have linked to. This is especially needed in a case like this, when this question is the first thing that pops up when googling "nestjs mock guard". – Enslev Nov 27 '19 at 13:50

2 Answers2

27

There are 3 issues with the example repo provided:

  1. There is a bug in Nestjs v6.1.1 with .overrideGuard() - see https://github.com/nestjs/nest/issues/2070

    I have confirmed that its fixed in 6.5.0.

  2. ForceFailGuard is in providers, but its dependency (ConfigService) is not available in the created TestingModule.

    If you want to mock ForceFailGuard, simply remove it from providers and let .overrideGuard() do its job.

  3. mock_ForceFailGuard had CanActivate as a property instead of canActivate.

Working example (nestjs v6.5.0):

import { CanActivate } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ForceFailGuard } from './force-fail.guard';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const mock_ForceFailGuard: CanActivate = { canActivate: jest.fn(() => true) };

    const app: TestingModule = await Test
      .createTestingModule({
        controllers: [AppController],
        providers: [
          AppService,
        ],
      })
      .overrideGuard(ForceFailGuard).useValue(mock_ForceFailGuard)
      .compile();

    appController = app.get<AppController>(AppController);
  });

  describe('root', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });
  });
});
jdpnielsen
  • 410
  • 5
  • 8
6

If you ever need/want to unit test your custom guard implementation in addition to the controller unit test, you could have something similar to the test below in order to expect for errors etc

// InternalGuard.ts
@Injectable()
export class InternalTokenGuard implements CanActivate {
  constructor(private readonly config: ConfigService) {
  }

  public async canActivate(context: ExecutionContext): Promise<boolean> {
    const token = this.config.get("internalToken");

    if (!token) {
      throw new Error(`No internal token was provided.`);
    }

    const request = context.switchToHttp().getRequest();
    const providedToken = request.headers["authorization"];

    if (token !== providedToken) {
      throw new UnauthorizedException();
    }

    return true;
  }
}

And your spec file

// InternalGuard.spec.ts
beforeEach(async () => {
  const module: TestingModule = await Test.createTestingModule({
    controllers: [],
    providers: [
      InternalTokenGuard,
      {
        provide: ConfigService,
        useValue: {
          get: jest.fn((key: string) => {
            if (key === "internalToken") {
              return 123;
            }
            return null;
          })
        }
      }
    ]
  }).compile();

  config = module.get<ConfigService>(ConfigService);
  guard = module.get<InternalTokenGuard>(InternalTokenGuard);
});

it("should throw UnauthorizedException when token is not Bearer", async () => {
  const context = {
    getClass: jest.fn(),
    getHandler: jest.fn(),
    switchToHttp: jest.fn(() => ({
      getRequest: jest.fn().mockReturnValue({
        headers: {
          authorization: "providedToken"
        }
      })
    }))
  } as any;

  await expect(guard.canActivate(context)).rejects.toThrow(
    UnauthorizedException
  );
  expect(context.switchToHttp).toHaveBeenCalled();
});
GabLeRoux
  • 16,715
  • 16
  • 63
  • 81
victorkurauchi
  • 1,390
  • 18
  • 23