199 lines
5.7 KiB
JavaScript
199 lines
5.7 KiB
JavaScript
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('<div id="root"></div>'),
|
|
(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;
|
|
}
|