Skip to content

Commit

Permalink
feat: user update service
Browse files Browse the repository at this point in the history
  • Loading branch information
nathan2slime committed Jul 13, 2024
1 parent 12d910d commit f900335
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 37 deletions.
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { AuthModule } from '~/app/auth/auth.module';
import { UserModule } from '~/app/user/user.module';

import { env } from '~/env';

Expand All @@ -15,6 +16,7 @@ import { env } from '~/env';
},
}),
AuthModule,
UserModule,
],
controllers: [],
providers: [],
Expand Down
2 changes: 2 additions & 0 deletions src/app/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { JwtRefreshStrategy } from '~/app/auth/refresh.strategy';
import { Session, SessionSchema } from '~/schemas/session.schema';
import { ACCESS_TOKEN_EXPIRES_IN } from '~/constants';
import { SessionService } from '~/app/session/session.service';
import { UserService } from '~/app/user/user.service';
import { env } from '~/env';

@Module({
Expand All @@ -32,6 +33,7 @@ import { env } from '~/env';
SessionService,
JwtService,
JwtStrategy,
UserService,
JwtRefreshStrategy,
],
exports: [AuthService],
Expand Down
24 changes: 5 additions & 19 deletions src/app/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { compare } from 'bcrypt';

import { SessionService } from '~/app/session/session.service';
import { SignInDto, SignUpDto } from '~/app/auth/auth.dto';
import { User } from '~/schemas/user.schema';
import { UserService } from '~/app/user/user.service';
import {
EMAIL_IS_ALREDY_IN_USE,
INVALID_CREDENTIALS,
Expand All @@ -17,20 +15,12 @@ import { Entity } from '~/types';
@Injectable()
export class AuthService {
constructor(
@InjectModel(User.name) private readonly userModel: Model<User>,
private readonly userService: UserService,
private readonly sessionService: SessionService,
) {}

async signIn(data: SignInDto) {
const user = await this.userModel.findOne(
{ email: data.email },
{
password: true,
email: true,
name: true,
role: true,
},
);
const user = await this.userService.getPassword({ email: data.email });
if (!user) throw new HttpException(USER_NOT_FOUND, HttpStatus.NOT_FOUND);

const isValidPassword = await compare(data.password, user.password);
Expand All @@ -49,15 +39,11 @@ export class AuthService {
}

async signUp(data: SignUpDto) {
const emailIsAlreadyInUse = await this.userModel.findOne({
email: data.email,
});
const emailIsAlreadyInUse = await this.userService.getByEmail(data.email);
if (!!emailIsAlreadyInUse)
throw new HttpException(EMAIL_IS_ALREDY_IN_USE, HttpStatus.CONFLICT);

const user = await this.userModel.create(data);
user.password = undefined;

const user = await this.userService.create(data);
const session = await this.sessionService.create(user.id);

return session;
Expand Down
2 changes: 2 additions & 0 deletions src/app/auth/tests/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { User } from '~/schemas/user.schema';
import { Session } from '~/schemas/session.schema';
import { SessionService } from '~/app/session/session.service';
import { Entity } from '~/types';
import { UserService } from '~/app/user/user.service';

describe('AuthController', () => {
let authController: AuthController;
Expand All @@ -33,6 +34,7 @@ describe('AuthController', () => {
AuthService,
JwtService,
SessionService,
UserService,
{
provide: getModelToken(User.name),
useValue: Model,
Expand Down
30 changes: 16 additions & 14 deletions src/app/auth/tests/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import {
INVALID_CREDENTIALS,
USER_NOT_FOUND,
} from '~/errors';
import { UserService } from '~/app/user/user.service';

describe('AuthService', () => {
let authService: AuthService;
let userModel: Model<User>;
let userService: UserService;
let sessionService: SessionService;

afterEach(() => jest.clearAllMocks());
Expand All @@ -31,6 +32,7 @@ describe('AuthService', () => {
providers: [
AuthService,
JwtService,
UserService,
SessionService,
{
provide: getModelToken(User.name),
Expand All @@ -44,7 +46,7 @@ describe('AuthService', () => {
}).compile();

authService = app.get<AuthService>(AuthService);
userModel = app.get<Model<User>>(getModelToken(User.name));
userService = app.get<UserService>(UserService);
sessionService = app.get<SessionService>(SessionService);
});

Expand All @@ -54,30 +56,30 @@ describe('AuthService', () => {
password: expect.anything(),
name: expect.anything(),
};
const user = {} as unknown as Entity<User>[];
const user = {} as unknown as Entity<User>;
const session = {} as unknown as Entity<Session>;

it('must create user and return his created session', async () => {
jest.spyOn(userModel, 'findOne').mockResolvedValue(null);
jest.spyOn(userModel, 'create').mockResolvedValue(user);
jest.spyOn(userService, 'getByEmail').mockResolvedValue(null);
jest.spyOn(userService, 'create').mockResolvedValue(user);
jest.spyOn(sessionService, 'create').mockResolvedValue(session);

const data = await authService.signUp(payload);

expect(data).toBe(session);
expect(sessionService.create).toHaveBeenCalledTimes(1);
expect(userModel.findOne).toHaveBeenCalledTimes(1);
expect(userService.getByEmail).toHaveBeenCalledTimes(1);
});

it('should return error if email is already in use', async () => {
jest.spyOn(userModel, 'findOne').mockResolvedValue(user);
jest.spyOn(userService, 'getByEmail').mockResolvedValue(user);
jest.spyOn(sessionService, 'create').mockResolvedValue(session);

await expect(authService.signUp(payload)).rejects.toThrow(
new HttpException(EMAIL_IS_ALREDY_IN_USE, HttpStatus.CONFLICT),
);
expect(sessionService.create).toHaveBeenCalledTimes(0);
expect(userModel.findOne).toHaveBeenCalledTimes(1);
expect(userService.getByEmail).toHaveBeenCalledTimes(1);
});
});

Expand Down Expand Up @@ -107,38 +109,38 @@ describe('AuthService', () => {
const session = {} as unknown as Entity<Session>;

it('must log in user and create a new session', async () => {
jest.spyOn(userModel, 'findOne').mockResolvedValue(user);
jest.spyOn(userService, 'getPassword').mockResolvedValue(user);
jest.spyOn(sessionService, 'findById').mockResolvedValue(null);
jest.spyOn(sessionService, 'create').mockResolvedValue(session);
jest.spyOn(bcrypt, 'compare').mockImplementation(async () => true);

const data = await authService.signIn(payload);

expect(data).toBe(session);
expect(userModel.findOne).toHaveBeenCalledTimes(1);
expect(userService.getPassword).toHaveBeenCalledTimes(1);
});

it('should return error when password is invalid', async () => {
jest.spyOn(userModel, 'findOne').mockResolvedValue(user);
jest.spyOn(userService, 'getPassword').mockResolvedValue(user);
jest.spyOn(sessionService, 'findById').mockResolvedValue(session);
jest.spyOn(bcrypt, 'compare').mockImplementation(async () => false);

await expect(authService.signIn(payload)).rejects.toThrow(
new HttpException(INVALID_CREDENTIALS, HttpStatus.UNAUTHORIZED),
);
expect(sessionService.findById).toHaveBeenCalledTimes(0);
expect(userModel.findOne).toHaveBeenCalledTimes(1);
expect(userService.getPassword).toHaveBeenCalledTimes(1);
});

it('should return error when user is not found', async () => {
jest.spyOn(userModel, 'findOne').mockResolvedValue(null);
jest.spyOn(userService, 'getPassword').mockResolvedValue(null);
jest.spyOn(bcrypt, 'compare').mockImplementation(async () => false);

await expect(authService.signIn(payload)).rejects.toThrow(
new HttpException(USER_NOT_FOUND, HttpStatus.NOT_FOUND),
);
expect(bcrypt.compare).toHaveBeenCalledTimes(0);
expect(userModel.findOne).toHaveBeenCalledTimes(1);
expect(userService.getPassword).toHaveBeenCalledTimes(1);
});
});
});
57 changes: 57 additions & 0 deletions src/app/user/tests/user.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Response, Request } from 'express';
import { Model } from 'mongoose';
import { getModelToken } from '@nestjs/mongoose';

import { User } from '~/schemas/user.schema';
import { Entity } from '~/types';
import { UserService } from '~/app/user/user.service';
import { UserController } from '~/app/user/user.controller';
import { UpdateUserProfileDto } from '~/app/user/user.dto';

describe('UserController', () => {
let userController: UserController;
let userService: UserService;

const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn(data => data),
} as unknown as Response;
const req = {
user: {
user: { _id: expect.anything() },
},
} as unknown as Request;
const body = {} as unknown as UpdateUserProfileDto;

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [
UserService,
{
provide: getModelToken(User.name),
useValue: Model,
},
],
}).compile();

userController = app.get<UserController>(UserController);
userService = app.get<UserService>(UserService);
});

afterEach(() => jest.clearAllMocks());

describe('update', () => {
const user = {} as Entity<User>;

it('must return user, access token and refresh token', async () => {
jest.spyOn(userService, 'getByIdUpdate').mockResolvedValue(user);

const data = await userController.update(body, req, res);

expect(userService.getByIdUpdate).toHaveBeenCalledTimes(1);
expect(data).toBe(user);
});
});
});
43 changes: 43 additions & 0 deletions src/app/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
Body,
Controller,
HttpStatus,
Put,
Req,
Res,
UseGuards,
} from '@nestjs/common';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';

import { UserService } from '~/app/user/user.service';
import { JwtAuthGuard } from '~/app/auth/auth.guard';
import { UpdateUserProfileDto } from '~/app/user/user.dto';
import { Session } from '~/schemas/session.schema';
import { Entity } from '~/types';
import { User } from '~/schemas/user.schema';

@ApiTags('User')
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}

@Put('update')
@UseGuards(JwtAuthGuard)
@ApiResponse({
status: 200,
description: 'Update user profile data',
})
async update(
@Body() body: UpdateUserProfileDto,
@Req() req: Request,
@Res() res: Response,
) {
const session = req.user as Session;
const user = session.user as Entity<User>;

const data = await this.userService.getByIdUpdate(user._id, body);

return res.status(HttpStatus.OK).json(data);
}
}
9 changes: 9 additions & 0 deletions src/app/user/user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional } from 'class-validator';

export class UpdateUserProfileDto {
@IsOptional()
@IsNotEmpty()
@ApiProperty()
name: string;
}
15 changes: 15 additions & 0 deletions src/app/user/user.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { UserController } from '~/app/user/user.controller';
import { UserService } from '~/app/user/user.service';
import { User, UserSchema } from '~/schemas/user.schema';

@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
16 changes: 12 additions & 4 deletions src/app/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { FilterQuery, Model } from 'mongoose';
import { FilterQuery, Model, Types } from 'mongoose';

import { User } from '~/schemas/user.schema';
import { SignUpDto } from '~/app/auth/auth.dto';
import { UpdateUserProfileDto } from './user.dto';

@Injectable()
export class UserService {
Expand All @@ -12,16 +13,23 @@ export class UserService {
) {}

async create(data: SignUpDto) {
return await this.userModel.create(data);
const user = await this.userModel.create(data);
user.password = undefined;

return user;
}

async getByEmail(email: string) {
return await this.userModel.findOne({ email });
return this.userModel.findOne({ email });
}

async getPassword(query: FilterQuery<User>) {
return await this.userModel.findOne(query, {
return this.userModel.findOne(query, {
password: true,
});
}

async getByIdUpdate(id: string | Types.ObjectId, data: UpdateUserProfileDto) {
return this.userModel.findByIdAndUpdate(id, data, { new: true });
}
}

0 comments on commit f900335

Please sign in to comment.