Files
slot/next-app/scripts/verify-react-stack.mjs
chpark d175f46a09 shop checkout flow + 3-game spin engine + admin board edit + lightweight cron
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>
2026-04-28 07:19:28 +09:00

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);
})();