upgrade project and add deploy workflow
Some checks failed
Deploy Abondance Bot / deploy (push) Failing after 24s

This commit is contained in:
Shinuwa 2026-06-28 20:02:44 +02:00
parent 0dabb7c3c0
commit 3a50bd061a
10 changed files with 358 additions and 108 deletions

6
.env.example Normal file
View file

@ -0,0 +1,6 @@
DISCORD_TOKEN=replace-with-your-discord-token
BOT_OWNER_ID=replace-with-your-user-id
APPLICATION_ID=replace-with-your-application-id
GUILD_URL=https://www.naeu.playblackdesert.com/fr-fr/Adventure/Guild/GuildProfile?guildName=Abondance&region=EU
DISCORD_GUILD_ID=replace-with-your-server-id
DISCORD_CHANNEL_ID=replace-with-your-channel-id

View file

@ -0,0 +1,28 @@
name: Deploy Abondance Bot
on:
push:
branches:
- main
jobs:
deploy:
runs-on: docker
steps:
- name: Deploy on WebApps
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
apt-get update
apt-get install -y openssh-client
mkdir -p ~/.ssh
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh -o StrictHostKeyChecking=no \
-i ~/.ssh/id_ed25519 \
-p 2911 \
shinuwa@192.168.0.100 \
"sudo /usr/local/bin/deploy-abondance-bot"

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules/
.env
*.tmp

View file

@ -1,7 +1,10 @@
import { initMembersChecker } from "./src/features/membersChecker.js";
import { initDiscordBot } from "./src/features/discordBot.js";
(async () => {
try {
await initDiscordBot();
await initMembersChecker();
})();
} catch (error) {
console.error("❌ Impossible de démarrer le bot :", error);
process.exitCode = 1;
}

124
package-lock.json generated
View file

@ -11,20 +11,21 @@
"dependencies": {
"cheerio": "^1.1.2",
"discord.js": "^14.23.2",
"dotenv": "^17.4.2",
"node-cron": "^4.2.1",
"node-fetch": "^3.3.2"
}
},
"node_modules/@discordjs/builders": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.12.2.tgz",
"integrity": "sha512-AugKmrgRJOHXEyMkABH/hXpAmIBKbYokCEl9VAM4Kh6FvkdobQ+Zhv7AR6K+y5hS7+VQ7gKXPYCe1JQmV07H1g==",
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
"integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/formatters": "^0.6.1",
"@discordjs/util": "^1.1.1",
"@discordjs/formatters": "^0.6.2",
"@discordjs/util": "^1.2.0",
"@sapphire/shapeshift": "^4.0.0",
"discord-api-types": "^0.38.26",
"discord-api-types": "^0.38.40",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.4",
"tslib": "^2.6.3"
@ -46,12 +47,12 @@
}
},
"node_modules/@discordjs/formatters": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz",
"integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==",
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
"license": "Apache-2.0",
"dependencies": {
"discord-api-types": "^0.38.1"
"discord-api-types": "^0.38.33"
},
"engines": {
"node": ">=16.11.0"
@ -61,20 +62,20 @@
}
},
"node_modules/@discordjs/rest": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz",
"integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==",
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz",
"integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/collection": "^2.1.1",
"@discordjs/util": "^1.1.1",
"@discordjs/util": "^1.2.0",
"@sapphire/async-queue": "^1.5.3",
"@sapphire/snowflake": "^3.5.3",
"@sapphire/snowflake": "^3.5.5",
"@vladfrangu/async_event_emitter": "^2.4.6",
"discord-api-types": "^0.38.16",
"magic-bytes.js": "^1.10.0",
"discord-api-types": "^0.38.40",
"magic-bytes.js": "^1.13.0",
"tslib": "^2.6.3",
"undici": "6.21.3"
"undici": "6.24.1"
},
"engines": {
"node": ">=18"
@ -95,20 +96,33 @@
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz",
"integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@discordjs/rest/node_modules/undici": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/@discordjs/util": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz",
"integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
"license": "Apache-2.0",
"dependencies": {
"discord-api-types": "^0.38.33"
},
"engines": {
"node": ">=18"
},
@ -298,33 +312,33 @@
}
},
"node_modules/discord-api-types": {
"version": "0.38.30",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.30.tgz",
"integrity": "sha512-KhAqlBrg+rVK+Ob7INMF5o63yW4/GUzRatG/AjyVsIO8lgcLyR8qCl2HokIVzWwmzkJYG0CEPXsKMOqau3E8NA==",
"version": "0.38.49",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.49.tgz",
"integrity": "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg==",
"license": "MIT",
"workspaces": [
"scripts/actions/documentation"
]
},
"node_modules/discord.js": {
"version": "14.23.2",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.23.2.tgz",
"integrity": "sha512-tU2NFr823X3TXEc8KyR/4m296KLxPai4nirN3q9kHCpY4TKj96n9lHZnyLzRNMui8EbL07jg9hgH2PWWfKMGIg==",
"version": "14.26.4",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz",
"integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/builders": "^1.12.1",
"@discordjs/builders": "^1.14.1",
"@discordjs/collection": "1.5.3",
"@discordjs/formatters": "^0.6.1",
"@discordjs/rest": "^2.6.0",
"@discordjs/util": "^1.1.1",
"@discordjs/formatters": "^0.6.2",
"@discordjs/rest": "^2.6.1",
"@discordjs/util": "^1.2.0",
"@discordjs/ws": "^1.2.3",
"@sapphire/snowflake": "3.5.3",
"discord-api-types": "^0.38.29",
"discord-api-types": "^0.38.40",
"fast-deep-equal": "3.1.3",
"lodash.snakecase": "4.1.1",
"magic-bytes.js": "^1.10.0",
"magic-bytes.js": "^1.13.0",
"tslib": "^2.6.3",
"undici": "6.21.3"
"undici": "6.24.1"
},
"engines": {
"node": ">=18"
@ -334,9 +348,9 @@
}
},
"node_modules/discord.js/node_modules/undici": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
@ -397,6 +411,18 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/encoding-sniffer": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
@ -507,9 +533,9 @@
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash.snakecase": {
@ -519,9 +545,9 @@
"license": "MIT"
},
"node_modules/magic-bytes.js": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz",
"integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==",
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
"integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
"license": "MIT"
},
"node_modules/node-cron": {
@ -696,9 +722,9 @@
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"

View file

@ -1,11 +1,15 @@
{
"name": "abondance-bot",
"version": "1.0.0",
"description": "",
"description": "Surveille les changements dans la liste des membres de la guilde Abondance",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"start": "node index.js",
"test": "node --test"
},
"engines": {
"node": ">=20"
},
"keywords": [],
"author": "",
@ -13,6 +17,7 @@
"dependencies": {
"cheerio": "^1.1.2",
"discord.js": "^14.23.2",
"dotenv": "^17.4.2",
"node-cron": "^4.2.1",
"node-fetch": "^3.3.2"
}

View file

@ -2,7 +2,7 @@ import { Client, GatewayIntentBits } from "discord.js";
import { config } from "../static/config.js";
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
intents: [GatewayIntentBits.Guilds],
});
async function initDiscordBot() {
@ -11,12 +11,13 @@ async function initDiscordBot() {
}
async function sendDiscordAlert(message) {
try {
const channel = await client.channels.fetch(config.textChannelId);
await channel.send(message);
} catch (err) {
console.error("❌ Erreur envoi Discord :", err);
if (!channel?.isTextBased()) {
throw new Error(`Le canal Discord ${config.textChannelId} n'est pas textuel`);
}
await channel.send(message);
}
export {

View file

@ -1,63 +1,164 @@
import cron from "node-cron";
import fetch from "node-fetch";
import fs from "fs";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import * as cheerio from "cheerio";
import { config } from "../static/config.js";
import { sendDiscordAlert } from "./discordBot.js"
import { sendDiscordAlert } from "./discordBot.js";
const membersJSON = "./src/static/data/members.json";
const membersJSON = new URL("../static/data/members.json", import.meta.url);
const scheduleTime = "30 9 * * *";
const scheduleTimezone = "Europe/Paris";
const requestTimeoutMs = 15_000;
const memberSelector = ".box_list_area .adventure_list_table li .guild_name .text a";
async function initMembersChecker() {
await checkGuildMembers();
cron.schedule(scheduleTime, checkGuildMembers, { timezone: scheduleTimezone });
function parseGuildMembers(html) {
const $ = cheerio.load(html);
const members = $(memberSelector)
.map((_, element) => $(element).text().trim())
.get()
.filter(Boolean);
if (members.length === 0) {
throw new Error(
"Aucun membre trouvé : la page est invalide ou sa structure HTML a changé",
);
}
async function getGuildMembers(guildUrl) {
const res = await fetch(guildUrl);
const html = await res.text();
const $ = cheerio.load(html);
const members = [];
$('.box_list_area .adventure_list_table li .guild_name .text a').each((i, el) => {
members.push($(el).text().trim());
});
if (new Set(members).size !== members.length) {
throw new Error("La liste extraite contient des membres en double");
}
return members;
}
async function checkGuildMembers() {
try {
const newMembers = await getGuildMembers(config.guild_url);
async function getGuildMembers(guildUrl, fetchImpl = fetch) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
let oldMembers = [];
if (fs.existsSync(membersJSON)) {
oldMembers = JSON.parse(fs.readFileSync(membersJSON, "utf-8"));
try {
const response = await fetchImpl(guildUrl, {
headers: {
Accept: "text/html,application/xhtml+xml",
"Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8",
"User-Agent": "AbondanceGuildBot/1.0",
},
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`La page de guilde a répondu avec le statut HTTP ${response.status}`);
}
const added = newMembers.filter(m => !oldMembers.includes(m));
const removed = oldMembers.filter(m => !newMembers.includes(m));
return parseGuildMembers(await response.text());
} catch (error) {
if (error.name === "AbortError") {
throw new Error(`La page de guilde n'a pas répondu sous ${requestTimeoutMs / 1000}s`);
}
if (added.length || removed.length) {
throw error;
} finally {
clearTimeout(timeout);
}
}
function readMembers(fileUrl = membersJSON) {
if (!fs.existsSync(fileUrl)) return null;
const members = JSON.parse(fs.readFileSync(fileUrl, "utf8"));
if (
!Array.isArray(members) ||
members.length === 0 ||
members.some((member) => typeof member !== "string" || !member.trim())
) {
throw new Error("Le fichier members.json ne contient pas une liste valide");
}
return members;
}
function writeMembers(members, fileUrl = membersJSON) {
const filePath = fileUrl instanceof URL ? fileURLToPath(fileUrl) : fileUrl;
const temporaryFile = `${filePath}.${process.pid}.tmp`;
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(temporaryFile, `${JSON.stringify(members, null, 2)}\n`);
fs.renameSync(temporaryFile, filePath);
}
function assertPlausibleChange(oldMembers, newMembers) {
if (oldMembers.length >= 10 && newMembers.length < oldMembers.length / 2) {
throw new Error(
`Extraction suspecte : ${newMembers.length} membres trouvés contre ${oldMembers.length} auparavant`,
);
}
}
function getChanges(oldMembers, newMembers) {
const oldSet = new Set(oldMembers);
const newSet = new Set(newMembers);
return {
added: newMembers.filter((member) => !oldSet.has(member)),
removed: oldMembers.filter((member) => !newSet.has(member)),
};
}
function formatChanges({ added, removed }) {
let message = "⚔️ **Changement dans la liste des membres :**\n";
if (added.length) message += ` Nouveaux membres : ${added.join(", ")}\n`;
if (removed.length) message += ` Membres partis : ${removed.join(", ")}`;
await sendDiscordAlert(message);
return message.trimEnd();
}
fs.mkdirSync("./src/static/data/", { recursive: true });
fs.writeFileSync(membersJSON, JSON.stringify(newMembers, null, 2));
console.log("✅ Vérification terminée", new Date().toLocaleString("fr-FR", { timeZone: scheduleTimezone }));
} catch (err) {
console.error("❌ Erreur checkGuildMembers:", err);
async function checkGuildMembers({
guildUrl = config.guild_url,
fileUrl = membersJSON,
notify = sendDiscordAlert,
fetchImpl = fetch,
} = {}) {
try {
await sendDiscordAlert(`⚠️ Erreur lors du check des membres: \`${err.message}\``);
} catch {}
const newMembers = await getGuildMembers(guildUrl, fetchImpl);
const oldMembers = readMembers(fileUrl);
if (oldMembers) {
assertPlausibleChange(oldMembers, newMembers);
const changes = getChanges(oldMembers, newMembers);
if (changes.added.length || changes.removed.length) {
await notify(formatChanges(changes));
}
}
writeMembers(newMembers, fileUrl);
console.log(
`✅ Vérification terminée : ${newMembers.length} membres`,
new Date().toLocaleString("fr-FR", { timeZone: scheduleTimezone }),
);
} catch (error) {
console.error("❌ Erreur checkGuildMembers:", error);
try {
await notify(`⚠️ Erreur lors du check des membres : ${error.message}`);
} catch (notificationError) {
console.error("❌ Impossible d'envoyer l'erreur sur Discord :", notificationError);
}
}
}
async function initMembersChecker() {
await checkGuildMembers();
cron.schedule(scheduleTime, checkGuildMembers, {
timezone: scheduleTimezone,
noOverlap: true,
});
}
export {
initMembersChecker
}
assertPlausibleChange,
checkGuildMembers,
formatChanges,
getChanges,
getGuildMembers,
initMembersChecker,
parseGuildMembers,
};

View file

@ -1,10 +1,22 @@
const config = {
token: "MTQyODI5MDE1Mjk4MzE3MTE2Mw.GFwDOo.-RPnn6meXQL3xAh1S0vCQ9N0H3PeXcxKGSIWAM",
botOwner: "139829632764477440",
applicationId: "1428290152983171163",
guild_url: "https://www.naeu.playblackdesert.com/en-us/Adventure/Guild/GuildProfile?guildName=Abondance&region=EU",
discordGuildId: "1205611509305057410",
textChannelId: "1428068401825321094",
};
import "dotenv/config";
function requiredEnv(name) {
const value = process.env[name]?.trim();
if (!value) {
throw new Error(`La variable d'environnement ${name} est obligatoire`);
}
return value;
}
const config = Object.freeze({
token: requiredEnv("DISCORD_TOKEN"),
botOwner: process.env.BOT_OWNER_ID?.trim(),
applicationId: process.env.APPLICATION_ID?.trim(),
guild_url: requiredEnv("GUILD_URL"),
discordGuildId: process.env.DISCORD_GUILD_ID?.trim(),
textChannelId: requiredEnv("DISCORD_CHANNEL_ID"),
});
export { config };

View file

@ -0,0 +1,65 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
assertPlausibleChange,
formatChanges,
getChanges,
getGuildMembers,
parseGuildMembers,
} from "../src/features/membersChecker.js";
const guildPage = (members) => `
<div class="box_list_area">
<ul class="adventure_list_table">
<li>
${members
.map(
(member) =>
`<span class="guild_name"><span class="text"><a>${member}</a></span></span>`,
)
.join("")}
</li>
</ul>
</div>
`;
test("parseGuildMembers extrait et nettoie les noms", () => {
assert.deepEqual(parseGuildMembers(guildPage([" Alpha ", "Beta"])), ["Alpha", "Beta"]);
});
test("parseGuildMembers refuse une page sans membres", () => {
assert.throws(() => parseGuildMembers("<html></html>"), /Aucun membre trouvé/);
});
test("parseGuildMembers refuse les doublons", () => {
assert.throws(() => parseGuildMembers(guildPage(["Alpha", "Alpha"])), /double/);
});
test("getChanges détecte les arrivées et départs", () => {
assert.deepEqual(getChanges(["Alpha", "Beta"], ["Beta", "Gamma"]), {
added: ["Gamma"],
removed: ["Alpha"],
});
});
test("assertPlausibleChange refuse une chute brutale de l'effectif", () => {
const oldMembers = Array.from({ length: 20 }, (_, index) => `Membre-${index}`);
assert.throws(
() => assertPlausibleChange(oldMembers, ["Membre-0", "Membre-1"]),
/Extraction suspecte/,
);
});
test("formatChanges produit le message Discord", () => {
assert.equal(
formatChanges({ added: ["Gamma"], removed: ["Alpha"] }),
"⚔️ **Changement dans la liste des membres :**\n" +
" Nouveaux membres : Gamma\n" +
" Membres partis : Alpha",
);
});
test("getGuildMembers refuse une erreur HTTP", async () => {
const fakeFetch = async () => ({ ok: false, status: 503 });
await assert.rejects(getGuildMembers("https://example.test", fakeFetch), /HTTP 503/);
});