diff --git a/backend/user-service/__tests__/routes/user.routes.test.ts b/backend/user-service/__tests__/routes/user.routes.test.ts index 9e2caae7a4..6f960e162b 100644 --- a/backend/user-service/__tests__/routes/user.routes.test.ts +++ b/backend/user-service/__tests__/routes/user.routes.test.ts @@ -1,17 +1,34 @@ import { MongoDBContainer, StartedMongoDBContainer } from '@testcontainers/mongodb' import express, { Express } from 'express' + +import { Proficiency } from '../../src/types/Proficiency' +import { Role } from '../../src/types/Role' +import connectToDatabase from '../../src/common/mongodb.util' +import logger from '../../src/common/logger.util' import mongoose from 'mongoose' import request from 'supertest' -import logger from '../../src/common/logger.util' -import connectToDatabase from '../../src/common/mongodb.util' import userRouter from '../../src/routes/user.routes' -import { Proficiency } from '../../src/types/Proficiency' -import { Role } from '../../src/types/Role' describe('User Routes', () => { let app: Express let startedContainer: StartedMongoDBContainer + const CREATE_USER_DTO1 = { + username: 'test1', + password: 'Test1234!', + email: 'test@gmail.com', + role: Role.ADMIN, + proficiency: Proficiency.INTERMEDIATE, + } + + const CREATE_USER_DTO2 = { + username: 'test2', + password: 'Test1234!', + email: 'test2@gmail.com', + role: Role.ADMIN, + proficiency: Proficiency.INTERMEDIATE, + } + beforeAll(async () => { const container: MongoDBContainer = new MongoDBContainer().withExposedPorts(27017) startedContainer = await container.start() @@ -36,20 +53,13 @@ describe('User Routes', () => { }) describe('POST /users', () => { - const CREATE_USER_DTO = { - username: 'test', - password: 'Test1234!', - email: 'test@gmail.com', - role: Role.ADMIN, - proficiency: Proficiency.INTERMEDIATE, - } it('should return 201 and return the new user', async () => { - const response = await request(app).post('/users').send(CREATE_USER_DTO) + const response = await request(app).post('/users').send(CREATE_USER_DTO1) expect(response.status).toBe(201) expect(response.body).toEqual( expect.objectContaining({ id: expect.any(String), - username: 'test', + username: 'test1', email: 'test@gmail.com', role: Role.ADMIN, proficiency: Proficiency.INTERMEDIATE, @@ -62,8 +72,43 @@ describe('User Routes', () => { expect(response.body).toHaveLength(4) }) it('should return 409 for duplicate username or email', async () => { - await request(app).post('/users').send(CREATE_USER_DTO) - const response = await request(app).post('/users').send(CREATE_USER_DTO) + await request(app).post('/users').send(CREATE_USER_DTO1) + const response = await request(app).post('/users').send(CREATE_USER_DTO1) + expect(response.status).toBe(409) + }) + }) + + describe('PUT /users', () => { + it('should return 200 for successful update', async () => { + const user1 = await request(app).post('/users').send(CREATE_USER_DTO1) + const response = await request(app).put(`/users/${user1.body.id}`).send({ + username: 'test3', + proficiency: Proficiency.ADVANCED, + }) + expect(response.status).toBe(200) + expect(response.body.username).toEqual('test3') + expect(response.body.proficiency).toEqual(Proficiency.ADVANCED) + }) + it('should return 500 for requests with invalid ids', async () => { + const response = await request(app).put('/users/111').send({ + username: 'test3', + proficiency: Proficiency.ADVANCED, + }) + expect(response.status).toBe(500) + expect(response.body).toHaveLength(1) + }) + it('should return 400 for invalid requests and a list of errors', async () => { + const response = await request(app).put('/users/111').send({}) + expect(response.status).toBe(400) + expect(response.body).toHaveLength(1) + }) + it('should return 409 for duplicate username', async () => { + const user1 = await request(app).post('/users').send(CREATE_USER_DTO1) + await request(app).post('/users').send(CREATE_USER_DTO2) + const response = await request(app).put(`/users/${user1.body.id}`).send({ + username: 'test2', + proficiency: Proficiency.ADVANCED, + }) expect(response.status).toBe(409) }) }) diff --git a/backend/user-service/src/controllers/user.controller.ts b/backend/user-service/src/controllers/user.controller.ts index b349a06eaa..040e6d5a20 100644 --- a/backend/user-service/src/controllers/user.controller.ts +++ b/backend/user-service/src/controllers/user.controller.ts @@ -1,10 +1,13 @@ -import { ValidationError } from 'class-validator' -import { Response } from 'express' -import { createUser, findOneUserByEmail, findOneUserByUsername } from '../models/user.repository' +import { createUser, findOneUserByEmail, findOneUserByUsername, updateUser } from '../models/user.repository' + import { CreateUserDto } from '../types/CreateUserDto' +import { Response } from 'express' import { TypedRequest } from '../types/TypedRequest' import { UserDto } from '../types/UserDto' +import { UserProfileDto } from '../types/UserProfileDto' +import { ValidationError } from 'class-validator' import { hashPassword } from './auth.controller' +import logger from '../common/logger.util' export async function handleCreateUser(request: TypedRequest, response: Response): Promise { const createDto = CreateUserDto.fromRequest(request) @@ -32,3 +35,29 @@ export async function handleCreateUser(request: TypedRequest, res response.status(201).json(dto).send() } + +export async function handleUpdateProfile(request: TypedRequest, response: Response): Promise { + const createDto = UserProfileDto.fromRequest(request) + const errors = await createDto.validate() + if (errors.length) { + const errorMessages = errors.map((error: ValidationError) => `INVALID_${error.property.toUpperCase()}`) + response.status(400).json(errorMessages).send() + return + } + + const id = request.params.id + + const duplicateUsername = await findOneUserByUsername(createDto.username) + if (duplicateUsername && duplicateUsername.id !== id) { + response.status(409).json(['DUPLICATE_USERNAME']).send() + return + } + + try { + const user = await updateUser(id, createDto) + response.status(200).json(user).send() + } catch (e) { + logger.error(e) + response.status(500).json(['INVALID_USER_ID']).send() + } +} diff --git a/backend/user-service/src/models/user.repository.ts b/backend/user-service/src/models/user.repository.ts index 164087c653..6a4e031d82 100644 --- a/backend/user-service/src/models/user.repository.ts +++ b/backend/user-service/src/models/user.repository.ts @@ -1,7 +1,9 @@ import { Model, model } from 'mongoose' + import { CreateUserDto } from '../types/CreateUserDto' import { IUser } from '../types/IUser' import { UserDto } from '../types/UserDto' +import { UserProfileDto } from '../types/UserProfileDto' import userSchema from './user.model' const userModel: Model = model('User', userSchema) @@ -26,7 +28,7 @@ export async function createUser(dto: CreateUserDto): Promise { return userModel.create(dto) } -export async function updateUser(id: string, dto: UserDto): Promise { +export async function updateUser(id: string, dto: UserDto | UserProfileDto): Promise { return userModel.findByIdAndUpdate(id, dto, { new: true }) } diff --git a/backend/user-service/src/routes/user.routes.ts b/backend/user-service/src/routes/user.routes.ts index e52c5208fe..a8fb4ba39c 100644 --- a/backend/user-service/src/routes/user.routes.ts +++ b/backend/user-service/src/routes/user.routes.ts @@ -1,8 +1,10 @@ +import { handleCreateUser, handleUpdateProfile } from '../controllers/user.controller' + import { Router } from 'express' -import { handleCreateUser } from '../controllers/user.controller' const router = Router() router.post('/', handleCreateUser) +router.put('/:id', handleUpdateProfile) export default router diff --git a/backend/user-service/src/types/UserProfileDto.ts b/backend/user-service/src/types/UserProfileDto.ts new file mode 100644 index 0000000000..d283f10bab --- /dev/null +++ b/backend/user-service/src/types/UserProfileDto.ts @@ -0,0 +1,31 @@ +import { IsDate, IsEnum, IsNotEmpty, IsOptional, IsString, ValidationError, validate } from 'class-validator' + +import { Proficiency } from './Proficiency' +import { TypedRequest } from './TypedRequest' + +export class UserProfileDto { + @IsString() + @IsNotEmpty() + username: string + + @IsDate() + updatedAt: Date + + @IsOptional() + @IsEnum(Proficiency) + proficiency?: Proficiency + + constructor(username: string, updatedAt: Date, proficiency?: Proficiency) { + this.username = username + this.updatedAt = updatedAt + this.proficiency = proficiency + } + + static fromRequest({ body: { username, proficiency } }: TypedRequest): UserProfileDto { + return new UserProfileDto(username, new Date(), proficiency) + } + + async validate(): Promise { + return validate(this) + } +}