382 lines
11 KiB
TypeScript
382 lines
11 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
}); |