// E2E 테스트: 가입 → 시드 데이터 → 발주 → 승인 + 메일 발송 // 전제: dev 서버 실행 중 (localhost:3000), DB 마이그레이션 완료 // 사용법: node scripts/test-e2e.mjs import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import pg from "pg"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const envPath = path.join(__dirname, "..", ".env.development"); if (fs.existsSync(envPath)) { for (const line of fs.readFileSync(envPath, "utf-8").split(/\r?\n/)) { const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/); if (m) process.env[m[1]] ??= m[2].replace(/^["']|["']$/g, ""); } } const BASE = "http://localhost:3000"; const TEST_EMAIL = "chpark@wace.me"; const TEST_PASSWORD = "test1234abcd"; let cookieJar = ""; function setCookies(res) { const sc = res.headers.getSetCookie?.() || res.headers.raw?.()["set-cookie"] || []; for (const c of sc) { const kv = c.split(";")[0]; cookieJar = cookieJar ? cookieJar.split("; ").filter((p) => !p.startsWith(kv.split("=")[0] + "=")).concat(kv).join("; ") : kv; } } async function api(path, init = {}) { const res = await fetch(`${BASE}${path}`, { ...init, headers: { "Content-Type": "application/json", ...(cookieJar ? { Cookie: cookieJar } : {}), ...(init.headers ?? {}), }, }); setCookies(res); const text = await res.text(); let json; try { json = JSON.parse(text); } catch { json = { raw: text }; } return { status: res.status, body: json }; } const log = (...a) => console.log("[e2e]", ...a); const fail = (msg) => { console.error("[e2e] ✖", msg); process.exit(1); }; // ===== 1. DB 정리 + 시드 (테스트 데이터) ===== log("1. DB 정리 + 시드"); const client = new pg.Client({ connectionString: process.env.DATABASE_URL }); await client.connect(); // 기존 테스트 사용자 삭제 (재실행 가능하게) await client.query(`DELETE FROM momo_order_items WHERE order_objid IN (SELECT objid FROM momo_orders WHERE customer_objid IN (SELECT objid FROM momo_users WHERE email = $1))`, [TEST_EMAIL]); await client.query(`DELETE FROM momo_orders WHERE customer_objid IN (SELECT objid FROM momo_users WHERE email = $1)`, [TEST_EMAIL]); await client.query(`DELETE FROM momo_users WHERE email = $1`, [TEST_EMAIL]); // 품목 / 재고 시드 (테스트 품목 3개 + 본사창고 재고 100개씩) const items = [ { code: "TEST-EGG-01", name: "M 유정란", price: 10000, taxFree: "Y" }, { code: "TEST-CHKN-01", name: "M 꽃계탕", price: 4500, taxFree: "Y" }, { code: "TEST-CLEAN-01", name: "빨강 탈취제", price: 9200, taxFree: "N" }, ]; for (const it of items) { await client.query( `INSERT INTO momo_items (objid, item_code, item_name, unit, unit_price, is_tax_free, status, regdate) VALUES ($1, $2, $3, 'EA', $4, $5, 'ACTIVE', NOW()) ON CONFLICT (item_code) DO UPDATE SET unit_price = EXCLUDED.unit_price, is_tax_free = EXCLUDED.is_tax_free, status='ACTIVE', is_del='N'`, [`TEST-${it.code}`, it.code, it.name, it.price, it.taxFree] ); } const wh = await client.query(`SELECT objid FROM momo_warehouses WHERE wh_type='STOCK' LIMIT 1`); const whObjid = wh.rows[0].objid; for (const it of items) { await client.query( `INSERT INTO momo_stocks (objid, wh_objid, item_objid, qty, update_date) VALUES ($1, $2, (SELECT objid FROM momo_items WHERE item_code = $3), 100, NOW()) ON CONFLICT (wh_objid, item_objid) DO UPDATE SET qty = 100, update_date = NOW()`, [`TEST-STK-${it.code}`, whObjid, it.code] ); } log(" 품목 3개 + 재고 100개씩 시드 완료"); // ===== 2. 가입 ===== log("2. 회원가입"); const sup = await api("/api/auth/signup", { method: "POST", body: JSON.stringify({ email: TEST_EMAIL, password: TEST_PASSWORD, companyName: "테스트거래처(wace)", ceoName: "박철현", bizNo: "123-45-67890", phone: "010-1234-5678", }), }); if (!sup.body.success) fail(`가입 실패: ${JSON.stringify(sup.body)}`); log(` ✔ 가입 성공 (USER 세션 발급, cookie=${cookieJar.slice(0, 30)}...)`); // ===== 3. 품목 조회 (USER 세션) ===== log("3. 품목 검색 (USER)"); const list = await api("/api/m/items/list", { method: "POST", body: JSON.stringify({ keyword: "TEST" }) }); const visible = (list.body.RESULTLIST || []).filter((r) => r.ITEM_CODE.startsWith("TEST-")); if (visible.length !== 3) fail(`품목 조회 실패: ${visible.length}개 (기대 3개)`); log(` ✔ ${visible.length}개 품목 노출, 재고 ${visible[0].STOCK_QTY}`); // ===== 4. 발주 작성 ===== log("4. 발주 요청"); const order = await api("/api/m/orders/save", { method: "POST", body: JSON.stringify({ lines: [ { itemObjid: visible.find((x) => x.ITEM_CODE === "TEST-EGG-01").OBJID, qty: 30 }, { itemObjid: visible.find((x) => x.ITEM_CODE === "TEST-CHKN-01").OBJID, qty: 20 }, { itemObjid: visible.find((x) => x.ITEM_CODE === "TEST-CLEAN-01").OBJID, qty: 11 }, ], memo: "E2E 테스트 발주", }), }); if (!order.body.success) fail(`발주 실패: ${JSON.stringify(order.body)}`); log(` ✔ 발주번호: ${order.body.orderNo}, objId: ${order.body.objId}`); // ===== 5. 어드민 로그인 (시드 관리자) ===== log("5. 관리자 로그인"); cookieJar = ""; // USER 세션 클리어 const adminLogin = await api("/api/auth/login", { method: "POST", body: JSON.stringify({ userId: "admin@momo.com", password: "admin1234" }), }); if (!adminLogin.body.success) { // 시드 비밀번호 해시가 환경에 따라 다를 수 있어 → DB에서 비밀번호 재설정 log(" 시드 비번 불일치 — bcrypt 재설정"); const bcrypt = (await import("bcryptjs")).default; const hash = await bcrypt.hash("admin1234", 10); await client.query(`UPDATE momo_users SET password_hash = $1 WHERE email = 'admin@momo.com'`, [hash]); const retry = await api("/api/auth/login", { method: "POST", body: JSON.stringify({ userId: "admin@momo.com", password: "admin1234" }), }); if (!retry.body.success) fail(`관리자 로그인 실패: ${JSON.stringify(retry.body)}`); } log(` ✔ 관리자 세션 발급`); // ===== 6. 승인 + 메일 발송 ===== log("6. 발주 승인 (재고차감 + 메일발송)"); const approve = await api("/api/m/orders/approve", { method: "POST", body: JSON.stringify({ objid: order.body.objId }), }); if (!approve.body.success) fail(`승인 실패: ${JSON.stringify(approve.body)}`); log(` ✔ 승인 완료, mailSent=${approve.body.mailSent}, mailError=${approve.body.mailError ?? "(none)"}`); // ===== 7. 결과 검증 ===== log("7. 후속 검증"); const r1 = await client.query(`SELECT status, total_amount, total_taxfree, total_taxable FROM momo_orders WHERE objid = $1`, [order.body.objId]); log(` 주문 상태: ${r1.rows[0].status} (기대 APPROVED)`); log(` 금액: 면세 ${r1.rows[0].total_taxfree} / 과세 ${r1.rows[0].total_taxable} / 합계 ${r1.rows[0].total_amount}`); if (r1.rows[0].status !== "APPROVED") fail("주문 상태 불일치"); const r2 = await client.query(`SELECT qty FROM momo_stocks WHERE wh_objid = $1 AND item_objid = (SELECT objid FROM momo_items WHERE item_code = 'TEST-EGG-01')`, [whObjid]); log(` M 유정란 재고: ${r2.rows[0].qty} (기대 70 = 100-30)`); if (Number(r2.rows[0].qty) !== 70) fail("재고 차감 불일치"); const r3 = await client.query(`SELECT to_email, subject, status, error_msg FROM momo_mail_logs WHERE ref_objid = $1 ORDER BY regdate DESC LIMIT 1`, [order.body.objId]); log(` 메일 로그: to=${r3.rows[0].to_email}, status=${r3.rows[0].status}, subject=${r3.rows[0].subject}`); if (r3.rows[0].error_msg) log(` 메일 에러: ${r3.rows[0].error_msg}`); log("✔ E2E 테스트 완료"); await client.end();