NestJS: Unit Test Joi Validation - node.js

I'am new using Node and Nest framework, and I have a problem when testing modules that import a Custom Config Module that uses Joi to validate env vars:
yarn test
Test suite failed to run
Config validation error: "APP_PORT" is required
app-config.module.ts
#Module({
imports: [
ConfigModule.forRoot({
expandVariables: true,
load: [configuration],
validationSchema: Joi.object({
APP_PORT: Joi.number().required()
}),
}),
],
providers: [AppConfigService],
exports: [AppConfigService],
})
export class AppConfigModule { }
app.config.ts
import { registerAs } from '#nestjs/config';
export default registerAs('app', () => ({
env: process.env.NODE_ENV,
port: process.env.APP_PORT || 3000
...
}));
invoice.service.spec.ts
describe('InvoiceService', () => {
let service: InvoiceService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
load: [configuration],
ignoreEnvFile: true,
}),
AppConfigModule
],
providers: [
InvoiceService,
....
],
}).compile();
service = module.get<InvoiceService>(InvoiceService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
I use env file to deploy locally, and I only set up ignoreEnvFile: true in test class because env file is ignore from github repo, and project integrate github actions that run unit test.
How is the best way to solved this problem? I would not like add env file to repo.
Exist any way to disable/fake/mock Joi validation method. I saw some examples using setupFiles but I'm not sure if it's a good practice.

May you please show the package.json file?
you must have installed npm i --save #nestjs/config then create .env file inside root directory. Moreover you should import that into you app module.
import { Module } from '#nestjs/common';
import { ConfigModule } from '#nestjs/config';
#Module({
imports: [
ConfigModule.forRoot({
envFilePath: ".development.env",
}),
],
})
export class AppModule {}
Ref: https://docs.nestjs.com/techniques/configuration

Related

How to use a custom middleware with nestjs-telegraf?

I am trying to implement custom Telegraf middleware with nestjs-telegraf library and connection to DB using Prisma.
My AppModule is:
#Module({
imports: [
TelegrafModule.forRootAsync({
imports: [ConfigModule, LoggerModule],
useFactory: (configService: ConfigService, logger: LoggerMiddleware) => {
return {
token: configService.get<string>("TELEGRAM_TOKEN")!,
middlewares: [sessionMiddleware, logger.use]
};
},
inject: [ConfigService, LoggerMiddleware]
}),
PrismaModule
],
controllers: [],
providers: [...someProviders]
})
export class AppModule {}
LoggerMiddleware:
#Injectable()
export class LoggerMiddleware {
constructor(private readonly prisma: PrismaService) {}
async use(ctx: Context, next: NextFunction) {
const listUser = await this.prisma.user.findMany()
console.log('listUser = ', listUser)
next()
}
}
LoggerModule:
#Module({
imports: [PrismaModule],
providers: [LoggerMiddleware, PrismaService],
exports: [LoggerMiddleware]
})
export class LoggerModule {}
It starts without errors and code reaches my logger middleware but then I am getting an error:
TypeError: Cannot read properties of undefined (reading 'prisma')
I have access to Prisma service from another provider and a connection to DB works.
At the start, nest initializes all dependencies successfully but I don't understand why during execution it fell with this error. What did I do wrong?
The answer was given by Alexander Bukhalo on github

Why does it throw an error cannot read property of undefined (reading 'length') when running a GraphQL Subscription with Redis?

Overview
Working on a NestJS project with GraphQL using a laptop with Window OS
Experimenting with GraphQL Subscriptions using graphql-redis-subscription#2.5.0 package
Redis is used in a docker container, see the docker-compose.yml below
The problem arose when the subscription postAdded is executed in GraphQL Playground. Instead of hanging to listen for events, it had crashed before I performed createPost mutation.
My code (I only include some important details)
posts.resolver.ts
import { Inject, UseGuards } from '#nestjs/common';
import { Args, Context, Mutation, Resolver, Subscription } from '#nestjs/graphql';
import { RedisPubSub } from 'graphql-redis-subscriptions';
import { GraphqlJwtAuthGuard } from '../auth/guards';
import { RequestWithUser } from '../auth/interfaces';
import { PUB_SUB } from '../pubsub/pubsub.module'
const POST_ADDED_EVENT = 'postAdded';
#Resolver(() => Post)
export class PostsResolver {
constructor(
private postsService: PostsService,
#Inject(PUB_SUB) private pubSub: RedisPubSub,
) {}
// my subscription (issue)
#Subscription(() => Post)
postAdded() {
return this.pubSub.asyncIterator(POST_ADDED_EVENT);
}
// createPost method
#Mutation(() => Post)
#UseGuards(GraphqlJwtAuthGuard)
async createPost(
#Args('input') createPostInput: CreatePostInput,
#Context() context: { req: RequestWithUser },
) {
// just create a new post (assuming it works)
const newPost = await this.postsService.create(
createPostInput,
context.req.user,
);
this.pubSub.publish(POST_ADDED_EVENT, { postAdded: newPost });
return newPost;
}
}
pubsub.module.ts
import { ConfigService } from '#nestjs/config';
import { RedisPubSub } from 'graphql-redis-subscriptions';
import { Global, Module } from '#nestjs/common';
export const PUB_SUB = 'PUB_SUB';
#Global()
#Module({
providers: [
{
provide: PUB_SUB,
useFactory: (configService: ConfigService) =>
new RedisPubSub({
connection: {
host: configService.get('REDIS_HOST'),
port: configService.get('REDIS_PORT'),
},
}),
inject: [ConfigService],
},
],
exports: [PUB_SUB],
})
export class PubSubModule {}
app.module.ts
import { PubSubModule } from './pubsub/pubsub.module';
#Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
playground: true,
autoSchemaFile: path.join(process.cwd(), 'src/schema.gql'),
installSubscriptionHandlers: true,
}),
PubSubModule,
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
REDIS_HOST: Joi.string().required(),
REDIS_PORT: Joi.number().required()
}),
}),
],
providers: [AppService, AppResolver],
})
export class AppModule {}
version: '3'
services:
redis:
image: 'redis:alpine'
ports:
- '6379:6379'
redis-commander:
image: rediscommander/redis-commander:latest
environment:
- REDIS_HOSTS=local:redis:6379
ports:
- '8081:8081'
depends_on:
- redis
All the environment variables have already been defined in .env file.
REDIS_HOST="localhost"
REDIS_PORT=6379
When I run yarn start:dev and execute the subscription in GraphQL Playground
subscription {
postAdded {
id
title
paragraphs
}
}
it raises an error like this:
{
"errors": [
{
"message": "Cannot read properties of undefined (reading 'length')",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"postAdded"
]
}
]
}
The terminal that monitors NestJS also raises an error like this:
[Nest] 8080 - 07/21/2022, 9:30:24 AM ERROR [ExceptionsHandler] Cannot read properties of undefined (reading 'length')
TypeError: Cannot read properties of undefined (reading 'length')
at JavascriptRedisParser.execute (C:\Users\HP\nestjs-project\node_modules\redis-parser\lib\parser.js:530:38)
at Object.data (C:\Users\HP\nestjs-project\node_modules\ioredis\built\DataHandler.js:25:20)
at TransformOperationExecutor.transform (C:\Users\HP\nestjs-project\node_modules\src\TransformOperationExecutor.ts:207:39)
at TransformOperationExecutor.transform (C:\Users\HP\nestjs-project\node_modules\src\TransformOperationExecutor.ts:327:31)
at TransformOperationExecutor.transform (C:\Users\HP\nestjs-project\node_modules\src\TransformOperationExecutor.ts:327:31)
at TransformOperationExecutor.transform (C:\Users\HP\nestjs-project\node_modules\src\TransformOperationExecutor.ts:327:31)
at TransformOperationExecutor.transform (C:\Users\HP\nestjs-project\node_modules\src\TransformOperationExecutor.ts:327:31)
at TransformOperationExecutor.transform (C:\Users\HP\nestjs-project\node_modules\src\TransformOperationExecutor.ts:327:31)
at ClassTransformer.instanceToPlain (C:\Users\HP\nestjs-project\node_modules\src\ClassTransformer.ts:25:21)
at Object.classToPlain (C:\Users\HP\nestjs-project\node_modules\src\index.ts:23:27)
I have installed all the necessary dependencies like ioredis, graphql-redis-subscriptions and even graphql-subscriptions but the errors still exist. Redis also seems to be running properly.
I have tried reading the error logs but it did not occur in my source code and doing some research on StackOverFlow but none seems to have solved the problem.
Are you by any chance using a global ClassSerializerInterceptor?? Because I was running into the same problem just today and I solved by removing the interceptor. It happened because the subscription needs to receive an instance of AsyncIterable but the class serializer turns it into a plain object.
Apart from that I would recommend you change the GraphQl config, remove the installSubscriptionHandlers and change the config like this:
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
playground: true,
autoSchemaFile: path.join(process.cwd(), 'src/schema.gql'),
// remove this option:
// installSubscriptionHandlers: true,
// add the following:
subscriptions: {
"graphql-ws": true // or config object
}
}),
You can read more about it in the nestjs docs
I hope this solves your problem.

NestJs with Fastify doesn't execute code after app.listen()

this is my first question here, so i'd like to apologize in advance if i miss something important.
So this is pretty basic, but i didn't find an answer anywhere, as the title states, my NestJs application running with Fastify, simply does not execute any code after the app.listen('port') line, this doesn't happen with Express. OBS: I'm referring to the main.ts file.
relevant code below:
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ logger: { level: 'warn' } }),
);
const configService = app.get(ConfigService);
await app.listen(configService.get<number>(ENV_KEYS.PORT), '0.0.0.0');
console.log(
`Application is running on: ${await app.getUrl()}\nApplication Version: ${version}`,
);
}
bootstrap();
The console.log() after await app.listen never executes, even if the app is working normally, and as far as my tests show, no code after await app.listen() ever executes.
I'd like to know how to overcome this issue, because i need to run some code after the app is already bootstrapped.
So thanks to #Micael Levi pointing me to an issue on github (github.com/nestjs/nest/issues/7572) i started to look into problems within my controllers, and the reason to freeze the application on app.listen() was because of "an double instance" of my AuthController, let me explain
I had my AuthController defined on AuthModule:
// auth.module.ts
#Module({
imports: [
PassportModule,
JwtModule.register({
secret: JWT_CONSTANTS.SECRET,
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService, LocalStrategy, JwtStrategy],
})
And in the AppModule, i was importing AuthModule while also declaring AuthController AGAIN:
// app.module.ts
#Module({
imports: [
ConfigModule.forRoot(getConfigServiceConfiguration()),
TypeOrmModule.forRootAsync({
useFactory: async (configService: ConfigService) =>
getDatabaseModuleOptionsAsync(configService),
inject: [ConfigService],
}),
AuthModule, // <-AuthController is declared within AuthModule scope
UsuarioModule,
ClienteModule,
],
controllers: [AppController, AuthController] // <- AuthController here again,
providers: [AppService],
})
Removing AuthController from controllers:[] in AppModule solved my problem. Rookie mistake, but for some reason this isn't a problem with express, and doesn't raise any compilation errors with Fastify either!

NestJs - ConfigModule.forRoot isGlobal not working

I am trying to load the "process.env.AUTH_SECRET" in AuthModule, but it is giving me the undefined error "Error: secretOrPrivateKey must have a value".
I did setup the "isGlobal: true" in AppModule and it was able to read "process.env.MONGO_URL" there fine.
I have also installed dotenv, which reads fine if I do:
import * as dotenv from 'dotenv';
dotenv.config();
export const jwtConstants = {
secret: process.env.AUTH_SECRET,
};
But I would rather do it the "NestJs" way as the doc says adding isGlobal should make the env available to all other modules.
auth.module.ts
import { Module } from '#nestjs/common';
import { AuthService } from './auth.service';
import { UserModule } from '../user/user.module';
import { PassportModule } from '#nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { JwtModule } from '#nestjs/jwt';
#Module({
imports: [
UserModule,
PassportModule,
JwtModule.register({
secret: process.env.AUTH_SECRET, //Cannot read this.
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy],
exports: [AuthService, JwtModule],
})
export class AuthModule {}
app.module.ts
import { Module } from '#nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { MongooseModule } from '#nestjs/mongoose';
import { ConfigModule } from '#nestjs/config';
import { AuthModule } from './auth/auth.module';
#Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
UserModule,
MongooseModule.forRoot(process.env.MONGO_URL), // Can read this fine
AuthModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
What am I missing or doing wrong? Just for reference, I am trying to follow this authentication tutorial.
Thank you,
Your MongooseModule and AuthModule are dependent on your ConfigModule, but they don't know it.
Your modules are busy being loaded up, however your ConfigModule has to preform an async i/o process (reading your .env) before it can be ready. Meanwhile, Nest carries on loading without waiting for ConfigModule to finish it's job unless another it finds another module is dependent on one of it's exports.
This is where a modules *Async flavoured methods come in to play. They give you control over the modules instantiation. In this context, they allow us to inject the ConfigService from the ConfigModule which will not happen until the ConfigService is ready.
So changing your JWTModule configuration from using .register to .registerAsync to inject the ConfigService is what you are after:
JWTModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => {
secret: config.get<string>('AUTH_SECRET'),
signOptions: { expiresIn: '60s' }
}
})
Now, JWTModule will not load until ConfigService is ready and available in scope. You will most likely need to do that for your MongooseModule too.
That is the "NestJS" way.
Saying that, if all you really needed to do was have your .env loaded into process.env, put:
import * as dotenv from 'dotenv';
import { resolve } from 'path';
dotenv.config({ path: resolve(__dirname, '../.env') });
At the start of your main.ts file. That would lead to dotenv synchronously loading it before anything else happened

Dotenv variable is undefined

I have one module (auth module) where I register JwtModule:
JwtModule.register({
secret: process.env.SECRET_KEY,
signOptions: { expiresIn: '60m' },
}),
Register function looks like this: static register(options: JwtModuleOptions): DynamicModule;
JwtModule is defined in node_modules (jwt.module.js)
let JwtModule = JwtModule_1 = class JwtModule {
static register(options) {
console.log(options);
console.log(process.env.SECRET_KEY);
return {
module: JwtModule_1,
providers: jwt_providers_1.createJwtProvider(options)
};
}
In app.module.ts I have
ConfigModule.forRoot({
isGlobal: true,
}),
process.env.SECRET_KEY is undefined in node_modules file (console.log(process.env.SECRET_KEY)), but signOptions: { expiresIn: '60m' } is defined.
If I try to write to console SECRET_KEY from .env everywhere else is defined, but in node_modules is undefined.
The ConfigModule's ConfigService runs dotenv's config() method, but all decorators run at the same time, so the config() method is not called yet. You can use the async registration instead and use the ConfigService like so
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get('SECRET_KEY')
})
})
Be sure that you Call the ConfigService in your App Module constructor :
export class AppModule {
constructor(private configService: ConfigService) {}
}

Resources