d175f46a09
Shop: /shop/[itemId], /shop/cart with checkout, /shop/order/[odId] Games: 3-game engine (fortunes, fivetreasures, bacara), /games/[game]/play Admin: /admin/boards inline rename + actions Cron: PG-row-lock cron helper (no Redis needed) Verify: 600/600 PASS over 50 iterations Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
163 lines
5.6 KiB
JavaScript
163 lines
5.6 KiB
JavaScript
#!/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);
|
|
})();
|