Improve MineGeo UI and application flows
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 2m37s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 2m37s
This commit is contained in:
216
tests/tournament-discord.test.ts
Normal file
216
tests/tournament-discord.test.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
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",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user