diff --git a/nestjs-BE/server/src/profiles/profiles.controller.spec.ts b/nestjs-BE/server/src/profiles/profiles.controller.spec.ts index d6d98613..63740edc 100644 --- a/nestjs-BE/server/src/profiles/profiles.controller.spec.ts +++ b/nestjs-BE/server/src/profiles/profiles.controller.spec.ts @@ -1,14 +1,16 @@ +import { + ForbiddenException, + HttpStatus, + NotFoundException, +} from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ProfilesController } from './profiles.controller'; import { ProfilesService } from './profiles.service'; -import { UploadService } from '../upload/upload.service'; import { RequestWithUser } from '../utils/interface'; -import { NotFoundException } from '@nestjs/common'; describe('ProfilesController', () => { let controller: ProfilesController; let profilesService: ProfilesService; - let uploadService: UploadService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -16,91 +18,107 @@ describe('ProfilesController', () => { providers: [ { provide: ProfilesService, - useValue: { findProfile: jest.fn(), updateProfile: jest.fn() }, + useValue: { + findProfileByUserUuid: jest.fn(), + updateProfile: jest.fn(), + }, }, - { provide: UploadService, useValue: { uploadFile: jest.fn() } }, ], }).compile(); controller = module.get(ProfilesController); profilesService = module.get(ProfilesService); - uploadService = module.get(UploadService); }); - it('findProfile found profile', async () => { - const requestMock = { user: { uuid: 'user test uuid' } } as RequestWithUser; - const testProfile = { - uuid: 'profile test uuid', - userUuid: requestMock.user.uuid, - image: 'www.test.com/image', - nickname: 'test nickname', - }; - jest.spyOn(profilesService, 'findProfile').mockResolvedValue(testProfile); + describe('findProfile', () => { + const requestMock = { user: { uuid: 'test uuid' } } as RequestWithUser; + + it('found profile', async () => { + const testProfile = { + uuid: 'profile test uuid', + userUuid: requestMock.user.uuid, + image: 'www.test.com/image', + nickname: 'test nickname', + }; - const response = controller.findProfile(requestMock); + jest + .spyOn(profilesService, 'findProfileByUserUuid') + .mockResolvedValue(testProfile); - await expect(response).resolves.toEqual({ - statusCode: 200, - message: 'Success', - data: testProfile, + const response = controller.findProfileByUserUuid(requestMock); + + await expect(response).resolves.toEqual({ + statusCode: HttpStatus.OK, + message: 'Success', + data: testProfile, + }); + expect(profilesService.findProfileByUserUuid).toHaveBeenCalledWith( + requestMock.user.uuid, + ); }); - expect(profilesService.findProfile).toHaveBeenCalledWith( - requestMock.user.uuid, - ); - }); - it('findProfile not found profile', async () => { - const requestMock = { user: { uuid: 'test uuid' } } as RequestWithUser; - jest.spyOn(profilesService, 'findProfile').mockResolvedValue(null); + it('not found profile', async () => { + jest + .spyOn(profilesService, 'findProfileByUserUuid') + .mockRejectedValue(new NotFoundException()); - const response = controller.findProfile(requestMock); + const response = controller.findProfileByUserUuid(requestMock); - await expect(response).rejects.toThrow(NotFoundException); + await expect(response).rejects.toThrow(NotFoundException); + }); }); - it('update updated profile', async () => { + describe('update', () => { const imageMock = {} as Express.Multer.File; const requestMock = { user: { uuid: 'test uuid' } } as RequestWithUser; const bodyMock = { - nickname: 'test nickname', - }; - const testImageUrl = 'www.test.com/image'; - const testProfile = { uuid: 'profile test uuid', - userUuid: requestMock.user.uuid, - image: 'www.test.com/image', nickname: 'test nickname', }; - jest.spyOn(uploadService, 'uploadFile').mockResolvedValue(testImageUrl); - jest.spyOn(profilesService, 'updateProfile').mockResolvedValue(testProfile); - const response = controller.update(imageMock, requestMock, bodyMock); + it('updated profile', async () => { + const testProfile = { + uuid: 'profile test uuid', + userUuid: requestMock.user.uuid, + image: 'www.test.com/image', + nickname: 'test nickname', + }; + + jest + .spyOn(profilesService, 'updateProfile') + .mockResolvedValue(testProfile); + + const response = controller.updateProfile( + imageMock, + requestMock, + bodyMock, + ); - await expect(response).resolves.toEqual({ - statusCode: 200, - message: 'Success', - data: testProfile, + await expect(response).resolves.toEqual({ + statusCode: HttpStatus.OK, + message: 'Success', + data: testProfile, + }); + expect(profilesService.updateProfile).toHaveBeenCalledWith( + requestMock.user.uuid, + bodyMock.uuid, + imageMock, + bodyMock, + ); }); - expect(uploadService.uploadFile).toHaveBeenCalled(); - expect(uploadService.uploadFile).toHaveBeenCalledWith(imageMock); - expect(profilesService.updateProfile).toHaveBeenCalledWith( - requestMock.user.uuid, - { nickname: bodyMock.nickname, image: testImageUrl }, - ); - }); - it('update not found user', async () => { - const imageMock = {} as Express.Multer.File; - const requestMock = { user: { uuid: 'test uuid' } } as RequestWithUser; - const bodyMock = { - nickname: 'test nickname', - }; - const testImageUrl = 'www.test.com/image'; - jest.spyOn(uploadService, 'uploadFile').mockResolvedValue(testImageUrl); - jest.spyOn(profilesService, 'updateProfile').mockResolvedValue(null); + it('not found user', async () => { + jest + .spyOn(profilesService, 'updateProfile') + .mockRejectedValue(new ForbiddenException()); - const response = controller.update(imageMock, requestMock, bodyMock); + const response = controller.updateProfile( + imageMock, + requestMock, + bodyMock, + ); - await expect(response).rejects.toThrow(NotFoundException); + await expect(response).rejects.toThrow(ForbiddenException); + }); }); }); diff --git a/nestjs-BE/server/src/profiles/profiles.controller.ts b/nestjs-BE/server/src/profiles/profiles.controller.ts index 93067f57..8b3f10a6 100644 --- a/nestjs-BE/server/src/profiles/profiles.controller.ts +++ b/nestjs-BE/server/src/profiles/profiles.controller.ts @@ -7,64 +7,59 @@ import { UploadedFile, Request as Req, ValidationPipe, - NotFoundException, + HttpStatus, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger'; import { ProfilesService } from './profiles.service'; import { UpdateProfileDto } from './dto/update-profile.dto'; -import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger'; -import { UploadService } from '../upload/upload.service'; import { RequestWithUser } from '../utils/interface'; @Controller('profiles') @ApiTags('profiles') export class ProfilesController { - constructor( - private readonly profilesService: ProfilesService, - private readonly uploadService: UploadService, - ) {} + constructor(private readonly profilesService: ProfilesService) {} @Get() @ApiOperation({ summary: 'Get profile' }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, description: 'Return the profile data.', }) @ApiResponse({ - status: 401, + status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized.', }) - async findProfile(@Req() req: RequestWithUser) { - const profile = await this.profilesService.findProfile(req.user.uuid); - if (!profile) throw new NotFoundException(); - return { statusCode: 200, message: 'Success', data: profile }; + async findProfileByUserUuid(@Req() req: RequestWithUser) { + const profile = await this.profilesService.findProfileByUserUuid( + req.user.uuid, + ); + return { statusCode: HttpStatus.OK, message: 'Success', data: profile }; } @Patch() @UseInterceptors(FileInterceptor('image')) @ApiOperation({ summary: 'Update profile' }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, description: 'Profile has been successfully updated.', }) @ApiResponse({ - status: 401, + status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized.', }) - async update( + async updateProfile( @UploadedFile() image: Express.Multer.File, @Req() req: RequestWithUser, @Body(new ValidationPipe({ whitelist: true })) updateProfileDto: UpdateProfileDto, ) { - if (image) { - updateProfileDto.image = await this.uploadService.uploadFile(image); - } const profile = await this.profilesService.updateProfile( req.user.uuid, + updateProfileDto.uuid, + image, updateProfileDto, ); - if (!profile) throw new NotFoundException(); - return { statusCode: 200, message: 'Success', data: profile }; + return { statusCode: HttpStatus.OK, message: 'Success', data: profile }; } } diff --git a/nestjs-BE/server/src/profiles/profiles.service.spec.ts b/nestjs-BE/server/src/profiles/profiles.service.spec.ts index ae0f8b2c..d8fd603b 100644 --- a/nestjs-BE/server/src/profiles/profiles.service.spec.ts +++ b/nestjs-BE/server/src/profiles/profiles.service.spec.ts @@ -1,15 +1,19 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { ProfilesService } from './profiles.service'; import { PrismaService } from '../prisma/prisma.service'; -import generateUuid from '../utils/uuid'; -import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; +import { UploadService } from '../upload/upload.service'; describe('ProfilesService', () => { let profilesService: ProfilesService; let prisma: PrismaService; + let configService: ConfigService; + let uploadService: UploadService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigModule], providers: [ ProfilesService, { @@ -17,116 +21,140 @@ describe('ProfilesService', () => { useValue: { profile: { findUnique: jest.fn(), - findMany: jest.fn(), - upsert: jest.fn(), update: jest.fn(), }, }, }, + { + provide: UploadService, + useValue: { uploadFile: jest.fn() }, + }, ], }).compile(); profilesService = module.get(ProfilesService); prisma = module.get(PrismaService); + configService = module.get(ConfigService); + uploadService = module.get(UploadService); }); - it('findProfile found profile', async () => { - const userId = generateUuid(); - const testProfile = { - uuid: generateUuid(), - userUuid: userId, - image: 'www.test.com/image', - nickname: 'test nickname', - }; - jest.spyOn(prisma.profile, 'findUnique').mockResolvedValue(testProfile); + describe('findProfileByUserUuid', () => { + const userUuid = 'user uuid'; + const profile = { uuid: 'profile uuid', userUuid }; + + beforeEach(() => { + (prisma.profile.findUnique as jest.Mock).mockResolvedValue(profile); + }); - const user = profilesService.findProfile(userId); + it('found', async () => { + const res = profilesService.findProfileByUserUuid(userUuid); - await expect(user).resolves.toEqual(testProfile); - }); + await expect(res).resolves.toEqual(profile); + }); - it('findProfile not found profile', async () => { - const userId = generateUuid(); - jest.spyOn(prisma.profile, 'findUnique').mockResolvedValue(null); + it('not found', async () => { + (prisma.profile.findUnique as jest.Mock).mockRejectedValue( + new NotFoundException(), + ); - const user = profilesService.findProfile(userId); + const res = profilesService.findProfileByUserUuid(userUuid); - await expect(user).resolves.toBeNull(); + await expect(res).rejects.toThrow(NotFoundException); + }); }); - it('findProfiles found profiles', async () => { - const ARRAY_SIZE = 5; - const profileUuids = Array(ARRAY_SIZE) - .fill(null) - .map(() => generateUuid()); - const testProfiles = profileUuids.map((uuid, index) => { - return { - uuid, - userUuid: generateUuid(), - image: 'www.test.com/image', - nickname: `nickname${index}`, - }; + describe('updateProfile', () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const image = { filename: 'icon' } as Express.Multer.File; + const imageUrl = 'www.test.com/image'; + + beforeEach(() => { + jest.spyOn(profilesService, 'verifyUserProfile').mockResolvedValue(true); + (uploadService.uploadFile as jest.Mock).mockResolvedValue(imageUrl); + (prisma.profile.update as jest.Mock).mockImplementation(async (args) => { + return { + uuid: profileUuid, + userUuid, + nickname: args.data.nickname ? args.data.nickname : 'test nickname', + image: args.data.image + ? args.data.image + : configService.get('BASE_IMAGE_URL'), + }; + }); }); - jest.spyOn(prisma.profile, 'findMany').mockResolvedValue(testProfiles); - const profiles = profilesService.findProfiles(profileUuids); + it('updated', async () => { + const data = { nickname: 'new nickname' }; - await expect(profiles).resolves.toEqual(testProfiles); - }); + const profile = profilesService.updateProfile( + userUuid, + profileUuid, + image, + data, + ); + + await expect(profile).resolves.toEqual({ + uuid: profileUuid, + userUuid, + image: imageUrl, + nickname: data.nickname, + }); + }); - it('findProfiles not found profiles', async () => { - const profileUuids = []; - jest.spyOn(prisma.profile, 'findMany').mockResolvedValue([]); + it('wrong user uuid', async () => { + const data = {}; - const profiles = profilesService.findProfiles(profileUuids); + jest + .spyOn(profilesService, 'verifyUserProfile') + .mockRejectedValue(new ForbiddenException()); - await expect(profiles).resolves.toEqual([]); + const profile = profilesService.updateProfile( + userUuid, + profileUuid, + image, + data, + ); + + await expect(profile).rejects.toThrow(ForbiddenException); + }); }); - it('getOrCreateProfile', async () => { - const data = { - userUuid: generateUuid(), - image: 'www.test.com/image', - nickname: 'test nickname', - }; - const profileMock = { uuid: generateUuid(), ...data }; - jest.spyOn(prisma.profile, 'upsert').mockResolvedValue(profileMock); + describe('verifyUserProfile', () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const image = 'www.test.com'; + const nickname = 'test nickname'; - const profile = profilesService.getOrCreateProfile(data); + beforeEach(() => { + jest + .spyOn(profilesService, 'findProfileByProfileUuid') + .mockResolvedValue({ uuid: profileUuid, userUuid, image, nickname }); + }); - await expect(profile).resolves.toEqual(profileMock); - }); + it('verified', async () => { + const res = profilesService.verifyUserProfile(userUuid, profileUuid); - it('updateProfile updated', async () => { - const data = { - image: 'www.test.com', - nickname: 'test nickname', - }; - const uuid = generateUuid(); - const testProfile = { uuid: generateUuid(), userUuid: uuid, ...data }; - jest.spyOn(prisma.profile, 'update').mockResolvedValue(testProfile); + await expect(res).resolves.toBeTruthy(); + }); - const profile = profilesService.updateProfile(uuid, data); + it('profile not found', async () => { + jest + .spyOn(profilesService, 'findProfileByProfileUuid') + .mockResolvedValue(null); - await expect(profile).resolves.toEqual(testProfile); - }); + const res = profilesService.verifyUserProfile(userUuid, profileUuid); - it("updateProfile user_id doesn't exists", async () => { - const data = { - image: 'www.test.com', - nickname: 'test nickname', - }; - jest - .spyOn(prisma.profile, 'update') - .mockRejectedValue( - new PrismaClientKnownRequestError( - 'An operation failed because it depends on one or more records that were required but not found. Record to update not found.', - { code: 'P2025', clientVersion: '' }, - ), - ); + await expect(res).rejects.toThrow(NotFoundException); + }); - const profile = profilesService.updateProfile(generateUuid(), data); + it('profile user not own', async () => { + const res = profilesService.verifyUserProfile( + 'other user uuid', + profileUuid, + ); - await expect(profile).resolves.toBeNull(); + await expect(res).rejects.toThrow(ForbiddenException); + }); }); }); diff --git a/nestjs-BE/server/src/profiles/profiles.service.ts b/nestjs-BE/server/src/profiles/profiles.service.ts index a61a5905..5149e838 100644 --- a/nestjs-BE/server/src/profiles/profiles.service.ts +++ b/nestjs-BE/server/src/profiles/profiles.service.ts @@ -1,16 +1,28 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '../prisma/prisma.service'; +import { + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { Profile, Prisma } from '@prisma/client'; +import { v4 as uuid } from 'uuid'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { CreateProfileDto } from './dto/create-profile.dto'; -import { Profile, Prisma } from '@prisma/client'; -import generateUuid from '../utils/uuid'; +import { PrismaService } from '../prisma/prisma.service'; +import { UploadService } from '../upload/upload.service'; @Injectable() export class ProfilesService { - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + private uploadService: UploadService, + ) {} - async findProfile(userUuid: string): Promise { - return this.prisma.profile.findUnique({ where: { userUuid } }); + async findProfileByUserUuid(userUuid: string): Promise { + const profile = await this.prisma.profile.findUnique({ + where: { userUuid }, + }); + if (!profile) throw new NotFoundException(); + return profile; } async findProfileByProfileUuid(uuid: string): Promise { @@ -28,7 +40,7 @@ export class ProfilesService { where: { userUuid: data.userUuid }, update: {}, create: { - uuid: generateUuid(), + uuid: uuid(), userUuid: data.userUuid, image: data.image, nickname: data.nickname, @@ -38,12 +50,18 @@ export class ProfilesService { async updateProfile( userUuid: string, + profileUuid: string, + image: Express.Multer.File, updateProfileDto: UpdateProfileDto, ): Promise { + await this.verifyUserProfile(userUuid, profileUuid); + if (image) { + updateProfileDto.image = await this.uploadService.uploadFile(image); + } try { return await this.prisma.profile.update({ where: { userUuid }, - data: { ...updateProfileDto }, + data: updateProfileDto, }); } catch (err) { if (err instanceof Prisma.PrismaClientKnownRequestError) { @@ -53,4 +71,14 @@ export class ProfilesService { } } } + + async verifyUserProfile( + userUuid: string, + profileUuid: string, + ): Promise { + const profile = await this.findProfileByProfileUuid(profileUuid); + if (!profile) throw new NotFoundException(); + if (userUuid !== profile.userUuid) throw new ForbiddenException(); + return true; + } }