import { mkdir, writeFile } from 'node:fs/promises'; import { request } from 'node:https'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const baseUrl = (process.env.LISGLOSIPS_BASE_URL || 'https://100.90.90.91').replace(/\/$/, ''); const timeoutMs = Number(process.env.LISGLOSIPS_SMOKE_TIMEOUT_MS || 30000); const maxAttempts = Number(process.env.LISGLOSIPS_SMOKE_ATTEMPTS || 2); const reportDir = resolve(dirname(fileURLToPath(import.meta.url)), '../reports'); function requestUrlOnce(path, attempt) { return new Promise((resolveRequest) => { const startedAt = Date.now(); const url = new URL(path, baseUrl); const req = request( url, { method: 'GET', rejectUnauthorized: process.env.LISGLOSIPS_REJECT_UNAUTHORIZED === '1', timeout: timeoutMs, headers: { Accept: 'application/json,text/html;q=0.9,*/*;q=0.8', 'User-Agent': 'lisglosips-remote-smoke/1.0', }, }, (res) => { const chunks = []; res.on('data', (chunk) => chunks.push(chunk)); res.on('end', () => { const body = Buffer.concat(chunks).toString('utf8'); resolveRequest({ path, url: url.toString(), attempt, ok: true, statusCode: res.statusCode || 0, durationMs: Date.now() - startedAt, contentType: String(res.headers['content-type'] || ''), body, }); }); } ); req.on('timeout', () => { req.destroy(new Error(`Request timed out after ${timeoutMs}ms`)); }); req.on('error', (error) => { resolveRequest({ path, url: url.toString(), attempt, ok: false, statusCode: 0, durationMs: Date.now() - startedAt, contentType: '', body: '', error: error.message, }); }); req.end(); }); } async function requestUrl(path) { let lastResult; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { lastResult = await requestUrlOnce(path, attempt); if (lastResult.ok) { return lastResult; } } return lastResult; } function parseJson(result) { try { return JSON.parse(result.body); } catch { return null; } } function expectStatus(result, expectedStatus) { return { name: `${result.path} returns ${expectedStatus}`, pass: result.ok && result.statusCode === expectedStatus, detail: result.error || `status=${result.statusCode}, duration=${result.durationMs}ms, attempt=${result.attempt}`, }; } function expectBody(result, name, predicate, detail) { return { name, pass: result.ok && predicate(result), detail: detail(result), }; } function reportLine(check) { return `| ${check.pass ? 'PASS' : 'FAIL'} | ${check.name} | ${check.detail.replace(/\|/g, '\\|')} |`; } const checks = []; const results = { root: await requestUrl('/'), live: await requestUrl('/api/v2/health/live'), ready: await requestUrl('/api/v2/health/ready'), captcha: await requestUrl('/api/v2/auth/captcha'), }; checks.push(expectStatus(results.root, 200)); checks.push( expectBody( results.root, 'frontend HTML contains app root', (result) => result.contentType.includes('text/html') && result.body.includes('
'), (result) => `contentType=${result.contentType}, bytes=${Buffer.byteLength(result.body, 'utf8')}, attempt=${result.attempt}` ) ); checks.push(expectStatus(results.live, 200)); checks.push( expectBody( results.live, 'live health reports ok', (result) => { const body = parseJson(result); return body?.status === 'ok' && body?.service === 'api' && body?.checks?.process === 'ok'; }, (result) => `body=${JSON.stringify(parseJson(result))}` ) ); checks.push(expectStatus(results.ready, 200)); checks.push( expectBody( results.ready, 'ready health reports database and redis ok', (result) => { const body = parseJson(result); return body?.status === 'ok' && body?.checks?.database === 'ok' && body?.checks?.redis === 'ok'; }, (result) => `body=${JSON.stringify(parseJson(result))}` ) ); checks.push(expectStatus(results.captcha, 200)); checks.push( expectBody( results.captcha, 'captcha endpoint returns id, image, and expiry', (result) => { const body = parseJson(result); return typeof body?.captchaId === 'string' && body.imageDataUrl?.startsWith('data:image/svg+xml;base64,') && typeof body.expiresAt === 'string'; }, (result) => { const body = parseJson(result); return body ? `captchaId=${body.captchaId}, expiresAt=${body.expiresAt}, attempt=${result.attempt}` : 'body is not JSON'; } ) ); const now = new Date(); const stamp = now.toISOString().replace(/[-:]/g, '').replace(/\..+$/, 'Z'); const reportPath = resolve(reportDir, `REMOTE_SMOKE_${stamp}.md`); const failed = checks.filter((check) => !check.pass); const report = [ '# Remote Smoke Test Report', '', `Date: ${now.toISOString()}`, `Base URL: ${baseUrl}`, `Timeout: ${timeoutMs}ms`, `Attempts: ${maxAttempts}`, '', '| Result | Check | Detail |', '| --- | --- | --- |', ...checks.map(reportLine), '', '## Raw Endpoints', '', ...Object.values(results).map((result) => `- ${result.path}: status=${result.statusCode}, duration=${result.durationMs}ms, attempt=${result.attempt}, contentType=${result.contentType || 'n/a'}`), '', ].join('\n'); await mkdir(reportDir, { recursive: true }); await writeFile(reportPath, report, 'utf8'); for (const check of checks) { console.log(`${check.pass ? 'PASS' : 'FAIL'} ${check.name} - ${check.detail}`); } console.log(`Report: ${reportPath}`); if (failed.length > 0) { process.exitCode = 1; }