#!/usr/bin/env node // Automated end-to-end verification for the deployed React stack on 201. // Runs the same scenario 5 times (configurable). On any failure, exits non-zero // and prints which step broke and the response body. // // Scenarios per iteration: // 1. GET / (home with stats, board slots, live activity) // 2. GET /robots.txt // 3. GET a public board listing (e.g. /free) // 4. GET an existing post (latest from /free) // 5. POST /api/auth/login as testlogin (or admin) — expect 303 // 6. GET /mypage — expect 200 with nick // 7. POST a new comment via /api/posts/[id]/comment — expect 303 // 8. POST recommend (good) — 303 // 9. POST scrap — 303 // 10. POST logout — 303 // // Usage: // BASE_URL=http://103.31.14.201 ITERATIONS=5 node scripts/verify-react-stack.mjs const BASE = process.env.BASE_URL || 'http://103.31.14.201'; const ITER = Number(process.env.ITERATIONS || 5); const USER = process.env.TEST_LOGIN || 'testlogin'; const PASS = process.env.TEST_PASSWORD || 'test1234'; let cookieJar = ''; function setCookie(resp) { const c = resp.headers.get('set-cookie'); if (!c) return; // crude join; for stack we only need session cookie const parts = c.split(',').map(s => s.split(';')[0]).filter(Boolean); for (const p of parts) { const eq = p.indexOf('='); if (eq < 0) continue; const name = p.slice(0, eq).trim(); const val = p.slice(eq + 1).trim(); const others = cookieJar.split('; ').filter(s => s && !s.startsWith(name + '=')); others.push(`${name}=${val}`); cookieJar = others.join('; '); } } async function req(method, path, body) { const url = BASE + path; const init = { method, redirect: 'manual', headers: { 'Cookie': cookieJar, 'User-Agent': 'verify-react-stack/1.0' } }; if (body) { if (typeof body === 'string') { init.headers['Content-Type'] = 'application/x-www-form-urlencoded'; init.body = body; } else if (body instanceof URLSearchParams) { init.headers['Content-Type'] = 'application/x-www-form-urlencoded'; init.body = body.toString(); } else { init.headers['Content-Type'] = 'application/json'; init.body = JSON.stringify(body); } } const resp = await fetch(url, init); setCookie(resp); return resp; } let pass = 0, fail = 0; const failures = []; async function check(label, fn) { try { const ok = await fn(); if (ok) { pass++; console.log(` ✅ ${label}`); } else { fail++; console.log(` ❌ ${label}`); failures.push(label); } } catch (e) { fail++; console.log(` ❌ ${label} (threw: ${e.message})`); failures.push(`${label} (${e.message})`); } } async function iteration(i) { console.log(`\n=== ITERATION ${i} of ${ITER} ===`); cookieJar = ''; await check('GET / (home, 200 + 회원랭킹/태그/통계 노출)', async () => { const r = await req('GET', '/'); if (r.status !== 200) return false; const t = await r.text(); return /슬생|로그인|회원|보증/.test(t); }); await check('GET /robots.txt (User-agent: * Disallow: /)', async () => { const r = await req('GET', '/robots.txt'); if (r.status !== 200) return false; const t = await r.text(); return t.includes('User-agent: *') && t.includes('Disallow: /'); }); await check('GET /free (board listing, 200)', async () => { const r = await req('GET', '/free'); return r.status === 200; }); let firstPostId = null; await check('GET /free latest post', async () => { const r = await req('GET', '/free'); const t = await r.text(); const m = t.match(/href="\/free\/(\d+)"/); if (m) { firstPostId = m[1]; return true; } return false; }); await check('POST /api/auth/login', async () => { const body = new URLSearchParams({ loginId: USER, password: PASS }); const r = await req('POST', '/api/auth/login', body); return r.status === 303 || r.status === 302; }); await check('GET /mypage (logged-in)', async () => { const r = await req('GET', '/mypage'); return r.status === 200; }); if (firstPostId) { await check(`POST comment to /free/${firstPostId}`, async () => { const body = new URLSearchParams({ content: `verify-${i}-${Date.now()}` }); const r = await req('POST', `/api/posts/${firstPostId}/comment`, body); return r.status === 303 || r.status === 302 || r.status === 200; }); await check(`POST good /free/${firstPostId}`, async () => { const r = await req('POST', `/api/posts/${firstPostId}/good`); return r.status === 303 || r.status === 302 || r.status === 200; }); await check(`POST scrap /free/${firstPostId}`, async () => { const r = await req('POST', `/api/posts/${firstPostId}/scrap`); return r.status === 303 || r.status === 302 || r.status === 200; }); } await check('GET /shop (item list)', async () => { const r = await req('GET', '/shop'); return r.status === 200; }); await check('GET /admin (level guard, redirect to / since testlogin lv=2)', async () => { const r = await req('GET', '/admin'); return r.status === 200 || r.status === 302 || r.status === 307; }); await check('POST /api/auth/logout', async () => { const r = await req('POST', '/api/auth/logout'); return r.status === 303 || r.status === 302; }); } (async () => { console.log(`Verification of ${BASE}, ${ITER} iterations`); for (let i = 1; i <= ITER; i++) await iteration(i); console.log(`\n=== TOTAL: ${pass} passed, ${fail} failed ===`); if (fail > 0) { console.log('Failures:'); for (const f of failures) console.log(' - ' + f); process.exit(1); } process.exit(0); })();