I have a NestJS backend with CSRF protection and an endpoint to get the CSRF token. I'm getting TypeError: req.csrfToken is not a function when testing this endpoint with jest and supertest.
My code is like this:
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cookieParser());
app.use(csurf({ cookie: true }));
await app.listen(process.env.BACKEND_SERVER_PORT);
}
// authController.ts
import { Controller, Req, Get } from "#nestjs/common";
import { Request } from "express";
import * as AuthDto from "./modules/auth/dto/auth.dto";
#Controller("auth")
export class AppController {
constructor() {}
#Get("csrf")
async getCsrfToken(#Req() req: Request): Promise<AuthDto.CsrfOutput> {
return { csrfToken: req.csrfToken() }; // <-- ERROR HAPPENS HERE
}
}
// app.controller.spec.ts
import { Test, TestingModule } from "#nestjs/testing";
import { INestApplication } from "#nestjs/common";
import request from "supertest";
import AppModule from "../src/app.module";
describe("AppController (e2e)", () => {
let app: INestApplication;
let server: any;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
it("/csrf (GET)", () => {
return request(server).get("/auth/csrf").expect(200).expect("hello");
// I'll add more validations here once I get past this error
});
});
I believe this may be related with the test itself, since this endpoint is working fine when called by an external client (our frontend application or Postman). It just doesn't work with supertest.
Anybody has an idea why? Thanks.
You only register csurf in main.ts, but your test uses AppModule directly. AppModule doesn't register csurf on its own. Therefore, when the test creates your AppModule, it doesn't have the necessary middleware.
Related
auth.controller file
`import { Test, TestingModule } from '#nestjs/testing';
import { AuthController } from './auth.controller';
describe('AuthController', () => {
let controller: AuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile();
controller = module.get<AuthController>(AuthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});`
auth.service file
`import { Injectable } from '#nestjs/common';
#Injectable({})
export class AuthService {
signin()
{
return { msg: 'i am signin' };
}
signup()
{
return { msg: 'i am signup' };
}
}`
i am beginner level and getting this type of error even if remove all the code to simple return message my vcode gives same error what can be the issue
I'm surprised you're not getting a Dependency Injection error related to a missing AuthService dependency. You need to provide a custom provider for the AuthService to inject into the AuthController for the purpose of testing.
This repository has a lot of testing examples to take a look at
Im new to NestJS and im trying to setup my end to end testing. It does not throw any errors but the request always returns 404.
The test look like this:
import { CreateProductDto } from './../src/products/dto/create-product.dto';
import { ProductsModule } from './../src/products/products.module';
import { Product } from './../src/products/entities/product.entity';
import { Repository } from 'typeorm';
import { INestApplication } from '#nestjs/common';
import * as request from 'supertest';
import { createTypeOrmConfig } from './utils';
import { Test, TestingModule } from '#nestjs/testing';
import { TypeOrmModule } from '#nestjs/typeorm';
describe('ProductsController (e2e)', () => {
let app: INestApplication;
let productRepository: Repository<Product>;
beforeEach(async () => {
const config = createTypeOrmConfig();
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot(config),
TypeOrmModule.forFeature([Product]),
ProductsModule,
],
}).compile();
app = moduleFixture.createNestApplication();
productRepository = moduleFixture.get('ProductRepository');
await app.init();
});
afterAll(async () => {
await app.close();
});
it('create', async () => {
// Arrange
const createProductDto = new CreateProductDto();
createProductDto.name = 'testname';
createProductDto.description = 'testdescription';
createProductDto.price = 120;
createProductDto.image = 'testimage';
// Act
const response = await request(app.getHttpServer())
.post(`/api/v1/products`)
.send(createProductDto);
// Assert
expect(response.status).toEqual(201);
});
});
I expected it to returns a 201 response. I tried more routes and same as before.
Do you have #Controller('api/v1/products') on your ProductsController? If not, you don't ever configure the test application to be served from /api/v1, you'd need to set the global prefix and enable versioning (assuming you usually do these in your main.ts). For a simple fix, remove the /api/v1 from your .post() method.
I'm tring to handle bodyparser errors with NestJS but I can't figure out how
this is what I have done so far;
main.ts
const expressServer = express.default();
const createFunction = async (expressInstance): Promise<void> => {
const app = await NestFactory.create(AppModule, new ExpressAdapter(expressInstance), {
cors: true,
bodyParser: true,
});
app.useGlobalFilters(new AllExceptionsFilter());
app.use(helmet());
await app.init();
};
createFunction(expressServer)
.then((v) => console.log('Nest ok'))
.catch((err) => console.error('Nest ko', err));
export const api = functions.region('europe-west3').https.onRequest(expressServer);
I tried to catch the error after bodyparser.
I then tried to use Filters
app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '#nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { APP_FILTER } from '#nestjs/core';
import { AllExceptionsFilter } from './catch-all.filter';
#Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule {}
and
catch-all.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '#nestjs/common';
#Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
But if I try to send a req via postman with a malformed JSON the server crashes
any idea of how should I do it?
Since this is error occurs outside of the NestJS context (it uses the body-parser library), you must handle it with an express middleware.
Try to implement one, which can detect, and handle these errors. For example:
app.use((err, req, res, next) => {
if (err instanceof SyntaxError &&
err.status >= 400 && err.status < 500 &&
err.message.indexOf('JSON') !== -1) {
res.status(400).send('send your own response here')
}
// ...
})
Is it possible to use ClassSerializeInterceptor while testing Nest.JS controllers?
Our issue is that the ClassSerializeInterceptor works fine as part of the app but does not run when the controller is instantiated as part of a unit test. I tried providing ClassSerializeInterdeptor as part of the Testing Module but no luck.
Example code:
Test
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService, ClassSerializerInterceptor],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should not expose exlcuded fields"', async () => {
// expect(appController.getHello()).toBe('Hello World!');
const s = await appController.getHello();
expect(s).toHaveProperty('shouldBeIncluded');
expect(s).not.toHaveProperty('shouldBeRemoved');
});
});
});
Test Entity:
#ObjectType()
#Entity()
export class TestEntity {
#Field(type => ID)
#PrimaryGeneratedColumn('uuid')
shouldBeIncluded: string;
#Column({ nullable: true })
#Exclude()
shouldBeRemoved: string;
}
Testing interceptors as a part of the request flow can only be done in e2e and partial integration test. You'll need to set up a supertest instance as described in the docs and send in the request to ensure that the ClassSerializeInterceptor is running as you expect it to.
You can use:
import { createMock } from '#golevelup/ts-jest';
import { Test } from '#nestjs/testing';
import { Reflector } from '#nestjs/core';
import {
CallHandler,
ClassSerializerInterceptor,
ExecutionContext,
} from '#nestjs/common';
const executionContext = createMock<ExecutionContext>();
const callHandler = createMock<CallHandler>();
const app = await Test.createTestingModule({}).compile();
const reflector = app.get(Reflector);
const serializer = new ClassSerializerInterceptor(reflector, {
strategy: 'excludeAll',
});
callHandler.handle.mockReturnValueOnce(from(['Hello world']));
const actualResult = await lastValueFrom(serializer.intercept(executionContext, callHandler));
expect(actualResult).toEqual('Hello world')
I'm trying to do a e2e testing to a route that has an AuthGuard from nestjs passport module and I don't really know how to approach it. When I run the tests it says:
[ExceptionHandler] Unknown authentication strategy "bearer"
I haven't mock it yet so I suppose it's because of that but I don't know how to do it.
This is what I have so far:
player.e2e-spec.ts
import { Test } from '#nestjs/testing';
import { INestApplication } from '#nestjs/common';
import * as request from 'supertest';
import { PlayerModule } from '../src/modules/player.module';
import { PlayerService } from '../src/services/player.service';
import { Repository } from 'typeorm';
describe('/player', () => {
let app: INestApplication;
const playerService = { updatePasswordById: (id, password) => undefined };
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [PlayerModule],
})
.overrideProvider(PlayerService)
.useValue(playerService)
.overrideProvider('PlayerRepository')
.useClass(Repository)
.compile();
app = module.createNestApplication();
await app.init();
});
it('PATCH /password', () => {
return request(app.getHttpServer())
.patch('/player/password')
.expect(200);
});
});
player.module.ts
import { Module } from '#nestjs/common';
import { PlayerService } from 'services/player.service';
import { PlayerController } from 'controllers/player.controller';
import { TypeOrmModule } from '#nestjs/typeorm';
import { Player } from 'entities/player.entity';
import { PassportModule } from '#nestjs/passport';
#Module({
imports: [
TypeOrmModule.forFeature([Player]),
PassportModule.register({ defaultStrategy: 'bearer' }),
],
providers: [PlayerService],
controllers: [PlayerController],
exports: [PlayerService],
})
export class PlayerModule {}
Below is a e2e test for an auth API based using TypeORM and the passportjs module for NestJs. the auth/authorize API checks to see if the user is logged in. The auth/login API validates a username/password combination and returns a JSON Web Token (JWT) if the lookup is successful.
import { HttpStatus, INestApplication } from '#nestjs/common';
import { Test } from '#nestjs/testing';
import { TypeOrmModule } from '#nestjs/typeorm';
import * as request from 'supertest';
import { UserAuthInfo } from '../src/user/user.auth.info';
import { UserModule } from '../src/user/user.module';
import { AuthModule } from './../src/auth/auth.module';
import { JWT } from './../src/auth/jwt.type';
import { User } from '../src/entity/user';
describe('AuthController (e2e)', () => {
let app: INestApplication;
let authToken: JWT;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [TypeOrmModule.forRoot(), AuthModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('should detect that we are not logged in', () => {
return request(app.getHttpServer())
.get('/auth/authorized')
.expect(HttpStatus.UNAUTHORIZED);
});
it('disallow invalid credentials', async () => {
const authInfo: UserAuthInfo = {username: 'auser', password: 'badpass'};
const response = await request(app.getHttpServer())
.post('/auth/login')
.send(authInfo);
expect(response.status).toBe(HttpStatus.UNAUTHORIZED);
});
it('return an authorization token for valid credentials', async () => {
const authInfo: UserAuthInfo = {username: 'auser', password: 'goodpass'};
const response = await request(app.getHttpServer())
.post('/auth/login')
.send(authInfo);
expect(response.status).toBe(HttpStatus.OK);
expect(response.body.user.username).toBe('auser');
expect(response.body.user.firstName).toBe('Adam');
expect(response.body.user.lastName).toBe('User');
authToken = response.body.token;
});
it('should show that we are logged in', () => {
return request(app.getHttpServer())
.get('/auth/authorized')
.set('Authorization', `Bearer ${authToken}`)
.expect(HttpStatus.OK);
});
});
Note since this is an end-to-end test, it doesn't use mocking (at least my end-to-end tests don't :)). Hope this is helpful.
You can just generate a valid token and send it inside header for authentication using passport (jwt,...):
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.compile();
productService = moduleFixture.get<ProductService>(ProductService);
userService = moduleFixture.get<UsersService>(UsersService);
authService = moduleFixture.get<AuthenticationService>(
AuthenticationService,
);
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
const signupResult = await authService.signup({
email: 'test#test.com',
name: 'test user',
password: 'testpassword',
});
accessToken = signupResult.token.access_token; // this line find a valid access token and you can pass this to your tests
});
Pass access_token to requests:
it('should return 201 and return product', async () => {
return request(app.getHttpServer())
.post('/products')
.set('Authorization', `Bearer ${accessToken}`) // this is for Authentication
.send({})
.expect(201)
.expect(({ body }) => {
expect(body.id).toBeDefined();
});
});