import test from 'node:test'; import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { once } from 'node:events'; import fs from 'node:fs/promises'; import net from 'node:net'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const projectRoot = fileURLToPath(new URL('../', import.meta.url)); const socialPreviewPath = '/assets/social-preview-v2.78d008df9860.jpg'; test('serves crawler-safe HEAD responses for the homepage and social preview', { timeout: 15_000 }, async (t) => { let port; try { port = await availablePort(); } catch (error) { if (error.code === 'EPERM') { t.skip('loopback listeners are disabled in this sandbox'); return; } throw error; } const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'meme-protocol-head-')); const child = spawn(process.execPath, ['server.js'], { cwd: projectRoot, env: { ...process.env, ADMIN_TOKEN: 'test-admin-token-000000000000', DATA_DIR: dataDir, HOST: '127.0.0.1', PORT: String(port), SEED_DEMO_MEMES: 'false', SITE_URL: `http://127.0.0.1:${port}` }, stdio: ['ignore', 'pipe', 'pipe'] }); t.after(async () => { if (child.exitCode === null && child.signalCode === null) { child.kill('SIGTERM'); await once(child, 'exit'); } await fs.rm(dataDir, { recursive: true, force: true }); }); await waitForStart(child); const headers = { 'user-agent': 'Twitterbot/1.0' }; const page = await fetch(`http://127.0.0.1:${port}/`, { method: 'HEAD', headers }); assert.equal(page.status, 200); assert.match(page.headers.get('content-type') || '', /^text\/html/); assert.ok(Number(page.headers.get('content-length')) > 0); assert.equal((await page.arrayBuffer()).byteLength, 0); const image = await fetch(`http://127.0.0.1:${port}${socialPreviewPath}`, { method: 'HEAD', headers }); assert.equal(image.status, 200); assert.equal(image.headers.get('content-type'), 'image/jpeg'); assert.match(image.headers.get('cache-control') || '', /immutable/); assert.ok(Number(image.headers.get('content-length')) < 300_000); assert.equal((await image.arrayBuffer()).byteLength, 0); }); async function availablePort() { const server = net.createServer(); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); const address = server.address(); const port = typeof address === 'object' && address ? address.port : 0; await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); return port; } async function waitForStart(child) { await new Promise((resolve, reject) => { let output = ''; const timeout = setTimeout(() => finish(new Error(`server did not start:\n${output}`)), 10_000); const onData = (chunk) => { output += chunk.toString(); if (output.includes('The Meme Protocol listening')) finish(); }; const onExit = (code, signal) => finish(new Error(`server exited before startup (${code ?? signal}):\n${output}`)); const finish = (error) => { clearTimeout(timeout); child.stdout.off('data', onData); child.stderr.off('data', onData); child.off('exit', onExit); error ? reject(error) : resolve(); }; child.stdout.on('data', onData); child.stderr.on('data', onData); child.once('exit', onExit); }); }