From e95068de3979cd01db227cd705f0fd26b1174c8e Mon Sep 17 00:00:00 2001 From: n9mi Date: Fri, 13 Sep 2024 09:49:59 +0700 Subject: [PATCH] feat: user update --- __tests__/user.spec.ts | 83 ++++++++++++++++++++++++++++++++++-------- src/controller/user.ts | 12 ++++++ src/model/user.ts | 6 +++ src/router/user.ts | 1 + src/service/user.ts | 46 +++++++++++++++++++---- src/validation/user.ts | 27 ++++++++++++++ 6 files changed, 152 insertions(+), 23 deletions(-) create mode 100644 src/validation/user.ts diff --git a/__tests__/user.spec.ts b/__tests__/user.spec.ts index d2ccc7f..61c6352 100644 --- a/__tests__/user.spec.ts +++ b/__tests__/user.spec.ts @@ -8,14 +8,15 @@ describe("GET /user/info", () => { let token: string = ""; beforeAll(async () => { - token = await UserTestUtil.getToken(); + await UserTestUtil.create(UserTestUtil.user); + token = await UserTestUtil.getToken(UserTestUtil.user.username, UserTestUtil.user.password); }); afterAll(async () => { await UserTestUtil.delete(); }); - it("should return 200 - get user info", async () => { + it("should return 200 - success get user info", async () => { const res = await supertest(web) .get(`${basePath}/user/info`) .set('Authorization', `Bearer ${token}`); @@ -25,6 +26,62 @@ describe("GET /user/info", () => { expect(res.body.data.username).toBe(UserTestUtil.user.username); expect(res.body.data.name).toBe(UserTestUtil.user.name); }); + + it("should return 401 - unauthorized", async () => { + const res = await supertest(web) + .get(`${basePath}/user/info`); + + logger.debug(res.body); + expect(res.status).toBe(401); + expect(res.body.errors).toBeDefined(); + }); +}); + +describe("PUT /user/update", () => { + const newUserData = { + name: "user update", + username: "user_update", + password: "userupdate" + }; + + afterAll(() => { + UserTestUtil.delete(); + }); + + it ("should return 200 - success updating user info", async () => { + await UserTestUtil.create(UserTestUtil.user); + const token = await UserTestUtil.getToken(UserTestUtil.user.username, UserTestUtil.user.password); + + const resUpdate = await supertest(web) + .put(`${basePath}/user/update`) + .send(newUserData) + .set('Authorization', `Bearer ${token}`); + + logger.debug("update response : ", resUpdate.body); + expect(resUpdate.status).toBe(200); + expect(resUpdate.body.status).toBe("success"); + + const resLoginWithNewCred = await supertest(web) + .post(`${basePath}/auth/login`) + .send({ + username: newUserData.username, + password: newUserData.password + }); + + logger.debug("login response : ", resLoginWithNewCred.body); + expect(resLoginWithNewCred.status).toBe(200); + expect(resLoginWithNewCred.body.data.token).toBeDefined(); + }); + + it("should return 401 - unauthorized", async () => { + const resUpdate = await supertest(web) + .put(`${basePath}/user/update`) + .send(newUserData); + + logger.debug("update response : ", resUpdate.body); + expect(resUpdate.status).toBe(401); + expect(resUpdate.body.errors).toBeDefined(); + }); }); class UserTestUtil { @@ -34,33 +91,27 @@ class UserTestUtil { password: "password" }; - static async create() { + static async create(user : { name: string, username: string, password: string }) { await prisma.user.create({ data: { - name: UserTestUtil.user.name, - username: UserTestUtil.user.username, - password: await bcrypt.hash(UserTestUtil.user.password, 10), + name: user.name, + username: user.username, + password: await bcrypt.hash(user.password, 10), token: "" } }); } static async delete() { - await prisma.user.deleteMany({ - where: { - username: UserTestUtil.user.username - } - }); + await prisma.user.deleteMany({}); } - static async getToken() { - await UserTestUtil.create(); - + static async getToken(username: string, password: string) { const loginRes = await supertest(web) .post(`${basePath}/auth/login`) .send({ - username: UserTestUtil.user.username, - password: UserTestUtil.user.password, + username: username, + password: password, }); return loginRes.body.data.token; diff --git a/src/controller/user.ts b/src/controller/user.ts index 422cae7..7007339 100644 --- a/src/controller/user.ts +++ b/src/controller/user.ts @@ -1,5 +1,6 @@ import { Request, Response, NextFunction } from "express"; import UserService from "../service/user"; +import { UserUpdateRequest } from "../model/user"; export class UserController { @@ -15,4 +16,15 @@ export class UserController { next(e); } } + + static async update(req: Request, res: Response, next: NextFunction) { + try { + const updateRes = await UserService.updateUser(res.locals.user.username, req.body as UserUpdateRequest); + + res.status(200) + .json(updateRes); + } catch (e) { + next(e); + } + } } \ No newline at end of file diff --git a/src/model/user.ts b/src/model/user.ts index da0f896..e1984f9 100644 --- a/src/model/user.ts +++ b/src/model/user.ts @@ -1,4 +1,10 @@ export type UserInfoResponse = { username: string, name: string +} + +export type UserUpdateRequest = { + username?: string, + name?: string, + password?: string } \ No newline at end of file diff --git a/src/router/user.ts b/src/router/user.ts index ada8ae2..c57a4f7 100644 --- a/src/router/user.ts +++ b/src/router/user.ts @@ -6,6 +6,7 @@ export const getUserRouter = (basePath: string) => { const userRouter = express.Router(); userRouter.use(accessValidation); userRouter.get(`${basePath}/user/info`, UserController.info); + userRouter.put(`${basePath}/user/update`, UserController.update); return userRouter; } \ No newline at end of file diff --git a/src/service/user.ts b/src/service/user.ts index 5b04748..cdae5d1 100644 --- a/src/service/user.ts +++ b/src/service/user.ts @@ -1,9 +1,13 @@ import prisma from "../application/database"; import { ResponseError } from "../error/response"; -import { UserInfoResponse } from "../model/user"; +import { StatusResponse } from "../model/common"; +import { UserInfoResponse, UserUpdateRequest } from "../model/user"; +import bcrypt from "bcrypt"; +import { Validation } from "../validation/validation"; +import { UserValidation } from "../validation/user"; export default class UserService { - static async getUserInfo(username: string) : Promise { + static async getUserInfo(username: string): Promise { const user = await prisma.user.findFirstOrThrow({ where: { username: { @@ -12,15 +16,43 @@ export default class UserService { } } }).catch((e) => { throw new ResponseError(404, "user not found") }); - - console.info({ - username: user.username, - name: user.name - }); return { username: user.username, name: user.name } } + + static async updateUser(username: string, req: UserUpdateRequest): Promise { + const updateReq = Validation.validate(UserValidation.INFO, req); + + const isExists = await prisma.user.count({ + where: { + username: { + equals: username, + mode: 'insensitive' + } + } + }) > 0; + if (!isExists) { + throw new ResponseError(404, "user not found"); + } + + if (req.password !== undefined) { + updateReq.password = await bcrypt.hash(req.password, 10); + } + + console.info(updateReq); + + await prisma.user.update({ + where: { + username: username, + }, + data: updateReq + }); + + return { + status: "success" + } + } } \ No newline at end of file diff --git a/src/validation/user.ts b/src/validation/user.ts new file mode 100644 index 0000000..178f089 --- /dev/null +++ b/src/validation/user.ts @@ -0,0 +1,27 @@ +import { z, ZodType } from "zod"; + +export class UserValidation { + static readonly INFO: ZodType = z.object({ + username: z.string({ + invalid_type_error: "username should be string", + }).min(4, { + message: "username should be more than 4 characters" + }).max(100, { + message: "username should less than 100 characters" + }).optional(), + name: z.string({ + invalid_type_error: "name should be string", + }).min(1, { + message: "name should be more than 1 characters" + }).max(100, { + message: "name should less than 100 characters" + }).optional(), + password: z.string({ + invalid_type_error: "password should be string", + }).min(6, { + message: "password should be more than 6 characters" + }).max(100, { + message: "password should less than 100 characters" + }).optional(), + }) +} \ No newline at end of file