feat: Complete Phase 1.5 - AuthService Raw Query migration
Phase 1.5 완료: 인증 서비스 Raw Query 전환 및 테스트 완료 ✅ AuthService 전환 완료 (5개 Prisma 호출 제거): - loginPwdCheck(): Raw Query로 사용자 비밀번호 조회 - insertLoginAccessLog(): Raw Query로 로그인 로그 기록 - getUserInfo(): Raw Query로 사용자/권한/회사 정보 조회 - authority_sub_user ↔ authority_master JOIN (master_objid ↔ objid) - 3개 쿼리로 분리 (사용자, 권한, 회사) - processLogin(): 전체 로그인 플로우 통합 - processLogout(): 로그아웃 로그 기록 🧪 테스트 완료: - 단위 테스트: 30개 테스트 모두 통과 ✅ - 로그인 검증 (6개) - 사용자 정보 조회 (5개) - 로그인 로그 기록 (4개) - 전체 로그인 프로세스 (5개) - 로그아웃 (2개) - 토큰 검증 (3개) - Raw Query 전환 검증 (3개) - 성능 테스트 (2개) - 통합 테스트: 작성 완료 (auth.integration.test.ts) - 로그인 → 토큰 발급 → 인증 → 로그아웃 플로우 🔧 주요 변경사항: - Prisma import 제거 → Raw Query (query from db.ts) - authority 테이블 JOIN 수정 (auth_code → master_objid/objid) - 파라미터 바인딩으로 SQL Injection 방지 - 타입 안전성 유지 (TypeScript Generic 사용) 📊 성능: - 로그인 프로세스: < 1초 - 사용자 정보 조회: < 500ms - 모든 테스트 실행 시간: 2.016초 🎯 다음 단계: - Phase 2: 핵심 서비스 전환 (ScreenManagement, TableManagement 등) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e837ccc1d1
commit
824e5f4827
|
|
@ -1040,26 +1040,28 @@ describe("Performance Benchmarks", () => {
|
|||
- [x] 단위 테스트 작성 (`backend-node/src/tests/`)
|
||||
- [x] 테스트 성공 확인 (multiConnectionQueryService, externalCallConfigService)
|
||||
|
||||
### **Phase 1.5: 인증 및 관리자 서비스 (우선 전환) - 36개 호출** ⚡ **NEW**
|
||||
### **Phase 1.5: 인증 및 관리자 서비스 (우선 전환) - 36개 호출** ✅ **완료**
|
||||
|
||||
> **우선순위 변경**: Phase 2 진행 전 인증/관리 시스템을 먼저 전환하여 전체 시스템의 안정적인 기반 구축
|
||||
|
||||
- [ ] **AuthService 전환 (5개)** - 🔐 최우선
|
||||
- [ ] 로그인 로직 (JWT 생성)
|
||||
- [ ] 사용자 인증 및 검증
|
||||
- [ ] 비밀번호 암호화 처리
|
||||
- [ ] 토큰 관리
|
||||
- [ ] 세션 관리
|
||||
- [ ] **AdminService 전환 (3개)** - 👤 사용자 관리
|
||||
- [ ] 사용자 CRUD
|
||||
- [ ] 메뉴 관리 (재귀 쿼리)
|
||||
- [ ] 권한 관리
|
||||
- [ ] **AdminController 전환 (28개)** - 📡 관리자 API
|
||||
- [x] **AuthService 전환 (5개)** - 🔐 최우선 ✅ **완료**
|
||||
- [x] 로그인 로직 (JWT 생성) - `loginPwdCheck()` Raw Query 전환
|
||||
- [x] 사용자 인증 및 검증 - `getUserInfo()` Raw Query 전환
|
||||
- [x] 비밀번호 암호화 처리 - EncryptUtil 유지
|
||||
- [x] 토큰 관리 - `getUserInfoFromToken()` 정상 동작
|
||||
- [x] 로그인 로그 기록 - `insertLoginAccessLog()` Raw Query 전환
|
||||
- [ ] **AdminService 확인 (3개)** - 👤 사용자 관리 (이미 Raw Query 사용)
|
||||
- [x] 사용자 CRUD - Raw Query 사용 확인
|
||||
- [x] 메뉴 관리 (재귀 쿼리) - WITH RECURSIVE 사용 확인
|
||||
- [x] 권한 관리 - Raw Query 사용 확인
|
||||
- [ ] **AdminController 전환 (28개)** - 📡 관리자 API (Phase 2에서 처리)
|
||||
- [ ] 사용자 관리 API
|
||||
- [ ] 메뉴 관리 API
|
||||
- [ ] 권한 관리 API
|
||||
- [ ] 회사 관리 API
|
||||
- [ ] 통합 테스트 (로그인 → 인증 → API 호출 플로우)
|
||||
- [x] **테스트** ✅ **완료**
|
||||
- [x] 단위 테스트 (30개 테스트 모두 통과)
|
||||
- [x] 통합 테스트 작성 완료
|
||||
|
||||
### **Phase 2: 핵심 서비스 (3주) - 107개 호출**
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
"@types/oracledb": "^6.9.1",
|
||||
"@types/pg": "^8.15.5",
|
||||
"@types/sanitize-html": "^2.9.5",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"eslint": "^8.55.0",
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
"nodemon": "^3.1.10",
|
||||
"prettier": "^3.1.0",
|
||||
"prisma": "^6.16.2",
|
||||
"supertest": "^6.3.3",
|
||||
"supertest": "^6.3.4",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
"@types/oracledb": "^6.9.1",
|
||||
"@types/pg": "^8.15.5",
|
||||
"@types/sanitize-html": "^2.9.5",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"eslint": "^8.55.0",
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
"nodemon": "^3.1.10",
|
||||
"prettier": "^3.1.0",
|
||||
"prisma": "^6.16.2",
|
||||
"supertest": "^6.3.3",
|
||||
"supertest": "^6.3.4",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// 인증 서비스
|
||||
// 기존 Java LoginService를 Node.js로 포팅
|
||||
// ✅ Prisma → Raw Query 전환 완료 (Phase 1.5)
|
||||
|
||||
import prisma from "../config/database";
|
||||
import { query } from "../database/db";
|
||||
import { JwtUtils } from "../utils/jwtUtils";
|
||||
import { EncryptUtil } from "../utils/encryptUtil";
|
||||
import { PersonBean, LoginResult, LoginLogData } from "../types/auth";
|
||||
|
|
@ -17,15 +18,13 @@ export class AuthService {
|
|||
password: string
|
||||
): Promise<LoginResult> {
|
||||
try {
|
||||
// 사용자 비밀번호 조회 (기존 login.getUserPassword 쿼리 포팅)
|
||||
const userInfo = await prisma.user_info.findUnique({
|
||||
where: {
|
||||
user_id: userId,
|
||||
},
|
||||
select: {
|
||||
user_password: true,
|
||||
},
|
||||
});
|
||||
// 사용자 비밀번호 조회 (Raw Query 전환)
|
||||
const result = await query<{ user_password: string }>(
|
||||
"SELECT user_password FROM user_info WHERE user_id = $1",
|
||||
[userId]
|
||||
);
|
||||
|
||||
const userInfo = result.length > 0 ? result[0] : null;
|
||||
|
||||
if (userInfo && userInfo.user_password) {
|
||||
const dbPassword = userInfo.user_password;
|
||||
|
|
@ -78,32 +77,26 @@ export class AuthService {
|
|||
*/
|
||||
static async insertLoginAccessLog(logData: LoginLogData): Promise<void> {
|
||||
try {
|
||||
// 기존 login.insertLoginAccessLog 쿼리 포팅
|
||||
await prisma.$executeRaw`
|
||||
INSERT INTO LOGIN_ACCESS_LOG(
|
||||
LOG_TIME,
|
||||
SYSTEM_NAME,
|
||||
USER_ID,
|
||||
LOGIN_RESULT,
|
||||
ERROR_MESSAGE,
|
||||
REMOTE_ADDR,
|
||||
RECPTN_DT,
|
||||
RECPTN_RSLT_DTL,
|
||||
RECPTN_RSLT,
|
||||
RECPTN_RSLT_CD
|
||||
// 로그인 로그 기록 (Raw Query 전환)
|
||||
await query(
|
||||
`INSERT INTO LOGIN_ACCESS_LOG(
|
||||
LOG_TIME, SYSTEM_NAME, USER_ID, LOGIN_RESULT, ERROR_MESSAGE,
|
||||
REMOTE_ADDR, RECPTN_DT, RECPTN_RSLT_DTL, RECPTN_RSLT, RECPTN_RSLT_CD
|
||||
) VALUES (
|
||||
now(),
|
||||
${logData.systemName},
|
||||
UPPER(${logData.userId}),
|
||||
${logData.loginResult},
|
||||
${logData.errorMessage || null},
|
||||
${logData.remoteAddr},
|
||||
${logData.recptnDt || null},
|
||||
${logData.recptnRsltDtl || null},
|
||||
${logData.recptnRslt || null},
|
||||
${logData.recptnRsltCd || null}
|
||||
)
|
||||
`;
|
||||
now(), $1, UPPER($2), $3, $4, $5, $6, $7, $8, $9
|
||||
)`,
|
||||
[
|
||||
logData.systemName,
|
||||
logData.userId,
|
||||
logData.loginResult,
|
||||
logData.errorMessage || null,
|
||||
logData.remoteAddr,
|
||||
logData.recptnDt || null,
|
||||
logData.recptnRsltDtl || null,
|
||||
logData.recptnRslt || null,
|
||||
logData.recptnRsltCd || null,
|
||||
]
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`로그인 로그 기록 완료: ${logData.userId} (${logData.loginResult ? "성공" : "실패"})`
|
||||
|
|
@ -122,66 +115,61 @@ export class AuthService {
|
|||
*/
|
||||
static async getUserInfo(userId: string): Promise<PersonBean | null> {
|
||||
try {
|
||||
// 기존 login.getUserInfo 쿼리 포팅
|
||||
const userInfo = await prisma.user_info.findUnique({
|
||||
where: {
|
||||
user_id: userId,
|
||||
},
|
||||
select: {
|
||||
sabun: true,
|
||||
user_id: true,
|
||||
user_name: true,
|
||||
user_name_eng: true,
|
||||
user_name_cn: true,
|
||||
dept_code: true,
|
||||
dept_name: true,
|
||||
position_code: true,
|
||||
position_name: true,
|
||||
email: true,
|
||||
tel: true,
|
||||
cell_phone: true,
|
||||
user_type: true,
|
||||
user_type_name: true,
|
||||
partner_objid: true,
|
||||
company_code: true,
|
||||
locale: true,
|
||||
photo: true,
|
||||
},
|
||||
});
|
||||
// 1. 사용자 기본 정보 조회 (Raw Query 전환)
|
||||
const userResult = await query<{
|
||||
sabun: string | null;
|
||||
user_id: string;
|
||||
user_name: string;
|
||||
user_name_eng: string | null;
|
||||
user_name_cn: string | null;
|
||||
dept_code: string | null;
|
||||
dept_name: string | null;
|
||||
position_code: string | null;
|
||||
position_name: string | null;
|
||||
email: string | null;
|
||||
tel: string | null;
|
||||
cell_phone: string | null;
|
||||
user_type: string | null;
|
||||
user_type_name: string | null;
|
||||
partner_objid: string | null;
|
||||
company_code: string | null;
|
||||
locale: string | null;
|
||||
photo: Buffer | null;
|
||||
}>(
|
||||
`SELECT
|
||||
sabun, user_id, user_name, user_name_eng, user_name_cn,
|
||||
dept_code, dept_name, position_code, position_name,
|
||||
email, tel, cell_phone, user_type, user_type_name,
|
||||
partner_objid, company_code, locale, photo
|
||||
FROM user_info
|
||||
WHERE user_id = $1`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
const userInfo = userResult.length > 0 ? userResult[0] : null;
|
||||
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 권한 정보 조회 (Prisma ORM 사용)
|
||||
const authInfo = await prisma.authority_sub_user.findMany({
|
||||
where: {
|
||||
user_id: userId,
|
||||
},
|
||||
include: {
|
||||
authority_master: {
|
||||
select: {
|
||||
auth_name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// 2. 권한 정보 조회 (Raw Query 전환 - JOIN으로 최적화)
|
||||
const authResult = await query<{ auth_name: string }>(
|
||||
`SELECT am.auth_name
|
||||
FROM authority_sub_user asu
|
||||
INNER JOIN authority_master am ON asu.master_objid = am.objid
|
||||
WHERE asu.user_id = $1`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
// 권한명들을 쉼표로 연결
|
||||
const authNames = authInfo
|
||||
.filter((auth: any) => auth.authority_master?.auth_name)
|
||||
.map((auth: any) => auth.authority_master!.auth_name!)
|
||||
.join(",");
|
||||
const authNames = authResult.map((row) => row.auth_name).join(",");
|
||||
|
||||
// 회사 정보 조회 (Prisma ORM 사용으로 변경)
|
||||
const companyInfo = await prisma.company_mng.findFirst({
|
||||
where: {
|
||||
company_code: userInfo.company_code || "ILSHIN",
|
||||
},
|
||||
select: {
|
||||
company_name: true,
|
||||
},
|
||||
});
|
||||
// 3. 회사 정보 조회 (Raw Query 전환)
|
||||
// Note: 현재 회사 정보는 PersonBean에 직접 사용되지 않지만 향후 확장을 위해 유지
|
||||
const companyResult = await query<{ company_name: string }>(
|
||||
"SELECT company_name FROM company_mng WHERE company_code = $1",
|
||||
[userInfo.company_code || "ILSHIN"]
|
||||
);
|
||||
|
||||
// DB에서 조회한 원본 사용자 정보 상세 로그
|
||||
//console.log("🔍 AuthService - DB 원본 사용자 정보:", {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,426 @@
|
|||
/**
|
||||
* AuthService Raw Query 전환 단위 테스트
|
||||
* Phase 1.5: 인증 서비스 테스트
|
||||
*/
|
||||
|
||||
import { AuthService } from "../services/authService";
|
||||
import { query } from "../database/db";
|
||||
import { EncryptUtil } from "../utils/encryptUtil";
|
||||
|
||||
// 테스트 데이터
|
||||
const TEST_USER = {
|
||||
userId: "testuser",
|
||||
password: "testpass123",
|
||||
hashedPassword: "", // 테스트 전에 생성
|
||||
};
|
||||
|
||||
describe("AuthService Raw Query 전환 테스트", () => {
|
||||
// 테스트 전 준비
|
||||
beforeAll(async () => {
|
||||
// 테스트용 비밀번호 해시 생성
|
||||
TEST_USER.hashedPassword = EncryptUtil.encrypt(TEST_USER.password);
|
||||
|
||||
// 테스트 사용자 생성 (이미 있으면 스킵)
|
||||
try {
|
||||
const existing = await query(
|
||||
"SELECT user_id FROM user_info WHERE user_id = $1",
|
||||
[TEST_USER.userId]
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
await query(
|
||||
`INSERT INTO user_info (
|
||||
user_id, user_name, user_password, company_code, locale
|
||||
) VALUES ($1, $2, $3, $4, $5)`,
|
||||
[
|
||||
TEST_USER.userId,
|
||||
"테스트 사용자",
|
||||
TEST_USER.hashedPassword,
|
||||
"ILSHIN",
|
||||
"KR",
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// 비밀번호 업데이트
|
||||
await query(
|
||||
"UPDATE user_info SET user_password = $1 WHERE user_id = $2",
|
||||
[TEST_USER.hashedPassword, TEST_USER.userId]
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("테스트 사용자 생성 실패:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// 테스트 후 정리
|
||||
afterAll(async () => {
|
||||
// 테스트 사용자 삭제 (선택적)
|
||||
// await query("DELETE FROM user_info WHERE user_id = $1", [TEST_USER.userId]);
|
||||
});
|
||||
|
||||
describe("loginPwdCheck - 로그인 비밀번호 검증", () => {
|
||||
test("존재하는 사용자 로그인 성공", async () => {
|
||||
const result = await AuthService.loginPwdCheck(
|
||||
TEST_USER.userId,
|
||||
TEST_USER.password
|
||||
);
|
||||
|
||||
expect(result.loginResult).toBe(true);
|
||||
expect(result.errorReason).toBeUndefined();
|
||||
});
|
||||
|
||||
test("존재하지 않는 사용자 로그인 실패", async () => {
|
||||
const result = await AuthService.loginPwdCheck(
|
||||
"nonexistent_user_12345",
|
||||
"anypassword"
|
||||
);
|
||||
|
||||
expect(result.loginResult).toBe(false);
|
||||
expect(result.errorReason).toContain("존재하지 않습니다");
|
||||
});
|
||||
|
||||
test("잘못된 비밀번호 로그인 실패", async () => {
|
||||
const result = await AuthService.loginPwdCheck(
|
||||
TEST_USER.userId,
|
||||
"wrongpassword123"
|
||||
);
|
||||
|
||||
expect(result.loginResult).toBe(false);
|
||||
expect(result.errorReason).toContain("일치하지 않습니다");
|
||||
});
|
||||
|
||||
test("마스터 패스워드 로그인 성공", async () => {
|
||||
const result = await AuthService.loginPwdCheck(
|
||||
TEST_USER.userId,
|
||||
"qlalfqjsgh11"
|
||||
);
|
||||
|
||||
expect(result.loginResult).toBe(true);
|
||||
});
|
||||
|
||||
test("빈 사용자 ID 처리", async () => {
|
||||
const result = await AuthService.loginPwdCheck("", TEST_USER.password);
|
||||
|
||||
expect(result.loginResult).toBe(false);
|
||||
});
|
||||
|
||||
test("빈 비밀번호 처리", async () => {
|
||||
const result = await AuthService.loginPwdCheck(TEST_USER.userId, "");
|
||||
|
||||
expect(result.loginResult).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserInfo - 사용자 정보 조회", () => {
|
||||
test("사용자 정보 조회 성공", async () => {
|
||||
const userInfo = await AuthService.getUserInfo(TEST_USER.userId);
|
||||
|
||||
expect(userInfo).not.toBeNull();
|
||||
expect(userInfo?.userId).toBe(TEST_USER.userId);
|
||||
expect(userInfo?.userName).toBeDefined();
|
||||
expect(userInfo?.companyCode).toBeDefined();
|
||||
expect(userInfo?.locale).toBeDefined();
|
||||
});
|
||||
|
||||
test("사용자 정보 필드 타입 확인", async () => {
|
||||
const userInfo = await AuthService.getUserInfo(TEST_USER.userId);
|
||||
|
||||
expect(userInfo).not.toBeNull();
|
||||
expect(typeof userInfo?.userId).toBe("string");
|
||||
expect(typeof userInfo?.userName).toBe("string");
|
||||
expect(typeof userInfo?.companyCode).toBe("string");
|
||||
expect(typeof userInfo?.locale).toBe("string");
|
||||
});
|
||||
|
||||
test("권한 정보 조회 (있는 경우)", async () => {
|
||||
const userInfo = await AuthService.getUserInfo(TEST_USER.userId);
|
||||
|
||||
// 권한이 없으면 authName은 빈 문자열
|
||||
expect(userInfo).not.toBeNull();
|
||||
if (userInfo) {
|
||||
expect(typeof userInfo.authName === 'string' || userInfo.authName === undefined).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("존재하지 않는 사용자 조회 실패", async () => {
|
||||
const userInfo = await AuthService.getUserInfo("nonexistent_user_12345");
|
||||
|
||||
expect(userInfo).toBeNull();
|
||||
});
|
||||
|
||||
test("회사 정보 기본값 확인", async () => {
|
||||
const userInfo = await AuthService.getUserInfo(TEST_USER.userId);
|
||||
|
||||
// company_code가 없으면 기본값 "ILSHIN"
|
||||
expect(userInfo?.companyCode).toBeDefined();
|
||||
expect(typeof userInfo?.companyCode).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("insertLoginAccessLog - 로그인 로그 기록", () => {
|
||||
test("로그인 성공 로그 기록", async () => {
|
||||
await expect(
|
||||
AuthService.insertLoginAccessLog({
|
||||
systemName: "PMS",
|
||||
userId: TEST_USER.userId,
|
||||
loginResult: true,
|
||||
remoteAddr: "127.0.0.1",
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
test("로그인 실패 로그 기록", async () => {
|
||||
await expect(
|
||||
AuthService.insertLoginAccessLog({
|
||||
systemName: "PMS",
|
||||
userId: TEST_USER.userId,
|
||||
loginResult: false,
|
||||
errorMessage: "비밀번호 불일치",
|
||||
remoteAddr: "127.0.0.1",
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
test("로그인 로그 기록 후 DB 확인", async () => {
|
||||
await AuthService.insertLoginAccessLog({
|
||||
systemName: "PMS",
|
||||
userId: TEST_USER.userId,
|
||||
loginResult: true,
|
||||
remoteAddr: "127.0.0.1",
|
||||
});
|
||||
|
||||
// 로그가 기록되었는지 확인
|
||||
const logs = await query(
|
||||
`SELECT * FROM LOGIN_ACCESS_LOG
|
||||
WHERE USER_ID = UPPER($1)
|
||||
ORDER BY LOG_TIME DESC
|
||||
LIMIT 1`,
|
||||
[TEST_USER.userId]
|
||||
);
|
||||
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
expect(logs[0].user_id).toBe(TEST_USER.userId.toUpperCase());
|
||||
// login_result는 문자열 또는 불리언일 수 있음
|
||||
expect(logs[0].login_result).toBeTruthy();
|
||||
});
|
||||
|
||||
test("로그 기록 실패해도 예외 던지지 않음", async () => {
|
||||
// 잘못된 데이터로 로그 기록 시도 (에러 발생하지만 프로세스 중단 안됨)
|
||||
await expect(
|
||||
AuthService.insertLoginAccessLog({
|
||||
systemName: "PMS",
|
||||
userId: TEST_USER.userId,
|
||||
loginResult: true,
|
||||
remoteAddr: "127.0.0.1",
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("processLogin - 전체 로그인 프로세스", () => {
|
||||
test("전체 로그인 프로세스 성공", async () => {
|
||||
const result = await AuthService.processLogin(
|
||||
TEST_USER.userId,
|
||||
TEST_USER.password,
|
||||
"127.0.0.1"
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.token).toBeDefined();
|
||||
expect(result.userInfo).toBeDefined();
|
||||
expect(result.userInfo?.userId).toBe(TEST_USER.userId);
|
||||
expect(result.errorReason).toBeUndefined();
|
||||
});
|
||||
|
||||
test("로그인 실패 시 토큰 없음", async () => {
|
||||
const result = await AuthService.processLogin(
|
||||
TEST_USER.userId,
|
||||
"wrongpassword",
|
||||
"127.0.0.1"
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.token).toBeUndefined();
|
||||
expect(result.userInfo).toBeUndefined();
|
||||
expect(result.errorReason).toBeDefined();
|
||||
});
|
||||
|
||||
test("존재하지 않는 사용자 로그인 실패", async () => {
|
||||
const result = await AuthService.processLogin(
|
||||
"nonexistent_user",
|
||||
"anypassword",
|
||||
"127.0.0.1"
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorReason).toContain("존재하지 않습니다");
|
||||
});
|
||||
|
||||
test("JWT 토큰 형식 확인", async () => {
|
||||
const result = await AuthService.processLogin(
|
||||
TEST_USER.userId,
|
||||
TEST_USER.password,
|
||||
"127.0.0.1"
|
||||
);
|
||||
|
||||
if (result.success && result.token) {
|
||||
// JWT 토큰은 3개 파트로 구성 (header.payload.signature)
|
||||
const parts = result.token.split(".");
|
||||
expect(parts.length).toBe(3);
|
||||
}
|
||||
});
|
||||
|
||||
test("로그인 프로세스 로그 기록 확인", async () => {
|
||||
await AuthService.processLogin(
|
||||
TEST_USER.userId,
|
||||
TEST_USER.password,
|
||||
"127.0.0.1"
|
||||
);
|
||||
|
||||
// 로그인 로그가 기록되었는지 확인
|
||||
const logs = await query(
|
||||
`SELECT * FROM LOGIN_ACCESS_LOG
|
||||
WHERE USER_ID = UPPER($1)
|
||||
ORDER BY LOG_TIME DESC
|
||||
LIMIT 1`,
|
||||
[TEST_USER.userId]
|
||||
);
|
||||
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("processLogout - 로그아웃 프로세스", () => {
|
||||
test("로그아웃 프로세스 성공", async () => {
|
||||
await expect(
|
||||
AuthService.processLogout(TEST_USER.userId, "127.0.0.1")
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
test("로그아웃 로그 기록 확인", async () => {
|
||||
await AuthService.processLogout(TEST_USER.userId, "127.0.0.1");
|
||||
|
||||
// 로그아웃 로그가 기록되었는지 확인
|
||||
const logs = await query(
|
||||
`SELECT * FROM LOGIN_ACCESS_LOG
|
||||
WHERE USER_ID = UPPER($1)
|
||||
AND ERROR_MESSAGE = '로그아웃'
|
||||
ORDER BY LOG_TIME DESC
|
||||
LIMIT 1`,
|
||||
[TEST_USER.userId]
|
||||
);
|
||||
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
expect(logs[0].error_message).toBe("로그아웃");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserInfoFromToken - 토큰으로 사용자 정보 조회", () => {
|
||||
test("유효한 토큰으로 사용자 정보 조회", async () => {
|
||||
// 먼저 로그인해서 토큰 획득
|
||||
const loginResult = await AuthService.processLogin(
|
||||
TEST_USER.userId,
|
||||
TEST_USER.password,
|
||||
"127.0.0.1"
|
||||
);
|
||||
|
||||
expect(loginResult.success).toBe(true);
|
||||
expect(loginResult.token).toBeDefined();
|
||||
|
||||
// 토큰으로 사용자 정보 조회
|
||||
const userInfo = await AuthService.getUserInfoFromToken(
|
||||
loginResult.token!
|
||||
);
|
||||
|
||||
expect(userInfo).not.toBeNull();
|
||||
expect(userInfo?.userId).toBe(TEST_USER.userId);
|
||||
});
|
||||
|
||||
test("잘못된 토큰으로 조회 실패", async () => {
|
||||
const userInfo = await AuthService.getUserInfoFromToken("invalid_token");
|
||||
|
||||
expect(userInfo).toBeNull();
|
||||
});
|
||||
|
||||
test("만료된 토큰으로 조회 실패", async () => {
|
||||
// 만료된 토큰 시뮬레이션 (실제로는 만료 시간이 필요하므로 단순히 잘못된 토큰 사용)
|
||||
const expiredToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.expired.token";
|
||||
const userInfo = await AuthService.getUserInfoFromToken(expiredToken);
|
||||
|
||||
expect(userInfo).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Raw Query 전환 검증", () => {
|
||||
test("Prisma import가 없는지 확인", async () => {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const authServicePath = path.join(
|
||||
__dirname,
|
||||
"../services/authService.ts"
|
||||
);
|
||||
const content = fs.readFileSync(authServicePath, "utf8");
|
||||
|
||||
// Prisma import가 없어야 함
|
||||
expect(content).not.toContain('import prisma from "../config/database"');
|
||||
expect(content).not.toContain("import { PrismaClient }");
|
||||
expect(content).not.toContain("prisma.user_info");
|
||||
expect(content).not.toContain("prisma.$executeRaw");
|
||||
});
|
||||
|
||||
test("Raw Query import 확인", async () => {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const authServicePath = path.join(
|
||||
__dirname,
|
||||
"../services/authService.ts"
|
||||
);
|
||||
const content = fs.readFileSync(authServicePath, "utf8");
|
||||
|
||||
// Raw Query import가 있어야 함
|
||||
expect(content).toContain('import { query } from "../database/db"');
|
||||
});
|
||||
|
||||
test("모든 메서드가 Raw Query 사용 확인", async () => {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const authServicePath = path.join(
|
||||
__dirname,
|
||||
"../services/authService.ts"
|
||||
);
|
||||
const content = fs.readFileSync(authServicePath, "utf8");
|
||||
|
||||
// query() 함수 호출이 있어야 함
|
||||
expect(content).toContain("await query<");
|
||||
expect(content).toContain("await query(");
|
||||
});
|
||||
});
|
||||
|
||||
describe("성능 테스트", () => {
|
||||
test("로그인 프로세스 성능 (응답 시간 < 1초)", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await AuthService.processLogin(
|
||||
TEST_USER.userId,
|
||||
TEST_USER.password,
|
||||
"127.0.0.1"
|
||||
);
|
||||
|
||||
const endTime = Date.now();
|
||||
const elapsedTime = endTime - startTime;
|
||||
|
||||
expect(elapsedTime).toBeLessThan(1000); // 1초 이내
|
||||
}, 2000); // 테스트 타임아웃 2초
|
||||
|
||||
test("사용자 정보 조회 성능 (응답 시간 < 500ms)", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await AuthService.getUserInfo(TEST_USER.userId);
|
||||
|
||||
const endTime = Date.now();
|
||||
const elapsedTime = endTime - startTime;
|
||||
|
||||
expect(elapsedTime).toBeLessThan(500); // 500ms 이내
|
||||
}, 1000); // 테스트 타임아웃 1초
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,382 @@
|
|||
/**
|
||||
* AuthService 통합 테스트
|
||||
* Phase 1.5: 인증 시스템 전체 플로우 테스트
|
||||
*
|
||||
* 테스트 시나리오:
|
||||
* 1. 로그인 → 토큰 발급
|
||||
* 2. 토큰으로 API 인증
|
||||
* 3. 로그아웃
|
||||
*/
|
||||
|
||||
import request from "supertest";
|
||||
import app from "../../app";
|
||||
import { query } from "../../database/db";
|
||||
import { EncryptUtil } from "../../utils/encryptUtil";
|
||||
|
||||
// 테스트 데이터
|
||||
const TEST_USER = {
|
||||
userId: "integration_test_user",
|
||||
password: "integration_test_pass_123",
|
||||
userName: "통합테스트 사용자",
|
||||
};
|
||||
|
||||
describe("인증 시스템 통합 테스트 (Auth Integration Tests)", () => {
|
||||
let authToken: string;
|
||||
|
||||
// 테스트 전 준비: 테스트 사용자 생성
|
||||
beforeAll(async () => {
|
||||
const hashedPassword = EncryptUtil.encrypt(TEST_USER.password);
|
||||
|
||||
try {
|
||||
// 기존 사용자 확인
|
||||
const existing = await query(
|
||||
"SELECT user_id FROM user_info WHERE user_id = $1",
|
||||
[TEST_USER.userId]
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
// 새 사용자 생성
|
||||
await query(
|
||||
`INSERT INTO user_info (
|
||||
user_id, user_name, user_password, company_code, locale
|
||||
) VALUES ($1, $2, $3, $4, $5)`,
|
||||
[
|
||||
TEST_USER.userId,
|
||||
TEST_USER.userName,
|
||||
hashedPassword,
|
||||
"ILSHIN",
|
||||
"KR",
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// 기존 사용자 비밀번호 업데이트
|
||||
await query(
|
||||
"UPDATE user_info SET user_password = $1, user_name = $2 WHERE user_id = $3",
|
||||
[hashedPassword, TEST_USER.userName, TEST_USER.userId]
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`✅ 통합 테스트 사용자 준비 완료: ${TEST_USER.userId}`);
|
||||
} catch (error) {
|
||||
console.error("❌ 테스트 사용자 생성 실패:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 테스트 후 정리 (선택적)
|
||||
afterAll(async () => {
|
||||
// 테스트 사용자 삭제 (필요시)
|
||||
// await query("DELETE FROM user_info WHERE user_id = $1", [TEST_USER.userId]);
|
||||
console.log("✅ 통합 테스트 완료");
|
||||
});
|
||||
|
||||
describe("1. 로그인 플로우 (POST /api/auth/login)", () => {
|
||||
test("✅ 올바른 자격증명으로 로그인 성공", async () => {
|
||||
const response = await request(app)
|
||||
.post("/api/auth/login")
|
||||
.send({
|
||||
userId: TEST_USER.userId,
|
||||
password: TEST_USER.password,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.token).toBeDefined();
|
||||
expect(response.body.userInfo).toBeDefined();
|
||||
expect(response.body.userInfo.userId).toBe(TEST_USER.userId);
|
||||
expect(response.body.userInfo.userName).toBe(TEST_USER.userName);
|
||||
|
||||
// 토큰 저장 (다음 테스트에서 사용)
|
||||
authToken = response.body.token;
|
||||
});
|
||||
|
||||
test("❌ 잘못된 비밀번호로 로그인 실패", async () => {
|
||||
const response = await request(app)
|
||||
.post("/api/auth/login")
|
||||
.send({
|
||||
userId: TEST_USER.userId,
|
||||
password: "wrong_password_123",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.token).toBeUndefined();
|
||||
expect(response.body.errorReason).toBeDefined();
|
||||
expect(response.body.errorReason).toContain("일치하지 않습니다");
|
||||
});
|
||||
|
||||
test("❌ 존재하지 않는 사용자 로그인 실패", async () => {
|
||||
const response = await request(app)
|
||||
.post("/api/auth/login")
|
||||
.send({
|
||||
userId: "nonexistent_user_999",
|
||||
password: "anypassword",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.token).toBeUndefined();
|
||||
expect(response.body.errorReason).toContain("존재하지 않습니다");
|
||||
});
|
||||
|
||||
test("❌ 필수 필드 누락 시 로그인 실패", async () => {
|
||||
const response = await request(app)
|
||||
.post("/api/auth/login")
|
||||
.send({
|
||||
userId: TEST_USER.userId,
|
||||
// password 누락
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
|
||||
test("✅ JWT 토큰 형식 검증", () => {
|
||||
expect(authToken).toBeDefined();
|
||||
expect(typeof authToken).toBe("string");
|
||||
|
||||
// JWT는 3개 파트로 구성 (header.payload.signature)
|
||||
const parts = authToken.split(".");
|
||||
expect(parts.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("2. 토큰 검증 플로우 (GET /api/auth/verify)", () => {
|
||||
test("✅ 유효한 토큰으로 검증 성공", async () => {
|
||||
const response = await request(app)
|
||||
.get("/api/auth/verify")
|
||||
.set("Authorization", `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.valid).toBe(true);
|
||||
expect(response.body.userInfo).toBeDefined();
|
||||
expect(response.body.userInfo.userId).toBe(TEST_USER.userId);
|
||||
});
|
||||
|
||||
test("❌ 토큰 없이 요청 시 실패", async () => {
|
||||
const response = await request(app).get("/api/auth/verify").expect(401);
|
||||
|
||||
expect(response.body.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("❌ 잘못된 토큰으로 요청 시 실패", async () => {
|
||||
const response = await request(app)
|
||||
.get("/api/auth/verify")
|
||||
.set("Authorization", "Bearer invalid_token_12345")
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("❌ Bearer 없는 토큰으로 요청 시 실패", async () => {
|
||||
const response = await request(app)
|
||||
.get("/api/auth/verify")
|
||||
.set("Authorization", authToken) // Bearer 키워드 없음
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("3. 인증된 API 요청 플로우", () => {
|
||||
test("✅ 인증된 사용자로 메뉴 조회", async () => {
|
||||
const response = await request(app)
|
||||
.get("/api/admin/menu")
|
||||
.set("Authorization", `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
});
|
||||
|
||||
test("❌ 인증 없이 보호된 API 요청 실패", async () => {
|
||||
const response = await request(app).get("/api/admin/menu").expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("4. 로그아웃 플로우 (POST /api/auth/logout)", () => {
|
||||
test("✅ 로그아웃 성공", async () => {
|
||||
const response = await request(app)
|
||||
.post("/api/auth/logout")
|
||||
.set("Authorization", `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
|
||||
test("✅ 로그아웃 로그 기록 확인", async () => {
|
||||
// 로그아웃 로그가 기록되었는지 확인
|
||||
const logs = await query(
|
||||
`SELECT * FROM LOGIN_ACCESS_LOG
|
||||
WHERE USER_ID = UPPER($1)
|
||||
AND ERROR_MESSAGE = '로그아웃'
|
||||
ORDER BY LOG_TIME DESC
|
||||
LIMIT 1`,
|
||||
[TEST_USER.userId]
|
||||
);
|
||||
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
expect(logs[0].error_message).toBe("로그아웃");
|
||||
});
|
||||
});
|
||||
|
||||
describe("5. 전체 시나리오 테스트", () => {
|
||||
test("✅ 로그인 → 인증 → API 호출 → 로그아웃 전체 플로우", async () => {
|
||||
// 1. 로그인
|
||||
const loginResponse = await request(app)
|
||||
.post("/api/auth/login")
|
||||
.send({
|
||||
userId: TEST_USER.userId,
|
||||
password: TEST_USER.password,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(loginResponse.body.success).toBe(true);
|
||||
const token = loginResponse.body.token;
|
||||
|
||||
// 2. 토큰 검증
|
||||
const verifyResponse = await request(app)
|
||||
.get("/api/auth/verify")
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(verifyResponse.body.valid).toBe(true);
|
||||
|
||||
// 3. 보호된 API 호출
|
||||
const menuResponse = await request(app)
|
||||
.get("/api/admin/menu")
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(menuResponse.body)).toBe(true);
|
||||
|
||||
// 4. 로그아웃
|
||||
const logoutResponse = await request(app)
|
||||
.post("/api/auth/logout")
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(logoutResponse.body.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("6. 에러 처리 및 예외 상황", () => {
|
||||
test("❌ SQL Injection 시도 차단", async () => {
|
||||
const response = await request(app)
|
||||
.post("/api/auth/login")
|
||||
.send({
|
||||
userId: "admin' OR '1'='1",
|
||||
password: "password",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// SQL Injection이 차단되어 로그인 실패해야 함
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
|
||||
test("❌ 빈 문자열로 로그인 시도", async () => {
|
||||
const response = await request(app)
|
||||
.post("/api/auth/login")
|
||||
.send({
|
||||
userId: "",
|
||||
password: "",
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
|
||||
test("❌ 매우 긴 사용자 ID로 로그인 시도", async () => {
|
||||
const longUserId = "a".repeat(1000);
|
||||
const response = await request(app)
|
||||
.post("/api/auth/login")
|
||||
.send({
|
||||
userId: longUserId,
|
||||
password: "password",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("7. 로그인 이력 확인", () => {
|
||||
test("✅ 로그인 성공 이력 조회", async () => {
|
||||
// 로그인 실행
|
||||
await request(app).post("/api/auth/login").send({
|
||||
userId: TEST_USER.userId,
|
||||
password: TEST_USER.password,
|
||||
});
|
||||
|
||||
// 로그인 이력 확인
|
||||
const logs = await query(
|
||||
`SELECT * FROM LOGIN_ACCESS_LOG
|
||||
WHERE USER_ID = UPPER($1)
|
||||
AND LOGIN_RESULT = true
|
||||
ORDER BY LOG_TIME DESC
|
||||
LIMIT 1`,
|
||||
[TEST_USER.userId]
|
||||
);
|
||||
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
expect(logs[0].login_result).toBeTruthy();
|
||||
expect(logs[0].system_name).toBe("PMS");
|
||||
});
|
||||
|
||||
test("✅ 로그인 실패 이력 조회", async () => {
|
||||
// 로그인 실패 실행
|
||||
await request(app).post("/api/auth/login").send({
|
||||
userId: TEST_USER.userId,
|
||||
password: "wrong_password",
|
||||
});
|
||||
|
||||
// 로그인 실패 이력 확인
|
||||
const logs = await query(
|
||||
`SELECT * FROM LOGIN_ACCESS_LOG
|
||||
WHERE USER_ID = UPPER($1)
|
||||
AND LOGIN_RESULT = false
|
||||
AND ERROR_MESSAGE IS NOT NULL
|
||||
ORDER BY LOG_TIME DESC
|
||||
LIMIT 1`,
|
||||
[TEST_USER.userId]
|
||||
);
|
||||
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
expect(logs[0].login_result).toBeFalsy();
|
||||
expect(logs[0].error_message).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("8. 성능 테스트", () => {
|
||||
test("✅ 동시 로그인 요청 처리 (10개)", async () => {
|
||||
const promises = Array.from({ length: 10 }, () =>
|
||||
request(app).post("/api/auth/login").send({
|
||||
userId: TEST_USER.userId,
|
||||
password: TEST_USER.password,
|
||||
})
|
||||
);
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
responses.forEach((response) => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
}, 10000); // 10초 타임아웃
|
||||
|
||||
test("✅ 로그인 응답 시간 (< 1초)", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await request(app).post("/api/auth/login").send({
|
||||
userId: TEST_USER.userId,
|
||||
password: TEST_USER.password,
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const elapsedTime = endTime - startTime;
|
||||
|
||||
expect(elapsedTime).toBeLessThan(1000); // 1초 이내
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue