Runner: Proxy und Startseite, register/wake (R12)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6cd50bba9f
commit
4afe7eb5cd
7 files changed
+360
-2
No files matched your search
@@ -249,3 +249,45 @@ test('cleanupOrphans: ein fehlschlagendes rm stoppt die übrigen nicht', async (
|
||||
try { await s.dev.cleanupOrphans(); } finally { console.error = orig; }
|
||||
assert.deepEqual(s.removed, ['preview-dev-x', 'preview-dev-y']);
|
||||
});
|
||||
|
||||
test('register: speichert Spezifikation ohne Start (sleeping); wake startet damit', async () => {
|
||||
const s = setup();
|
||||
assert.deepEqual(await s.dev.register('a', job('a')), { state: 'sleeping' });
|
||||
assert.equal(s.runs.length, 0); assert.equal(s.prepared.length, 0);
|
||||
assert.deepEqual(await s.dev.wake('a'), { state: 'starting' });
|
||||
await s.dev.idle();
|
||||
assert.equal(s.runs.length, 1);
|
||||
assert.deepEqual(await s.dev.wake('a'), { state: 'online', target: 'http://preview-dev-a:5173' });
|
||||
});
|
||||
|
||||
test('wake: unbekannter sub ⇒ unknown, nach stop ebenfalls', async () => {
|
||||
const s = setup();
|
||||
assert.deepEqual(await s.dev.wake('x'), { state: 'unknown' });
|
||||
await s.dev.register('a', job('a'));
|
||||
await s.dev.stop('a');
|
||||
assert.deepEqual(await s.dev.wake('a'), { state: 'unknown' });
|
||||
});
|
||||
|
||||
test('register: läuft mit gleichem sha ⇒ unverändert; mit anderem sha ⇒ Neustart; wake nutzt neue Spezifikation', async () => {
|
||||
const s = setup();
|
||||
await s.dev.ensure('a', job('a')); await s.dev.idle();
|
||||
assert.deepEqual(await s.dev.register('a', job('a')), { state: 'online' });
|
||||
assert.equal(s.runs.length, 1);
|
||||
assert.deepEqual(await s.dev.register('a', job('a', { sha: '1234567' })), { state: 'starting' });
|
||||
await s.dev.idle();
|
||||
assert.equal(s.runs.length, 2);
|
||||
assert.equal(s.prepared[1].job.sha, '1234567');
|
||||
assert.ok(s.removed.includes('preview-dev-a'));
|
||||
});
|
||||
|
||||
test('register: schlafender sub mit neuem sha startet nicht, wake nutzt das neue sha', async () => {
|
||||
const s = setup();
|
||||
await s.dev.ensure('a', job('a')); await s.dev.idle();
|
||||
s.clock.t += 21 * MIN; await s.dev.sweep();
|
||||
assert.equal(s.dev.status('a').state, 'sleeping');
|
||||
assert.deepEqual(await s.dev.register('a', job('a', { sha: '7654321' })), { state: 'sleeping' });
|
||||
assert.equal(s.runs.length, 1);
|
||||
await s.dev.wake('a'); await s.dev.idle();
|
||||
assert.equal(s.runs.length, 2);
|
||||
assert.equal(s.prepared[1].job.sha, '7654321');
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import http from 'node:http';
|
||||
import net from 'node:net';
|
||||
import crypto from 'node:crypto';
|
||||
import { createProxy } from '../src/proxy.js';
|
||||
|
||||
const config = { baseHost: 'project.webklar.com' };
|
||||
|
||||
// Test-Ziel: HTTP-Echo + WebSocket-Echo per rohem Upgrade
|
||||
function startTarget() {
|
||||
const seen = [];
|
||||
const srv = http.createServer((req, res) => {
|
||||
const chunks = [];
|
||||
req.on('data', (c) => chunks.push(c));
|
||||
req.on('end', () => {
|
||||
seen.push({ method: req.method, url: req.url, headers: req.headers });
|
||||
res.writeHead(201, { 'X-Test': 'ja' });
|
||||
res.end(`${req.method} ${req.url} ${Buffer.concat(chunks).toString()}`);
|
||||
});
|
||||
});
|
||||
srv.on('upgrade', (req, socket) => {
|
||||
const acc = crypto.createHash('sha1').update(`${req.headers['sec-websocket-key']}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest('base64');
|
||||
socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${acc}\r\n\r\n`);
|
||||
socket.on('data', (d) => socket.write(d)); // Echo
|
||||
socket.on('end', () => socket.end());
|
||||
socket.on('error', () => {});
|
||||
});
|
||||
return new Promise((r) => srv.listen(0, '127.0.0.1', () => r({ srv, seen, port: srv.address().port })));
|
||||
}
|
||||
|
||||
async function withProxy(devImpl, fn, opts = {}) {
|
||||
const touched = [];
|
||||
const dev = { touch: (s) => touched.push(s), ...devImpl };
|
||||
const proxy = createProxy({ config, dev, ...opts });
|
||||
await new Promise((r) => proxy.listen(0, '127.0.0.1', r));
|
||||
try { await fn({ port: proxy.address().port, touched }); } finally { proxy.closeAllConnections?.(); await new Promise((r) => proxy.close(r)); }
|
||||
}
|
||||
|
||||
const get = (port, host, { method = 'GET', path = '/', body } = {}) => new Promise((resolve, reject) => {
|
||||
const req = http.request({ port, host: '127.0.0.1', method, path, headers: { Host: host } }, (res) => {
|
||||
const c = []; res.on('data', (d) => c.push(d)); res.on('end', () => resolve({ res, text: Buffer.concat(c).toString() }));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.end(body);
|
||||
});
|
||||
|
||||
test('online: Durchreichen inkl. POST-Body, X-Forwarded-*, Host, touch', async () => {
|
||||
const t = await startTarget();
|
||||
try {
|
||||
await withProxy({ wake: async (s) => ({ state: 'online', target: `http://127.0.0.1:${t.port}` }) }, async ({ port, touched }) => {
|
||||
const { res, text } = await get(port, 'demo.project.webklar.com:443', { method: 'POST', path: '/a?b=1', body: 'hallo' });
|
||||
assert.equal(res.statusCode, 201);
|
||||
assert.equal(res.headers['x-test'], 'ja');
|
||||
assert.equal(text, 'POST /a?b=1 hallo');
|
||||
assert.deepEqual(touched, ['demo']);
|
||||
const h = t.seen[0].headers;
|
||||
assert.equal(h.host, 'demo.project.webklar.com:443');
|
||||
assert.equal(h['x-forwarded-host'], 'demo.project.webklar.com:443');
|
||||
assert.equal(h['x-forwarded-proto'], 'https');
|
||||
assert.ok(h['x-forwarded-for']);
|
||||
});
|
||||
} finally { t.srv.closeAllConnections?.(); await new Promise((r) => t.srv.close(r)); }
|
||||
});
|
||||
|
||||
test('WebSocket-Upgrade wird durchgereicht (Echo)', async () => {
|
||||
const t = await startTarget();
|
||||
try {
|
||||
await withProxy({ wake: async () => ({ state: 'online', target: `http://127.0.0.1:${t.port}` }) }, async ({ port, touched }) => {
|
||||
const sock = net.connect(port, '127.0.0.1');
|
||||
sock.on('error', () => {});
|
||||
let buf = '';
|
||||
const got = new Promise((resolve) => sock.on('data', (d) => { buf += d.toString('latin1'); if (buf.includes('\r\n\r\n') && buf.endsWith('ping-pong')) resolve(); }));
|
||||
sock.write('GET /ws HTTP/1.1\r\nHost: demo.project.webklar.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n');
|
||||
await new Promise((r) => { const i = setInterval(() => { if (buf.includes('\r\n\r\n')) { clearInterval(i); r(); } }, 5); });
|
||||
assert.match(buf, /^HTTP\/1\.1 101/);
|
||||
try {
|
||||
sock.write('ping-pong');
|
||||
await got;
|
||||
assert.ok(buf.endsWith('ping-pong'));
|
||||
assert.deepEqual(touched, ['demo']);
|
||||
} finally { sock.destroy(); }
|
||||
});
|
||||
} finally { t.srv.closeAllConnections?.(); await new Promise((r) => t.srv.close(r)); }
|
||||
});
|
||||
|
||||
test('starting ⇒ 503 mit Text, Retry-After und Meta-Refresh', async () => {
|
||||
await withProxy({ wake: async () => ({ state: 'starting' }) }, async ({ port, touched }) => {
|
||||
const { res, text } = await get(port, 'demo.project.webklar.com');
|
||||
assert.equal(res.statusCode, 503);
|
||||
assert.equal(res.headers['retry-after'], '3');
|
||||
assert.match(text, /Preview startet …/);
|
||||
assert.match(text, /<meta http-equiv="refresh" content="3">/);
|
||||
assert.deepEqual(touched, []);
|
||||
});
|
||||
});
|
||||
|
||||
test('failed ⇒ 503 mit Fehlerseite', async () => {
|
||||
await withProxy({ wake: async () => ({ state: 'failed' }) }, async ({ port }) => {
|
||||
const { res, text } = await get(port, 'demo.project.webklar.com');
|
||||
assert.equal(res.statusCode, 503);
|
||||
assert.match(text, /Preview konnte nicht starten/);
|
||||
assert.match(text, /Protokoll im WOMS/);
|
||||
assert.ok(!text.includes('http-equiv="refresh"'));
|
||||
});
|
||||
});
|
||||
|
||||
test('wake unknown ⇒ 503 „nicht verfügbar“', async () => {
|
||||
await withProxy({ wake: async () => ({ state: 'unknown' }) }, async ({ port }) => {
|
||||
const { res, text } = await get(port, 'demo.project.webklar.com');
|
||||
assert.equal(res.statusCode, 503);
|
||||
assert.match(text, /Diese Preview ist gerade nicht verfügbar\./);
|
||||
});
|
||||
});
|
||||
|
||||
test('Speicherschutz (wake wirft 503) ⇒ 503-Seite, kein Absturz', async () => {
|
||||
await withProxy({ wake: async () => { throw Object.assign(new Error('Server ausgelastet'), { status: 503 }); } }, async ({ port }) => {
|
||||
const { res, text } = await get(port, 'demo.project.webklar.com');
|
||||
assert.equal(res.statusCode, 503);
|
||||
assert.match(text, /ausgelastet/);
|
||||
});
|
||||
});
|
||||
|
||||
test('fremder Host, Großschreibung, ungültiger sub, mehrstufig ⇒ 404 ohne wake', async () => {
|
||||
let woken = 0;
|
||||
await withProxy({ wake: async () => { woken++; return { state: 'starting' }; } }, async ({ port }) => {
|
||||
for (const h of ['evil.example.com', 'project.webklar.com', 'Demo.project.webklar.com', 'a_b.project.webklar.com', 'a.b.project.webklar.com', '.project.webklar.com', 'demo.project.webklar.com.evil.com', '']) {
|
||||
const { res } = await get(port, h);
|
||||
assert.equal(res.statusCode, 404, h);
|
||||
}
|
||||
assert.equal(woken, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('Upstream-Fehler ⇒ 502 deutscher Text', async () => {
|
||||
const dead = net.createServer(); await new Promise((r) => dead.listen(0, '127.0.0.1', r));
|
||||
const port0 = dead.address().port; await new Promise((r) => dead.close(r));
|
||||
await withProxy({ wake: async () => ({ state: 'online', target: `http://127.0.0.1:${port0}` }) }, async ({ port }) => {
|
||||
const { res, text } = await get(port, 'demo.project.webklar.com');
|
||||
assert.equal(res.statusCode, 502);
|
||||
assert.match(text, /nicht erreichbar/);
|
||||
});
|
||||
});
|
||||
@@ -17,8 +17,10 @@ const jobs = {
|
||||
queueLength: () => 0,
|
||||
};
|
||||
const stopped = [];
|
||||
const registered = [];
|
||||
const dev = {
|
||||
ensure: async (sub) => ({ sub, state: 'starting' }),
|
||||
register: async (sub, j) => { registered.push([sub, j]); return { state: 'sleeping', extra: 'x' }; },
|
||||
stop: async (sub) => { stopped.push(sub); },
|
||||
list: () => [],
|
||||
};
|
||||
@@ -133,3 +135,19 @@ test('readMemAvailable liest MemAvailable in Bytes', () => {
|
||||
writeFileSync(p, 'MemTotal: 8000000 kB\nMemFree: 100000 kB\nMemAvailable: 2048000 kB\n');
|
||||
assert.equal(readMemAvailable(p), 2048000 * 1024);
|
||||
});
|
||||
|
||||
test('PUT /dev/:sub: Auth, 400, 200 ⇒ register', async () => {
|
||||
await withServer(config, async (base) => {
|
||||
const put = (path, headers, body) => fetch(base + path, { method: 'PUT', headers, body: JSON.stringify(body) });
|
||||
assert.equal((await put('/dev/demo', { 'Content-Type': 'application/json' }, good)).status, 401);
|
||||
assert.equal((await put('/dev/demo', auth, { ...good, sub: 'andere' })).status, 400);
|
||||
assert.equal((await put('/dev/demo', auth, { ...good, sha: 'zz' })).status, 400);
|
||||
assert.equal((await put('/dev/DEMO', auth, good)).status, 400);
|
||||
assert.equal(registered.length, 0);
|
||||
const r = await put('/dev/demo', auth, good);
|
||||
assert.equal(r.status, 200);
|
||||
assert.deepEqual(await r.json(), { state: 'sleeping' });
|
||||
assert.equal(registered[0][0], 'demo');
|
||||
assert.equal(registered[0][1].repo, 'JUSN/demo');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user