I have an e2e test where I test the registration (email unique)
The Test is:
it('Register a default user: /api/v1/auth/email/register (POST)', async () => {
return request(app.getHttpServer())
.post('/auth/email/register')
.send({
"name": newUserName,
"username": newUsername,
"email": TESTER_EMAIL,
"password": TESTER_PASSWORD
})
.expect(201);
});
The second time, with the same values, I expect a 400 Status code, and I got it.
it('Register a default user: /api/v1/auth/email/register (POST)', async () => {
return request(app.getHttpServer())
.post('/auth/email/register')
.send({
"name": newUserName,
"username": newUsername,
"email": TESTER_EMAIL,
"password": TESTER_PASSWORD
})
.expect(400)
.expect(({ body }) => {
console.log(body);
});
});
If I analyze the Body, I can see:
{
index: 0,
code: 11000,
keyPattern: { email: 1 },
keyValue: { email: 'john.doe#example.com' }
}
and it is correct, Because I have an index unique on my mongoDB.
But I expect the same response that I receive from my production API.
{
"statusCode": 400,
"message": [
"username already exist",
"email already exist"
],
"error": "Bad Request"
}
The controller is simple, I have a route like:
#Post('email/register')
#HttpCode(HttpStatus.CREATED)
async register(#Body() authRegisterLoginDto: AuthRegisterLoginDto) {
return this.authService.register(authRegisterLoginDto);
}
In my service:
async register(authRegisterLoginDto: AuthRegisterLoginDto) {
const hash = crypto.createHash('sha256').update(randomStringGenerator()).digest('hex');
const user = await this.usersService.create({
...authRegisterLoginDto,
hash,
});
await this.mailService.userSignUp({
to: user.email,
data: {
hash,
},
});
}
and in my userService(wehre I get the error) is:
async create(userDto: UserDto): Promise<IUsers> {
try {
return await this.userModel.create(userDto);
} catch (err) {
throw new HttpException(err, HttpStatus.BAD_REQUEST);
}
}
How can I get the same response that I get from my "prod" API?
UPDATE.
main.ts
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe, VersioningType } from '#nestjs/common';
import { DocumentBuilder, SwaggerModule } from '#nestjs/swagger';
import { TransformationInterceptor } from './interceptors/transformInterceptor';
import { TransformError } from './interceptors/transformErrorInterceptor';
import { useContainer } from 'class-validator';
import { ConfigService } from '#nestjs/config';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
//added for custom validator
useContainer(app.select(AppModule), {fallbackOnErrors: true});
//custom response
app.useGlobalInterceptors(new TransformationInterceptor)
app.useGlobalInterceptors(new TransformError)
app.setGlobalPrefix(configService.get('app.apiPrefix'), {
exclude: ['/'],
});
app.enableVersioning({
type: VersioningType.URI,
});
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
const config = new DocumentBuilder()
.setTitle('API')
.setDescription('The API description')
.setVersion('1.0')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
name: 'JWT',
description: 'Enter JWT token',
in: 'header',
},
'JWT-auth', // This name here is important for matching up with #ApiBearerAuth() in your controller!
)
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/doc', app, document);
app.enableCors();
await app.listen(configService.get('app.port'));
}
bootstrap();
jest-e2e.json
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
I don't see how your E2E test bootstraps the app but make sure all transformation pipes are included and everything else that might be involved altering error response.
To get the same effect in the e2e test always include the setup you have in main.ts except swagger docs or some unrelated stuff.
in your case, I'd try this
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalInterceptors(new TransformationInterceptor);
app.useGlobalInterceptors(new TransformError);
await app.init();
});
Thanks to #n1md7, I imported
useContainer(app.select(AppModule), { fallbackOnErrors: true });
into my e2e test. I modified it because I want to use the
#Validate(UniqueValidator, ['username'], {
message: 'username already exist',
})
in my Dto. (MongoDB and class-validator unique validation - NESTJS)
Related
I am using TypeScript and have Server and Client application. Below is the server code.
Server Code
import express, { Express } from "express";
import { graphqlHTTP } from "express-graphql";
import { buildSchema } from "type-graphql";
import { TaskResolver } from "./resolvers/task.resolver";
import { pgDatasource } from "./configs/db.config";
import { SeatBandingResolver } from "./resolvers/seatBanding.resolver";
import { GuestChatResolver } from "./resolvers/guestChat.resolver";
import { RateResolver } from "./resolvers/rate.resolver";
import { YearResolver } from "./resolvers/year.resolver";
import { ImplementationRateResolver } from "./resolvers/implementationRate.resolver";
import { UserResolver } from "./resolvers/user.resolver";
import { ReportResolver } from "./resolvers/report.resolver";
// Subscriptions
const ws = require("ws");
const { useServer } = require("graphql-ws/lib/use/ws");
const { execute, subscribe } = require("graphql");
const main = async () => {
const app: Express = express();
try {
//connect to db
await pgDatasource.initialize();
} catch (err) {
throw err;
}
//build gql schema
let schema = await buildSchema({
resolvers: [
SeatBandingResolver,
GuestChatResolver,
RateResolver,
YearResolver,
ImplementationRateResolver,
UserResolver,
],
validate: false,
// pubSub: new PubSub()
});
let schemaDoc = await buildSchema({
resolvers: [ReportResolver],
validate: false,
});
//ql schema for report
const docServer = graphqlHTTP((req, res) => {
return {
schema: schemaDoc,
graphiql: true,
context: {
req: req,
header: req.headers,
},
};
});
//setting a graphql server instance
const graphqServer = graphqlHTTP((req, res, graphQLParams) => {
return {
schema,
context: {
req: req,
header: req.headers,
},
graphiql: true,
};
});
app.use(cors());
//graphql endpoint : change it to backend
app.use("/graphql", graphqServer);
//for report : change name to google api
app.use("/doc", docServer);
//test route
app.get("/", (req, res) => {
res.json({
message: "Hello world",
});
});
let server = app.listen(3001, () => {
console.log("server started");
const wsServer = new ws.WebSocketServer({
host: "localhost",
// server,
path: "/graphql",
port: 3001,
});
useServer(
{
schema,
execute,
subscribe,
onConnect: (ctx) => {
console.log("Connect");
},
onSubscribe: (ctx, msg) => {
console.log("Subscribe");
},
onNext: (ctx, msg, args, result) => {
console.debug("Next");
},
onError: (ctx, msg, errors) => {
console.error("Error");
},
onComplete: (ctx, msg) => {
console.log("Complete");
},
},
wsServer
);
});
};
//starting a server
main()
.then(async (_) => {
// await addColumn()
})
.catch((err) => {
console.log(err);
});
Subscription Code at Client Side
import { Year } from "../entities/year.entity";
import { NewYear } from "../inputs/addYear.input";
import {
Arg,
Ctx,
Field,
Int,
Mutation,
ObjectType,
Query,
Resolver,
Root,
Subscription,
UseMiddleware,
} from "type-graphql";
import { Request } from "express";
import { Response } from "../helpers/response.helper";
import { Pagination } from "../inputs/pagination.input";
import { isAuth } from "../helpers/auth.helper";
import { PubSub, PubSubEngine } from "graphql-subscriptions";
const pubSub = new PubSub();
#ObjectType()
class MessagePayload {
#Field()
message: string;
}
#Resolver(() => Year)
export class YearResolver {
#Mutation(() => String)
async sendMessage(#Arg("message") message: string): Promise<string> {
console.log("in send subscription");
pubSub.publish("MESSAGE_NOTIFICATION", { message });
return message;
}
//calling the subscription
#Subscription(() => MessagePayload || null, {
topics: "MESSAGE_NOTIFICATION",
})
async receiveMessage(
#Root() root: MessagePayload
): Promise<MessagePayload | null> {
console.log("in publisher");
console.log(root, "in recieve message");
pubSub.asyncIterator("MESSAGE_NOTIFICATION");
return { message: "hello from the subscription" };
}
}
The issue I am facing here is Subscription is not working properly and the data is always null.
Can anyone help me to identify what I am missing here?
Thanks.
I'm not sure for 100% because your code descriptions are kinda confusing, but it looks like you should return pubSub.asyncIterator('MESSAGE_NOTIFICATION') in method receiveMessage. This method is called to start streaming messages to client at selected channel (MESSAGE_NOTIFICATION), not sending them. To send messages use pubsub. Of course you should change typing too.
You can find a similiar implementation here.
I am trying to log the grpc request and response in my nestjs project. It seems that the middleware would help me do that. However, the middleware in nesjs is for HTTP only. So, is there any other way that can be used to solve my problem? Maybe interceptors?
Based on the examples on this github. I used the following code to log the request/response and redact the logs for a gRPC client:
// users.module.ts
#Module({
imports: [
ConfigModule.forRoot(),
ClientsModule.registerAsync([
{
name: USERS_PACKAGE,
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
transport: Transport.GRPC,
options: {
package: 'users',
credentials: credentials.createInsecure(),
protoPath: join(__dirname, './proto/users.proto'),
url: configService.get<string>('services.users.url'),
loader: { keepCase: true },
channelOptions: {
interceptors: [
(options, nextCall) => {
const logger: Logger = new Logger('UsersModule');
const redactedFields: string[] = configService.get<string[]>('logger.redact.fields');
const path: string = options.method_definition.path;
logger.verbose('-------------------------------------- GRPC CALL ----------------------------------');
const interceptingCall = new InterceptingCall(
nextCall(options),
GrpcLoggingInterceptor(path, redactedFields),
);
return interceptingCall;
},
],
},
},
}),
inject: [ConfigService],
},
]),
],
providers: [GetUsersClient],
exports: [GetUsersClient],
})
export class UsersModule {}
The grpc logging interceptor to intercept the grpc requests
// grpc-logging.interceptor.ts
import { Logger } from '#nestjs/common';
export const GrpcLoggingInterceptor = (path: string, redactedFields: string[]) => {
const logger: Logger = new Logger('GrpcLoggingInterceptor');
function redact(data: string): string {
return JSON.stringify(data, (k, v) => (redactedFields.includes(k) ? `[Redacted]` : v));
}
return {
start: function (metadata, listener, next) {
const newListener = {
onReceiveMetadata: function (metadata, next) {
logger.verbose(`response metadata : ${redact(metadata)}`);
next(metadata);
},
onReceiveMessage: function (message, next) {
logger.verbose(`response body : ${redact(message)}`);
next(message);
},
onReceiveStatus: function (status, next) {
logger.verbose(`response status : ${redact(status)}`);
next(status);
},
};
next(metadata, newListener);
},
sendMessage: function (message, next) {
logger.verbose(`path: ${JSON.stringify(path)}`);
logger.verbose(`request body : ${redact(message)}`);
next(message);
},
};
};
The fields to redact:
// config.yaml
logger:
redact:
fields:
- token
- password
- address
- telephone
You can use an Interceptor. In fact, that's exactly what my OgmaInterceptor does, and there's even a plugin for gRPC. In general, just something simple like
#Injectable()
export class LoggingInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const start = Date.now()
console.log('Request start');
return next.handle().pipe(
tap((data) => {
console.log(`Request took ${Date.now() - start}ms. Response length: ${Buffer.from(JSON.stringify(data) ?? '').length} bytes`);
})
);
}
}
would be a simple first pass at writing something for this
I use nodeJS on the backend and Vue with Vuex on the frontend. I have the logic for logging the user and everything works great, I put the JWT token in the Cookie and from there I read it and decode and log the user. When I refresh the page, my data is lost, so I'm wondering if it's a good practice to use vuex-persistedstate and save the entire state after refreshing?
This is my Vuex Store:
import { createApp } from 'vue';
import { createStore } from 'vuex';
import router from './router';
import createPersistedState from 'vuex-persistedstate';
import Cookies from 'js-cookie';
import './bootstrap.min.css';
import App from './App.vue';
import API from './API';
const store = createStore({
state: {
user: {},
userLoading: null,
userError: null,
},
plugins: [
createPersistedState({
storage: {
getItem: key => Cookies.get(key),
setItem: (key, value) =>
Cookies.set(key, value, { expires: 3, secure: true }),
removeItem: key => Cookies.remove(key),
},
}),
],
getters: {},
mutations: {
SET_USER(state, payload) {
state.user = payload;
},
SET_USER_LOADING(state, payload) {
state.userLoading = payload;
},
SET_USER_STATUS_FAIL(state, payload) {
state.userError = payload;
},
},
actions: {
async login({ commit }, User) {
commit('SET_USER_LOADING', true);
commit('SET_USER', []);
try {
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const { data } = await API.post(`/api/v1/users/login`, User, config);
commit('SET_USER', data);
commit('SET_USER_LOADING', false);
} catch (error) {
commit('SET_USER_LOADING', false);
commit(
'SET_USER_STATUS_FAIL',
error.response && error.response.data.message
? error.response.data.message
: error.message
);
}
},
},
});
Need to know the best practice to maintain the react GQL configurations settings where we set all query, mutation, or other policies, and the better way to handle file upload setting?
There are many approaches with different use cases. The following code might be helpful in your case.
import {
ApolloClient,
ApolloLink,
DefaultOptions,
InMemoryCache,
} from "#apollo/client";
import { onError } from "#apollo/client/link/error";
import { createUploadLink } from "apollo-upload-client";
const authMiddleware = new ApolloLink((operation: any, forward: any) => {
const token = localStorage.getItem("token") || null;
operation.setContext({
headers: {
authorization: `Bearer ${token}`,
},
});
return forward(operation);
});
const defaultOptions: DefaultOptions = {
watchQuery: {
fetchPolicy: "cache-and-network",
errorPolicy: "ignore",
},
query: {
fetchPolicy: "network-only",
errorPolicy: "all",
},
mutate: {
errorPolicy: "all",
},
};
const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }: any) => {
// You can modify do something with the errors
}
);
const client = new ApolloClient({
cache: new InMemoryCache(),
connectToDevTools: true,
link: ApolloLink.from([
errorLink,
authMiddleware,
createUploadLink({
uri: process.env.REACT_APP_GRAPHQL_URI || "http://localhost:3001/graphql",
}),
]),
defaultOptions: defaultOptions,
});
export default client;
it's my first time making questions on stackoverflow :).
So I'm developing a test / learning application to learn how to use NestJS and Vue.
I am was currently trying to implement several server-side unit tests (using Jest). When trying to create a testingmodule from my UsersService I get the error below on running "npm test users.service.spec"
Nest can't resolve dependencies of the UsersService (?,
ConfigService). Please make sure that the argument at index [0] is
available in the _RootTestModule context.
at Injector.lookupComponentInExports (../node_modules/#nestjs/core/injector/injector.js:183:19)
I am not sure if I am incorrectly instantiating the test model, or misconceived the injection of config.service, the code itself works, but may be implemented incorrectly.
Does anyone have any ideas how to solve this problem?
Git link: https://github.com/Lindul/FuelTracker_NestJs_MySql_Vue.git
users.service.spec.ts
import { Test } from '#nestjs/testing';
import { UsersService } from './users.service';
import { UserDto } from './dto/user.dto';
import { UserLoginRegRequestDto } from './dto/user-login-reg-request.dto';
import { ConfigService } from '../shared/config/config.service';
describe('User Service Tests', () => {
let loginTeste: UserLoginRegRequestDto = {
login: 'LoginJestTeste',
password: 'PassJestTeste',
}
const userMock: UserDto = {
id: 1,
login: 'hugombsantos',
password: 'wefewkfnwekfnwekjnf',
isActive: true,
needChangePass: false,
userType: 0,
};
const dataFindAllMock: UserDto[] = [{
id: 1,
login: 'hugombsantos',
password: 'wefewkfnwekfnwekjnf',
isActive: true,
needChangePass: false,
userType: 0,
}, {
id: 2,
login: 'user2',
password: 'sdgsdgdsgsdgsgsdg',
isActive: true,
needChangePass: false,
userType: 0,
}];
let service: UsersService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
ConfigService,
UsersService,
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('UsersService está defenido', () => {
expect(service).toBeDefined();
});
});
user.service.ts
import { Injectable, Inject, HttpException, HttpStatus } from '#nestjs/common';
import { Users } from '../models/users.schema';
import { UserDto } from './dto/user.dto';
import { UserLoginRegRequestDto } from './dto/user-login-reg-request.dto';
import { UserLoginResponseDto } from './dto/user-login-response.dto';
import { sign } from 'jsonwebtoken';
import { ConfigService } from '../shared/config/config.service';
import { JwtPayload } from './auth/jwt-payload.model';
import { ErroMessageDto } from '../shared/config/dto/erro-message.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import bcrypt = require('bcrypt-nodejs');
const SALT_FACTOR = 8;
#Injectable()
export class UsersService {
constructor(
#Inject('UsersRepository') private usersRepository: typeof Users,
private readonly configService: ConfigService,
) { }
/* istanbul ignore next */
async findAll(): Promise<UserDto[]> {
const users = await this.usersRepository.findAll<Users>();
return users.map(user => {
return new UserDto(user);
});
}
/* istanbul ignore next */
async findUser(id: number): Promise<Users> {
const user = await this.usersRepository.findOne<Users>({
where: { id },
});
return user;
}
/* istanbul ignore next */
async findUserformLogin(login: string): Promise<UserDto> {
return await this.usersRepository.findOne<Users>({
where: { login },
});
}
/* istanbul ignore next */
async findUserforLogin(id: number, login: string): Promise<Users> {
return await this.usersRepository.findOne<Users>({
where: { id, login },
});
}
/* istanbul ignore next */
async creatUserOnDb(createUser: UserLoginRegRequestDto): Promise<Users> {
try {
const user = new Users();
user.login = createUser.login;
user.password = await this.hashPass(createUser.password);
return await user.save();
} catch (err) {
if (err.original.code === 'ER_DUP_ENTRY') {
throw new HttpException(
new ErroMessageDto(
HttpStatus.CONFLICT,
`Login '${err.errors[0].value}' already exists`),
HttpStatus.CONFLICT,
);
}
throw new HttpException(
new ErroMessageDto(
HttpStatus.INTERNAL_SERVER_ERROR,
err),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/* istanbul ignore next */
async updateUserOnDb(user: Users): Promise<Users> {
try {
return await user.save();
} catch (err) {
if (err.original.code === 'ER_DUP_ENTRY') {
throw new HttpException(
new ErroMessageDto(
HttpStatus.CONFLICT,
`Login '${err.errors[0].value}' already exists`),
HttpStatus.CONFLICT,
);
}
throw new HttpException(
new ErroMessageDto(
HttpStatus.INTERNAL_SERVER_ERROR,
err),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/* istanbul ignore next */
async delete(id: number): Promise<UserDto> {
const user = await this.findUser(+id);
await user.destroy();
return new UserDto(user);
}
async login(
userLoginRequestDto: UserLoginRegRequestDto,
): Promise<UserLoginResponseDto> {
const login = userLoginRequestDto.login;
const password = userLoginRequestDto.password;
const user = await this.findUserformLogin(login);
if (!user) {
throw new HttpException(
new ErroMessageDto(
HttpStatus.UNAUTHORIZED,
'Invalid login or password.'),
HttpStatus.UNAUTHORIZED,
);
}
const isMatch = await this.comparePass(password, user.password);
if (!isMatch) {
throw new HttpException(
new ErroMessageDto(
HttpStatus.UNAUTHORIZED,
'Invalid login or password.'),
HttpStatus.UNAUTHORIZED,
);
}
const token = await this.signToken(user);
return new UserLoginResponseDto(token);
}
async create(createUser: UserLoginRegRequestDto): Promise<UserLoginResponseDto> {
const userData = await this.creatUserOnDb(createUser);
// when registering then log user in automatically by returning a token
const token = await this.signToken(userData);
return new UserLoginResponseDto(token);
}
async updateUser(id: number | string, updateUserDto: UpdateUserDto): Promise<UserLoginResponseDto> {
const user = await this.findUser(+id);
if (!user) {
throw new HttpException(
new ErroMessageDto(
HttpStatus.NOT_FOUND,
'User not found.'),
HttpStatus.NOT_FOUND,
);
}
user.login = updateUserDto.login || user.login;
user.userType = updateUserDto.userType || user.userType;
user.needChangePass = false;
const isMatch = await this.comparePass(updateUserDto.password, user.password);
if (updateUserDto.password && !isMatch) {
user.password = await this.hashPass(updateUserDto.password);
} else {
user.password = user.password;
}
const userData = await this.updateUserOnDb(user);
const token = await this.signToken(userData);
return new UserLoginResponseDto(token);
}
async signToken(user: UserDto): Promise<string> {
const payload: JwtPayload = {
id: user.id,
login: user.login,
};
const token = sign(payload, this.configService.jwtConfig.jwtPrivateKey,
{
expiresIn: this.configService.jwtConfig.valideTime,
});
return token;
}
async hashPass(passToHash: string): Promise<string> {
const saltValue = await bcrypt.genSaltSync(SALT_FACTOR);
return await bcrypt.hashSync(passToHash, saltValue);
}
async comparePass(passToCheck: string, correntPass: string) {
return await bcrypt.compareSync(passToCheck, correntPass);
}
}
users.modules.ts
import { Module } from '#nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import {userProviders} from './users.providers';
import { DatabaseModule } from '../database/database.module';
import { JwtStrategy } from './auth/jwt-strategy';
import { ConfigService } from '../shared/config/config.service';
#Module({
imports: [DatabaseModule, ConfigService],
controllers: [UsersController],
providers: [UsersService,
...userProviders, JwtStrategy],
exports: [UsersService],
})
export class UsersModule {}
config.service.ts
import { Injectable } from '#nestjs/common';
import { JwtConfig } from './interfaces/jwt-config.interface';
import { config } from '../../../config/config.JwT';
import { config as dbConfigData } from '../../../config/config.db';
import { SequelizeOrmConfig } from './interfaces/sequelize-orm-config.interface';
#Injectable()
export class ConfigService {
get sequelizeOrmConfig(): SequelizeOrmConfig {
return dbConfigData.databaseOpt;
}
get jwtConfig(): JwtConfig {
return {
jwtPrivateKey: config.jwtPrivateKey,
valideTime: config.valideTime,
};
}
}
You need to provide some value for Nest to inject for your UserRepository. It looks like you are using the TypeORM #InjectRepository() decorator, so in your testing module you'll need to add something like this under your providers
{
provide: getRepositoryToken('UsersRepository'),
useValue: MyMockUserRepo,
}
So that Nest can know what value to inject for that string. MyMockUserRepo would be your mock repository functionality, so that you aren't making actual database calls to your backend