import assert from "node:assert/strict"; import { test } from "node:test"; import { createHmac } from "node:crypto"; import sharp from "sharp"; import { bugReportSchema, bugMessageSchema, supportCommand, escapeSupportMarkdown, type BugReportRecord, } from "../shared/bug-reports"; import { verifySupportWebhook } from "../lib/support-signature"; import { supportScreenshot } from "../lib/support-images"; import { LinearSupportClient } from "../lib/linear-support-client"; const id = "00000000-0000-4000-8000-000000000001", team = "00000000-0000-4000-8000-000000000002"; test("reports exclude arbitrary URLs, secrets in page queries, malformed messages and reward privilege fields", () => { const valid = { clientId: id, title: "Wrong map", description: "The map loads with the wrong colors.", pagePath: "/servers", }; assert.equal(bugReportSchema.safeParse(valid).success, true); for (const value of [ { ...valid, pagePath: "https://private.example" }, { ...valid, pagePath: "/auth?code=secret" }, { ...valid, pagePath: "//evil" }, { ...valid, name_glow: true }, { ...valid, description: "short" }, ]) assert.equal(bugReportSchema.safeParse(value).success, false); assert.equal( bugMessageSchema.safeParse({ clientId: id, text: "#player-report-glow grant", }).success, true, "player text is text, not a staff command", ); assert.equal( bugMessageSchema.safeParse({ clientId: id, text: "x\u202e" }).success, false, ); }); test("only exact staff prefixes publish replies or explicitly change glow", () => { assert.deepEqual(supportCommand("#player-report-send Dear User\nThanks!"), { kind: "reply", text: "Dear User\nThanks!", }); assert.deepEqual(supportCommand("#player-report-glow grant"), { kind: "glow", enabled: true, }); assert.deepEqual(supportCommand("#player-report-glow revoke"), { kind: "glow", enabled: false, }); for (const body of [ "Internal staff note", "#user-report-message\n#player-report-send x", "quoted #player-report-send x", "#player-report-glow grant everyone", "#player-report-send", ]) assert.equal(supportCommand(body), null); assert.ok( escapeSupportMarkdown("#player-report-glow grant").startsWith("\\#"), ); }); test("webhooks require exact raw-body HMAC and a recent timestamp", () => { const raw = JSON.stringify({ webhookTimestamp: 100_000, body: "Привет" }); const sign = createHmac("sha256", "test-secret").update(raw).digest("hex"); assert.equal(verifySupportWebhook(raw, sign, "test-secret", 100_001), true); for (const [body, sig, key, time] of [ [raw + " ", sign, "test-secret", 100_001], [raw, "ff", "test-secret", 100_001], [raw, sign, "wrong", 100_001], [raw, sign, "test-secret", 160_001], [raw, sign, "test-secret", 39_999], ]) assert.equal( verifySupportWebhook( body as string, sig as string, key as string, time as number, ), false, ); }); test("screenshots reject disguised documents and oversized files, strip metadata and normalize images", async () => { const png = await sharp({ create: { width: 50, height: 30, channels: 3, background: "#abcdef" }, }) .png() .toBuffer(); const webp = await supportScreenshot(png); const meta = await sharp(webp).metadata(); assert.equal(meta.format, "webp"); assert.equal(meta.width, 50); assert.equal(meta.exif, undefined); await assert.rejects( supportScreenshot( Buffer.from( '', ), ), /PNG, JPG, or WebP/, ); await assert.rejects( supportScreenshot(new Uint8Array(5 * 1024 * 1024 + 1)), /smaller than 5 MB/, ); await assert.rejects(supportScreenshot(Buffer.from("not an image"))); }); test("Linear sends stable issue/comment UUIDs and recovers a timed-out creation without duplicates", async () => { let issue: unknown = null, comment: unknown = null, creates = 0, comments = 0; const bodies: unknown[] = []; const request = (async (_url, init) => { const body = JSON.parse(String(init?.body)); bodies.push(body); if (body.query.startsWith("query SupportIssue")) return Response.json( issue ? { data: { issue } } : { errors: [ { message: "Entity not found", extensions: { code: "ENTITY_NOT_FOUND" }, }, ], }, ); if (body.query.includes("issueCreate")) { creates++; assert.equal(body.variables.input.id, id); issue = { id, identifier: "TEST-1", team: { id: team } }; throw new Error("Response lost"); } if (body.query.startsWith("query SupportComment")) return Response.json( comment ? { data: { comment } } : { errors: [{ message: "Entity not found" }] }, ); if (body.query.includes("commentCreate")) { comments++; comment = { id: body.variables.input.id, issue: { id } }; return Response.json({ data: { commentCreate: { success: true, comment } }, }); } throw new Error("Unexpected query"); }) as typeof fetch; const client = new LinearSupportClient("test-key", team, "P-TEST-1", request); const report = { id, title: "Test issue", display_name: "Player", description: "#player-report-glow grant", page_path: "/", linear_asset_url: null, } as BugReportRecord; assert.equal((await client.createIssue(report)).identifier, "TEST-1"); await client.createIssue(report); assert.equal(creates, 1); await client.comment(team, id, "Player", "#player-report-send Spoof"); await client.comment(team, id, "Player", "#player-report-send Spoof"); assert.equal(comments, 1); const mutation = ( bodies as { variables: { input?: { body?: string; description?: string } }; }[] ).find((body) => body.variables.input?.body); assert.ok( mutation?.variables.input?.body?.startsWith("#user-report-message\n"), ); assert.ok(mutation?.variables.input?.body?.includes("\\#player-report-send")); }); test("Linear partial GraphQL failures and screenshot PUT failures are never accepted as delivered", async () => { const client = new LinearSupportClient( "test-key", team, undefined, async () => Response.json({ data: { issue: {} }, errors: [{ message: "Denied" }] }), ); await assert.rejects(client.issue(id), /temporarily unavailable/); const upload = new LinearSupportClient( "test-key", team, undefined, async (url) => String(url).includes("graphql") ? Response.json({ data: { fileUpload: { success: true, uploadFile: { uploadUrl: "https://upload.test/file", assetUrl: "https://asset.test/file", headers: [], }, }, }, }) : new Response(null, { status: 403 }), ); await assert.rejects( upload.upload(new Uint8Array([1]), "test.webp"), /screenshot upload failed/, ); });