Files
apex.ermnvldmr.com/assets/composition-failover/worker.test.mjs
T

266 lines
13 KiB
JavaScript

import assert from 'node:assert/strict';
import { test } from 'node:test';
import worker from './worker.mjs';
const env = {
APEX_PUBLIC_HOST: 'sso.example.com',
APEX_ZONE: 'example.com',
APEX_ORIGIN_PRIMARY: 'primary.example.com',
APEX_ORIGIN_SECONDARY: 'secondary.example.com',
APEX_READY_PATH: '/.well-known/apex-ready',
APEX_READY_TOKEN: 'test-only-readiness-secret',
};
const url = 'https://sso.example.com/api/callback?state=a%2Fb';
const ready = () => new Response(null, { status: 204 });
function intercept(t, handler) {
const calls = [];
t.mock.method(globalThis, 'fetch', async (request, options) => {
calls.push({ request, options });
return handler(request, options, calls.length);
});
return calls;
}
function checkProbe(call, origin) {
assert.equal(call.request.url, 'https://sso.example.com/.well-known/apex-ready');
assert.equal(call.request.method, 'GET');
assert.equal(call.request.headers.get('X-APEX-Ready-Token'), env.APEX_READY_TOKEN);
assert.equal(call.request.headers.get('Cookie'), null);
assert.equal(call.request.headers.get('Authorization'), null);
assert.equal(call.options.cf.resolveOverride, origin);
assert.equal(call.options.redirect, 'manual');
assert.equal(call.options.cache, 'no-store');
assert.deepEqual(call.options.cf.cacheTtlByStatus, { '100-599': -1 });
}
test('ready primary receives the original POST exactly once with credentials and body', async (t) => {
const calls = intercept(t, async (request, options, count) => {
if (count === 1) return ready();
assert.equal(await request.text(), 'code=one-use&state=a%2Fb');
return new Response('accepted', { status: 201 });
});
const request = new Request(url, {
method: 'POST', body: 'code=one-use&state=a%2Fb',
headers: { Cookie: 'session=original', Authorization: 'Bearer original',
'X-APEX-Ready-Token': 'client-supplied', 'Content-Type': 'application/x-www-form-urlencoded' },
});
const response = await worker.fetch(request, env);
assert.equal(response.status, 201);
assert.equal(await response.text(), 'accepted');
assert.equal(calls.length, 2);
checkProbe(calls[0], env.APEX_ORIGIN_PRIMARY);
const forwarded = calls[1];
assert.equal(forwarded.request.url, url);
assert.equal(forwarded.request.method, 'POST');
assert.equal(forwarded.request.headers.get('Cookie'), 'session=original');
assert.equal(forwarded.request.headers.get('Authorization'), 'Bearer original');
assert.equal(forwarded.request.headers.get('X-APEX-Ready-Token'), null);
assert.equal(forwarded.options.cf.resolveOverride, env.APEX_ORIGIN_PRIMARY);
assert.equal(forwarded.options.redirect, 'manual');
assert.equal(forwarded.options.cache, 'no-store');
assert.match(response.headers.get('Cache-Control'), /no-store/);
});
test('unready primary cancels unread body and selects ready secondary', async (t) => {
let canceled = false;
const calls = intercept(t, (_, __, count) => count === 1
? new Response(new ReadableStream({ cancel() { canceled = true; } }), { status: 503 })
: count === 2 ? ready() : new Response('secondary'));
const response = await worker.fetch(new Request(url), env);
assert.equal(await response.text(), 'secondary');
assert.equal(canceled, true);
assert.equal(calls.length, 3);
checkProbe(calls[1], env.APEX_ORIGIN_SECONDARY);
assert.equal(calls[2].options.cf.resolveOverride, env.APEX_ORIGIN_SECONDARY);
});
test('probe deadline aborts at five seconds even if fetch ignores cancellation', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
let finishPrimary;
let canceled = false;
const calls = intercept(t, (_, __, count) => count === 1
? new Promise((resolve) => { finishPrimary = resolve; })
: count === 2 ? ready() : new Response('secondary'));
const pending = worker.fetch(new Request(url), env);
await Promise.resolve();
t.mock.timers.tick(4999);
assert.equal(calls.length, 1);
assert.equal(calls[0].options.signal.aborted, false);
t.mock.timers.tick(1);
const response = await pending;
assert.equal(await response.text(), 'secondary');
assert.equal(calls[0].options.signal.aborted, true);
finishPrimary(new Response(new ReadableStream({ cancel() { canceled = true; } })));
await new Promise((resolve) => setImmediate(resolve));
assert.equal(canceled, true);
assert.equal(calls.length, 3);
});
test('two unavailable origins return no-store 503 without client dispatch', async (t) => {
const calls = intercept(t, () => new Response('down', { status: 503 }));
const response = await worker.fetch(new Request(url), env);
assert.equal(response.status, 503);
assert.match(response.headers.get('Cache-Control'), /no-store/);
assert.equal(calls.length, 2);
assert.ok(calls.every(({ request }) => new URL(request.url).pathname === env.APEX_READY_PATH));
});
test('readiness redirect is never accepted or followed', async (t) => {
const calls = intercept(t, (_, __, count) => count === 1
? new Response(null, { status: 302, headers: { Location: 'https://evil.example/' } })
: count === 2 ? ready() : new Response('secondary'));
assert.equal(await (await worker.fetch(new Request(url), env)).text(), 'secondary');
assert.equal(calls.length, 3);
assert.equal(calls[0].options.redirect, 'manual');
});
test('application health can explicitly require 200 while 204 remains the default', async (t) => {
const bindings = { ...env, APEX_READY_STATUS: '200' };
const calls = intercept(t, (_, __, count) => count === 1
? new Response(null, { status: 204 })
: count === 2 ? new Response('Healthy', { status: 200 }) : new Response('secondary'));
const response = await worker.fetch(new Request(url), bindings);
assert.equal(await response.text(), 'secondary');
assert.equal(calls.length, 3);
assert.equal(calls[2].options.cf.resolveOverride, env.APEX_ORIGIN_SECONDARY);
});
test('an unconfigured 200 health response cannot release application traffic', async (t) => {
const calls = intercept(t, () => new Response('login page', { status: 200 }));
assert.equal((await worker.fetch(new Request(url), env)).status, 503);
assert.equal(calls.length, 2);
});
for (const method of ['GET', 'POST']) {
test(`failed ${method} dispatch is never replayed on another origin`, async (t) => {
const calls = intercept(t, (_, __, count) => {
if (count === 1) return ready();
throw new DOMException('origin timed out', 'TimeoutError');
});
const response = await worker.fetch(new Request(url, { method,
...(method === 'POST' ? { body: 'one-use' } : {}) }), env);
assert.equal(response.status, 502);
assert.match(response.headers.get('Cache-Control'), /no-store/);
assert.equal(calls.length, 2);
});
}
test('origin errors preserve status and body without probing secondary', async (t) => {
const calls = intercept(t, (_, __, count) => count === 1 ? ready() : new Response('origin error', { status: 500 }));
const response = await worker.fetch(new Request(url), env);
assert.equal(response.status, 500);
assert.equal(await response.text(), 'origin error');
assert.equal(calls.length, 2);
});
test('auth redirect retains distinct Set-Cookie headers and Location but cannot cache', async (t) => {
const cookies = ['session=first; Secure; HttpOnly', 'state=second; Expires=Wed, 21 Oct 2030 07:28:00 GMT'];
const headers = new Headers({ Location: '/continue?state=a%2Fb', 'Cache-Control': 'public, max-age=3600' });
for (const cookie of cookies) headers.append('Set-Cookie', cookie);
const calls = intercept(t, (_, __, count) => count === 1 ? ready() : new Response(null, { status: 302, headers }));
const response = await worker.fetch(new Request(url), env);
assert.equal(response.status, 302);
assert.equal(response.headers.get('Location'), '/continue?state=a%2Fb');
assert.deepEqual(response.headers.getSetCookie(), cookies);
assert.match(response.headers.get('Cache-Control'), /no-store/);
assert.equal(response.headers.get('Cloudflare-CDN-Cache-Control'), 'no-store');
assert.equal(calls.length, 2);
});
test('hostile hosts and public readiness requests never reach origins', async (t) => {
const calls = intercept(t, () => { throw new Error('must not dispatch'); });
for (const target of ['https://evil.example/', 'https://sso.example.com.evil.example/', 'https://sso.example.com:444/']) {
assert.equal((await worker.fetch(new Request(target), env)).status, 421);
}
for (const path of [env.APEX_READY_PATH, env.APEX_READY_PATH + '?probe=1', '/.well-known/%61pex-ready']) {
const response = await worker.fetch(new Request('https://sso.example.com' + path), env);
assert.equal(response.status, 404);
assert.match(response.headers.get('Cache-Control'), /no-store/);
}
assert.equal(calls.length, 0);
});
test('missing and invalid bindings fail closed without diagnostics or probes', async (t) => {
const calls = intercept(t, () => { throw new Error('must not dispatch'); });
const invalid = Object.keys(env).map((name) => ({ ...env, [name]: undefined }));
for (const [name, value] of [
['APEX_ZONE', 'com'], ['APEX_PUBLIC_HOST', 'https://sso.example.com'],
['APEX_ORIGIN_PRIMARY', 'primary.other.example'], ['APEX_ORIGIN_PRIMARY', env.APEX_PUBLIC_HOST],
['APEX_ORIGIN_PRIMARY', env.APEX_ORIGIN_SECONDARY], ['APEX_ORIGIN_SECONDARY', '127.0.0.1'],
['APEX_ORIGIN_SECONDARY', '-invalid.example.com'], ['APEX_ORIGIN_PRIMARY', 'a..example.com'],
['APEX_READY_PATH', '//evil.example/'], ['APEX_READY_PATH', '/ready?query=yes'],
['APEX_READY_PATH', '/ready#fragment'], ['APEX_READY_PATH', '/a/../ready'],
['APEX_READY_PATH', '/%72eady'], ['APEX_READY_TOKEN', 'secret\r\ninjected: yes'],
['APEX_READY_STATUS', '302'], ['APEX_READY_STATUS', '401'],
['APEX_READY_STATUS', '503'], ['APEX_READY_STATUS', '2xx'],
['APEX_READY_STATUS', ''], ['APEX_READY_STATUS', 200], ['APEX_READY_STATUS', null],
]) invalid.push({ ...env, [name]: value });
invalid.push(null);
for (const bindings of invalid) {
const response = await worker.fetch(new Request(url), bindings);
assert.equal(response.status, 503);
assert.match(response.headers.get('Cache-Control'), /no-store/);
assert.equal(await response.text(), 'Service unavailable');
}
assert.equal(calls.length, 0);
});
test('response remains streaming without buffering the body', async (t) => {
let controller;
const body = new ReadableStream({ start(value) { controller = value; } });
intercept(t, (_, __, count) => count === 1 ? ready() : new Response(body));
const response = await worker.fetch(new Request(url), env);
const reader = response.body.getReader();
controller.enqueue(new TextEncoder().encode('first'));
assert.equal(new TextDecoder().decode((await reader.read()).value), 'first');
controller.close();
assert.equal((await reader.read()).done, true);
});
test('failed readiness fetch selects secondary without exposing exception details', async (t) => {
const calls = intercept(t, (_, __, count) => {
if (count === 1) throw new Error(env.APEX_READY_TOKEN);
return count === 2 ? ready() : new Response('secondary');
});
const response = await worker.fetch(new Request(url), env);
assert.equal(await response.text(), 'secondary');
assert.equal(calls.length, 3);
});
test('secondary probe has its own five-second deadline and no client dispatch follows', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const calls = intercept(t, () => new Promise(() => {}));
const pending = worker.fetch(new Request(url), env);
t.mock.timers.tick(5000);
await new Promise((resolve) => setImmediate(resolve));
assert.equal(calls.length, 2);
assert.equal(calls[0].options.signal.aborted, true);
assert.equal(calls[1].options.signal.aborted, false);
t.mock.timers.tick(4999);
assert.equal(calls[1].options.signal.aborted, false);
t.mock.timers.tick(1);
const response = await pending;
assert.equal(response.status, 503);
assert.equal(calls[1].options.signal.aborted, true);
assert.equal(calls.length, 2);
});
test('unread probe cancellation cannot stall selection', async (t) => {
const calls = intercept(t, (_, __, count) => count === 1
? new Response(new ReadableStream({ cancel() { return new Promise(() => {}); } }), { status: 500 })
: count === 2 ? ready() : new Response('secondary'));
assert.equal(await (await worker.fetch(new Request(url), env)).text(), 'secondary');
assert.equal(calls.length, 3);
});
test('platform WebSocket upgrade response is returned unchanged', async (t) => {
// Node cannot construct status 101; the fetch boundary represents Workers' response.
const upgrade = { status: 101, webSocket: {}, headers: new Headers({ Upgrade: 'websocket' }) };
const calls = intercept(t, (_, __, count) => count === 1 ? ready() : upgrade);
const response = await worker.fetch(new Request(url, { headers: { Upgrade: 'websocket' } }), env);
assert.equal(response, upgrade);
assert.equal(calls.length, 2);
assert.equal(calls[1].request.headers.get('Upgrade'), 'websocket');
});