@point3/logto-module
Version:
포인트3 내부 logto Authentication 모듈입니다
305 lines (253 loc) • 11.2 kB
text/typescript
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, LoggerService, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { LogtoTokenGuard } from './guard';
import {
LogtoTokenVerifier, LogtoTokenVerifierToken,
AccessToken, AccessTokenPayload
} from '../token';
import { p3Values } from 'point3-common-tool';
import { LogtoLoggerServiceToken } from 'client';
describe('LogtoTokenGuard 테스트', () => {
let guard: LogtoTokenGuard;
let tokenUtil: jest.Mocked<LogtoTokenVerifier>;
let reflector: jest.Mocked<Reflector>;
let logger: jest.Mocked<LoggerService>;
// 사용자가 제공한 실제 JWT 토큰
const testToken = 'eyJhbGciOiJFUzM4NCIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImxKUjU3SkFqVmV1dHk4eWljVzUtdFFySDM2WFl6NUlzWFhXSDVzeXV0dEEifQ.eyJ1c2VyUm9sZXMiOlsicDMtQ0lTTy0wIl0sIm1hbmFnZXJJZCI6Im1hbmFnZXItMDE5NjQ0NWMtOGVjNy03MDc4LWExNDItNGU3ZGI5YTRhYWVhIiwiY2xpZW50SWQiOiJwb2ludDMtMDE5NjNjODUtNDQ2ZS03NGM5LWFmNzktNDhlMjU0NjVjMzI3IiwianRpIjoiV0RYTmxoTWkwT0tHQ1pTRzFKZnBrIiwic3ViIjoieXVsaXVmdHNvMWQwIiwiaWF0IjoxNzQ5MDI0NzIzLCJleHAiOjE3NDkwMjgzMjMsInNjb3BlIjoiIiwiY2xpZW50X2lkIjoiNXFydmk5eW0wajJ0YTJ6YXBnbHU0IiwiaXNzIjoiaHR0cHM6Ly9sb2d0by5wb2ludDMuaW8vb2lkYyIsImF1ZCI6Imh0dHBzOi8vZGVmYXVsdC5sb2d0by5hcHAvYXBpIn0.nZdzvdxQ74m2oFEklVTfQlcqYBkRrRxtHQEgz1L6DjST9_9Wa7H7J1gKJVEjm8NnjFCQXljYM_hTVx1ABTmUgDrEKVjtHFVKUyPoSzxQitXexwmBZY5l8WdyqJDqAy8d';
// 토큰 데이터와 일치하는 모의 페이로드
const mockPayload: AccessTokenPayload = {
userRoles: ['p3-CISO-0'],
managerId: 'manager-0196445c-8ec7-7078-a142-4e7db9a4aaea',
clientId: 'point3-019663c85-446e-74c9-af79-48e25465c327',
jti: 'WDXNlhMi0OKGCZSG1Jfpk',
sub: 'yuliuftso1d0',
iat: 1749024723,
exp: 1749028323,
scope: '',
client_id: '5qrvi9ym0j2ta2zapglu4',
iss: 'https://logto.point3.io/oidc',
aud: 'https://default.logto.app/api'
};
beforeEach(async () => {
const mockTokenUtil = {
verifyToken: jest.fn(),
};
const mockReflector = {
get: jest.fn(),
};
const mockLogger = {
warn: jest.fn(),
error: jest.fn(),
log: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
LogtoTokenGuard,
{
provide: LogtoTokenVerifierToken,
useValue: mockTokenUtil,
},
{
provide: Reflector,
useValue: mockReflector,
},
{
provide: LogtoLoggerServiceToken,
useValue: mockLogger,
},
],
}).compile();
guard = module.get<LogtoTokenGuard>(LogtoTokenGuard);
tokenUtil = module.get(LogtoTokenVerifierToken);
reflector = module.get(Reflector);
logger = module.get(LogtoLoggerServiceToken);
// 각 테스트 전에 모의 함수 초기화
jest.clearAllMocks();
});
const createMockExecutionContext = (headers: any = {}, route: any = { path: '/test' }): ExecutionContext => {
const mockRequest = {
headers,
route,
user: undefined // Guard에서 설정됨
};
return {
switchToHttp: () => ({
getRequest: () => mockRequest,
getResponse: jest.fn(),
getNext: jest.fn(),
}),
getHandler: jest.fn(),
getClass: jest.fn(),
getArgs: jest.fn(),
getArgByIndex: jest.fn(),
switchToRpc: jest.fn(),
switchToWs: jest.fn(),
getType: jest.fn(),
} as ExecutionContext;
};
describe('🔐 성공적인 인증 테스트', () => {
it('유효한 토큰이 제공되었을 때 인증하고 사용자 데이터를 설정해야 함', async () => {
// 준비
const context = createMockExecutionContext({
authorization: `Bearer ${testToken}`,
});
// Reflector 모의 설정 - 특정 역할 요구사항
reflector.get
.mockReturnValueOnce(undefined) // requiredScopes
.mockReturnValueOnce(['p3-CISO-0']); // requiredRoles
// 성공적인 토큰 검증 모의
tokenUtil.verifyToken.mockResolvedValueOnce(mockPayload);
// 실행
const result = await guard.canActivate(context);
const request = context.switchToHttp().getRequest();
// 검증
expect(result).toBe(true);
expect(tokenUtil.verifyToken).toHaveBeenCalledWith(
testToken,
undefined,
['p3-CISO-0']
);
// 사용자 데이터가 올바르게 설정되었는지 확인
expect(request.user).toEqual({
userId: 'yuliuftso1d0',
managerId: expect.objectContaining({
toString: expect.any(Function)
}),
clientId: expect.objectContaining({
toString: expect.any(Function)
}),
});
// GUID 값 검증
expect(request.user.managerId.toString()).toContain('manager');
expect(request.user.managerId.toString()).toContain('0196445c-8ec7-7078-a142-4e7db9a4aaea');
expect(request.user.clientId.toString()).toContain('point3');
expect(request.user.clientId.toString()).toContain('019663c85-446e-74c9-af79-48e25465c327');
});
it('필수 스코프나 역할이 없을 때도 동작해야 함', async () => {
// 준비
const context = createMockExecutionContext({
authorization: `Bearer ${testToken}`,
});
// Reflector 모의 설정 - 요구사항 없음
reflector.get
.mockReturnValueOnce(undefined) // requiredScopes
.mockReturnValueOnce(undefined); // requiredRoles
// 성공적인 토큰 검증 모의
tokenUtil.verifyToken.mockResolvedValueOnce(mockPayload);
// 실행
const result = await guard.canActivate(context);
// 검증
expect(result).toBe(true);
expect(tokenUtil.verifyToken).toHaveBeenCalledWith(
testToken,
undefined,
undefined
);
});
});
describe('🚫 토큰 추출 실패 테스트', () => {
it('Authorization 헤더가 없을 때 UnauthorizedException을 던져야 함', async () => {
// 준비
const context = createMockExecutionContext({}); // 헤더 없음
reflector.get
.mockReturnValueOnce(undefined)
.mockReturnValueOnce(['p3-CISO-0']);
// 실행 & 검증
await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
await expect(guard.canActivate(context)).rejects.toThrow('Authorization header is missing');
});
it('Authorization 헤더가 Bearer가 아닐 때 UnauthorizedException을 던져야 함', async () => {
// 준비
const context = createMockExecutionContext({
authorization: 'Basic sometoken',
});
reflector.get
.mockReturnValueOnce(undefined)
.mockReturnValueOnce(['p3-CISO-0']);
// 실행 & 검증
await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
await expect(guard.canActivate(context)).rejects.toThrow('Authorization token type not supported');
});
it('Bearer 헤더에서 토큰을 올바르게 추출해야 함', async () => {
// 준비
const context = createMockExecutionContext({
authorization: `Bearer ${testToken}`,
});
reflector.get
.mockReturnValueOnce(undefined)
.mockReturnValueOnce(['p3-CISO-0']);
tokenUtil.verifyToken.mockResolvedValueOnce(mockPayload);
// 실행
await guard.canActivate(context);
// 검증 - 토큰이 올바르게 추출되어 verifyToken에 전달됨
expect(tokenUtil.verifyToken).toHaveBeenCalledWith(
testToken,
undefined,
['p3-CISO-0']
);
});
});
describe('❌ 토큰 검증 실패 테스트', () => {
it('토큰 검증에서 UnauthorizedException이 발생하면 다시 던져야 함', async () => {
// 준비
const context = createMockExecutionContext({
authorization: `Bearer ${testToken}`,
});
reflector.get
.mockReturnValueOnce(undefined)
.mockReturnValueOnce(['p3-CISO-0']);
const authError = new UnauthorizedException('Invalid token');
tokenUtil.verifyToken.mockRejectedValueOnce(authError);
// 실행 & 검증
await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
});
it('다른 에러가 발생하면 일반적인 에러 메시지를 던져야 함', async () => {
// 준비
const context = createMockExecutionContext({
authorization: `Bearer ${testToken}`,
});
reflector.get
.mockReturnValueOnce(undefined)
.mockReturnValueOnce(['p3-CISO-0']);
tokenUtil.verifyToken.mockRejectedValueOnce(new Error('Some other error'));
// 실행 & 검증
await expect(guard.canActivate(context)).rejects.toThrow('요청을 처리하지 못하였습니다.');
});
});
describe('🔍 실제 JWT 토큰 분석', () => {
it('제공된 JWT 토큰의 페이로드를 올바르게 디코딩해야 함', () => {
// JWT 토큰을 수동으로 디코딩하여 모의 데이터와 비교
const [header, payload, signature] = testToken.split('.');
const decodedPayload = JSON.parse(Buffer.from(payload, 'base64url').toString());
console.log('🔍 디코딩된 토큰 페이로드:');
console.log(JSON.stringify(decodedPayload, null, 2));
// 모의 페이로드가 실제 토큰과 일치하는지 확인
expect(decodedPayload.userRoles).toEqual(['p3-CISO-0']);
expect(decodedPayload.managerId).toBe('manager-0196445c-8ec7-7078-a142-4e7db9a4aaea');
expect(decodedPayload.clientId).toBe('point3-01963c85-446e-74c9-af79-48e25465c327');
expect(decodedPayload.sub).toBe('yuliuftso1d0');
expect(decodedPayload.iss).toBe('https://logto.point3.io/oidc');
// 토큰 만료 시간 확인 (Unix timestamp)
const expirationDate = new Date(decodedPayload.exp * 1000);
const issuedDate = new Date(decodedPayload.iat * 1000);
console.log(`📅 토큰 발급 시간: ${issuedDate.toISOString()}`);
console.log(`⏰ 토큰 만료 시간: ${expirationDate.toISOString()}`);
console.log(`🏢 발급자: ${decodedPayload.iss}`);
console.log(`👤 사용자 역할: ${decodedPayload.userRoles.join(', ')}`);
});
it('토큰에서 추출된 GUID 값들이 올바른 형식인지 확인해야 함', () => {
const [header, payload, signature] = testToken.split('.');
const decodedPayload = JSON.parse(Buffer.from(payload, 'base64url').toString());
// managerId GUID 검증
const managerId = p3Values.Guid.parse(decodedPayload.managerId);
expect(managerId.Prefix == 'manager');
// clientId GUID 검증
const clientId = p3Values.Guid.parse(decodedPayload.clientId);
expect(clientId.Prefix == 'point3');
console.log('✅ GUID 형식 검증 완료:');
console.log(` Manager ID: ${managerId.toString()}`);
console.log(` Client ID: ${clientId.toString()}`);
});
});
});