Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add update profile api route #28

Merged
merged 3 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 60 additions & 15 deletions backend/user-service/__tests__/routes/user.routes.test.ts
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
role: Role.ADMIN,
proficiency: Proficiency.INTERMEDIATE,
}

const CREATE_USER_DTO2 = {
username: 'test2',
password: 'Test1234!',
email: '[email protected]',
role: Role.ADMIN,
proficiency: Proficiency.INTERMEDIATE,
}

beforeAll(async () => {
const container: MongoDBContainer = new MongoDBContainer().withExposedPorts(27017)
startedContainer = await container.start()
Expand All @@ -36,20 +53,13 @@ describe('User Routes', () => {
})

describe('POST /users', () => {
const CREATE_USER_DTO = {
username: 'test',
password: 'Test1234!',
email: '[email protected]',
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: '[email protected]',
role: Role.ADMIN,
proficiency: Proficiency.INTERMEDIATE,
Expand All @@ -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 404 for requests with invalid ids', async () => {
const response = await request(app).put('/users/111').send({
username: 'test3',
proficiency: Proficiency.ADVANCED,
})
expect(response.status).toBe(404)
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)
})
})
Expand Down
33 changes: 30 additions & 3 deletions backend/user-service/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
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'

export async function handleCreateUser(request: TypedRequest<CreateUserDto>, response: Response): Promise<void> {
Expand Down Expand Up @@ -32,3 +34,28 @@ export async function handleCreateUser(request: TypedRequest<CreateUserDto>, res

response.status(201).json(dto).send()
}

export async function handleUpdateProfile(request: TypedRequest<UserProfileDto>, response: Response): Promise<void> {
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) {
response.status(404).json([e]).send()
glemenneo marked this conversation as resolved.
Show resolved Hide resolved
}
}
4 changes: 3 additions & 1 deletion backend/user-service/src/models/user.repository.ts
Original file line number Diff line number Diff line change
@@ -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<IUser> = model('User', userSchema)
Expand All @@ -26,7 +28,7 @@ export async function createUser(dto: CreateUserDto): Promise<IUser> {
return userModel.create(dto)
}

export async function updateUser(id: string, dto: UserDto): Promise<IUser | null> {
export async function updateUser(id: string, dto: UserDto | UserProfileDto): Promise<IUser | null> {
return userModel.findByIdAndUpdate(id, dto, { new: true })
}

Expand Down
4 changes: 3 additions & 1 deletion backend/user-service/src/routes/user.routes.ts
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions backend/user-service/src/types/UserProfileDto.ts
Original file line number Diff line number Diff line change
@@ -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>): UserProfileDto {
return new UserProfileDto(username, new Date(), proficiency)
}

async validate(): Promise<ValidationError[]> {
return validate(this)
}
}