From 4ccb0acb01ced8ce8db7ba46eb520e2cd296337b Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Tue, 10 Sep 2024 21:03:14 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=84=B1=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit upsert 동작을 통해서 get or create 동작을 만들어서 동시에 새로운 유저를 만들 경우의 문제 해결 --- nestjs-BE/server/src/auth/auth.controller.ts | 27 +++++++------------ .../server/src/profiles/profiles.service.ts | 8 +++--- nestjs-BE/server/src/users/users.service.ts | 8 +++--- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/nestjs-BE/server/src/auth/auth.controller.ts b/nestjs-BE/server/src/auth/auth.controller.ts index c31533fd..38bcc293 100644 --- a/nestjs-BE/server/src/auth/auth.controller.ts +++ b/nestjs-BE/server/src/auth/auth.controller.ts @@ -41,24 +41,15 @@ export class AuthController { kakaoUserDto.kakaoUserId, ); if (!kakaoUserAccount) throw new NotFoundException(); - const email = kakaoUserAccount.email; - const user = await this.usersService.findUserByEmailAndProvider( - email, - 'kakao', - ); - let userUuid = user?.uuid; - if (!userUuid) { - const data = { email, provider: 'kakao' }; - const createdUser = await this.usersService.createUser(data); - userUuid = createdUser.uuid; - const profileData = { - user_id: createdUser.uuid, - image: customEnv.BASE_IMAGE_URL, - nickname: '익명의 사용자', - }; - await this.profilesService.createProfile(profileData); - } - const tokenData = await this.authService.login(userUuid); + const userData = { email: kakaoUserAccount.email, provider: 'kakao' }; + const user = await this.usersService.getOrCreateUser(userData); + const profileData = { + user_id: user.uuid, + image: customEnv.BASE_IMAGE_URL, + nickname: '익명의 사용자', + }; + await this.profilesService.getOrCreateProfile(profileData); + const tokenData = await this.authService.login(user.uuid); return { statusCode: 200, message: 'Success', data: tokenData }; } diff --git a/nestjs-BE/server/src/profiles/profiles.service.ts b/nestjs-BE/server/src/profiles/profiles.service.ts index 5fdc1483..b0226221 100644 --- a/nestjs-BE/server/src/profiles/profiles.service.ts +++ b/nestjs-BE/server/src/profiles/profiles.service.ts @@ -19,9 +19,11 @@ export class ProfilesService { }); } - async createProfile(data: CreateProfileDto): Promise { - return this.prisma.profile.create({ - data: { + async getOrCreateProfile(data: CreateProfileDto): Promise { + return this.prisma.profile.upsert({ + where: { user_id: data.user_id }, + update: {}, + create: { uuid: generateUuid(), user_id: data.user_id, image: data.image, diff --git a/nestjs-BE/server/src/users/users.service.ts b/nestjs-BE/server/src/users/users.service.ts index eb2f1f16..4d65e639 100644 --- a/nestjs-BE/server/src/users/users.service.ts +++ b/nestjs-BE/server/src/users/users.service.ts @@ -17,9 +17,11 @@ export class UsersService { }); } - async createUser(data: CreateUserDto): Promise { - return this.prisma.user.create({ - data: { + async getOrCreateUser(data: CreateUserDto): Promise { + return this.prisma.user.upsert({ + where: { email_provider: { email: data.email, provider: data.provider } }, + update: {}, + create: { uuid: generateUuid(), email: data.email, provider: data.provider, From 0f5218721e29671dcce69e0ce6fdabb576d1c7cd Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Tue, 10 Sep 2024 21:03:43 +0900 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20uuid=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/package-lock.json | 8 ++++++++ nestjs-BE/server/package.json | 1 + 2 files changed, 9 insertions(+) diff --git a/nestjs-BE/server/package-lock.json b/nestjs-BE/server/package-lock.json index a161dbd3..ab081798 100644 --- a/nestjs-BE/server/package-lock.json +++ b/nestjs-BE/server/package-lock.json @@ -41,6 +41,7 @@ "@types/node": "^20.3.1", "@types/passport-jwt": "^3.0.13", "@types/supertest": "^2.0.12", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", @@ -4020,6 +4021,13 @@ "@types/superagent": "*" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.11.7", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.7.tgz", diff --git a/nestjs-BE/server/package.json b/nestjs-BE/server/package.json index 70d9ad39..bd6f68fd 100644 --- a/nestjs-BE/server/package.json +++ b/nestjs-BE/server/package.json @@ -52,6 +52,7 @@ "@types/node": "^20.3.1", "@types/passport-jwt": "^3.0.13", "@types/supertest": "^2.0.12", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", From 21cc4e5d718de77badcbc471b177e24bc02f7542 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Tue, 10 Sep 2024 21:05:36 +0900 Subject: [PATCH 3/5] =?UTF-8?q?remove:=20=EB=B6=8E=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/src/users/dto/update-user.dto.ts | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 nestjs-BE/server/src/users/dto/update-user.dto.ts diff --git a/nestjs-BE/server/src/users/dto/update-user.dto.ts b/nestjs-BE/server/src/users/dto/update-user.dto.ts deleted file mode 100644 index fc59078e..00000000 --- a/nestjs-BE/server/src/users/dto/update-user.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PartialType } from '@nestjs/mapped-types'; -import { CreateUserDto } from './create-user.dto'; -import { ApiProperty } from '@nestjs/swagger'; - -export class UpdateUserDto extends PartialType(CreateUserDto) { - @ApiProperty({ - example: 'newpassword', - description: 'The new password of the user', - }) - password?: string; - uuid?: string; -} From 45bfa8881e5f2927c7126734939dea8adba60726 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Wed, 11 Sep 2024 16:35:53 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80?= =?UTF-8?q?=20=EC=95=8A=EB=8A=94=20=EB=B3=80=EC=88=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/src/auth/auth.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/nestjs-BE/server/src/auth/auth.service.ts b/nestjs-BE/server/src/auth/auth.service.ts index 57543bfb..cbc32a75 100644 --- a/nestjs-BE/server/src/auth/auth.service.ts +++ b/nestjs-BE/server/src/auth/auth.service.ts @@ -2,7 +2,6 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { jwtConstants, kakaoOauthConstants } from './constants'; import { stringify } from 'qs'; -import { PrismaService } from '../prisma/prisma.service'; import { RefreshTokensService } from './refresh-tokens.service'; @Injectable() @@ -10,7 +9,6 @@ export class AuthService { constructor( private jwtService: JwtService, private refreshTokensService: RefreshTokensService, - protected prisma: PrismaService, ) {} async getKakaoAccount(kakaoUserId: number) { From 2e568b88babca0fab502ab0b56253672d41b3cbe Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Wed, 11 Sep 2024 17:52:11 +0900 Subject: [PATCH 5/5] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/src/auth/auth.controller.spec.ts | 32 ++----------------- .../src/profiles/profiles.service.spec.ts | 32 ++++--------------- .../server/src/users/users.service.spec.ts | 27 +++------------- 3 files changed, 13 insertions(+), 78 deletions(-) diff --git a/nestjs-BE/server/src/auth/auth.controller.spec.ts b/nestjs-BE/server/src/auth/auth.controller.spec.ts index 07cc7907..a1f2a77a 100644 --- a/nestjs-BE/server/src/auth/auth.controller.spec.ts +++ b/nestjs-BE/server/src/auth/auth.controller.spec.ts @@ -29,13 +29,13 @@ describe('AuthController', () => { provide: UsersService, useValue: { findUserByEmailAndProvider: jest.fn(), - createUser: jest.fn(), + getOrCreateUser: jest.fn(), }, }, { provide: ProfilesService, useValue: { - createProfile: jest.fn(), + getOrCreateProfile: jest.fn(), }, }, { @@ -62,7 +62,7 @@ describe('AuthController', () => { jest .spyOn(authService, 'getKakaoAccount') .mockResolvedValue(kakaoUserAccountMock); - jest.spyOn(usersService, 'findUserByEmailAndProvider').mockResolvedValue({ + jest.spyOn(usersService, 'getOrCreateUser').mockResolvedValue({ uuid: 'user uuid', } as User); jest.spyOn(authService, 'login').mockResolvedValue(tokenMock); @@ -74,32 +74,6 @@ describe('AuthController', () => { message: 'Success', data: tokenMock, }); - expect(usersService.createUser).not.toHaveBeenCalled(); - }); - - it('kakaoLogin user login first time', async () => { - const requestMock = { kakaoUserId: 0 }; - const kakaoUserAccountMock = { email: 'kakao email' }; - const tokenMock = { - refresh_token: 'refresh token', - access_token: 'access token', - }; - jest - .spyOn(authService, 'getKakaoAccount') - .mockResolvedValue(kakaoUserAccountMock); - jest - .spyOn(usersService, 'createUser') - .mockResolvedValue({ uuid: 'user uuid' } as User); - jest.spyOn(authService, 'login').mockResolvedValue(tokenMock); - - const response = controller.kakaoLogin(requestMock); - - await expect(response).resolves.toEqual({ - statusCode: 200, - message: 'Success', - data: tokenMock, - }); - expect(usersService.createUser).toHaveBeenCalled(); }); it('kakaoLogin kakao login fail', async () => { diff --git a/nestjs-BE/server/src/profiles/profiles.service.spec.ts b/nestjs-BE/server/src/profiles/profiles.service.spec.ts index eed7adf2..8afc4e06 100644 --- a/nestjs-BE/server/src/profiles/profiles.service.spec.ts +++ b/nestjs-BE/server/src/profiles/profiles.service.spec.ts @@ -18,7 +18,7 @@ describe('ProfilesService', () => { profile: { findUnique: jest.fn(), findMany: jest.fn(), - create: jest.fn(), + upsert: jest.fn(), update: jest.fn(), }, }, @@ -83,38 +83,18 @@ describe('ProfilesService', () => { await expect(profiles).resolves.toEqual([]); }); - it('createProfile created', async () => { + it('getOrCreateProfile', async () => { const data = { user_id: generateUuid(), image: 'www.test.com/image', nickname: 'test nickname', }; - const testProfile = { uuid: generateUuid(), ...data }; - jest.spyOn(prisma.profile, 'create').mockResolvedValue(testProfile); + const profileMock = { uuid: generateUuid(), ...data }; + jest.spyOn(prisma.profile, 'upsert').mockResolvedValue(profileMock); - const profile = profilesService.createProfile(data); + const profile = profilesService.getOrCreateProfile(data); - await expect(profile).resolves.toEqual(testProfile); - }); - - it("createProfile user_id doesn't exists", async () => { - const data = { - user_id: generateUuid(), - image: 'www.test.com/image', - nickname: 'test nickname', - }; - jest - .spyOn(prisma.profile, 'create') - .mockRejectedValue( - new PrismaClientKnownRequestError( - 'Foreign key constraint failed on the field: `user_id`', - { code: 'P2003', clientVersion: '' }, - ), - ); - - const profile = profilesService.createProfile(data); - - await expect(profile).rejects.toThrow(PrismaClientKnownRequestError); + await expect(profile).resolves.toEqual(profileMock); }); it('updateProfile updated', async () => { diff --git a/nestjs-BE/server/src/users/users.service.spec.ts b/nestjs-BE/server/src/users/users.service.spec.ts index f6154742..c2771936 100644 --- a/nestjs-BE/server/src/users/users.service.spec.ts +++ b/nestjs-BE/server/src/users/users.service.spec.ts @@ -2,7 +2,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UsersService } from './users.service'; import { PrismaService } from '../prisma/prisma.service'; import generateUuid from '../utils/uuid'; -import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; describe('UsersService', () => { let usersService: UsersService; @@ -17,7 +16,7 @@ describe('UsersService', () => { useValue: { user: { findUnique: jest.fn(), - create: jest.fn(), + upsert: jest.fn(), }, }, }, @@ -55,37 +54,19 @@ describe('UsersService', () => { await expect(user).resolves.toBeNull(); }); - it('createUser created', async () => { + it('getOrCreateUser', async () => { const testUser = { uuid: generateUuid(), email: 'test@test.com', provider: 'kakao', }; - jest.spyOn(prisma.user, 'create').mockResolvedValue(testUser); + jest.spyOn(prisma.user, 'upsert').mockResolvedValue(testUser); - const user = usersService.createUser({ + const user = usersService.getOrCreateUser({ email: 'test@test.com', provider: 'kakao', }); await expect(user).resolves.toEqual(testUser); }); - - it('createUser user already exists', async () => { - jest - .spyOn(prisma.user, 'create') - .mockRejectedValue( - new PrismaClientKnownRequestError( - 'Unique constraint failed on the constraint: `User_email_provider_key`', - { code: 'P2025', clientVersion: '' }, - ), - ); - - const user = usersService.createUser({ - email: 'test@test.com', - provider: 'kakao', - }); - - await expect(user).rejects.toThrow(PrismaClientKnownRequestError); - }); });