Auth 도메인 테스트 명세서
1. 개요
이 문서는 Auth 도메인의 테스트 범위, 시나리오, 환경 설정을 정의합니다. 도메인 및 API 수준에서의 테스트를 모두 포함합니다.
2. 테스트 범위
2.1 단위 테스트
-
엔티티 테스트
- AuthToken
- Session
- LoginAttempt
- TwoFactorAuth
- OAuthProvider
- SecuritySettings (값 객체)
-
서비스 테스트
- AuthService
- TokenService
- SessionService
- TwoFactorAuthService
- OAuthService
- SecurityService
- AppAuthenticationService
-
이벤트 핸들러 테스트
- AuthEventHandler
- SessionEventHandler
- SecurityEventHandler
-
캐시 서비스 테스트
- TokenCacheService
- SessionCacheService
- SecurityCacheService
-
컨트롤러 레이어
- 입력값 검증
- 응답 형식
- 권한 검증
- 에러 처리
-
가드/인터셉터
- JWT 인증 가드
- 역할 기반 권한 가드
- 앱 토큰 인증 가드
- 로깅 인터셉터
2.2 통합 테스트
-
API 엔드포인트 테스트
- 로그인/로그아웃
- 토큰 발급/갱신
- 2FA 활성화/검증
- OAuth 연동
- 보안 설정 관리
- 앱 토큰 관리
- 앱 보안 인증
-
데이터베이스 연동 테스트
- Prisma ORM 리포지토리
- 트랜잭션 처리
- 인덱스 성능
- 토큰 저장/조회
- 권한 정보 관리
- 세션 관리
-
캐시 연동 테스트
- Redis 토큰 캐시
- 세션 캐시
- 캐시 무효화
- 토큰 캐시
- 권한 캐시
- 블랙리스트 관리
-
이벤트 발행/구독 테스트
- 인증 이벤트
- 보안 이벤트
2.3 E2E 테스트
-
인증 시나리오
- 이메일/비밀번호 로그인
- OAuth 로그인
- 2FA 인증
- 토큰 갱신
- 세션 관리
- 앱 인증 플로우
-
에러 처리
- 잘못된 인증 정보
- 만료된 토큰
- 2FA 실패
- 보안 위반
- 앱 토큰 검증 실패
-
성능 테스트
- 동시 로그인 처리
- 토큰 검증 성능
- 세션 관리 성능
- 동시 인증 요청
- 권한 검증 성능
- 캐시 성능 (히트율, 응답 시간, 메모리 사용량, 동기화 성능)
3. 테스트 시나리오
3.1 AuthToken 엔티티 테스트
describe('AuthToken Entity', () => {
describe('validate', () => {
it('유효한 토큰 검증', () => {
const token = new AuthToken();
token.setToken('valid-token');
token.setExpiry(new Date(Date.now() + 3600000));
expect(token.isValid()).toBe(true);
});
it('만료된 토큰 검증', () => {
const token = new AuthToken();
token.setToken('expired-token');
token.setExpiry(new Date(Date.now() - 3600000));
expect(token.isValid()).toBe(false);
});
});
});
3.2 AuthService 테스트
describe('AuthService', () => {
describe('login', () => {
it('로그인 성공', async () => {
const dto = {
email: 'test@example.com',
password: 'password123'
};
const result = await authService.login(dto);
expect(result).toBeDefined();
expect(result.accessToken).toBeDefined();
expect(result.refreshToken).toBeDefined();
});
it('잘못된 인증 정보', async () => {
const dto = {
email: 'test@example.com',
password: 'wrong-password'
};
await expect(authService.login(dto))
.rejects
.toThrow(AuthenticationError);
});
});
});
3.3 API 컨트롤러 테스트
describe('AuthController (e2e)', () => {
describe('POST /auth/login', () => {
it('로그인 성공', () => {
return request(app.getHttpServer())
.post('/v1/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
})
.expect(200)
.expect(res => {
expect(res.body.data.accessToken).toBeDefined();
expect(res.body.data.refreshToken).toBeDefined();
});
});
it('잘못된 인증 정보', () => {
return request(app.getHttpServer())
.post('/v1/auth/login')
.send({
email: 'test@example.com',
password: 'wrong-password'
})
.expect(401)
.expect(res => {
expect(res.body.code).toBe('INVALID_CREDENTIALS');
});
});
});
describe('POST /auth/refresh', () => {
it('토큰 갱신 성공', () => {
return request(app.getHttpServer())
.post('/v1/auth/refresh')
.send({ refreshToken: validRefreshToken })
.expect(200)
.expect(res => {
expect(res.body.data).toHaveProperty('accessToken');
expect(res.body.data.accessToken).not.toBe(oldAccessToken);
});
});
it('만료된 토큰 거부', () => {
return request(app.getHttpServer())
.post('/v1/auth/refresh')
.send({ refreshToken: expiredRefreshToken })
.expect(401);
});
});
});
3.4 OAuth 통합 테스트
describe('OAuthService', () => {
describe('handleCallback', () => {
it('OAuth 콜백 처리 성공', async () => {
const code = 'valid-oauth-code';
const provider = 'google';
const result = await oauthService.handleCallback(provider, code);
expect(result).toBeDefined();
expect(result.accessToken).toBeDefined();
expect(result.user).toBeDefined();
});
it('잘못된 OAuth 코드', async () => {
const code = 'invalid-code';
const provider = 'google';
await expect(oauthService.handleCallback(provider, code))
.rejects
.toThrow(OAuthError);
});
});
});
3.5 앱 보안 인증 테스트
describe('App Security Authentication', () => {
it('디바이스 인증용 챌린지 생성', async () => {
const deviceInfo = {
uuid: '0a1b2c3d-4e5f-6789-0a1b-2c3d4e5f6789',
platform: 'iOS',
version: '1.2.0',
timestamp: Date.now()
};
// 디바이스 ID 해시 생성
const deviceIdStr = JSON.stringify(deviceInfo);
const deviceIdHash = crypto.createHash('sha256')
.update(`${deviceIdStr}:APP_SECRET`)
.digest('base64url');
const response = await request(app.getHttpServer())
.post('/v1/auth/app/challenge')
.send({ deviceId: deviceInfo, deviceIdHash })
.expect(200);
expect(response.body.data).toHaveProperty('challenge');
expect(response.body.data).toHaveProperty('serverTimestamp');
expect(response.body.data).toHaveProperty('nonce');
});
it('챌린지 검증 및 앱 토큰 발급', async () => {
// 1. 챌린지 요청
const deviceInfo = {
uuid: '0a1b2c3d-4e5f-6789-0a1b-2c3d4e5f6789',
platform: 'iOS',
version: '1.2.0',
timestamp: Date.now()
};
const deviceIdStr = JSON.stringify(deviceInfo);
const deviceIdHash = crypto.createHash('sha256')
.update(`${deviceIdStr}:APP_SECRET`)
.digest('base64url');
const challengeResponse = await request(app.getHttpServer())
.post('/v1/auth/app/challenge')
.send({ deviceId: deviceInfo, deviceIdHash })
.expect(200);
const { challenge, serverTimestamp, nonce } = challengeResponse.body.data;
// 2. 타임윈도우 계산
const date = new Date(serverTimestamp);
date.setMinutes(Math.floor(date.getMinutes() / 10) * 10);
date.setSeconds(0);
date.setMilliseconds(0);
const timeWindow = date.toISOString()
.replace(/[^0-9]/g, '')
.slice(0, 12);
// 3. 암호화 키 생성
const keyMaterial = `${timeWindow}:${deviceIdHash}:${nonce}`;
const key = crypto.createHmac('sha256', 'APP_SECRET')
.update(keyMaterial)
.digest();
// 4. 챌린지 암호화
const iv = Buffer.alloc(12, 0);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([
cipher.update(challenge, 'utf8'),
cipher.final()
]);
const tag = cipher.getAuthTag();
const encryptedChallenge = Buffer.concat([encrypted, tag]).toString('base64');
// 5. 검증 및 토큰 발급 요청
const verifyResponse = await request(app.getHttpServer())
.post('/v1/auth/app/verify')
.send({
deviceId: deviceInfo,
deviceIdHash,
encryptedChallenge,
nonce
})
.expect(200);
expect(verifyResponse.body.data).toHaveProperty('appToken');
expect(verifyResponse.body.data).toHaveProperty('expiresAt');
});
});
4. 테스트 환경 설정
4.1 단위 테스트 환경
// jest.config.js
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest'
},
collectCoverageFrom: [
'**/*.(t|j)s',
'!**/*.module.ts',
'!**/main.ts',
'!**/index.ts'
],
coverageDirectory: '../coverage',
testEnvironment: 'node'
};
4.2 통합 테스트 환경
// test/jest-e2e.js
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: '.',
testEnvironment: 'node',
testRegex: '.e2e-spec.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest'
},
setupFilesAfterEnv: ['./jest.setup.ts']
};
// jest.setup.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
beforeAll(async () => {
// 테스트 데이터 설정
await prisma.user.create({
data: {
email: 'test@example.com',
password: '$2b$10$EpRnTzVlqHNP0.fUbXUwSOyuiXe/QLSUG6xNekdHgTGmrpHEfIoxm', // 'password123'
role: 'USER'
}
});
});
afterAll(async () => {
// 테스트 데이터 정리
await prisma.user.deleteMany();
await prisma.$disconnect();
});
4.3 성능 테스트 환경
// k6.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
stages: [
{ duration: '30s', target: 100 }, // 램프 업
{ duration: '1m', target: 100 }, // 부하 유지
{ duration: '30s', target: 0 } // 램프 다운
],
thresholds: {
http_req_duration: ['p(95)\<200'], // 요청의 95%가 200ms 이내에 완료되어야 함
http_req_failed: ['rate\<0.01'], // 1% 미만의 요청만 실패 허용
},
};
export default function() {
const url = 'http://localhost:3000/v1/auth/app/validate';
const payload = JSON.stringify({
appToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
});
const params = {
headers: {
'Content-Type': 'application/json',
},
};
let res = http.post(url, payload, params);
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 100ms': (r) => r.timings.duration < 100,
});
sleep(1);
}
5. 테스트 커버리지
테스트는 다음 영역을 커버해야 합니다:
5.1 도메인 로직 커버리지
- 엔티티 및 값 객체 내 모든 비즈니스 로직
- 도메인 서비스 로직
- 이벤트 핸들러
- 예외 처리 로직
5.2 API 커버리지
- 모든 엔드포인트 테스트
- 입력 유효성 검증
- 에러 처리 및 에러 응답 형식
- 권한 제어
- 헤더 및 상태 코드 검증
5.3 보안 테스트
- 토큰 서명 검증
- 권한 검증
- 유효하지 않은 토큰 처리
- 리플레이 공격 방지
- 앱 보안 인증 절차
5.4 성능 테스트
- 응답 시간 테스트
- 처리량 테스트
- 동시성 테스트
- 캐시 성능 측정
변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 0.5.0 | 2025-03-30 | bok@weltcorp.com | API 테스트 명세와 도메인 테스트 명세 통합 |
| 0.4.0 | 2025-03-30 | bok@weltcorp.com | 앱 보안 인증 테스트 시나리오 추가 |
| 0.3.0 | 2025-03-29 | bok@weltcorp.com | 성능 테스트 추가 |
| 0.2.0 | 2025-03-26 | bok@weltcorp.com | API 엔드포인트 테스트 추가 |
| 0.1.0 | 2025-03-26 | bok@weltcorp.com | 초기 버전 작성 |