diff --git a/tests-js/bugreport-proxy-worker.test.mjs b/tests-js/bugreport-proxy-worker.test.mjs index 5739e2be0..cfcd1c310 100644 --- a/tests-js/bugreport-proxy-worker.test.mjs +++ b/tests-js/bugreport-proxy-worker.test.mjs @@ -118,3 +118,53 @@ test("non-error routing is untouched", async () => { new Request("https://proxy.example/update", { method: "GET" }), SECRET_ENV); assert.equal(unauth.status, 401, "a wrong/absent secret still 401s first"); }); + +const FIXTURE = join(dirname(fileURLToPath(import.meta.url)), + "..", "tests", "fixtures", "bugreport_contract", "automatic_report.md"); +const REPORT_ENV = { GITHUB_TOKEN: "g", TARGET_REPO: "a/b" }; + +// POST a report to the Worker with GitHub stubbed; returns the response, its JSON +// body, and every request the Worker sent upstream. +async function postReport(worker, payload) { + const realFetch = globalThis.fetch; + const upstream = []; + globalThis.fetch = async (u, init) => { + upstream.push({ url: String(u), body: init && init.body ? JSON.parse(init.body) : null }); + return new Response(JSON.stringify({ html_url: "https://github.com/a/b/issues/1", number: 1 }), + { status: 201, headers: { "Content-Type": "application/json" } }); + }; + try { + const res = await worker.fetch(new Request("https://proxy.example/", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) }), REPORT_ENV); + return { res, body: await res.json(), upstream }; + } finally { + globalThis.fetch = realFetch; + } +} + +test("a body without the report's sections is still filed, flagged in the note", async () => { + const worker = await loadWorker(); + for (const payload of [{ title: "%n", body: "%s%s%s" }, { title: "A".repeat(128), body: "name" }]) { + const { res, upstream } = await postReport(worker, payload); + assert.equal(res.status, 201, "a report is never refused for its shape"); + assert.equal(upstream.length, 1); + const filed = upstream[0].body.body; + assert.ok(filed.startsWith("> Filed through the localm bug reporter on behalf of a user."), filed.slice(0, 120)); + assert.match(filed, /does not have the sections a report built by localm contains/); + assert.ok(filed.endsWith(payload.body)); + } +}); + +test("a report built by localm is filed, prefixed with the reporter note", async () => { + const worker = await loadWorker(); + const report = readFileSync(FIXTURE, "utf8"); + const { res, body, upstream } = await postReport(worker, { title: "localm bug report", body: report }); + assert.equal(res.status, 201); + assert.equal(body.number, 1); + assert.equal(upstream.length, 1); + const filed = upstream[0].body.body; + assert.ok(filed.startsWith("> Filed through the localm bug reporter on behalf of a user."), filed.slice(0, 120)); + assert.ok(filed.endsWith(report), "the report follows the note unchanged"); + assert.doesNotMatch(filed, /does not have the sections/, "a real report is not flagged"); +}); \ No newline at end of file diff --git a/tools/bugreport-proxy/worker.js b/tools/bugreport-proxy/worker.js index 33ff69125..7e2c84d47 100644 --- a/tools/bugreport-proxy/worker.js +++ b/tools/bugreport-proxy/worker.js @@ -101,9 +101,10 @@ async function fileReport(request, env) { let payload; try { payload = await request.json(); } catch { return json({ error: "invalid JSON" }, 400); } const title = (String(payload.title || "").slice(0, 200).trim()) || "localm bug report"; - const body = String(payload.body || "").slice(0, 60000); - if (!body.trim()) return json({ error: "empty report body" }, 400); - const issue = { title, body }; + const report = String(payload.body || "").slice(0, 60000); + if (!report.trim()) return json({ error: "empty report body" }, 400); + const note = looksLikeLocalmReport(report) ? REPORTER_NOTE : REPORTER_NOTE_UNSHAPED; + const issue = { title, body: note + report }; if (env.ISSUE_LABELS) { const labels = env.ISSUE_LABELS.split(",").map((s) => s.trim()).filter(Boolean); if (labels.length) issue.labels = labels; @@ -119,6 +120,26 @@ async function fileReport(request, env) { return json({ ok: true, url: data.html_url, number: data.number }, 201); } +// Prepended to the body of every issue this proxy files. +const REPORTER_NOTE = + "> Filed through the localm bug reporter on behalf of a user. " + + "This text was not written by the repository owner.\n\n"; + +// Prepended instead of REPORTER_NOTE when the body lacks the REPORT_SECTIONS headings. +const REPORTER_NOTE_UNSHAPED = + "> Filed through the localm bug reporter on behalf of a user. " + + "This text was not written by the repository owner.\n" + + "> It does not have the sections a report built by localm contains.\n\n"; + +// Section headings every report built by localm's reporter contains. +const REPORT_SECTIONS = ["## What happened", "## Environment"]; + +// True when *text* has every heading in REPORT_SECTIONS on a line of its own. +function looksLikeLocalmReport(text) { + const lines = text.split(/\r?\n/).map((line) => line.trim()); + return REPORT_SECTIONS.every((h) => lines.includes(h)); +} + // ---------------------------- issues ------------------------------------ async function listIssues(request, env, url) {