UNPKG

@point3/logto-module

Version:

포인트3 내부 logto Authentication 모듈입니다

305 lines (253 loc) 11.2 kB
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()}`); }); }); });