Skip to content

Commit b3b466e

Browse files
committed
Merge branch 'bug-be-#253' into feature-shared-#211
2 parents 8c21c30 + b9ecbed commit b3b466e

File tree

13 files changed

+468
-221
lines changed

13 files changed

+468
-221
lines changed

apps/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@nestjs/common": "^10.0.0",
2525
"@nestjs/config": "^3.3.0",
2626
"@nestjs/core": "^10.0.0",
27+
"@nestjs/jwt": "^10.2.0",
2728
"@nestjs/mapped-types": "*",
2829
"@nestjs/passport": "^10.0.3",
2930
"@nestjs/platform-express": "^10.0.0",
Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,80 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import { AuthController } from './auth.controller';
33
import { AuthService } from './auth.service';
4-
4+
import { JwtService } from '@nestjs/jwt';
5+
import { InvalidTokenException } from '../exception/invalid.exception';
6+
// import { LoginRequiredException } from '../exception/login.exception';
7+
// TODO: 테스트 코드 개선
58
describe('AuthController', () => {
6-
let controller: AuthController;
9+
let authController: AuthController;
710

811
beforeEach(async () => {
912
const module: TestingModule = await Test.createTestingModule({
1013
controllers: [AuthController],
1114
providers: [
15+
AuthService,
16+
JwtService,
1217
{
1318
provide: AuthService,
14-
useValue: {},
19+
useValue: {
20+
findUser: jest.fn(),
21+
createUser: jest.fn(),
22+
findUserById: jest.fn(),
23+
},
24+
},
25+
{
26+
provide: JwtService,
27+
useValue: {
28+
sign: jest.fn().mockReturnValue('test-token'),
29+
verify: jest.fn((token: string) => {
30+
if (token === 'invalid-token') {
31+
throw new InvalidTokenException();
32+
}
33+
return { sub: 1, provider: 'naver' };
34+
}),
35+
},
1536
},
1637
],
1738
}).compile();
1839

19-
controller = module.get<AuthController>(AuthController);
40+
authController = module.get<AuthController>(AuthController);
2041
});
2142

22-
it('should be defined', () => {
23-
expect(controller).toBeDefined();
43+
it('컨트롤러 클래스가 정상적으로 인스턴스화된다.', () => {
44+
expect(authController).toBeDefined();
45+
});
46+
47+
describe('getProfile', () => {
48+
it('JWT 토큰이 유효한 경우 profile을 return한다.', async () => {
49+
const req = {
50+
user: { sub: 1, email: '[email protected]', provider: 'naver' },
51+
} as any;
52+
const result = await authController.getProfile(req);
53+
expect(result).toEqual({
54+
message: '인증된 사용자 정보',
55+
user: req.user,
56+
});
57+
});
58+
59+
// it('JWT 토큰이 유효가지 않은 경우 InvalidTokenException을 throw한다.', async () => {
60+
// const req = {
61+
// headers: { authorization: 'Bearer invalid-token' },
62+
// user: undefined,
63+
// } as any;
64+
// try {
65+
// await authController.getProfile(req);
66+
// } catch (error) {
67+
// expect(error).toBeInstanceOf(InvalidTokenException);
68+
// }
69+
// });
70+
71+
// it('JWT 토큰이 없는 경우 LoginRequiredException을 throw한다.', async () => {
72+
// const req = { headers: {}, user: undefined } as any;
73+
// try {
74+
// await authController.getProfile(req);
75+
// } catch (error) {
76+
// expect(error).toBeInstanceOf(LoginRequiredException);
77+
// }
78+
// });
2479
});
2580
});

apps/backend/src/auth/auth.controller.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
22
import { AuthGuard } from '@nestjs/passport';
33
import { AuthService } from './auth.service';
4+
import { JwtService } from '@nestjs/jwt';
5+
import { JwtAuthGuard } from './guards/jwt-auth.guard';
46

57
@Controller('auth')
68
export class AuthController {
7-
constructor(private readonly authService: AuthService) {}
9+
constructor(
10+
private readonly authService: AuthService,
11+
private readonly jwtService: JwtService,
12+
) {}
813

914
@Get('naver')
1015
@UseGuards(AuthGuard('naver'))
@@ -17,9 +22,14 @@ export class AuthController {
1722
@UseGuards(AuthGuard('naver'))
1823
async naverCallback(@Req() req) {
1924
// 네이버 인증 후 사용자 정보 반환
25+
const user = req.user;
26+
// TODO: 후에 권한 (workspace 조회, 편집 기능)도 payload에 추가
27+
const payload = { sub: user.id, provider: user.provider };
28+
const token = this.jwtService.sign(payload);
2029
return {
2130
message: '네이버 로그인 성공',
22-
user: req.user,
31+
user,
32+
accessToken: token,
2333
};
2434
}
2535

@@ -34,8 +44,25 @@ export class AuthController {
3444
@UseGuards(AuthGuard('kakao'))
3545
async kakaoCallback(@Req() req) {
3646
// 카카오 인증 후 사용자 정보 반환
47+
const user = req.user;
48+
// TODO: 후에 권한 (workspace 조회, 편집 기능)도 payload에 추가
49+
const payload = { sub: user.id, provider: user.provider };
50+
const token = this.jwtService.sign(payload);
3751
return {
3852
message: '카카오 로그인 성공',
53+
user,
54+
accessToken: token,
55+
};
56+
}
57+
58+
// Example: 로그인한 사용자만 접근할 수 있는 엔드포인트
59+
// auth/profile
60+
@Get('profile')
61+
@UseGuards(JwtAuthGuard) // JWT 인증 검사
62+
async getProfile(@Req() req) {
63+
// JWT 토큰을 검증하고 사용자 정보 반환
64+
return {
65+
message: '인증된 사용자 정보',
3966
user: req.user,
4067
};
4168
}

apps/backend/src/auth/auth.module.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,30 @@ import { AuthService } from './auth.service';
55
import { AuthController } from './auth.controller';
66
import { NaverStrategy } from './strategies/naver.strategy';
77
import { KakaoStrategy } from './strategies/kakao.strategy';
8+
import { JwtModule } from '@nestjs/jwt';
9+
import { JwtAuthGuard } from './guards/jwt-auth.guard';
10+
import { ConfigModule, ConfigService } from '@nestjs/config';
11+
812
@Module({
9-
imports: [UserModule],
13+
imports: [
14+
UserModule,
15+
ConfigModule.forRoot({ isGlobal: true }),
16+
JwtModule.registerAsync({
17+
imports: [ConfigModule],
18+
inject: [ConfigService],
19+
useFactory: async (configService: ConfigService) => ({
20+
secret: configService.get<string>('JWT_SECRET'),
21+
signOptions: { expiresIn: '1h' },
22+
}),
23+
}),
24+
],
1025
controllers: [AuthController],
11-
providers: [AuthService, NaverStrategy, KakaoStrategy, UserRepository],
26+
providers: [
27+
AuthService,
28+
NaverStrategy,
29+
KakaoStrategy,
30+
UserRepository,
31+
JwtAuthGuard,
32+
],
1233
})
1334
export class AuthModule {}
Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,78 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import { AuthService } from './auth.service';
33
import { UserRepository } from '../user/user.repository';
4+
import { CreateUserDto } from './dto/createUser.dto';
5+
import { User } from '../user/user.entity';
46

57
describe('AuthService', () => {
6-
let service: AuthService;
8+
let authService: AuthService;
9+
let userRepository: UserRepository;
710

811
beforeEach(async () => {
912
const module: TestingModule = await Test.createTestingModule({
1013
providers: [
1114
AuthService,
1215
{
1316
provide: UserRepository,
14-
useValue: {},
17+
useValue: {
18+
findOne: jest.fn(),
19+
create: jest.fn(),
20+
save: jest.fn(),
21+
},
1522
},
1623
],
1724
}).compile();
1825

19-
service = module.get<AuthService>(AuthService);
26+
authService = module.get<AuthService>(AuthService);
27+
userRepository = module.get<UserRepository>(UserRepository);
2028
});
2129

22-
it('should be defined', () => {
23-
expect(service).toBeDefined();
30+
it('서비스 클래스가 정상적으로 인스턴스화된다.', () => {
31+
expect(authService).toBeDefined();
32+
});
33+
34+
describe('findUser', () => {
35+
it('id에 해당하는 사용자를 찾아 성공적으로 반환한다.', async () => {
36+
const dto: CreateUserDto = {
37+
providerId: 'test-provider-id',
38+
provider: 'naver',
39+
40+
};
41+
const user = new User();
42+
jest.spyOn(userRepository, 'findOne').mockResolvedValue(user);
43+
44+
const result = await authService.findUser(dto);
45+
expect(result).toEqual(user);
46+
});
47+
48+
it('id에 해당하는 사용자가 없을 경우 null을 return한다.', async () => {
49+
jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);
50+
const dto: CreateUserDto = {
51+
providerId: 'unknown-id',
52+
provider: 'naver',
53+
54+
};
55+
56+
const result = await authService.findUser(dto);
57+
expect(result).toBeNull();
58+
});
59+
});
60+
61+
describe('createUser', () => {
62+
it('사용자를 성공적으로 생성한다', async () => {
63+
const dto: CreateUserDto = {
64+
providerId: 'new-provider-id',
65+
provider: 'naver',
66+
67+
};
68+
const user = new User();
69+
jest.spyOn(userRepository, 'create').mockReturnValue(user);
70+
jest.spyOn(userRepository, 'save').mockResolvedValue(user);
71+
72+
const result = await authService.createUser(dto);
73+
expect(result).toEqual(user);
74+
expect(userRepository.create).toHaveBeenCalledWith(dto);
75+
expect(userRepository.save).toHaveBeenCalledWith(user);
76+
});
2477
});
2578
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
2+
import { JwtService } from '@nestjs/jwt';
3+
import { LoginRequiredException } from '../../exception/login.exception';
4+
import { InvalidTokenException } from '../../exception/invalid.exception';
5+
6+
@Injectable()
7+
export class JwtAuthGuard implements CanActivate {
8+
constructor(private readonly jwtService: JwtService) {}
9+
10+
async canActivate(context: ExecutionContext): Promise<boolean> {
11+
const request = context.switchToHttp().getRequest();
12+
const authorizationHeader = request.headers['authorization'];
13+
14+
if (!authorizationHeader) {
15+
// console.log('Authorization header missing');
16+
throw new LoginRequiredException();
17+
}
18+
19+
const token = authorizationHeader.split(' ')[1];
20+
21+
try {
22+
const decodedToken = this.jwtService.verify(token, {
23+
secret: process.env.JWT_SECRET,
24+
});
25+
request.user = decodedToken;
26+
return true;
27+
} catch (error) {
28+
// console.log('Invalid token');
29+
throw new InvalidTokenException();
30+
}
31+
}
32+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ForbiddenException } from '@nestjs/common';
2+
3+
export class InvalidTokenException extends ForbiddenException {
4+
constructor() {
5+
super(`유효하지 않은 JWT 토큰입니다.`);
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ForbiddenException } from '@nestjs/common';
2+
3+
export class LoginRequiredException extends ForbiddenException {
4+
constructor() {
5+
super(`로그인이 필요한 서비스입니다.`);
6+
}
7+
}

apps/backend/src/exception/user.exception.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)