0

I have a nestjs application and some nestjs tests written in jest and they make use of guards. So in the tests i have created fake guards and trying to override them.

Due to multiple overrideGuard configurations I am seeing test failures and my test failures are like

 ● Organization › POST /organizations

    expected 201 "Created", got 500 "Internal Server Error"

      at Test.Object.<anonymous>.Test._assertStatus (node_modules/supertest/lib/test.js:268:12)
      at Test.Object.<anonymous>.Test._assertFunction (node_modules/supertest/lib/test.js:283:11)
      at Test.Object.<anonymous>.Test.assert (node_modules/supertest/lib/test.js:173:18)
      at Server.localAssert (node_modules/supertest/lib/test.js:131:12)

Other error is and i think the main reason for above error would be as well

[Nest] 94156  - 09/21/2022, 11:06:43 PM   ERROR [ExceptionsHandler] Invalid role(s): []
AccessControlError: Invalid role(s): []

Guards are bound to endpoint in this way, attaching some part of the code

import * as common from "@nestjs/common";
import * as swagger from "@nestjs/swagger";
import * as nestAccessControl from "nest-access-control";
import * as defaultAuthGuard from "../../auth/defaultAuth.guard";

@common.UseGuards(defaultAuthGuard.DefaultAuthGuard, nestAccessControl.ACGuard)
export class OrganizationControllerBase {
  constructor(
    protected readonly service: OrganizationService,
    protected readonly rolesBuilder: nestAccessControl.RolesBuilder
  ) {}

  @common.UseInterceptors(AclValidateRequestInterceptor)
  @nestAccessControl.UseRoles({
    resource: "Organization",
    action: "create",
    possession: "any",
  })
  @common.Post()
  @swagger.ApiCreatedResponse({ type: Organization })
  @swagger.ApiForbiddenResponse({ type: errors.ForbiddenException })
  async create(
    @common.Body() data: OrganizationCreateInput
  ): Promise<Organization> {
    return await this.service.create({
      data: data,
      select: {
        id: true,
        createdAt: true,
        updatedAt: true,
        name: true,
      },
    });
  }
}

Some parts of spec code(tests) looks like below

const basicAuthGuard = {
  canActivate: (context: ExecutionContext) => {
    const argumentHost = context.switchToHttp();
    const request = argumentHost.getRequest();
    request.user = {
      roles: ["user"],
    };
    return true;
  },
};

const acGuard = {
  canActivate: () => {
    return true;
  },
};

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      providers: [
        {
          provide: OrganizationService,
          useValue: service,
        },
      ],
      controllers: [OrganizationController],
      imports: [MorganModule.forRoot(), ACLModule],
    })
      .overrideGuard(DefaultAuthGuard)
      .useValue(basicAuthGuard)
      .overrideGuard(ACGuard)
      .useValue(acGuard)
      .compile();

    app = moduleRef.createNestApplication();
    await app.init();
  });

When I removed the .overrideGuard(ACGuard).useValue(acGuard) then test passed fine but I am not sure why is that happening ?

Why the later overrideGuard setting cancelling the previous overrideGuard configuration ?

Edit 1 :

Project on github : https://github.com/shubhwip/sample-app

Steps to reproduce :

SRJ
  • 2,092
  • 3
  • 17
  • 36
  • Do you happen to have something that reproduces this behavior? Code that can be ran locally? I've not heard of this before – Jay McDoniel Sep 25 '22 at 06:58
  • it is on github `https://github.com/shubhwip/sample-app`. clone then run `npm install` and then `npm run test`. – SRJ Sep 25 '22 at 08:00

1 Answers1

1

Your guard mocks are actually working fine, and you can verify this by adding in a console.log() to your mocks. The issue is coming from your AclFilterResponseInterceptor and AclValidateRequestInterceptor interceptors, where you pass role: permissionRoles.role. That value is undefined, so when going into the accesscontrol library, it is invalid and the error is thrown subsequently.

I was able to fix this in your local code by using role: context.switchToHttp().getRequest().user.roles.

I believe this fails when mocking the AC guard because the AcGuard actually mutates the roles metadata. By modifying a reference to the roles, the metadata pulled later in the interceptor is the mutated metadata, not the directly set metadata. One of JavaScript's pass-by-reference quirks with objects.

SRJ
  • 2,092
  • 3
  • 17
  • 36
Jay McDoniel
  • 57,339
  • 7
  • 135
  • 147
  • Thank you very much Jay. I really appreciate it. I wanted to know since in my mocked `AcGuard` I am returning true directly so i am not mutating any roles data so how that is later affected in interceptors. Yes by adding `role: context.switchToThttp().getRequest().user.roles` it works and if the state is mutated then how should we handle this ? in `nest-access-control` layer or in the `sample app` itself ? – SRJ Sep 26 '22 at 09:42
  • 1
    To me, it looks like you're doing e2e testing, so my preference is that as little is mocked as possible. The fact that the metadata gets mutated makes it hard to say what the actual mock guard functionality should be – Jay McDoniel Sep 26 '22 at 13:07