123 lines
6.4 KiB
TypeScript
123 lines
6.4 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { forwardingRoute, trustedForwardRoute } from '../src/forwarding/routes.ts';
|
|
import { ForwardingState } from '../src/forwarding/state.ts';
|
|
|
|
const sessionId = 'a'.repeat(64);
|
|
|
|
function makeEnv(previewOrigin?: string, appOrigin?: string) {
|
|
const calls: Array<{ request: Request; id: string }> = [];
|
|
const env = {
|
|
PREVIEW_ORIGIN: previewOrigin,
|
|
APP_ORIGIN: appOrigin,
|
|
SSH_SESSIONS: {
|
|
idFromString(id: string) { return id; },
|
|
get(id: string) {
|
|
return { fetch(request: Request) { calls.push({ request, id }); return Promise.resolve(new Response('ok')); } };
|
|
},
|
|
},
|
|
} as never;
|
|
return { env, calls };
|
|
}
|
|
|
|
test('GET reports preview availability only when configured', async () => {
|
|
assert.deepEqual(await (await forwardingRoute(new Request('https://main.test/api/forward'), makeEnv().env, 'acct')).json(), { previewAvailable: false });
|
|
assert.deepEqual(await (await forwardingRoute(new Request('https://main.test/api/forward'), makeEnv('https://preview.test').env, 'acct')).json(), { previewAvailable: true });
|
|
});
|
|
|
|
test('GET with a session queries the owned Durable Object', async () => {
|
|
const state = makeEnv();
|
|
const response = await forwardingRoute(new Request(`https://main.test/api/forward?session=${sessionId}`), state.env, 'acct');
|
|
assert.equal(response.status, 200);
|
|
assert.equal(state.calls.length, 1);
|
|
assert.equal(state.calls[0].id, sessionId);
|
|
assert.equal(state.calls[0].request.method, 'GET');
|
|
assert.equal(state.calls[0].request.headers.get('x-account-id'), 'acct');
|
|
assert.equal((await forwardingRoute(new Request('https://main.test/api/forward?session=bad'), state.env, 'acct')).status, 400);
|
|
});
|
|
|
|
test('trusted POST requires explicit confirmation and forwards trusted mode', async () => {
|
|
const missing = makeEnv();
|
|
const rejected = await forwardingRoute(new Request(`https://main.test/api/forward?session=${sessionId}`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ port: 80 }),
|
|
}), missing.env, 'acct');
|
|
assert.equal(rejected.status, 400); assert.equal(missing.calls.length, 0);
|
|
|
|
const configured = makeEnv();
|
|
const accepted = await forwardingRoute(new Request(`https://main.test/api/forward?session=${sessionId}`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ port: 80, trusted: true }),
|
|
}), configured.env, 'acct');
|
|
assert.equal(accepted.status, 200); assert.equal(configured.calls.length, 1);
|
|
assert.equal(await configured.calls[0].request.text(), JSON.stringify({ port: 80, mode: 'trusted' }));
|
|
assert.equal(configured.calls[0].request.headers.get('x-preview-origin'), 'https://main.test');
|
|
});
|
|
|
|
test('isolated mode without preview configuration returns 503 and invalid mode returns 400', async () => {
|
|
const isolated = makeEnv();
|
|
const result = await forwardingRoute(new Request(`https://main.test/api/forward?session=${sessionId}`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ port: 80, mode: 'isolated' }),
|
|
}), isolated.env, 'acct');
|
|
assert.equal(result.status, 503); assert.equal(isolated.calls.length, 0);
|
|
const invalid = await forwardingRoute(new Request(`https://main.test/api/forward?session=${sessionId}`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ port: 80, mode: 'other', trusted: true }),
|
|
}), isolated.env, 'acct');
|
|
assert.equal(invalid.status, 400);
|
|
});
|
|
|
|
test('DELETE works without preview configuration', async () => {
|
|
const state = makeEnv();
|
|
const result = await forwardingRoute(new Request(`https://main.test/api/forward?session=${sessionId}`, { method: 'DELETE' }), state.env, 'acct');
|
|
assert.equal(result.status, 200); assert.equal(state.calls.length, 1);
|
|
});
|
|
|
|
test('trusted forwarding validates session, strips token, derives path, and preserves body', async () => {
|
|
const state = makeEnv();
|
|
// 传入可复用的字符串 body,避免 Node 对转发 ReadableStream 强制要求 duplex: half。
|
|
const request = {
|
|
method: 'POST', url: `https://main.test/_forward/${sessionId}/form?a=1`,
|
|
headers: new Headers({ 'x-account-id': 'forged', 'x-preview-token': 'secret' }), body: 'payload',
|
|
} as unknown as Request;
|
|
assert.equal((await trustedForwardRoute(request, state.env, 'real-account')).status, 200);
|
|
const forwarded = state.calls[0].request;
|
|
assert.equal(forwarded.headers.get('x-account-id'), 'real-account');
|
|
assert.equal(forwarded.headers.get('x-preview-token'), null);
|
|
assert.equal(forwarded.headers.get('x-preview-path'), '/form?a=1');
|
|
assert.equal(await forwarded.text(), 'payload');
|
|
assert.equal((await trustedForwardRoute(new Request('https://main.test/_forward/not-hex/form'), state.env, 'acct')).status, 404);
|
|
assert.equal((await trustedForwardRoute(new Request(`https://main.test/_forward/${sessionId}/form`, { headers: { 'Service-Worker': 'script' } }), state.env, 'acct')).status, 403);
|
|
});
|
|
|
|
function fakeSession() {
|
|
let ready = true;
|
|
return {
|
|
isForwardReady: () => ready,
|
|
openForward: async () => ({ close: async () => {} }),
|
|
close: () => { ready = false; },
|
|
} as never;
|
|
}
|
|
|
|
test('ForwardingState rejects cross-mode grants and clear invalidates both paths', async () => {
|
|
const state = new ForwardingState();
|
|
await state.create(fakeSession(), 80, 'https://main.test', sessionId, 'isolated');
|
|
assert.equal((await state.trusted(new Request('https://main.test/'))).status, 410);
|
|
state.clear();
|
|
await state.create(fakeSession(), 80, 'https://main.test', sessionId, 'trusted');
|
|
assert.equal((await state.preview(new Request('https://main.test/'))).status, 410);
|
|
state.clear();
|
|
assert.equal((await state.trusted(new Request('https://main.test/'))).status, 410);
|
|
assert.equal((await state.preview(new Request('https://main.test/'))).status, 410);
|
|
});
|
|
|
|
test('ForwardingState reports restorable metadata and applies the retention deadline', async () => {
|
|
const state = new ForwardingState();
|
|
const session = fakeSession();
|
|
const retainedUntil = Date.now() + 8 * 60_000;
|
|
await state.create(session, 8080, 'https://main.test', sessionId, 'trusted', retainedUntil);
|
|
assert.equal(state.owns(session), true);
|
|
assert.deepEqual(state.status(retainedUntil), {
|
|
active: true, port: 8080, mode: 'trusted', expiresAt: retainedUntil,
|
|
});
|
|
state.stop();
|
|
assert.deepEqual(state.status(), { active: false });
|
|
});
|