456 lines
16 KiB
TypeScript
456 lines
16 KiB
TypeScript
/**
|
|
* Database Manager 테스트
|
|
*
|
|
* Phase 1 기반 구조 검증
|
|
*/
|
|
|
|
import { query, queryOne, transaction, getPoolStatus } from "../database/db";
|
|
import { QueryBuilder } from "../utils/queryBuilder";
|
|
import { DatabaseValidator } from "../utils/databaseValidator";
|
|
|
|
describe("Database Manager Tests", () => {
|
|
describe("QueryBuilder", () => {
|
|
test("SELECT 쿼리 생성 - 기본", () => {
|
|
const { query: sql, params } = QueryBuilder.select("users", {
|
|
where: { user_id: "test_user" },
|
|
});
|
|
|
|
expect(sql).toContain("SELECT * FROM users");
|
|
expect(sql).toContain("WHERE user_id = $1");
|
|
expect(params).toEqual(["test_user"]);
|
|
});
|
|
|
|
test("SELECT 쿼리 생성 - 복잡한 조건", () => {
|
|
const { query: sql, params } = QueryBuilder.select("users", {
|
|
columns: ["user_id", "username", "email"],
|
|
where: { status: "active", role: "admin" },
|
|
orderBy: "created_at DESC",
|
|
limit: 10,
|
|
offset: 20,
|
|
});
|
|
|
|
expect(sql).toContain("SELECT user_id, username, email FROM users");
|
|
expect(sql).toContain("WHERE status = $1 AND role = $2");
|
|
expect(sql).toContain("ORDER BY created_at DESC");
|
|
expect(sql).toContain("LIMIT $3");
|
|
expect(sql).toContain("OFFSET $4");
|
|
expect(params).toEqual(["active", "admin", 10, 20]);
|
|
});
|
|
|
|
test("SELECT 쿼리 생성 - JOIN", () => {
|
|
const { query: sql, params } = QueryBuilder.select("users", {
|
|
columns: ["users.user_id", "users.username", "departments.dept_name"],
|
|
joins: [
|
|
{
|
|
type: "LEFT",
|
|
table: "departments",
|
|
on: "users.dept_id = departments.dept_id",
|
|
},
|
|
],
|
|
where: { "users.status": "active" },
|
|
});
|
|
|
|
expect(sql).toContain("LEFT JOIN departments");
|
|
expect(sql).toContain("ON users.dept_id = departments.dept_id");
|
|
expect(sql).toContain("WHERE users.status = $1");
|
|
expect(params).toEqual(["active"]);
|
|
});
|
|
|
|
test("INSERT 쿼리 생성 - RETURNING", () => {
|
|
const { query: sql, params } = QueryBuilder.insert(
|
|
"users",
|
|
{
|
|
user_id: "new_user",
|
|
username: "John Doe",
|
|
email: "john@example.com",
|
|
},
|
|
{
|
|
returning: ["id", "user_id"],
|
|
}
|
|
);
|
|
|
|
expect(sql).toContain("INSERT INTO users");
|
|
expect(sql).toContain("(user_id, username, email)");
|
|
expect(sql).toContain("VALUES ($1, $2, $3)");
|
|
expect(sql).toContain("RETURNING id, user_id");
|
|
expect(params).toEqual(["new_user", "John Doe", "john@example.com"]);
|
|
});
|
|
|
|
test("INSERT 쿼리 생성 - UPSERT", () => {
|
|
const { query: sql, params } = QueryBuilder.insert(
|
|
"users",
|
|
{
|
|
user_id: "user123",
|
|
username: "Jane",
|
|
email: "jane@example.com",
|
|
},
|
|
{
|
|
onConflict: {
|
|
columns: ["user_id"],
|
|
action: "DO UPDATE",
|
|
updateSet: ["username", "email"],
|
|
},
|
|
returning: ["*"],
|
|
}
|
|
);
|
|
|
|
expect(sql).toContain("ON CONFLICT (user_id) DO UPDATE");
|
|
expect(sql).toContain(
|
|
"SET username = EXCLUDED.username, email = EXCLUDED.email"
|
|
);
|
|
expect(sql).toContain("RETURNING *");
|
|
});
|
|
|
|
test("UPDATE 쿼리 생성", () => {
|
|
const { query: sql, params } = QueryBuilder.update(
|
|
"users",
|
|
{ username: "Updated Name", email: "updated@example.com" },
|
|
{ user_id: "user123" },
|
|
{ returning: ["*"] }
|
|
);
|
|
|
|
expect(sql).toContain("UPDATE users");
|
|
expect(sql).toContain("SET username = $1, email = $2");
|
|
expect(sql).toContain("WHERE user_id = $3");
|
|
expect(sql).toContain("RETURNING *");
|
|
expect(params).toEqual([
|
|
"Updated Name",
|
|
"updated@example.com",
|
|
"user123",
|
|
]);
|
|
});
|
|
|
|
test("DELETE 쿼리 생성", () => {
|
|
const { query: sql, params } = QueryBuilder.delete("users", {
|
|
user_id: "user_to_delete",
|
|
});
|
|
|
|
expect(sql).toContain("DELETE FROM users");
|
|
expect(sql).toContain("WHERE user_id = $1");
|
|
expect(params).toEqual(["user_to_delete"]);
|
|
});
|
|
|
|
test("COUNT 쿼리 생성", () => {
|
|
const { query: sql, params } = QueryBuilder.count("users", {
|
|
status: "active",
|
|
});
|
|
|
|
expect(sql).toContain("SELECT COUNT(*) as count FROM users");
|
|
expect(sql).toContain("WHERE status = $1");
|
|
expect(params).toEqual(["active"]);
|
|
});
|
|
});
|
|
|
|
describe("DatabaseValidator", () => {
|
|
test("테이블명 검증 - 유효한 이름", () => {
|
|
expect(DatabaseValidator.validateTableName("users")).toBe(true);
|
|
expect(DatabaseValidator.validateTableName("user_info")).toBe(true);
|
|
expect(DatabaseValidator.validateTableName("_internal_table")).toBe(true);
|
|
expect(DatabaseValidator.validateTableName("table123")).toBe(true);
|
|
});
|
|
|
|
test("테이블명 검증 - 유효하지 않은 이름", () => {
|
|
expect(DatabaseValidator.validateTableName("")).toBe(false);
|
|
expect(DatabaseValidator.validateTableName("123table")).toBe(false);
|
|
expect(DatabaseValidator.validateTableName("user-table")).toBe(false);
|
|
expect(DatabaseValidator.validateTableName("user table")).toBe(false);
|
|
expect(DatabaseValidator.validateTableName("SELECT")).toBe(false); // 예약어
|
|
expect(DatabaseValidator.validateTableName("a".repeat(64))).toBe(false); // 너무 긺
|
|
});
|
|
|
|
test("컬럼명 검증 - 유효한 이름", () => {
|
|
expect(DatabaseValidator.validateColumnName("user_id")).toBe(true);
|
|
expect(DatabaseValidator.validateColumnName("created_at")).toBe(true);
|
|
expect(DatabaseValidator.validateColumnName("is_active")).toBe(true);
|
|
});
|
|
|
|
test("컬럼명 검증 - 유효하지 않은 이름", () => {
|
|
expect(DatabaseValidator.validateColumnName("user-id")).toBe(false);
|
|
expect(DatabaseValidator.validateColumnName("user id")).toBe(false);
|
|
expect(DatabaseValidator.validateColumnName("WHERE")).toBe(false); // 예약어
|
|
});
|
|
|
|
test("데이터 타입 검증", () => {
|
|
expect(DatabaseValidator.validateDataType("VARCHAR")).toBe(true);
|
|
expect(DatabaseValidator.validateDataType("VARCHAR(255)")).toBe(true);
|
|
expect(DatabaseValidator.validateDataType("INTEGER")).toBe(true);
|
|
expect(DatabaseValidator.validateDataType("TIMESTAMP")).toBe(true);
|
|
expect(DatabaseValidator.validateDataType("JSONB")).toBe(true);
|
|
expect(DatabaseValidator.validateDataType("INTEGER[]")).toBe(true);
|
|
expect(DatabaseValidator.validateDataType("DECIMAL(10,2)")).toBe(true);
|
|
});
|
|
|
|
test("WHERE 조건 검증", () => {
|
|
expect(
|
|
DatabaseValidator.validateWhereClause({
|
|
user_id: "test",
|
|
status: "active",
|
|
})
|
|
).toBe(true);
|
|
|
|
expect(
|
|
DatabaseValidator.validateWhereClause({
|
|
"config->>type": "form", // JSON 쿼리
|
|
})
|
|
).toBe(true);
|
|
|
|
expect(
|
|
DatabaseValidator.validateWhereClause({
|
|
"invalid-column": "value",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
test("페이지네이션 검증", () => {
|
|
expect(DatabaseValidator.validatePagination(1, 10)).toBe(true);
|
|
expect(DatabaseValidator.validatePagination(5, 100)).toBe(true);
|
|
|
|
expect(DatabaseValidator.validatePagination(0, 10)).toBe(false); // page < 1
|
|
expect(DatabaseValidator.validatePagination(1, 0)).toBe(false); // pageSize < 1
|
|
expect(DatabaseValidator.validatePagination(1, 2000)).toBe(false); // pageSize > 1000
|
|
});
|
|
|
|
test("ORDER BY 검증", () => {
|
|
expect(DatabaseValidator.validateOrderBy("created_at")).toBe(true);
|
|
expect(DatabaseValidator.validateOrderBy("created_at ASC")).toBe(true);
|
|
expect(DatabaseValidator.validateOrderBy("created_at DESC")).toBe(true);
|
|
|
|
expect(DatabaseValidator.validateOrderBy("created_at INVALID")).toBe(
|
|
false
|
|
);
|
|
expect(DatabaseValidator.validateOrderBy("invalid-column ASC")).toBe(
|
|
false
|
|
);
|
|
});
|
|
|
|
test("UUID 검증", () => {
|
|
expect(
|
|
DatabaseValidator.validateUUID("550e8400-e29b-41d4-a716-446655440000")
|
|
).toBe(true);
|
|
expect(DatabaseValidator.validateUUID("invalid-uuid")).toBe(false);
|
|
});
|
|
|
|
test("이메일 검증", () => {
|
|
expect(DatabaseValidator.validateEmail("test@example.com")).toBe(true);
|
|
expect(DatabaseValidator.validateEmail("user.name@domain.co.kr")).toBe(
|
|
true
|
|
);
|
|
expect(DatabaseValidator.validateEmail("invalid-email")).toBe(false);
|
|
expect(DatabaseValidator.validateEmail("test@")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Integration Tests (실제 DB 연결 필요)", () => {
|
|
// 실제 데이터베이스 연결이 필요한 테스트들
|
|
// DB 연결 실패 시 스킵되도록 설정
|
|
|
|
beforeAll(async () => {
|
|
// DB 연결 테스트
|
|
try {
|
|
await query("SELECT 1 as test");
|
|
console.log("✅ 데이터베이스 연결 성공 - Integration Tests 실행");
|
|
} catch (error) {
|
|
console.warn("⚠️ 데이터베이스 연결 실패 - Integration Tests 스킵");
|
|
console.warn("DB 연결 오류:", error);
|
|
}
|
|
});
|
|
|
|
test("실제 쿼리 실행 테스트", async () => {
|
|
try {
|
|
const result = await query(
|
|
"SELECT NOW() as current_time, version() as pg_version"
|
|
);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toHaveProperty("current_time");
|
|
expect(result[0]).toHaveProperty("pg_version");
|
|
expect(result[0].pg_version).toContain("PostgreSQL");
|
|
|
|
console.log("🕐 현재 시간:", result[0].current_time);
|
|
console.log("📊 PostgreSQL 버전:", result[0].pg_version);
|
|
} catch (error) {
|
|
console.error("❌ 쿼리 실행 테스트 실패:", error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
test("파라미터화된 쿼리 테스트", async () => {
|
|
try {
|
|
const testValue = "test_value_" + Date.now();
|
|
const result = await query(
|
|
"SELECT $1 as input_value, $2 as number_value, $3 as boolean_value",
|
|
[testValue, 42, true]
|
|
);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].input_value).toBe(testValue);
|
|
expect(parseInt(result[0].number_value)).toBe(42); // PostgreSQL은 숫자를 문자열로 반환
|
|
expect(
|
|
result[0].boolean_value === true || result[0].boolean_value === "true"
|
|
).toBe(true); // PostgreSQL boolean 처리
|
|
|
|
console.log("📝 파라미터 테스트 결과:", result[0]);
|
|
} catch (error) {
|
|
console.error("❌ 파라미터 쿼리 테스트 실패:", error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
test("단일 행 조회 테스트", async () => {
|
|
try {
|
|
// 존재하는 데이터 조회
|
|
const result = await queryOne("SELECT 1 as value, 'exists' as status");
|
|
expect(result).not.toBeNull();
|
|
expect(result?.value).toBe(1);
|
|
expect(result?.status).toBe("exists");
|
|
|
|
// 존재하지 않는 데이터 조회
|
|
const emptyResult = await queryOne(
|
|
"SELECT * FROM (SELECT 1 as id) t WHERE id = 999"
|
|
);
|
|
expect(emptyResult).toBeNull();
|
|
|
|
console.log("🔍 단일 행 조회 결과:", result);
|
|
} catch (error) {
|
|
console.error("❌ 단일 행 조회 테스트 실패:", error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
test("트랜잭션 테스트", async () => {
|
|
try {
|
|
const result = await transaction(async (client) => {
|
|
const res1 = await client.query(
|
|
"SELECT 1 as value, 'first' as label"
|
|
);
|
|
const res2 = await client.query(
|
|
"SELECT 2 as value, 'second' as label"
|
|
);
|
|
const res3 = await client.query("SELECT $1 as computed_value", [
|
|
res1.rows[0].value + res2.rows[0].value,
|
|
]);
|
|
|
|
return {
|
|
res1: res1.rows,
|
|
res2: res2.rows,
|
|
res3: res3.rows,
|
|
transaction_id: Math.random().toString(36).substr(2, 9),
|
|
};
|
|
});
|
|
|
|
expect(result.res1[0].value).toBe(1);
|
|
expect(result.res1[0].label).toBe("first");
|
|
expect(result.res2[0].value).toBe(2);
|
|
expect(result.res2[0].label).toBe("second");
|
|
expect(parseInt(result.res3[0].computed_value)).toBe(3); // PostgreSQL은 숫자를 문자열로 반환
|
|
expect(result.transaction_id).toBeDefined();
|
|
|
|
console.log("🔄 트랜잭션 테스트 결과:", {
|
|
first_value: result.res1[0].value,
|
|
second_value: result.res2[0].value,
|
|
computed_value: result.res3[0].computed_value,
|
|
transaction_id: result.transaction_id,
|
|
});
|
|
} catch (error) {
|
|
console.error("❌ 트랜잭션 테스트 실패:", error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
test("트랜잭션 롤백 테스트", async () => {
|
|
try {
|
|
await expect(
|
|
transaction(async (client) => {
|
|
await client.query("SELECT 1 as value");
|
|
// 의도적으로 오류 발생
|
|
throw new Error("의도적인 롤백 테스트");
|
|
})
|
|
).rejects.toThrow("의도적인 롤백 테스트");
|
|
|
|
console.log("🔄 트랜잭션 롤백 테스트 성공");
|
|
} catch (error) {
|
|
console.error("❌ 트랜잭션 롤백 테스트 실패:", error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
test("연결 풀 상태 확인", () => {
|
|
try {
|
|
const status = getPoolStatus();
|
|
|
|
expect(status).toHaveProperty("totalCount");
|
|
expect(status).toHaveProperty("idleCount");
|
|
expect(status).toHaveProperty("waitingCount");
|
|
expect(typeof status.totalCount).toBe("number");
|
|
expect(typeof status.idleCount).toBe("number");
|
|
expect(typeof status.waitingCount).toBe("number");
|
|
|
|
console.log("🏊♂️ 연결 풀 상태:", {
|
|
총_연결수: status.totalCount,
|
|
유휴_연결수: status.idleCount,
|
|
대기_연결수: status.waitingCount,
|
|
});
|
|
} catch (error) {
|
|
console.error("❌ 연결 풀 상태 확인 실패:", error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
test("데이터베이스 메타데이터 조회", async () => {
|
|
try {
|
|
// 현재 데이터베이스 정보 조회
|
|
const dbInfo = await query(`
|
|
SELECT
|
|
current_database() as database_name,
|
|
current_user as current_user,
|
|
inet_server_addr() as server_address,
|
|
inet_server_port() as server_port
|
|
`);
|
|
|
|
expect(dbInfo).toHaveLength(1);
|
|
expect(dbInfo[0].database_name).toBeDefined();
|
|
expect(dbInfo[0].current_user).toBeDefined();
|
|
|
|
console.log("🗄️ 데이터베이스 정보:", {
|
|
데이터베이스명: dbInfo[0].database_name,
|
|
현재사용자: dbInfo[0].current_user,
|
|
서버주소: dbInfo[0].server_address,
|
|
서버포트: dbInfo[0].server_port,
|
|
});
|
|
} catch (error) {
|
|
console.error("❌ 데이터베이스 메타데이터 조회 실패:", error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
test("테이블 존재 여부 확인", async () => {
|
|
try {
|
|
// 시스템 테이블 조회로 안전하게 테스트
|
|
const tables = await query(`
|
|
SELECT table_name, table_type
|
|
FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND table_type = 'BASE TABLE'
|
|
LIMIT 5
|
|
`);
|
|
|
|
expect(Array.isArray(tables)).toBe(true);
|
|
console.log(`📋 발견된 테이블 수: ${tables.length}`);
|
|
|
|
if (tables.length > 0) {
|
|
console.log(
|
|
"📋 테이블 목록 (최대 5개):",
|
|
tables.map((t) => t.table_name).join(", ")
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("❌ 테이블 존재 여부 확인 실패:", error);
|
|
throw error;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// 테스트 실행 방법:
|
|
// npm test -- database.test.ts
|