본문으로 건너뛰기

Auth 도메인 테스트 명세서

1. 개요

이 문서는 Auth 도메인의 테스트 범위, 시나리오, 환경 설정을 정의합니다. 도메인 및 API 수준에서의 테스트를 모두 포함합니다.

2. 테스트 범위

2.1 단위 테스트

  1. 엔티티 테스트

    • AuthToken
    • Session
    • LoginAttempt
    • TwoFactorAuth
    • OAuthProvider
    • SecuritySettings (값 객체)
  2. 서비스 테스트

    • AuthService
    • TokenService
    • SessionService
    • TwoFactorAuthService
    • OAuthService
    • SecurityService
    • AppAuthenticationService
  3. 이벤트 핸들러 테스트

    • AuthEventHandler
    • SessionEventHandler
    • SecurityEventHandler
  4. 캐시 서비스 테스트

    • TokenCacheService
    • SessionCacheService
    • SecurityCacheService
  5. 컨트롤러 레이어

    • 입력값 검증
    • 응답 형식
    • 권한 검증
    • 에러 처리
  6. 가드/인터셉터

    • JWT 인증 가드
    • 역할 기반 권한 가드
    • 앱 토큰 인증 가드
    • 로깅 인터셉터

2.2 통합 테스트

  1. API 엔드포인트 테스트

    • 로그인/로그아웃
    • 토큰 발급/갱신
    • 2FA 활성화/검증
    • OAuth 연동
    • 보안 설정 관리
    • 앱 토큰 관리
    • 앱 보안 인증
  2. 데이터베이스 연동 테스트

    • Prisma ORM 리포지토리
    • 트랜잭션 처리
    • 인덱스 성능
    • 토큰 저장/조회
    • 권한 정보 관리
    • 세션 관리
  3. 캐시 연동 테스트

    • Redis 토큰 캐시
    • 세션 캐시
    • 캐시 무효화
    • 토큰 캐시
    • 권한 캐시
    • 블랙리스트 관리
  4. 이벤트 발행/구독 테스트

    • 인증 이벤트
    • 보안 이벤트

2.3 E2E 테스트

  1. 인증 시나리오

    • 이메일/비밀번호 로그인
    • OAuth 로그인
    • 2FA 인증
    • 토큰 갱신
    • 세션 관리
    • 앱 인증 플로우
  2. 에러 처리

    • 잘못된 인증 정보
    • 만료된 토큰
    • 2FA 실패
    • 보안 위반
    • 앱 토큰 검증 실패
  3. 성능 테스트

    • 동시 로그인 처리
    • 토큰 검증 성능
    • 세션 관리 성능
    • 동시 인증 요청
    • 권한 검증 성능
    • 캐시 성능 (히트율, 응답 시간, 메모리 사용량, 동기화 성능)

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.02025-03-30bok@weltcorp.comAPI 테스트 명세와 도메인 테스트 명세 통합
0.4.02025-03-30bok@weltcorp.com앱 보안 인증 테스트 시나리오 추가
0.3.02025-03-29bok@weltcorp.com성능 테스트 추가
0.2.02025-03-26bok@weltcorp.comAPI 엔드포인트 테스트 추가
0.1.02025-03-26bok@weltcorp.com초기 버전 작성