From 2e1b1e37067c3ab7a09cc66f958b3cc37122414a Mon Sep 17 00:00:00 2001 From: gabrielm2q Date: Sat, 14 Dec 2024 18:23:26 -0300 Subject: [PATCH 1/3] feat(fga-eps-mds/2024.2-ARANDU-DOC#59): add edit user functionality --- src/auth/guards/auth.guard.ts | 2 +- src/dtos/update-user.dto.ts | 38 ++++++++++++++++++++++++++++++++--- src/users/users.controller.ts | 23 +++++++++++++++++++++ src/users/users.module.ts | 10 ++++----- src/users/users.service.ts | 38 ++++++++++++++++++++++++++++++++--- 5 files changed, 99 insertions(+), 12 deletions(-) diff --git a/src/auth/guards/auth.guard.ts b/src/auth/guards/auth.guard.ts index 58063d5..cd91ba1 100644 --- a/src/auth/guards/auth.guard.ts +++ b/src/auth/guards/auth.guard.ts @@ -1,7 +1,7 @@ import { - Injectable, CanActivate, ExecutionContext, + Injectable, UnauthorizedException, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; diff --git a/src/dtos/update-user.dto.ts b/src/dtos/update-user.dto.ts index dfd37fb..2b2e998 100644 --- a/src/dtos/update-user.dto.ts +++ b/src/dtos/update-user.dto.ts @@ -1,4 +1,36 @@ -import { PartialType } from '@nestjs/mapped-types'; -import { CreateUserDto } from './create-user.dto'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsEmail, IsOptional, IsString, MinLength } from 'class-validator'; -export class UpdateUserDto extends PartialType(CreateUserDto) {} +export class UpdateUserDto { + + @ApiProperty({ + example: 'Nome', + required: false + }) + @IsString() + @MinLength(3) + @IsOptional() + name?: string + + @ApiProperty({ + example: 'Username', + required: false + }) + @IsString() + @MinLength(3) + @IsOptional() + username?: string + + @ApiProperty({ + example: 'email@email.com', + required: false + }) + @IsEmail() + @IsOptional() + email?: string + + @IsBoolean() + @IsOptional() + isVerified?: boolean + +} diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 0117c3c..1330665 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -8,6 +8,7 @@ import { Patch, Post, Query, + Req, UseGuards, UsePipes, ValidationPipe, @@ -18,6 +19,7 @@ import { Roles } from 'src/auth/guards/roles.decorator'; import { RolesGuard } from 'src/auth/guards/roles.guard'; import { CreateUserDto } from '../dtos/create-user.dto'; import { UpdateRoleDto } from '../dtos/update-role.dto'; +import { UpdateUserDto } from '../dtos/update-user.dto'; import { UserRole } from '../dtos/user-role.enum'; import { UsersService } from './users.service'; @@ -38,6 +40,27 @@ export class UsersController { } } + @Patch('') + @UseGuards(JwtAuthGuard) + @UsePipes(ValidationPipe) + async updateUser( + @Req() req, + @Body() body: UpdateUserDto + ) { + try { + const user = await this.usersService.updateUser(req.userId, body) + + return { + message: `User ${user.username} updated successfully!` + } + } catch ( error ) { + if (error instanceof NotFoundException) { + throw new NotFoundException(`User with ID ${req.userId} not found`); + } + throw error; + } + } + @Get('verify') async verifyUser(@Query('token') token: string) { const user = await this.usersService.verifyUser(token); diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 98b58a5..a60a252 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; import { JwtModule } from '@nestjs/jwt'; -import { UsersController } from './users.controller'; -import { UsersService } from './users.service'; -import { UserSchema } from './interface/user.schema'; +import { MongooseModule } from '@nestjs/mongoose'; +import { AuthService } from 'src/auth/auth.service'; import { EmailService } from './email.service'; import { RefreshTokenSchema } from './interface/refresh-token.schema'; import { ResetTokenSchema } from './interface/reset-token.schema'; -import { AuthService } from 'src/auth/auth.service'; +import { UserSchema } from './interface/user.schema'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; @Module({ imports: [ diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 419d5c6..49738d1 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,7 +1,7 @@ import { - ConflictException, - Injectable, - NotFoundException, + ConflictException, + Injectable, + NotFoundException, } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { MongoError } from 'mongodb'; @@ -9,6 +9,7 @@ import { Model, Types } from 'mongoose'; import { CreateUserDtoFederated } from '../dtos/create-user-federated.dto'; import { CreateUserDto } from '../dtos/create-user.dto'; import { UpdateRoleDto } from '../dtos/update-role.dto'; +import { UpdateUserDto } from '../dtos/update-user.dto'; import { UserRole } from '../dtos/user-role.enum'; import { EmailService } from './email.service'; import { User } from './interface/user.interface'; @@ -44,6 +45,37 @@ export class UsersService { } } + async updateUser ( userId: string, updateUserDto: UpdateUserDto ): Promise { + await this.getUserById(userId); + + if ( updateUserDto.email ) updateUserDto.isVerified = false; + + var updateAttr = Object.fromEntries( + Object.entries(updateUserDto).filter(([key, value]) => value !== null) + ) + + try { + const updatedUser = await this.userModel.findByIdAndUpdate( + userId, + { $set: updateAttr }, + { + upsert: false, + new: true + } + ) + + if ( updateAttr['isVerified'] === false ) await this.emailService.sendVerificationEmail(updateAttr['email']); + + return updatedUser; + + } catch ( error ) { + if ( error instanceof MongoError ) { + throw new NotFoundException(`User with ID ${userId} not found!`) + } + throw error + } + } + async createFederatedUser( createFederatedUserDto: CreateUserDtoFederated, ): Promise { From c86835c7df89aa9e4cefe8b4c8798111b09542f9 Mon Sep 17 00:00:00 2001 From: gabrielm2q Date: Sat, 14 Dec 2024 18:24:47 -0300 Subject: [PATCH 2/3] feat(fga-eps-mds/2024.2-ARANDU-DOC#59): add edit user tests --- test/user.controller.spec.ts | 40 ++++++++++++++++++++++ test/user.service.spec.ts | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/test/user.controller.spec.ts b/test/user.controller.spec.ts index 3feb8ec..85cfcf4 100644 --- a/test/user.controller.spec.ts +++ b/test/user.controller.spec.ts @@ -7,6 +7,7 @@ import { UpdateRoleDto } from 'src/dtos/update-role.dto'; import { UserRole } from 'src/dtos/user-role.enum'; import { UsersController } from 'src/users/users.controller'; import { UsersService } from 'src/users/users.service'; +import { UpdateUserDto } from '../src/dtos/update-user.dto'; describe('UsersController', () => { let controller: UsersController; @@ -18,9 +19,18 @@ describe('UsersController', () => { role: UserRole.ALUNO, }; + const mockUpdatedUser = { + _id: 'mockUserId', + name: 'Mock User', + username: 'mockusername', + email: 'mock@example.com', + role: UserRole.ALUNO, + } + const mockUserService = { createUser: jest.fn().mockResolvedValue(mockUser), verifyUser: jest.fn().mockResolvedValue(mockUser), + updateUser: jest.fn().mockResolvedValue(mockUpdatedUser), getSubscribedJourneys: jest.fn().mockResolvedValue([]), getUsers: jest.fn().mockResolvedValue([mockUser]), addPointToUser: jest.fn().mockResolvedValue(mockUser), @@ -63,6 +73,36 @@ describe('UsersController', () => { }); }); + it('should update an user', async () => { + const userId = 'mockUserId'; + + const updateUserDto: UpdateUserDto = { + name: 'Mock User', + username: 'mockusername', + email: 'mock@example.com' + } + + const req = { userId: userId } as any; + + expect(await controller.updateUser(req, updateUserDto)).toEqual({ + message: `User ${mockUpdatedUser.username} updated successfully!` + }) + }) + + it('should return an error while trying to update user', async () => { + const updateUserDto: UpdateUserDto = { + name: 'Mock User', + username: 'mockusername', + email: 'mock@example.com' + } + + const req = { userId: 'idInexistente' } as any; + + mockUserService.updateUser.mockRejectedValueOnce(new NotFoundException("User with ID 'idInexistente' not found")); + + await expect(controller.updateUser(req, updateUserDto)).rejects.toBeInstanceOf(NotFoundException) + }) + it('should verify a user', async () => { const token = 'validToken'; await expect(controller.verifyUser(token)).resolves.toEqual({ diff --git a/test/user.service.spec.ts b/test/user.service.spec.ts index 175c04d..c97900f 100644 --- a/test/user.service.spec.ts +++ b/test/user.service.spec.ts @@ -1,6 +1,7 @@ import { ConflictException, NotFoundException } from '@nestjs/common'; import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; +import { MongoError } from 'mongodb'; import { Model, Types } from 'mongoose'; import { UpdateRoleDto } from '../src/dtos/update-role.dto'; import { UserRole } from '../src/dtos/user-role.enum'; @@ -26,6 +27,20 @@ describe('UsersService', () => { save: jest.fn().mockResolvedValue(this), }; + const mockUpdatedUser = { + _id: 'mockId', + name: 'Updated Mock Name', + email: 'updatedmock@example.com', + username: 'updatedMockUsername', + password: 'mockPassword', + role: UserRole.ALUNO, + verificationToken: 'mockToken', + isVerified: false, + subscribedJourneys: [new Types.ObjectId(), new Types.ObjectId()], + completedTrails: [new Types.ObjectId(), new Types.ObjectId()], + save: jest.fn().mockResolvedValue(this), + }; + const mockUserList = [ mockUser, { @@ -53,6 +68,7 @@ describe('UsersService', () => { findById: jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(mockUser), }), + findByIdAndUpdate: jest.fn().mockReturnValue(mockUpdatedUser), find: jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(mockUserList), }), @@ -88,6 +104,56 @@ describe('UsersService', () => { expect(result).toEqual(mockUserList); }); + it('should update an user', async () => { + const updateUserDto = { + name: 'Updated Mock Name', + email: 'updatedmock@example.com', + username: 'updatedMockUsername' + } + + const result = await service.updateUser('mockId', updateUserDto); + + expect(result).toEqual(mockUpdatedUser); + }) + + it('should return an error while trying to find an user to update', async () => { + const updateUserDto = { + name: 'Updated Mock Name', + email: 'updatedmock@example.com', + username: 'updatedMockUsername' + } + + jest.spyOn(model, 'findById').mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + } as any); + + await expect(service.updateUser('inexistentId', updateUserDto)).rejects.toBeInstanceOf(NotFoundException) + }) + + it('should return an error while trying to update an user', async () => { + const updateUserDto = { + name: 'Updated Mock Name', + email: 'updatedmock@example.com', + username: 'updatedMockUsername' + } + + jest.spyOn(model, 'findByIdAndUpdate').mockRejectedValueOnce(new MongoError(``)); + + await expect(service.updateUser('mockId', updateUserDto)).rejects.toBeInstanceOf(NotFoundException) + }) + + it('should return a not found error while trying to update an user', async () => { + const updateUserDto = { + name: 'Updated Mock Name', + email: 'updatedmock@example.com', + username: 'updatedMockUsername' + } + + jest.spyOn(model, 'findByIdAndUpdate').mockRejectedValueOnce(new NotFoundException()); + + await expect(service.updateUser('mockId', updateUserDto)).rejects.toBeInstanceOf(NotFoundException) + }) + it('should verify a user', async () => { const result = await service.verifyUser('mockToken'); expect(result).toEqual(mockUser); From 406f775f517f48f2c3cbe089ed39e30e734f93be Mon Sep 17 00:00:00 2001 From: gabrielm2q Date: Sat, 14 Dec 2024 18:33:48 -0300 Subject: [PATCH 3/3] fix(fga-eps-mds/2024.2-ARANDU-DOC#59): removing unused variable --- src/users/users.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 49738d1..e0d6b21 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -51,7 +51,7 @@ export class UsersService { if ( updateUserDto.email ) updateUserDto.isVerified = false; var updateAttr = Object.fromEntries( - Object.entries(updateUserDto).filter(([key, value]) => value !== null) + Object.entries(updateUserDto).filter(([value]) => value !== null) ) try {