Version 2.0.0 - update of everything
This commit is contained in:
85 files changed
+13065
-2107
No files matched your search
@@ -0,0 +1,227 @@
|
||||
// Test de backend/src/server/Server.js avec un vrai serveur socket.io (port local) et des clients socket.io-client.
|
||||
// Toutes les dépendances (bot Discord, utilisateurs, players, Admin, playlists, Google...) sont simulées :
|
||||
// aucun bot Discord n'est connecté, aucun fichier de data/ n'est lu ni écrit.
|
||||
const assert = require("assert")
|
||||
const Module = require("module")
|
||||
const path = require("path")
|
||||
|
||||
const ROOT = require("path").resolve(__dirname, "../../..")
|
||||
const SRC = ROOT + "/backend/src"
|
||||
const { io: ioClient } = require(ROOT + "/frontend/node_modules/socket.io-client")
|
||||
const norm = (p) => p.replace(/\\/g, "/").toLowerCase()
|
||||
const PORT = 45000 + Math.floor(Math.random() * 1000)
|
||||
|
||||
const uncaught = []
|
||||
process.on("uncaughtException", (e) => { uncaught.push(e); console.error("uncaughtException", e) })
|
||||
process.on("unhandledRejection", (e) => { uncaught.push(e); console.error("unhandledRejection", e) })
|
||||
|
||||
// --- Utilisateurs : chaque lecture renvoie un NOUVEL objet (comme loadUsers() après chaque saveUsers()) ---
|
||||
const raw = {
|
||||
A: { id: "A", tokens: ["tA1", "tA2"], labels: ["ADMIN"] },
|
||||
B: { id: "B", tokens: ["tB"], labels: [] },
|
||||
C: { id: "C", tokens: ["tC"], labels: [] },
|
||||
}
|
||||
function makeUser(r) {
|
||||
if (!r) return null
|
||||
const labels = [...r.labels]
|
||||
return {
|
||||
identity: { id: r.id, username: "user" + r.id, global_name: "User " + r.id, avatar: null },
|
||||
labels, auth: { ok: true }, tokens: [...r.tokens],
|
||||
isAdmin: () => labels.includes("ADMIN"),
|
||||
isFullBanned: () => labels.includes("BAN"),
|
||||
isBanned: (g) => labels.includes("BAN_" + g),
|
||||
isMod: (g) => labels.includes("ADMIN") || labels.includes("MOD_" + g),
|
||||
isDeleted: () => labels.includes("DELETED"),
|
||||
needUpdate: () => false, justUpdated() {}, clearToken() {},
|
||||
removeToken(t) { r.tokens = r.tokens.filter(x => x !== t) },
|
||||
}
|
||||
}
|
||||
const usersStub = {
|
||||
getUserById: (id) => makeUser(raw[id]),
|
||||
getUserByToken: (t) => makeUser(Object.values(raw).find(r => r.tokens.includes(t))),
|
||||
setGuildBan: async (id, g) => { const r = raw[id]; const l = "BAN_" + g; r.labels = r.labels.includes(l) ? r.labels.filter(x => x !== l) : [...r.labels, l]; return true },
|
||||
setGuildMod: async () => true,
|
||||
deleteAccount: async (id) => { raw[id].labels = ["DELETED"]; raw[id].tokens = [] },
|
||||
updateIdentity: async () => true,
|
||||
addUser: async () => null,
|
||||
}
|
||||
|
||||
// --- Bot / players ---
|
||||
const guilds = new Map([
|
||||
["g1", { id: "g1", name: "Guilde 1", owner: "C", allMembers: ["A", "B", "C"] }],
|
||||
["g2", { id: "g2", name: "Guilde 2", owner: "A", allMembers: ["A", "B"] }],
|
||||
])
|
||||
const AllPlayers = new Map()
|
||||
const seekCalls = []
|
||||
class FakePlayer {
|
||||
constructor(guildId) { if (AllPlayers.has(guildId)) return AllPlayers.get(guildId); this.guildId = guildId; this.connected = false; this.queue = { current: { title: "x" } }; AllPlayers.set(guildId, this) }
|
||||
isConnected() { return this.connected }
|
||||
getState() { return { v: 2, guildId: this.guildId, status: this.connected ? "playing" : "idle" } }
|
||||
async setDuration(t) { seekCalls.push(t); return { ok: true } }
|
||||
async skip() { return "no_music" }
|
||||
}
|
||||
let ioRef = null
|
||||
const adminStub = {
|
||||
ADMIN_ROOM: "ADMIN", LOGS_ROOM: "ADMIN_LOGS",
|
||||
init(io) { ioRef = io }, attach() {},
|
||||
syncUserSockets(userId) {
|
||||
const user = usersStub.getUserById(userId)
|
||||
for (const s of ioRef.sockets.sockets.values()) {
|
||||
if (s.data.userId !== userId) continue
|
||||
for (const room of [...s.rooms]) if (guilds.has(room) && user.isBanned(room)) s.leave(room)
|
||||
}
|
||||
process.emit("USERS_UPDATE")
|
||||
},
|
||||
}
|
||||
const fakeLog = { LogType: class { constructor() { this.step = { init() {}, end() {} } } log() {} warn() {} error(m) { if (process.env.VERBOSE) console.log("[error]", m) } } }
|
||||
let reportResult = false
|
||||
let finderThrows = true
|
||||
const stubs = {
|
||||
"loguix": fakeLog,
|
||||
[norm(SRC + "/utils/SafeMetric")]: { getGuildMetrics: () => [], toSafeNumber: (v) => v },
|
||||
[norm(SRC + "/server/auth/Session")]: { checkSession: () => false, addSession: () => "sess", removeSession() {} },
|
||||
[norm(SRC + "/server/auth/User")]: usersStub,
|
||||
[norm(SRC + "/player/Player")]: { Player: FakePlayer, AllPlayers, offlineState: (g) => ({ v: 2, guildId: g, status: "offline" }), getPlayer: () => { throw new Error("getPlayer ne doit plus être appelé") }, getAllPlayers: () => [...AllPlayers.values()] },
|
||||
[norm(SRC + "/discord/Bot")]: { isReady: async () => true, getGuilds: () => guilds, getGuildMember: () => null, getMembersVoices: () => new Map(), getChannel: () => null, getGuildRoles: () => [] },
|
||||
[norm(SRC + "/server/auth/DiscordAuth")]: {},
|
||||
[norm(SRC + "/discord/ReportSender")]: { Report: class { async send() { return reportResult } } },
|
||||
[norm(SRC + "/player/Finder")]: { search: async () => { if (finderThrows) throw new Error("panne simulée"); return [] } },
|
||||
[norm(SRC + "/playlists/PlaylistManager")]: { syncYoutube: async () => ({ ok: true }) },
|
||||
[norm(SRC + "/playlists/History")]: { getPersonalHistory: () => [], addToPersonalHistory() {}, clearPersonalHistory: async () => {} },
|
||||
[norm(SRC + "/lyrics/Lyrics")]: {},
|
||||
[norm(SRC + "/discord/ServerSettings")]: { getSecureRole: () => null },
|
||||
[norm(SRC + "/discord/MediaBase")]: {},
|
||||
[norm(SRC + "/playlists/Google/OAuth2")]: { getStatus: (id) => ({ configured: false, linked: false, user: id }), isSyncing: () => false },
|
||||
[norm(SRC + "/utils/Database/Configuration")]: { getPort: () => PORT },
|
||||
[norm(SRC + "/player/List")]: { List: class { getPrevious() { return [] } } },
|
||||
[norm(SRC + "/server/Admin")]: adminStub,
|
||||
[norm(SRC + "/utils/AudioBufferCheck")]: { isAudioFile: () => false },
|
||||
[norm(SRC + "/player/Song")]: { Song: class {} },
|
||||
[norm(SRC + "/media/MediaInformation")]: {},
|
||||
}
|
||||
const origLoad = Module._load
|
||||
Module._load = function (request, parent) {
|
||||
if (stubs[request]) return stubs[request]
|
||||
if (request.startsWith(".") && parent) {
|
||||
const resolved = norm(path.resolve(path.dirname(parent.filename), request)).replace(/\.js$/, "")
|
||||
if (stubs[resolved]) return stubs[resolved]
|
||||
}
|
||||
return origLoad.apply(this, arguments)
|
||||
}
|
||||
|
||||
const Server = require(SRC + "/server/Server.js")
|
||||
assert.strictEqual(typeof Server.init, "function", "Server.init exporté")
|
||||
Server.init()
|
||||
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
|
||||
const clients = []
|
||||
function connect(token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const s = ioClient("http://127.0.0.1:" + PORT, { auth: { token }, reconnection: false, transports: ["websocket"] })
|
||||
clients.push(s)
|
||||
s.once("/USER/READY", () => resolve(s))
|
||||
s.once("AUTH_ERROR", (e) => reject(new Error("AUTH_ERROR " + e)))
|
||||
setTimeout(() => reject(new Error("timeout connexion " + token)), 3000)
|
||||
})
|
||||
}
|
||||
function request(s, event, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
s.once(event, resolve)
|
||||
s.emit(event, data)
|
||||
setTimeout(() => reject(new Error("pas de réponse " + event)), 3000)
|
||||
})
|
||||
}
|
||||
function serverSocket(client) { return ioRef.sockets.sockets.get(client.id) }
|
||||
|
||||
;(async () => {
|
||||
await sleep(100)
|
||||
const a1 = await connect("tA1"), a2 = await connect("tA2"), b = await connect("tB")
|
||||
|
||||
// --- Rooms conservées lors d'un changement de guilde ---
|
||||
serverSocket(a1).join("ADMIN_LOGS")
|
||||
assert.strictEqual(await request(a1, "/GUILD/JOIN", "g1"), true)
|
||||
assert.strictEqual(await request(a1, "/GUILD/JOIN", "g2"), true)
|
||||
const rooms = [...serverSocket(a1).rooms]
|
||||
assert.ok(rooms.includes("g2") && !rooms.includes("g1"), "guilde précédente quittée : " + rooms)
|
||||
for (const r of ["user:A", "ADMIN", "ADMIN_LOGS", a1.id]) assert.ok(rooms.includes(r), "room conservée : " + r)
|
||||
assert.strictEqual(await request(a1, "/GUILD/JOIN", "inconnue"), false)
|
||||
|
||||
// --- Présence multi-appareils ---
|
||||
await request(a1, "/GUILD/JOIN", "g1")
|
||||
await request(a2, "/GUILD/JOIN", "g1")
|
||||
await request(b, "/GUILD/JOIN", "g1")
|
||||
let members = await request(b, "/USERS/LIST")
|
||||
assert.deepStrictEqual(members.map(m => m.id).sort(), ["A", "B"], "A une seule fois")
|
||||
a1.disconnect()
|
||||
await sleep(150)
|
||||
members = await request(b, "/USERS/LIST")
|
||||
assert.ok(members.some(m => m.id === "A"), "A toujours présent (2e appareil)")
|
||||
const info = await request(b, "/GUILD/INFO")
|
||||
assert.deepStrictEqual(info.members.map(m => m.id), ["A"])
|
||||
a2.disconnect()
|
||||
await sleep(150)
|
||||
members = await request(b, "/USERS/LIST")
|
||||
assert.ok(!members.some(m => m.id === "A"), "A absent après la dernière déconnexion")
|
||||
|
||||
// --- PLAYER_STATE : jamais de création de player, /GUILD/UPDATE seulement au changement de connexion ---
|
||||
let guildUpdates = 0, playerUpdates = 0
|
||||
b.on("/GUILD/UPDATE", () => guildUpdates++)
|
||||
b.on("/PLAYER/UPDATE", () => playerUpdates++)
|
||||
process.emit("PLAYER_STATE", "g1")
|
||||
process.emit("PLAYERS_UPDATE")
|
||||
await sleep(100)
|
||||
assert.strictEqual(AllPlayers.size, 0, "aucun player créé")
|
||||
assert.strictEqual(playerUpdates, 1, "état hors-ligne envoyé à la room g1")
|
||||
const player = new FakePlayer("g1"); player.connected = true
|
||||
process.emit("PLAYER_STATE", "g1"); await sleep(50)
|
||||
process.emit("PLAYER_STATE", "g1"); await sleep(50)
|
||||
assert.strictEqual(guildUpdates, 2, "1re émission + passage à connecté uniquement (" + guildUpdates + ")")
|
||||
assert.strictEqual((await request(b, "/PLAYER/STATE")).status, "playing")
|
||||
|
||||
// --- SEEK : 0 accepté, négatif refusé, refus du player transmis ---
|
||||
assert.strictEqual(await request(b, "/PLAYER/SEEK", { time: 0 }), true)
|
||||
assert.strictEqual(await request(b, "/PLAYER/SEEK", 0), true)
|
||||
assert.deepStrictEqual(seekCalls, [0, 0])
|
||||
assert.strictEqual(await request(b, "/PLAYER/SEEK", { time: -1 }), false)
|
||||
assert.strictEqual(await request(b, "/PLAYER/SEEK", {}), false)
|
||||
player.queue.current = null
|
||||
assert.strictEqual(await request(b, "/PLAYER/SEEK", { time: 5 }), false, "pas de musique : false (pas de ReferenceError)")
|
||||
assert.strictEqual(await request(b, "/PLAYER/FORWARD"), "no_music")
|
||||
|
||||
// --- Exception dans un handler : réponse false, process intact ---
|
||||
assert.strictEqual(await request(b, "/SEARCH", "test"), false)
|
||||
// --- /REPORT : résultat réel de l'envoi ---
|
||||
assert.strictEqual(await request(b, "/REPORT", { level: "bug", desc: "x" }), false)
|
||||
reportResult = true
|
||||
assert.strictEqual(await request(b, "/REPORT", { level: "bug", desc: "x" }), true)
|
||||
|
||||
// --- /MOD/USERS/BAN (plus de UsersBySocket) + droits relus à chaque requête ---
|
||||
const c = await connect("tC")
|
||||
await request(c, "/GUILD/JOIN", "g1")
|
||||
assert.strictEqual(await request(c, "/MOD/USERS/BAN", "B"), true, "le propriétaire bannit B")
|
||||
await sleep(50)
|
||||
assert.ok(!serverSocket(b).rooms.has("g1"), "B a quitté la room g1")
|
||||
assert.strictEqual(await request(b, "/PLAYER/STATE"), false, "B banni : refus immédiat (utilisateur relu)")
|
||||
assert.strictEqual(await request(b, "/GUILD/JOIN", "g1"), false)
|
||||
assert.strictEqual(await request(c, "/MOD/USERS/BAN", "B"), true, "levée du ban")
|
||||
assert.strictEqual(await request(b, "/GUILD/JOIN", "g1"), true)
|
||||
|
||||
// --- Bannissement global en cours de session ---
|
||||
raw.B.labels.push("BAN")
|
||||
const authError = new Promise(r => b.once("AUTH_ERROR", r))
|
||||
b.emit("/USER/INFO")
|
||||
assert.strictEqual(await authError, "Vous êtes banni du serveur")
|
||||
await sleep(100)
|
||||
assert.strictEqual(b.connected, false)
|
||||
|
||||
// --- /GOOGLE/STATUS par ack ---
|
||||
const status = await new Promise(r => c.emit("/GOOGLE/STATUS", null, r))
|
||||
assert.strictEqual(status.user, "C")
|
||||
|
||||
await sleep(100)
|
||||
assert.deepStrictEqual(uncaught, [], "aucune exception non gérée")
|
||||
console.log("server-socket.test.js : OK")
|
||||
clients.forEach(s => s.disconnect())
|
||||
ioRef.close()
|
||||
setTimeout(() => process.exit(0), 100)
|
||||
})().catch((e) => { console.error(e); process.exit(1) })
|
||||
Reference in new issue
Block a user