221 lines
7.2 KiB
TypeScript
221 lines
7.2 KiB
TypeScript
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-close"), { kind: "close" });
|
|
assert.equal(supportCommand("#player-report-close everyone"), null);
|
|
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(
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>',
|
|
),
|
|
),
|
|
/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/,
|
|
);
|
|
});
|