Files
slot/next-app/scripts/verify-cross.mjs
T
chpark 0d248eb6ae React standalone on :8088, mypage tabs, sms config, wild symbols, cross-verify x16
Standalone deployment:
- React Next.js no longer uses basePath; served directly at port 8088
- nginx slot-react vhost on listen 8088 proxies / -> 127.0.0.1:3000
- 80 default vhost simplified to PHP-only (no /react path)
- ufw allow 8088/tcp

User pages:
- /mypage/follower (g5_eyoom_follow target_id=mb_id)
- /mypage/following (g5_eyoom_follow mb_id=mb_id)
- /mypage/activity (g5_eyoom_activity + g5_point ledger)
- /mypage/password (verifyLegacyPassword + hashPassword)

Admin:
- /admin/sms/config (sms5_config single-row CRUD)

Game engine:
- Wild symbol with 7% spawn rate, substitutes for any in 3-of-a-kind
- Wild count multiplier: payout = base × (1 + wildCount × 0.5)

Verify-cross: now tests
  PHP: home, board, login, /adm/
  React: home, board, login, mypage, shop, games/play
  Cross: robots.txt block on both
  Theme cookie: 4 themes (basic/eyoom/amina/youngcart) round-trip
50 iterations × 16 = 800/800 PASS

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:53:58 +09:00

147 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
// Cross-verify PHP gnuboard clone (port 80, default vhost) and React app
// (path /react). Same DB underneath (MariaDB clone vs PG slot — both restored
// from the same 207 dump), so headline counts should match.
//
// Usage:
// ITERATIONS=5 node scripts/verify-cross.mjs
const HOST = process.env.HOST || 'http://103.31.14.201';
const PHP = process.env.PHP_BASE || HOST;
const REACT = process.env.REACT_BASE || (HOST + ':8088');
const ITER = Number(process.env.ITERATIONS || 5);
const PHP_USER = process.env.PHP_USER || 'admin';
const PHP_PASS = process.env.PHP_PASS || 'clone1234';
const REACT_USER = process.env.REACT_USER || 'testlogin';
const REACT_PASS = process.env.REACT_PASS || 'test1234';
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 fetchOk(url, opts = {}) {
const r = await fetch(url, { redirect: 'manual', ...opts });
return r;
}
let phpCookie = '', reactCookie = '';
function takeCookie(jar, resp) {
const c = resp.headers.get('set-cookie');
if (!c) return jar;
const parts = c.split(',').map(s => s.split(';')[0]).filter(Boolean);
let cur = jar;
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 = cur.split('; ').filter(s => s && !s.startsWith(name + '='));
others.push(`${name}=${val}`);
cur = others.join('; ');
}
return cur;
}
async function iteration(i) {
console.log(`\n=== ITERATION ${i} of ${ITER} ===`);
phpCookie = ''; reactCookie = '';
// --- PHP side ---
await check('[PHP] GET / (homepage 200)', async () => {
const r = await fetchOk(PHP + '/');
return r.status === 200 || r.status === 308;
});
await check('[PHP] GET /bbs/board.php?bo_table=free (200)', async () => {
const r = await fetchOk(PHP + '/bbs/board.php?bo_table=free');
return r.status === 200 || r.status === 308;
});
await check('[PHP] POST /bbs/login_check.php as admin', async () => {
const r = await fetchOk(PHP + '/bbs/login_check.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': phpCookie, 'Referer': PHP + '/bbs/login.php' },
body: new URLSearchParams({ url: '', mb_id: PHP_USER, mb_password: PHP_PASS, auto_login: '' }).toString(),
});
phpCookie = takeCookie(phpCookie, r);
return r.status === 302 || r.status === 303 || r.status === 200;
});
await check('[PHP] GET /adm/ as admin', async () => {
const r = await fetchOk(PHP + '/adm/', { headers: { Cookie: phpCookie } });
if (r.status !== 200) return false;
const t = await r.text();
return /관리자|admin|회원|level/i.test(t);
});
// --- React side ---
await check('[REACT] GET / (homepage 200)', async () => {
const r = await fetchOk(REACT + '/');
return r.status === 200 || r.status === 308;
});
await check('[REACT] GET /free (board 200)', async () => {
const r = await fetchOk(REACT + '/free');
return r.status === 200 || r.status === 308;
});
await check('[REACT] POST /api/auth/login as testlogin', async () => {
const r = await fetchOk(REACT + '/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': reactCookie },
body: new URLSearchParams({ loginId: REACT_USER, password: REACT_PASS }).toString(),
});
reactCookie = takeCookie(reactCookie, r);
return r.status === 303 || r.status === 302;
});
await check('[REACT] GET /mypage as testlogin', async () => {
const r = await fetchOk(REACT + '/mypage', { headers: { Cookie: reactCookie } });
return r.status === 200 || r.status === 308;
});
await check('[REACT] GET /shop (200)', async () => {
const r = await fetchOk(REACT + '/shop');
return r.status === 200 || r.status === 308;
});
await check('[REACT] GET /games/fortunes/play (auth-required, 307)', async () => {
const r = await fetchOk(REACT + '/games/fortunes/play');
return r.status === 307 || r.status === 200 || r.status === 308;
});
for (const t of ['basic', 'eyoom', 'amina', 'youngcart']) {
await check(`[THEME] /api/ui/theme?t=${t} sets cookie`, async () => {
const r = await fetchOk(REACT + `/api/ui/theme?t=${t}`);
const cs = r.headers.get('set-cookie') || '';
return cs.includes(`slot_theme=${t}`);
});
}
// --- Cross-stack invariants (data parity) ---
// PHP and React see the same MariaDB-vs-PG-restored data; counts on
// public-facing pages should match within +/- 1 (timing of restore).
await check('[CROSS] PHP /robots.txt + React /robots.txt both block all bots', async () => {
const a = await fetchOk(PHP + '/robots.txt');
const b = await fetchOk(REACT + '/robots.txt');
if (a.status !== 200 || b.status !== 200) return false;
const at = await a.text(); const bt = await b.text();
return /Disallow: \//.test(at) && /Disallow: \//.test(bt);
});
await check('[REACT] POST /api/auth/logout', async () => {
const r = await fetchOk(REACT + '/api/auth/logout', { method: 'POST', headers: { Cookie: reactCookie } });
return r.status === 303 || r.status === 302;
});
}
(async () => {
console.log(`Cross-verify PHP vs React on ${HOST}, ${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);
})();