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

프로필 모듈 개선 #381

Merged
merged 22 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5592f25
refactor: 사용하지 않는 파일 제거
Conut-1 Feb 17, 2025
d2544d9
refactor: 사용하지 않는 함수 제거
Conut-1 Feb 17, 2025
adfe989
fix: 유저 프로필 매치 동작 가드로 변경
Conut-1 Feb 17, 2025
b01bb71
refactor: 유저 프로필 매치 가드를 프로필 모듈로 이동
Conut-1 Feb 17, 2025
95dc3e9
refactor: 테스트 리팩토링
Conut-1 Feb 17, 2025
98d4845
refactor: 서비스에서 테스트하는 동작 제거
Conut-1 Feb 17, 2025
395213c
refactor: 테스트 리팩토링
Conut-1 Feb 17, 2025
231b861
test: JwtAuthGuard 적용 테스트
Conut-1 Feb 18, 2025
2535c5d
test: MatchUserProfileGuard 적용 테스트
Conut-1 Feb 18, 2025
fd533b1
refactor: 유저 생성 함수
Conut-1 Feb 19, 2025
e88a7cb
refactor: 토큰 생성 함수
Conut-1 Feb 19, 2025
90d98d4
test: 프로필 가져오기 테스트
Conut-1 Feb 19, 2025
71d45f2
chore: lodash 패키지 설치
Conut-1 Feb 19, 2025
76685a6
fix: 패스 패러미터로 프로필 uuid 받기
Conut-1 Feb 19, 2025
a9fde79
test: 동작 변경에 따라서 테스트 수정
Conut-1 Feb 19, 2025
2ad1922
fix: userUuid를 제외하고 프로필 반환
Conut-1 Feb 19, 2025
2b5f10a
test: /profiles/:profile_uuid PATCH 테스트 추가
Conut-1 Feb 19, 2025
3c1cb64
fix: IsOptional 추가
Conut-1 Feb 19, 2025
7bb1d24
docs: swagger 추가
Conut-1 Feb 19, 2025
19673bc
test: 프로필 반환값 테스트 수정
Conut-1 Feb 20, 2025
132438f
test: MatchUserProfileGuard 테스트
Conut-1 Feb 20, 2025
be87791
fix: ?. 옵셔널 체이닝 사용
Conut-1 Feb 20, 2025
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
8 changes: 8 additions & 0 deletions nestjs-BE/server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions nestjs-BE/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"dotenv": "^16.3.1",
"lodash": "^4.17.21",
"mongoose": "^8.0.2",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
Expand All @@ -49,6 +50,7 @@
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/lodash": "^4.17.15",
"@types/multer": "^1.4.11",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^3.0.13",
Expand Down
3 changes: 0 additions & 3 deletions nestjs-BE/server/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { MatchUserProfileGuard } from './guards/match-user-profile.guard';
import { IsProfileInSpaceGuard } from './guards/is-profile-in-space.guard';
import { UsersModule } from '../users/users.module';
import { ProfilesModule } from '../profiles/profiles.module';
Expand All @@ -27,12 +26,10 @@ import { ProfileSpaceModule } from '../profile-space/profile-space.module';
AuthService,
JwtStrategy,
{ provide: APP_GUARD, useClass: JwtAuthGuard },
MatchUserProfileGuard,
IsProfileInSpaceGuard,
],
exports: [
AuthService,
MatchUserProfileGuard,
ProfilesModule,
IsProfileInSpaceGuard,
ProfileSpaceModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { Test, TestingModule } from '@nestjs/testing';
import { InviteCode, Space } from '@prisma/client';
import { InviteCodesController } from './invite-codes.controller';
import { InviteCodesService } from './invite-codes.service';
import { MatchUserProfileGuard } from '../auth/guards/match-user-profile.guard';
import { ProfilesService } from '../profiles/profiles.service';
import { MatchUserProfileGuard } from '../profiles/guards/match-user-profile.guard';
import { IsProfileInSpaceGuard } from '../auth/guards/is-profile-in-space.guard';
import { ProfileSpaceService } from '../profile-space/profile-space.service';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
import { InviteCodesService } from './invite-codes.service';
import { CreateInviteCodeDto } from './dto/create-invite-code.dto';
import { MatchUserProfileGuard } from '../auth/guards/match-user-profile.guard';
import { IsProfileInSpaceGuard } from '../auth/guards/is-profile-in-space.guard';
import { MatchUserProfileGuard } from '../profiles/guards/match-user-profile.guard';

@Controller('inviteCodes')
@ApiTags('inviteCodes')
Expand Down
3 changes: 2 additions & 1 deletion nestjs-BE/server/src/invite-codes/invite-codes.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { InviteCodesService } from './invite-codes.service';
import { InviteCodesController } from './invite-codes.controller';
import { SpacesModule } from '../spaces/spaces.module';
import { AuthModule } from '../auth/auth.module';
import { ProfilesModule } from '../profiles/profiles.module';

@Module({
imports: [AuthModule, SpacesModule],
imports: [AuthModule, SpacesModule, ProfilesModule],
controllers: [InviteCodesController],
providers: [InviteCodesService],
})
Expand Down
13 changes: 5 additions & 8 deletions nestjs-BE/server/src/profiles/dto/update-profile.dto.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import { PartialType } from '@nestjs/mapped-types';
import { ApiProperty } from '@nestjs/swagger';
import { MaxLength } from 'class-validator';
import { CreateProfileDto } from './create-profile.dto';
import { IsOptional, MaxLength } from 'class-validator';
import { MAX_NAME_LENGTH } from '../../config/constants';

export class UpdateProfileDto extends PartialType(CreateProfileDto) {
export class UpdateProfileDto {
@IsOptional()
@MaxLength(MAX_NAME_LENGTH)
@ApiProperty({
example: 'new nickname',
description: 'Updated nickname of the profile',
required: false,
})
nickname?: string;
nickname: string;

@ApiProperty({
example: 'new image.png',
description: 'Updated Profile image file',
required: false,
})
image?: string;

uuid?: string;
image: Express.Multer.File;
}
1 change: 0 additions & 1 deletion nestjs-BE/server/src/profiles/entities/profile.entity.ts

This file was deleted.

116 changes: 116 additions & 0 deletions nestjs-BE/server/src/profiles/guards/match-user-profile.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { MatchUserProfileGuard } from './match-user-profile.guard';
import { ProfilesService } from '../profiles.service';

import type { ExecutionContext } from '@nestjs/common';
import type { TestingModule } from '@nestjs/testing';
import type { Profile } from '@prisma/client';

describe('MatchUserProfileGuard', () => {
const userUuid = 'user uuid';
const profileUuid = 'profile uuid';
let guard: MatchUserProfileGuard;
let profilesService: ProfilesService;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [{ provide: ProfilesService, useValue: {} }],
}).compile();

profilesService = module.get<ProfilesService>(ProfilesService);

guard = new MatchUserProfileGuard(profilesService);
});

it('throw bad request when profile uuid not include', async () => {
const context = createExecutionContext({ user: { uuid: userUuid } });

await expect(guard.canActivate(context)).rejects.toThrow(
BadRequestException,
);
});

it('throw bad request when user uuid not include (body)', async () => {
const context = createExecutionContext({
body: { profile_uuid: profileUuid },
});

await expect(guard.canActivate(context)).rejects.toThrow(
BadRequestException,
);
});

it('throw bad request when user uuid not include (query)', async () => {
const context = createExecutionContext({
query: { profile_uuid: profileUuid },
});

await expect(guard.canActivate(context)).rejects.toThrow(
BadRequestException,
);
});

it('throw bad request when user uuid not include (params)', async () => {
const context = createExecutionContext({
params: { profile_uuid: profileUuid },
});

await expect(guard.canActivate(context)).rejects.toThrow(
BadRequestException,
);
});

it('throw forbidden when profile not found', async () => {
const context = createExecutionContext({
user: { uuid: userUuid },
params: { profile_uuid: profileUuid },
});
profilesService.findProfileByProfileUuid = jest.fn(async () => null);

await expect(guard.canActivate(context)).rejects.toThrow(
ForbiddenException,
);
});

it("throw forbidden when profile not user's profile", async () => {
const context = createExecutionContext({
user: { uuid: userUuid },
params: { profile_uuid: profileUuid },
});
profilesService.findProfileByProfileUuid = jest.fn(
async () => ({ userUuid: 'other user uuid' }) as Profile,
);

await expect(guard.canActivate(context)).rejects.toThrow(
ForbiddenException,
);
});

it('return true when valid user', async () => {
const context = createExecutionContext({
user: { uuid: userUuid },
params: { profile_uuid: profileUuid },
});
profilesService.findProfileByProfileUuid = jest.fn(
async () => ({ userUuid }) as Profile,
);

await expect(guard.canActivate(context)).resolves.toBeTruthy();
});
});

function createExecutionContext(request: object): ExecutionContext {
const innerRequest = {
body: {},
params: {},
query: {},
...request,
};
const context: ExecutionContext = {
switchToHttp: () => ({
getRequest: () => innerRequest,
}),
} as ExecutionContext;
return context;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class MatchUserProfileGuard implements CanActivate {

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const userUuid = request.user.uuid;
const userUuid = request.user?.uuid;
const profileUuid =
request.body.profile_uuid ||
request.query.profile_uuid ||
Expand Down
64 changes: 12 additions & 52 deletions nestjs-BE/server/src/profiles/profiles.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
import {
ForbiddenException,
HttpStatus,
NotFoundException,
} from '@nestjs/common';
import { HttpStatus } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { ProfilesController } from './profiles.controller';
import { ProfilesService } from './profiles.service';

import type { UpdateProfileDto } from './dto/update-profile.dto';

describe('ProfilesController', () => {
let controller: ProfilesController;
let profilesService: ProfilesService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ProfilesController],
providers: [
{
provide: ProfilesService,
useValue: {
findProfileByUserUuid: jest.fn(),
updateProfile: jest.fn(),
},
},
],
providers: [{ provide: ProfilesService, useValue: {} }],
}).compile();

controller = module.get<ProfilesController>(ProfilesController);
Expand All @@ -40,9 +30,7 @@ describe('ProfilesController', () => {
nickname: 'test nickname',
};

jest
.spyOn(profilesService, 'findProfileByUserUuid')
.mockResolvedValue(testProfile);
profilesService.findProfileByUserUuid = jest.fn(async () => testProfile);

const response = controller.findProfileByUserUuid(userUuidMock);

Expand All @@ -55,41 +43,28 @@ describe('ProfilesController', () => {
userUuidMock,
);
});

it('not found profile', async () => {
jest
.spyOn(profilesService, 'findProfileByUserUuid')
.mockRejectedValue(new NotFoundException());

const response = controller.findProfileByUserUuid(userUuidMock);

await expect(response).rejects.toThrow(NotFoundException);
});
});

describe('update', () => {
const imageMock = {} as Express.Multer.File;
const userUuidMock = 'user uuid';
const profileUuidMock = 'profile uuid';
const bodyMock = {
uuid: 'profile test uuid',
nickname: 'test nickname',
};
} as UpdateProfileDto;

it('updated profile', async () => {
const testProfile = {
uuid: 'profile test uuid',
userUuid: userUuidMock,
uuid: profileUuidMock,
userUuid: 'user uuid',
image: 'www.test.com/image',
nickname: 'test nickname',
};

jest
.spyOn(profilesService, 'updateProfile')
.mockResolvedValue(testProfile);
profilesService.updateProfile = jest.fn(async () => testProfile);

const response = controller.updateProfile(
imageMock,
userUuidMock,
profileUuidMock,
bodyMock,
);

Expand All @@ -99,25 +74,10 @@ describe('ProfilesController', () => {
data: testProfile,
});
expect(profilesService.updateProfile).toHaveBeenCalledWith(
userUuidMock,
bodyMock.uuid,
imageMock,
bodyMock,
);
});

it('not found user', async () => {
jest
.spyOn(profilesService, 'updateProfile')
.mockRejectedValue(new ForbiddenException());

const response = controller.updateProfile(
profileUuidMock,
imageMock,
userUuidMock,
bodyMock,
);

await expect(response).rejects.toThrow(ForbiddenException);
});
});
});
Loading