Initial commit: SolLunar Kitchen under version control
This commit is contained in:
commit
601c85f236
37 changed files with 6206 additions and 0 deletions
121
server/e2e-test.js
Normal file
121
server/e2e-test.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* E2E test: boots mock-btcpay + server, then exercises:
|
||||
* health → settings save → webhook auto-register → create invoice →
|
||||
* poll status → (mock settles + fires signed webhook) → status Paid → SSE saw the update
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const PORT = 8787, MOCK_PORT = 8899, BASE = `http://localhost:${PORT}`;
|
||||
let failures = 0;
|
||||
function check(name, cond, extra = '') {
|
||||
console.log((cond ? ' PASS ' : ' FAIL ') + name + (extra ? ` [${extra}]` : ''));
|
||||
if (!cond) failures++;
|
||||
}
|
||||
async function j(method, path, body) {
|
||||
const r = await fetch(BASE + path, {
|
||||
method,
|
||||
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
return { status: r.status, body: await r.json().catch(() => ({})) };
|
||||
}
|
||||
|
||||
function boot(file, env) {
|
||||
const c = spawn(process.execPath, [file], {
|
||||
env: { ...process.env, ...env },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
c.stdout.on('data', d => process.env.QUIET || console.log(' [' + file + '] ' + d.toString().trim()));
|
||||
c.stderr.on('data', d => process.stderr.write(d));
|
||||
return c;
|
||||
}
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
/* boot */
|
||||
const mock = boot('mock-btcpay.js', { MOCK_PORT: String(MOCK_PORT), MOCK_SETTLE_MS: '6000' });
|
||||
const srv = boot('server.js', {
|
||||
PORT: String(PORT),
|
||||
DATA_FILE: '/tmp/fest484-e2e-invoices.json',
|
||||
WEBHOOK_PUBLIC_URL: BASE,
|
||||
MOCK_API_KEY: 'mock-store-key',
|
||||
});
|
||||
try { fs.rmSync('/tmp/fest484-e2e-invoices.json'); } catch { }
|
||||
await sleep(1200);
|
||||
|
||||
try {
|
||||
/* 1 health */
|
||||
const h = await j('GET', '/health');
|
||||
check('health ok', h.status === 200 && h.body.ok === true);
|
||||
|
||||
/* 2 save settings (points at the mock) */
|
||||
const s = await j('POST', '/api/settings', { url: `http://localhost:${MOCK_PORT}`, store: 'mock-store-1234', apiKey: 'mock-store-key' });
|
||||
check('settings saved', s.status === 200 && s.body.ok === true, JSON.stringify(s.body));
|
||||
|
||||
/* 3 auto-register webhook (mock returns a secret) */
|
||||
const w = await j('POST', '/api/webhook/register', {});
|
||||
check('webhook registered', w.status === 200 && w.body.ok === true, JSON.stringify(w.body));
|
||||
|
||||
/* 4 create invoice */
|
||||
const inv = await j('POST', '/api/invoices', {
|
||||
amount: 24.5, currency: 'USD', orderCode: 'F484-TEST1', description: 'E2E test order',
|
||||
metadata: { pickup: '11:00 – 12:00', name: 'E2E' },
|
||||
});
|
||||
check('invoice created', inv.status === 201 && inv.body.id, inv.body.id || JSON.stringify(inv.body));
|
||||
check('has bolt11', typeof inv.body.bolt11 === 'string' && inv.body.bolt11.startsWith('lnbc'));
|
||||
check('has btc address', typeof inv.body.btcAddress === 'string' && inv.body.btcAddress.startsWith('bc1'));
|
||||
|
||||
/* 5 open SSE and wait for the status event */
|
||||
const sseEvents = [];
|
||||
const ac = new AbortController();
|
||||
const es = fetch(BASE + '/api/invoices/' + inv.body.id + '/events', { signal: ac.signal })
|
||||
.then(async r => {
|
||||
const reader = r.body.getReader();
|
||||
const dec = new TextDecoder();
|
||||
let buf = '';
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buf += dec.decode(value, { stream: true });
|
||||
let i;
|
||||
while ((i = buf.indexOf('\n\n')) >= 0) {
|
||||
const frame = buf.slice(0, i); buf = buf.slice(i + 2);
|
||||
for (const line of frame.split('\n')) if (line.startsWith('data: ')) sseEvents.push(JSON.parse(line.slice(6)));
|
||||
}
|
||||
}
|
||||
}).catch(() => { });
|
||||
await sleep(300);
|
||||
check('sse initial status New', sseEvents.some(e => e.type === 'status' && e.status === 'New'), JSON.stringify(sseEvents));
|
||||
|
||||
/* 6 poll until Paid (webhook from mock settles it after ~6s) */
|
||||
let paid = null;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const st = await j('GET', '/api/invoices/' + inv.body.id);
|
||||
if (st.body.status === 'Paid') { paid = st.body; break; }
|
||||
await sleep(1000);
|
||||
}
|
||||
check('invoice reached Paid (webhook or poll)', Boolean(paid));
|
||||
check('sse received Paid event', sseEvents.some(e => e.type === 'status' && e.status === 'Paid'), JSON.stringify(sseEvents));
|
||||
ac.abort();
|
||||
|
||||
/* 7 webhook rejects bad signature */
|
||||
const bad = await fetch(BASE + '/api/btcpay/webhook', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'BTCPay-Sig': 'sha256=' + 'ab'.repeat(32) },
|
||||
body: JSON.stringify({ event: 'InvoiceSettled', invoice: inv.body.id }),
|
||||
});
|
||||
check('webhook rejects bad sig (401)', bad.status === 401);
|
||||
|
||||
/* 8 rate limit sanity (not critical) */
|
||||
const rl = await j('POST', '/api/invoices', { amount: 1, currency: 'USD' });
|
||||
check('invoice create still works for 2nd invoice', rl.status === 201);
|
||||
|
||||
console.log(failures === 0 ? '\nE2E: ALL PASS ✅' : `\nE2E: ${failures} FAILURE(S) ❌`);
|
||||
process.exit(failures === 0 ? 0 : 1);
|
||||
} catch (e) {
|
||||
console.error('E2E crashed:', e);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
mock.kill('SIGTERM');
|
||||
srv.kill('SIGTERM');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue