dev-mcps/dev-server-mcp/test/preview-proxy.test.js

184 lines
7.5 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import http from "node:http";
import { PreviewProxy } from "../src/preview-proxy.js";
/** A stand-in for a previewed application: answers with the path it was actually asked for. */
async function application(body = "the app") {
const server = http.createServer((request, response) => {
response.writeHead(200, { "content-type": "text/plain" });
response.end(`${body} at ${request.url}`);
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
return { server, port: server.address().port, close: () => new Promise((r) => server.close(r)) };
}
async function proxyFor(instances) {
const touched = [];
const proxy = new PreviewProxy({
port: 0,
resolve: (key) => {
const found = instances[key];
return found ? { port: found, touch: () => touched.push(key) } : null;
}
});
await proxy.listen();
return { proxy, touched, port: proxy.server.address().port };
}
function fetchNoRedirect(url, headers = {}) {
return fetch(url, { headers, redirect: "manual" });
}
test("a valid token is exchanged for a cookie, and the token leaves the address bar", async () => {
// The token travels in a link a person was given; leaving it in the URL would send it onward in
// Referer to whatever the previewed page links to.
const app = await application();
const { proxy, port } = await proxyFor({ "exec-a": app.port });
try {
const token = proxy.issueToken("exec-a");
const answer = await fetchNoRedirect(`http://127.0.0.1:${port}/exec-a/?t=${token}`);
assert.equal(answer.status, 302);
// The path the browser is at, not the one this proxy receives. The gateway strips /preview
// before forwarding, so redirecting to what arrived would send the browser out of the mount
// and into the gateway's own 404 - which is exactly what happened the first time.
assert.equal(answer.headers.get("location"), "/preview/exec-a/");
const cookie = answer.headers.get("set-cookie");
assert.match(cookie, /dev_server_preview_exec-a=/);
assert.match(cookie, /HttpOnly/);
assert.match(cookie, /Path=\/preview\/exec-a\//, "the cookie must not be usable on another preview");
} finally { await proxy.close(); await app.close(); }
});
test("the cookie from one preview does not open another", async () => {
const app = await application();
const { proxy, port } = await proxyFor({ "exec-a": app.port, "exec-b": app.port });
try {
const tokenA = proxy.issueToken("exec-a");
proxy.issueToken("exec-b");
const answer = await fetchNoRedirect(`http://127.0.0.1:${port}/exec-b/`, {
cookie: `dev_server_preview_exec-a=${tokenA}`
});
assert.equal(answer.status, 403);
} finally { await proxy.close(); await app.close(); }
});
test("an execution key alone is not enough to open a preview", async () => {
// rootExecutionId is on screen in the editor and in every event the flow logs. If knowing it
// were sufficient, the preview would effectively be public.
const app = await application();
const { proxy, port } = await proxyFor({ "exec-a": app.port });
try {
proxy.issueToken("exec-a");
const answer = await fetchNoRedirect(`http://127.0.0.1:${port}/exec-a/`);
assert.equal(answer.status, 403);
} finally { await proxy.close(); await app.close(); }
});
test("a wrong token is refused", async () => {
const app = await application();
const { proxy, port } = await proxyFor({ "exec-a": app.port });
try {
proxy.issueToken("exec-a");
const answer = await fetchNoRedirect(`http://127.0.0.1:${port}/exec-a/?t=not-the-token`);
assert.equal(answer.status, 403);
} finally { await proxy.close(); await app.close(); }
});
test("a restart invalidates the link the previous start handed out", async () => {
const app = await application();
const { proxy, port } = await proxyFor({ "exec-a": app.port });
try {
const first = proxy.issueToken("exec-a");
proxy.issueToken("exec-a");
const answer = await fetchNoRedirect(`http://127.0.0.1:${port}/exec-a/?t=${first}`);
assert.equal(answer.status, 403);
} finally { await proxy.close(); await app.close(); }
});
test("an authorised request reaches the application at the path it expects", async () => {
// The app was not written to live under a prefix, so what reaches it must be the path it would
// see at the root - the key and the /preview mount are ours, not its business.
const app = await application("inner");
const { proxy, port } = await proxyFor({ "exec-a": app.port });
try {
const token = proxy.issueToken("exec-a");
const answer = await fetch(`http://127.0.0.1:${port}/exec-a/orders?page=2`, {
headers: { cookie: `dev_server_preview_exec-a=${token}` }
});
assert.equal(answer.status, 200);
assert.equal(await answer.text(), "inner at /orders?page=2");
} finally { await proxy.close(); await app.close(); }
});
test("reading the preview keeps it alive", async () => {
// A person takes hours where a model takes seconds. Without this the instance's own idle clock
// would reclaim a preview somebody is still looking at.
const app = await application();
const { proxy, touched, port } = await proxyFor({ "exec-a": app.port });
try {
const token = proxy.issueToken("exec-a");
await fetch(`http://127.0.0.1:${port}/exec-a/`, { headers: { cookie: `dev_server_preview_exec-a=${token}` } });
assert.deepEqual(touched, ["exec-a"]);
} finally { await proxy.close(); await app.close(); }
});
test("an execution with nothing running says so, rather than failing obscurely", async () => {
const { proxy, port } = await proxyFor({});
try {
const answer = await fetchNoRedirect(`http://127.0.0.1:${port}/exec-missing/`);
assert.equal(answer.status, 404);
assert.match(await answer.text(), /No preview is running/);
} finally { await proxy.close(); }
});
test("a key that is not one safe segment is refused before anything is looked up", async () => {
const { proxy, port } = await proxyFor({});
try {
for (const path of ["/", "/..%2fetc/", "/with%20space/"]) {
const answer = await fetchNoRedirect(`http://127.0.0.1:${port}${path}`);
assert.equal(answer.status, 404, `accepted ${path}`);
}
} finally { await proxy.close(); }
});
test("an application that has died answers as a gateway failure, not a hang", async () => {
const app = await application();
const dead = app.port;
await app.close();
const { proxy, port } = await proxyFor({ "exec-a": dead });
try {
const token = proxy.issueToken("exec-a");
const answer = await fetchNoRedirect(`http://127.0.0.1:${port}/exec-a/`, {
cookie: `dev_server_preview_exec-a=${token}`
});
assert.equal(answer.status, 502);
} finally { await proxy.close(); }
});
test("the mount the gateway publishes is one setting, used by both the redirect and the cookie", async () => {
// These were written separately once and disagreed: the cookie assumed /preview, the redirect
// did not, and the link led out of the mount. One parameter now, so they cannot drift again.
const app = await application();
const proxy = new PreviewProxy({ port: 0, mountPath: "/elsewhere", resolve: () => ({ port: app.port }) });
await proxy.listen();
try {
const token = proxy.issueToken("exec-a");
const answer = await fetchNoRedirect(`http://127.0.0.1:${proxy.server.address().port}/exec-a/?t=${token}`);
assert.equal(answer.headers.get("location"), "/elsewhere/exec-a/");
assert.match(answer.headers.get("set-cookie"), /Path=\/elsewhere\/exec-a\//);
} finally { await proxy.close(); await app.close(); }
});