Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ project adheres to [Semantic Versioning](https://semver.org/).
- Playwright setup note in the Quick start section of `README.md`.

### Fixed
- Saved-case GUI paths with malformed percent-encoding now return HTTP 400.
`decodeURIComponent` used to throw, and the request became HTTP 500.
- The workbench comparison panel now says the comparison is unavailable when
the pair is not comparable. It previously said no compared field differs,
which disagreed with the Markdown download for the same result.
Expand Down
14 changes: 11 additions & 3 deletions bin/aas-gui.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,14 @@ compareButton.addEventListener('click',async()=>{
</script></body></html>`;
}

function savedPathId(pathname, prefix) {
try {
return decodeURIComponent(pathname.slice(prefix.length));
} catch {
return null;
}
}

export function hasOnlySingleOptions(params, allowed) {
return [...params.keys()].every((key) => allowed.has(key) && params.getAll(key).length === 1);
}
Expand Down Expand Up @@ -717,7 +725,7 @@ export function createGuiServer({
return;
}
if (request.method === "POST" && url.pathname.startsWith("/api/replay-saved/")) {
const runId = decodeURIComponent(url.pathname.slice("/api/replay-saved/".length));
const runId = savedPathId(url.pathname, "/api/replay-saved/");
if (!isReviewRunId(runId) || url.search) { sendJson(response, 400, { error: "Invalid saved verification request." }); return; }
try {
assertFullStackNodeVersion(
Expand Down Expand Up @@ -821,7 +829,7 @@ export function createGuiServer({
return;
}
if (request.method === "GET" && url.pathname.startsWith("/api/review/")) {
const runId = decodeURIComponent(url.pathname.slice("/api/review/".length));
const runId = savedPathId(url.pathname, "/api/review/");
if (!isReviewRunId(runId) || url.search) { sendJson(response, 400, { error: "Invalid case review request." }); return; }
let content;
try { content = renderCaseMarkdown(inspectCase(runId, { outputRoot })); }
Expand All @@ -830,7 +838,7 @@ export function createGuiServer({
response.end(content); return;
}
if (request.method === "GET" && url.pathname.startsWith("/api/bundle/")) {
const runId = decodeURIComponent(url.pathname.slice("/api/bundle/".length));
const runId = savedPathId(url.pathname, "/api/bundle/");
if (!isReviewRunId(runId)) { sendJson(response, 400, { error: "Invalid saved run ID." }); return; }
let bundle;
try { bundle = exportRunBundle(runId, { outputRoot }); }
Expand Down
19 changes: 19 additions & 0 deletions test/gui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,25 @@ test("GUI compare refuses a symlinked runs directory", (t) => {
});
});

test("malformed percent-encoding in a saved-case path is a client error", async () => {
const server = createGuiServer({ outputRoot: mkdtempSync(join(tmpdir(), "aas-gui-pct-")) });
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const origin = `http://127.0.0.1:${server.address().port}`;
try {
for (const path of ["/api/bundle/%ZZ", "/api/review/%E0%A4%A", "/api/replay-saved/%"]) {
const response = await requestServer(server, path, {
method: path.startsWith("/api/replay-saved/") ? "POST" : "GET",
headers: { origin },
});
assert.equal(response.status, 400, path);
assert.match(response.body, /Invalid/);
assert.doesNotMatch(response.body, /Request failed/);
}
} finally {
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});

test("GUI exposes a domain selector defaulting to refund", () => {
const page = renderPage();
assert.match(page, /<select id="domain">/);
Expand Down
Loading