Add MineGeo gameplay systems and interface
This commit is contained in:
293
tests/discord.test.ts
Normal file
293
tests/discord.test.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
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.vercel.app", 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.vercel.app",
|
||||
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.vercel.app",
|
||||
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.vercel.app",
|
||||
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.vercel.app",
|
||||
source,
|
||||
);
|
||||
assert.deepEqual(body.allowed_mentions, { parse: [] });
|
||||
assert.equal(
|
||||
body.embeds[0].url,
|
||||
`https://minegeo.vercel.app/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.vercel.app", source)
|
||||
.nonce,
|
||||
);
|
||||
assert.notEqual(
|
||||
body.nonce,
|
||||
announcementMessage(
|
||||
"another",
|
||||
"party",
|
||||
"https://minegeo.vercel.app",
|
||||
source,
|
||||
).nonce,
|
||||
);
|
||||
const map = announcementMessage(job.id, "map", "https://minegeo.vercel.app", {
|
||||
...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.vercel.app",
|
||||
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.vercel.app",
|
||||
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/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user