Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 2m37s
217 lines
6.3 KiB
TypeScript
217 lines
6.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { DiscordClient, DiscordDeliveryError } from "../lib/discord-client";
|
|
import {
|
|
deliverTournamentAnnouncement,
|
|
tournamentAnnouncementMessage,
|
|
TOURNAMENT_NEWS_CHANNEL,
|
|
TOURNAMENT_EVENT_ROLES,
|
|
type TournamentAnnouncementJob,
|
|
type TournamentAnnouncementSource,
|
|
} from "../lib/tournament-discord";
|
|
|
|
const now = Date.parse("2026-10-18T10:00:00Z");
|
|
const id = "00000000-0000-4000-8000-000000000001";
|
|
const source: TournamentAnnouncementSource = {
|
|
id,
|
|
name: "Cave cup",
|
|
owner: "Pavel",
|
|
starts_at: new Date(now + 15 * 60000).toISOString(),
|
|
server_timezone: "UTC",
|
|
player_limit: 32,
|
|
round_count: 3,
|
|
round_time_seconds: 300,
|
|
timer_mode: "shared",
|
|
status: "scheduled",
|
|
announce_discord: true,
|
|
};
|
|
const job: TournamentAnnouncementJob = {
|
|
id,
|
|
tournament_id: id,
|
|
state: "sending",
|
|
attempts: 1,
|
|
next_attempt_at: new Date(now).toISOString(),
|
|
lease_owner: id,
|
|
lease_until: new Date(now + 120000).toISOString(),
|
|
channel_id: null,
|
|
message_id: null,
|
|
sent_at: null,
|
|
last_error: null,
|
|
};
|
|
|
|
test("tournament Discord announcements pin their channel, public URL, explicit role mentions and stable nonce", async () => {
|
|
const unsafe = {
|
|
...source,
|
|
name: "Cup @everyone <@123> [bad](https://evil.example)",
|
|
owner: "Owner <@&999>",
|
|
};
|
|
const message = tournamentAnnouncementMessage(id, unsafe);
|
|
assert.deepEqual(message.allowed_mentions, {
|
|
roles: [...TOURNAMENT_EVENT_ROLES],
|
|
users: [],
|
|
replied_user: false,
|
|
});
|
|
assert.ok(
|
|
message.content.startsWith(
|
|
TOURNAMENT_EVENT_ROLES.map((role) => `<@&${role}>`).join(", "),
|
|
),
|
|
);
|
|
assert.ok(message.content.includes(`https://minegeo.site/tournaments/${id}`));
|
|
assert.ok(!message.content.includes("Cup @everyone <@123>"));
|
|
assert.match(
|
|
message.embeds[0].fields.find(
|
|
(field) => field.name === "Starts · server time",
|
|
)!.value,
|
|
/UTC/,
|
|
);
|
|
assert.match(
|
|
message.embeds[0].fields.find((field) => field.name === "Format")!.value,
|
|
/3 tournament rounds · 5 mini-rounds each/,
|
|
);
|
|
assert.equal(message.nonce, tournamentAnnouncementMessage(id, source).nonce);
|
|
assert.notEqual(
|
|
message.nonce,
|
|
tournamentAnnouncementMessage("another-event", source).nonce,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
tournamentAnnouncementMessage(id, { ...source, id: "../../elsewhere" }),
|
|
/Invalid tournament link/,
|
|
);
|
|
const client = new DiscordClient("test-token", async (url, init) => {
|
|
assert.equal(
|
|
url,
|
|
`https://discord.com/api/v10/channels/${TOURNAMENT_NEWS_CHANNEL}/messages`,
|
|
);
|
|
assert.deepEqual(
|
|
JSON.parse(String(init?.body)).allowed_mentions,
|
|
message.allowed_mentions,
|
|
);
|
|
return Response.json({
|
|
id: "223456789012345678",
|
|
channel_id: TOURNAMENT_NEWS_CHANNEL,
|
|
});
|
|
});
|
|
assert.equal(
|
|
await client.send(TOURNAMENT_NEWS_CHANNEL, message),
|
|
"223456789012345678",
|
|
);
|
|
});
|
|
|
|
test("tournament announcement timer describes shared and per-mini-round budgets", () => {
|
|
const timer = (value: TournamentAnnouncementSource) =>
|
|
tournamentAnnouncementMessage(id, value).embeds[0].fields.find(
|
|
(field) => field.name === "Round timer",
|
|
)!.value;
|
|
assert.equal(timer(source), "300 seconds shared across all 5 mini-rounds");
|
|
assert.equal(
|
|
timer({ ...source, timer_mode: "perMini", round_time_seconds: 90 }),
|
|
"90 seconds per mini-round",
|
|
);
|
|
assert.equal(
|
|
timer({ ...source, round_time_seconds: 3000 }),
|
|
"3000 seconds shared across all 5 mini-rounds",
|
|
);
|
|
});
|
|
|
|
async function deliver(
|
|
value: TournamentAnnouncementSource | null,
|
|
failure?: Error,
|
|
attempts = 1,
|
|
) {
|
|
const patches: Partial<TournamentAnnouncementJob>[] = [];
|
|
const sends: string[] = [];
|
|
await deliverTournamentAnnouncement(
|
|
{ ...job, attempts },
|
|
{
|
|
now: () => now,
|
|
load: async () => value,
|
|
client: {
|
|
send: async (channel) => {
|
|
sends.push(channel);
|
|
if (failure) throw failure;
|
|
return "223456789012345678";
|
|
},
|
|
},
|
|
finish: async (patch) => {
|
|
patches.push(patch);
|
|
},
|
|
},
|
|
);
|
|
return { patches, sends };
|
|
}
|
|
|
|
test("only opted-in scheduled tournaments are announced during their last15minutes", async () => {
|
|
for (const value of [
|
|
null,
|
|
{ ...source, announce_discord: false },
|
|
{ ...source, status: "cancelled" },
|
|
{ ...source, status: "running" },
|
|
{ ...source, starts_at: new Date(now).toISOString() },
|
|
{ ...source, id: "different" },
|
|
]) {
|
|
const result = await deliver(value);
|
|
assert.equal(result.sends.length, 0);
|
|
assert.equal(result.patches[0].state, "skipped");
|
|
assert.equal(result.patches[0].lease_owner, null);
|
|
}
|
|
const sent = await deliver(source);
|
|
assert.deepEqual(sent.sends, [TOURNAMENT_NEWS_CHANNEL]);
|
|
assert.equal(sent.patches[0].state, "sent");
|
|
assert.equal(sent.patches[0].sent_at, new Date(now).toISOString());
|
|
assert.equal(
|
|
(
|
|
await deliver({
|
|
...source,
|
|
starts_at: new Date(now + 60000).toISOString(),
|
|
})
|
|
).patches[0].state,
|
|
"sent",
|
|
);
|
|
});
|
|
|
|
test("a rescheduled tournament releases its old lease until15minutes before its new start", async () => {
|
|
const result = await deliver({
|
|
...source,
|
|
starts_at: new Date(now + 2 * 3600000).toISOString(),
|
|
});
|
|
assert.equal(result.sends.length, 0);
|
|
assert.equal(result.patches[0].state, "pending");
|
|
assert.equal(
|
|
result.patches[0].next_attempt_at,
|
|
new Date(now + 105 * 60000).toISOString(),
|
|
);
|
|
});
|
|
|
|
test("Discord rate limits retry durably and configuration failures stop after safe errors", async () => {
|
|
const limited = await deliver(
|
|
source,
|
|
new DiscordDeliveryError("Discord rate limit.", 180),
|
|
);
|
|
assert.equal(limited.patches[0].state, "pending");
|
|
assert.equal(
|
|
limited.patches[0].next_attempt_at,
|
|
new Date(now + 180000).toISOString(),
|
|
);
|
|
assert.equal(limited.patches[0].lease_until, null);
|
|
assert.equal(
|
|
(
|
|
await deliver(
|
|
source,
|
|
new DiscordDeliveryError("Missing permissions.", 60, true),
|
|
)
|
|
).patches[0].state,
|
|
"failed",
|
|
);
|
|
assert.equal(
|
|
(await deliver(source, new Error("secret request body"))).patches[0]
|
|
.last_error,
|
|
"Tournament announcement could not be confirmed. Delivery will retry.",
|
|
);
|
|
assert.equal(
|
|
(await deliver(source, new DiscordDeliveryError("Retry."), 8)).patches[0]
|
|
.state,
|
|
"failed",
|
|
);
|
|
});
|