nimuli/scripts/crawl-all-routes.mjs

159 lines
7.0 KiB
JavaScript

/**
* Route crawler: logs in via Playwright, visits all app routes,
* reports HTTP errors and JS/Blade errors in the page body.
*
* Usage: node scripts/crawl-all-routes.mjs
*/
import { chromium } from '@playwright/test';
import { spawnSync } from 'child_process';
const BASE = process.env.APP_URL ?? 'https://app.nimuli.com';
const EMAIL = 'nexxo@nimuli.com';
const PASS = 'NimuliDev2026!';
let pass = 0;
let fail = 0;
const failures = [];
console.log(`\n${'═'.repeat(47)}`);
console.log(' Nimuli — Route Crawler');
console.log(` ${new Date().toISOString()}`);
console.log(` Base: ${BASE}`);
console.log(`${'═'.repeat(47)}\n`);
// ── DB helper (safe: no shell, all args explicit) ─
function dbQuery(sql) {
const result = spawnSync(
'docker',
['compose', 'exec', '-T', 'mysql', 'mysql', '-unimuli', '-pnimuli', 'nimuli', '-se', sql],
{ encoding: 'utf8' }
);
if (result.status !== 0) return '';
const lines = result.stdout.trim().split('\n').filter(Boolean);
return lines[lines.length - 1]?.trim() ?? '';
}
// ── Get workspace ULID ────────────────────────
const wsUlid = dbQuery(
"SELECT w.ulid FROM workspaces w JOIN workspace_members wm ON wm.workspace_id=w.id JOIN users u ON u.id=wm.user_id WHERE u.email='nexxo@nimuli.com' ORDER BY w.created_at LIMIT 1"
);
if (!wsUlid) { console.error(' ✗ No workspace for nexxo@nimuli.com'); process.exit(1); }
console.log(` Workspace: ${wsUlid}\n`);
// ── Get sample resource IDs ───────────────────
const linkUlid = dbQuery(
"SELECT l.ulid FROM links l JOIN workspace_members wm ON wm.workspace_id=l.workspace_id JOIN users u ON u.id=wm.user_id WHERE u.email='nexxo@nimuli.com' LIMIT 1"
);
const qrUlid = dbQuery(
"SELECT q.ulid FROM qr_codes q JOIN workspace_members wm ON wm.workspace_id=q.workspace_id JOIN users u ON u.id=wm.user_id WHERE u.email='nexxo@nimuli.com' LIMIT 1"
);
const bioSlug = dbQuery(
"SELECT bp.slug FROM bio_pages bp JOIN workspace_members wm ON wm.workspace_id=bp.workspace_id JOIN users u ON u.id=wm.user_id WHERE u.email='nexxo@nimuli.com' LIMIT 1"
);
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1280, height: 900 } });
const page = await ctx.newPage();
// Collect JS errors per-page
const pageErrors = [];
page.on('console', msg => { if (msg.type() === 'error') pageErrors.push(msg.text()); });
page.on('pageerror', err => { pageErrors.push(`PageError: ${err.message}`); });
// ── Login ─────────────────────────────────────
console.log('[ LOGIN ]');
await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' });
await page.fill('input[name=email]', EMAIL);
await page.fill('input[name=password]', PASS);
await page.click('button[type=submit]');
await page.waitForLoadState('networkidle');
if (page.url().includes('/login')) {
console.error(' ✗ Still on login page — wrong credentials or app error');
await browser.close();
process.exit(1);
}
console.log(` ✓ Authenticated: ${page.url()}\n`);
// ── Route check helper ────────────────────────
const BLADE_ERR_RE = /Undefined variable|ErrorException|Whoops|Class .* not found|Call to .* on null|syntax error/i;
async function check(path, label) {
pageErrors.length = 0;
const resp = await page.goto(`${BASE}/${path}`, { waitUntil: 'networkidle' });
const status = resp?.status() ?? 0;
const body = await page.content();
const bladeErr = BLADE_ERR_RE.test(body)
? (body.match(/(?:Undefined variable \w+|ErrorException.{0,60}|Class .{0,30} not found)/)?.[0] ?? 'Blade error')
: null;
const consoleErrs = [...pageErrors];
if (status >= 400 || bladeErr || consoleErrs.length > 0) {
let reason = status >= 400 ? `HTTP ${status}` : '';
if (bladeErr) reason += (reason ? ', ' : '') + bladeErr;
if (consoleErrs.length) reason += (reason ? ', ' : '') + `${consoleErrs.length} JS error(s): ${consoleErrs[0].slice(0, 80)}`;
console.error(`${label}${reason}`);
failures.push({ path, label, reason });
fail++;
} else {
console.log(`${label}${status}`);
pass++;
}
}
// ── Static routes ─────────────────────────────
console.log('[ STATIC ROUTES ]');
await check('', 'root /');
await check('forgot-password', 'forgot-password');
// ── Profile routes ────────────────────────────
console.log('\n[ PROFILE ROUTES ]');
await check('profile', 'profile');
await check('profile/personal', 'profile/personal');
await check('profile/preferences', 'profile/preferences');
await check('profile/security', 'profile/security');
// ── Workspace routes ──────────────────────────
console.log(`\n[ WORKSPACE ROUTES: ${wsUlid} ]`);
await check(`w/${wsUlid}`, 'dashboard');
await check(`w/${wsUlid}/links`, 'links');
await check(`w/${wsUlid}/qr`, 'qr-codes');
await check(`w/${wsUlid}/bio`, 'bio-pages');
await check(`w/${wsUlid}/analytics`, 'analytics');
await check(`w/${wsUlid}/domains`, 'domains');
await check(`w/${wsUlid}/team`, 'team');
await check(`w/${wsUlid}/billing`, 'billing');
await check(`w/${wsUlid}/settings`, 'settings');
await check(`w/${wsUlid}/settings/api-tokens`, 'settings/api-tokens');
await check(`w/${wsUlid}/settings/webhooks`, 'settings/webhooks');
// ── AI routes ─────────────────────────────────
console.log('\n[ AI ROUTES ]');
await check(`w/${wsUlid}/ai/insights`, 'ai/insights');
await check(`w/${wsUlid}/ai/anomalies`, 'ai/anomalies');
await check(`w/${wsUlid}/ai/ab-generator`, 'ai/ab-generator');
// ── Resource routes ───────────────────────────
console.log('\n[ RESOURCE ROUTES ]');
if (linkUlid) await check(`w/${wsUlid}/links/${linkUlid}`, 'link show');
else console.log(' — no links in DB, skipping');
if (qrUlid) await check(`w/${wsUlid}/qr/${qrUlid}`, 'qr show');
else console.log(' — no QR codes in DB, skipping');
if (bioSlug) await check(`bio/${bioSlug}`, 'bio public');
else console.log(' — no bio pages in DB, skipping');
await browser.close();
// ── Summary ───────────────────────────────────
const total = pass + fail;
console.log(`\n${'═'.repeat(47)}`);
console.log(` Passed: ${pass} / ${total}`);
if (fail > 0) {
console.error(` Failed: ${fail}`);
console.error('\n Failures:');
for (const f of failures) console.error(` - ${f.label}: ${f.reason}`);
console.error(`${'═'.repeat(47)}\n`);
process.exit(1);
}
console.log(' ✅ All routes OK');
console.log(`${'═'.repeat(47)}\n`);