Daniel
Daniel

Reputation: 2531

Nestjs unit-test - mock method guard

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.

Upvotes: 21

Views: 18930

Answers (2)

victorkurauchi
victorkurauchi

Reputation: 1409

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

Upvotes: 7

jdpnielsen
jdpnielsen

Reputation: 470

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

Upvotes: 33

Related Questions