All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 5m57s
294 lines
9.0 KiB
TypeScript
294 lines
9.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
announcementMessage,
|
|
discordConfiguration,
|
|
DiscordClient,
|
|
DiscordDeliveryError,
|
|
closedPartyMessage,
|
|
} from "../lib/discord-client";
|
|
import { deliverDiscordAnnouncement } from "../lib/discord-delivery";
|
|
import type { DiscordAnnouncement } from "../shared/discord";
|
|
const channel = "123456789012345678",
|
|
messageId = "223456789012345678",
|
|
target = "00000000-0000-4000-8000-000000000001";
|
|
const job: DiscordAnnouncement = {
|
|
id: target,
|
|
kind: "party",
|
|
target_id: target,
|
|
state: "sending",
|
|
attempts: 1,
|
|
next_attempt_at: new Date(0).toISOString(),
|
|
lease_owner: "worker",
|
|
lease_until: new Date(100000).toISOString(),
|
|
channel_id: null,
|
|
message_id: null,
|
|
last_error: null,
|
|
created_at: new Date(0).toISOString(),
|
|
sent_at: null,
|
|
};
|
|
const source = {
|
|
id: target,
|
|
name: "Map @everyone [click](https://evil.example)",
|
|
owner: "Host <@123>",
|
|
difficulty: "normal",
|
|
players: 1,
|
|
limit: 8,
|
|
};
|
|
test("closed Discord parties turn red, lose their join prompt, and delete idempotently", async () => {
|
|
const payload = closedPartyMessage("https://minegeo.site", target);
|
|
assert.equal(payload.embeds[0].color, 0xed4245);
|
|
assert.ok(payload.embeds[0].description.includes("five minutes"));
|
|
assert.ok(!JSON.stringify(payload).includes("Join this party"));
|
|
assert.deepEqual(payload.allowed_mentions, { parse: [] });
|
|
const methods: string[] = [];
|
|
const client = new DiscordClient("private-token", async (url, init) => {
|
|
assert.equal(
|
|
url,
|
|
`https://discord.com/api/v10/channels/${channel}/messages/${messageId}`,
|
|
);
|
|
methods.push(init!.method!);
|
|
if (init?.method === "PATCH") {
|
|
assert.deepEqual(JSON.parse(String(init.body)), payload);
|
|
return Response.json({ id: messageId, channel_id: channel });
|
|
}
|
|
assert.equal(init?.body, undefined);
|
|
return new Response(null, { status: 204 });
|
|
});
|
|
assert.equal(
|
|
await client.closeParty(
|
|
channel,
|
|
messageId,
|
|
"https://minegeo.site",
|
|
target,
|
|
),
|
|
true,
|
|
);
|
|
await client.remove(channel, messageId);
|
|
assert.deepEqual(methods, ["PATCH", "DELETE"]);
|
|
const missing = new DiscordClient("token", async () =>
|
|
Response.json({ code: 10008 }, { status: 404 }),
|
|
);
|
|
assert.equal(
|
|
await missing.closeParty(
|
|
channel,
|
|
messageId,
|
|
"https://minegeo.site",
|
|
target,
|
|
),
|
|
false,
|
|
);
|
|
await missing.remove(channel, messageId);
|
|
await assert.rejects(
|
|
new DiscordClient("token", async () =>
|
|
Response.json({ code: 10003 }, { status: 404 }),
|
|
).remove(channel, messageId),
|
|
/channel was not found/,
|
|
);
|
|
await assert.rejects(
|
|
new DiscordClient("token", async () =>
|
|
Response.json({ retry_after: 10 }, { status: 429 }),
|
|
).remove(channel, messageId),
|
|
(e) => e instanceof DiscordDeliveryError && e.retryAfter === 10,
|
|
);
|
|
});
|
|
test("Discord config requires a public HTTPS site and valid channel IDs without exposing a public token", () => {
|
|
const env = {
|
|
MINEGEO_URL: "https://minegeo.site",
|
|
DISCORD_BOT_TOKEN: "test-token",
|
|
DISCORD_PARTIES_CHANNEL_ID: channel,
|
|
};
|
|
assert.equal(discordConfiguration(env).partyChannel, channel);
|
|
for (const url of [
|
|
"http://localhost:3000",
|
|
"https://user:secret@example.com",
|
|
"https://example.com/private?key=secret",
|
|
])
|
|
assert.equal(
|
|
discordConfiguration({ ...env, MINEGEO_URL: url }).origin,
|
|
null,
|
|
);
|
|
for (const id of ["bad", "123", `${channel}/messages`])
|
|
assert.equal(
|
|
discordConfiguration({ ...env, DISCORD_PARTIES_CHANNEL_ID: id })
|
|
.partyChannel,
|
|
null,
|
|
);
|
|
assert.equal(
|
|
discordConfiguration({ ...env, DISCORD_BOT_TOKEN: "secret\nheader" }).token,
|
|
null,
|
|
);
|
|
});
|
|
test("Discord announcements use fixed public links, bounded escaped text, suppressed mentions and a stable nonce", () => {
|
|
const body = announcementMessage(
|
|
job.id,
|
|
"party",
|
|
"https://minegeo.site",
|
|
source,
|
|
);
|
|
assert.deepEqual(body.allowed_mentions, { parse: [] });
|
|
assert.equal(
|
|
body.embeds[0].url,
|
|
`https://minegeo.site/multiplayer/${target}`,
|
|
);
|
|
assert.ok(!body.embeds[0].title.includes("[click]("));
|
|
assert.equal(body.nonce.length, 24);
|
|
assert.equal(body.enforce_nonce, true);
|
|
assert.equal(
|
|
body.nonce,
|
|
announcementMessage(job.id, "party", "https://minegeo.site", source)
|
|
.nonce,
|
|
);
|
|
assert.notEqual(
|
|
body.nonce,
|
|
announcementMessage(
|
|
"another",
|
|
"party",
|
|
"https://minegeo.site",
|
|
source,
|
|
).nonce,
|
|
);
|
|
const map = announcementMessage(job.id, "map", "https://minegeo.site", {
|
|
...source,
|
|
name: "A".repeat(500),
|
|
locations: 10,
|
|
});
|
|
assert.equal(map.embeds[0].title.length, 256);
|
|
assert.equal(map.embeds[0].fields[1].value, "10");
|
|
});
|
|
test("Discord REST sends the bot token only to Discord, confirms message and channel identity, and honors rate limits", async () => {
|
|
const payload = announcementMessage(
|
|
job.id,
|
|
"party",
|
|
"https://minegeo.site",
|
|
source,
|
|
);
|
|
const client = new DiscordClient("private-token", async (url, init) => {
|
|
assert.equal(
|
|
url,
|
|
`https://discord.com/api/v10/channels/${channel}/messages`,
|
|
);
|
|
assert.equal(
|
|
(init?.headers as Record<string, string>).Authorization,
|
|
"Bot private-token",
|
|
);
|
|
assert.equal(init?.redirect, "error");
|
|
assert.deepEqual(JSON.parse(String(init?.body)), payload);
|
|
return Response.json({ id: messageId, channel_id: channel });
|
|
});
|
|
assert.equal(await client.send(channel, payload), messageId);
|
|
const limited = new DiscordClient("token", async () =>
|
|
Response.json({ retry_after: 65.4 }, { status: 429 }),
|
|
);
|
|
await assert.rejects(
|
|
limited.send(channel, payload),
|
|
(e) =>
|
|
e instanceof DiscordDeliveryError && e.retryAfter === 66 && !e.permanent,
|
|
);
|
|
for (const status of [401, 403, 404]) {
|
|
const bad = new DiscordClient("secret-token", async () =>
|
|
Response.json({ message: "secret-token internal raw error" }, { status }),
|
|
);
|
|
await assert.rejects(
|
|
bad.send(channel, payload),
|
|
(e) =>
|
|
e instanceof DiscordDeliveryError &&
|
|
e.permanent &&
|
|
!e.message.includes("secret-token"),
|
|
);
|
|
}
|
|
await assert.rejects(
|
|
new DiscordClient("token", async () =>
|
|
Response.json({ id: messageId, channel_id: "wrong" }),
|
|
).send(channel, payload),
|
|
/did not confirm/,
|
|
);
|
|
await assert.rejects(
|
|
new DiscordClient("token", async () => {
|
|
throw new Error("token-secret");
|
|
}).send(channel, payload),
|
|
/could not be reached/,
|
|
);
|
|
});
|
|
test("durable delivery skips unavailable parties, records acknowledged sends and preserves recoverable failures", async () => {
|
|
const run = async (options: {
|
|
source: typeof source | null;
|
|
fail?: Error;
|
|
attempts?: number;
|
|
}) => {
|
|
const patches: Partial<DiscordAnnouncement>[] = [],
|
|
requests: unknown[] = [];
|
|
await deliverDiscordAnnouncement(
|
|
{ ...job, attempts: options.attempts ?? 1 },
|
|
{
|
|
origin: "https://minegeo.site",
|
|
channel,
|
|
now: () => 1000,
|
|
load: async () => options.source,
|
|
client: {
|
|
send: async (...args) => {
|
|
requests.push(args);
|
|
if (options.fail) throw options.fail;
|
|
return messageId;
|
|
},
|
|
},
|
|
finish: async (patch) => {
|
|
patches.push(patch);
|
|
},
|
|
},
|
|
);
|
|
return { patch: patches.at(-1)!, requests };
|
|
};
|
|
const skipped = await run({ source: null });
|
|
assert.equal(skipped.patch.state, "skipped");
|
|
assert.equal(skipped.requests.length, 0);
|
|
const sent = await run({ source });
|
|
assert.equal(sent.patch.state, "sent");
|
|
assert.equal(sent.patch.message_id, messageId);
|
|
assert.equal(sent.patch.lease_owner, null);
|
|
const rate = await run({
|
|
source,
|
|
fail: new DiscordDeliveryError("Rate limited", 65),
|
|
});
|
|
assert.equal(rate.patch.state, "pending");
|
|
assert.equal(Date.parse(rate.patch.next_attempt_at!), 66000);
|
|
assert.equal(
|
|
(
|
|
await run({
|
|
source,
|
|
fail: new DiscordDeliveryError("Bad credentials", 60, true),
|
|
})
|
|
).patch.state,
|
|
"failed",
|
|
);
|
|
assert.equal(
|
|
(await run({ source, attempts: 8, fail: new Error("private raw error") }))
|
|
.patch.state,
|
|
"failed",
|
|
);
|
|
});
|
|
test("Discord setup check only reads bot/channel details and rejects non-server text channels", async () => {
|
|
const paths: string[] = [];
|
|
const bot = new DiscordClient("token", async (url, init) => {
|
|
assert.equal(init?.method, "GET");
|
|
paths.push(String(url));
|
|
return Response.json(
|
|
String(url).endsWith("@me")
|
|
? { bot: true, username: "MineGeo" }
|
|
: { guild_id: channel, type: 0 },
|
|
);
|
|
});
|
|
assert.equal((await bot.check([channel, channel])).botName, "MineGeo");
|
|
assert.equal(paths.length, 2);
|
|
await assert.rejects(
|
|
new DiscordClient("token", async (url) =>
|
|
Response.json(
|
|
String(url).endsWith("@me")
|
|
? { bot: true, username: "Test" }
|
|
: { guild_id: channel, type: 2 },
|
|
),
|
|
).check([channel]),
|
|
/text or announcement channel/,
|
|
);
|
|
});
|