Version 2.1.0 - Update UI
This commit is contained in:
18 files changed
+3508
-330
No files matched your search
@@ -20,6 +20,7 @@ const raw = {
|
||||
A: { id: "A", tokens: ["tA1", "tA2"], labels: ["ADMIN"] },
|
||||
B: { id: "B", tokens: ["tB"], labels: [] },
|
||||
C: { id: "C", tokens: ["tC"], labels: [] },
|
||||
D: { id: "D", tokens: ["tD"], labels: [] },
|
||||
}
|
||||
function makeUser(r) {
|
||||
if (!r) return null
|
||||
@@ -48,7 +49,7 @@ const usersStub = {
|
||||
|
||||
// --- Bot / players ---
|
||||
const guilds = new Map([
|
||||
["g1", { id: "g1", name: "Guilde 1", owner: "C", allMembers: ["A", "B", "C"] }],
|
||||
["g1", { id: "g1", name: "Guilde 1", owner: "C", allMembers: ["A", "B", "C", "D"] }],
|
||||
["g2", { id: "g2", name: "Guilde 2", owner: "A", allMembers: ["A", "B"] }],
|
||||
])
|
||||
const AllPlayers = new Map()
|
||||
@@ -76,9 +77,68 @@ const adminStub = {
|
||||
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
|
||||
|
||||
// --- Playlists personnelles (mémoire) : l'ordre est appliqué par le vrai PlaylistUtils (chargé après les stubs) ---
|
||||
const personal = {
|
||||
C: [{ playlistId: "1", title: "Un", songs: [] }, { playlistId: "2", title: "Deux", songs: [] }, { playlistId: "3", title: "Trois", songs: [] }],
|
||||
}
|
||||
const playlistsStub = {
|
||||
syncYoutube: async () => ({ ok: true }),
|
||||
getPlaylistsOfUser: (id) => personal[id] || [],
|
||||
getPlaylistOfUser: (id, pid) => (personal[id] || []).find(p => p.playlistId === String(pid)) || null,
|
||||
reorderPlaylists: (id, order) => require(SRC + "/playlists/PlaylistUtils").applyOrder(personal[id] || [], order),
|
||||
hasRoomForPlaylist: (id) => (personal[id] || []).length < 500,
|
||||
MAX_PLAYLISTS: 500,
|
||||
addPlaylistCopy: (id, playlist) => { const copy = { ...playlist, playlistId: "copy" + Date.now() }; (personal[id] ||= []).push(copy); return copy },
|
||||
}
|
||||
|
||||
// --- Playlists partagées (mémoire) et réglage par serveur ---
|
||||
const sharedSettings = new Map()
|
||||
const getShared = (g) => ({ enabled: false, editors: "members", ...(sharedSettings.get(g) || {}) })
|
||||
const serverSettingsStub = {
|
||||
getSecureRole: () => null,
|
||||
getSharedPlaylists: getShared,
|
||||
setSharedPlaylists: (g, patch) => {
|
||||
if (!patch || !Object.keys(patch).length) return null
|
||||
if (patch.enabled !== undefined && typeof patch.enabled !== "boolean") return null
|
||||
if (patch.editors !== undefined && !["members", "mods"].includes(patch.editors)) return null
|
||||
const next = { ...getShared(g), ...patch }
|
||||
sharedSettings.set(g, next)
|
||||
return next
|
||||
},
|
||||
}
|
||||
const guildLists = { g1: [{ playlistId: "10", title: "Commune", songs: [], createdBy: { id: "A", username: "userA" } }] }
|
||||
const guildPlaylistsStub = {
|
||||
getPlaylists: (g) => guildLists[g] || [],
|
||||
getPlaylist: (g, id) => (guildLists[g] || []).find(p => p.playlistId === String(id)) || null,
|
||||
countPlaylists: (g) => (guildLists[g] || []).length,
|
||||
deletePlaylist: (g, id) => { const list = guildLists[g] || []; const i = list.findIndex(p => p.playlistId === String(id)); if (i === -1) return false; list.splice(i, 1); return true },
|
||||
createPlaylist: async (g, input, user) => {
|
||||
const playlist = { playlistId: String(Date.now()), title: input.name, songs: [], scope: "guild", guildId: g, createdBy: { id: user.id, username: user.username } }
|
||||
;(guildLists[g] ||= []).push(playlist)
|
||||
return { ok: true, playlist }
|
||||
},
|
||||
}
|
||||
|
||||
// --- Paroles : searchCandidates seulement (getById absent → UNAVAILABLE) ---
|
||||
const lyricsCalls = []
|
||||
const lyricsStub = {
|
||||
searchCandidates: async (q) => { lyricsCalls.push(q); return { ok: true, results: [{ id: 1, track: q, artist: "x", album: null, duration: 100, synced: true, instrumental: false }] } },
|
||||
getLyricsForSong: async (song) => { lyricsCalls.push(song); return { ok: false, error: "NOT_FOUND", songKey: null } },
|
||||
findLyrics: async (query) => { lyricsCalls.push(query); return { ok: false, error: "NOT_FOUND", songKey: null } },
|
||||
}
|
||||
|
||||
const stubs = {
|
||||
"loguix": fakeLog,
|
||||
[norm(SRC + "/utils/SafeMetric")]: { getGuildMetrics: () => [], toSafeNumber: (v) => v },
|
||||
[norm(SRC + "/utils/SafeMetric")]: {
|
||||
getGuildMetrics: (g) => g === "g1" ? [
|
||||
{ name: "numberOfMusicPlayed_g1", value: 12 },
|
||||
{ name: "numberOfSecondsPlayed_g1", value: "3600.4" },
|
||||
{ name: "numberOfCommands_g1", value: NaN },
|
||||
] : [],
|
||||
toSafeNumber: (v, fallback = 0) => typeof v === "number" ? (Number.isFinite(v) ? v : fallback) : (typeof v === "string" && /^\s*-?\d+(\.\d+)?\s*$/.test(v) ? Number(v) : fallback),
|
||||
toSafeDuration: (v) => { const n = Number(v); return Number.isFinite(n) && n > 0 && n <= 86400 ? n : 0 },
|
||||
},
|
||||
[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()] },
|
||||
@@ -86,10 +146,11 @@ const stubs = {
|
||||
[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/PlaylistManager")]: playlistsStub,
|
||||
[norm(SRC + "/playlists/GuildPlaylists")]: guildPlaylistsStub,
|
||||
[norm(SRC + "/playlists/History")]: { getPersonalHistory: () => [], addToPersonalHistory() {}, clearPersonalHistory: async () => {} },
|
||||
[norm(SRC + "/lyrics/Lyrics")]: {},
|
||||
[norm(SRC + "/discord/ServerSettings")]: { getSecureRole: () => null },
|
||||
[norm(SRC + "/lyrics/Lyrics")]: lyricsStub,
|
||||
[norm(SRC + "/discord/ServerSettings")]: serverSettingsStub,
|
||||
[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 },
|
||||
@@ -218,6 +279,112 @@ function serverSocket(client) { return ioRef.sockets.sockets.get(client.id) }
|
||||
const status = await new Promise(r => c.emit("/GOOGLE/STATUS", null, r))
|
||||
assert.strictEqual(status.user, "C")
|
||||
|
||||
// --- Playlists partagées : option, droits, poussées (réponses par acquittement) ---
|
||||
const ackReq = (s, event, data) => new Promise((resolve, reject) => {
|
||||
s.emit(event, data, resolve)
|
||||
setTimeout(() => reject(new Error("pas d'acquittement " + event)), 3000)
|
||||
})
|
||||
const d = await connect("tD")
|
||||
assert.strictEqual(await request(d, "/GUILD/JOIN", "g1"), true)
|
||||
let list = await ackReq(d, "/GUILD/PLAYLISTS/LIST")
|
||||
assert.ok(list.ok && list.guildId === "g1" && list.settings.enabled === false, "LIST désactivé : " + JSON.stringify(list))
|
||||
assert.deepStrictEqual(list.playlists, [], "désactivé : playlists masquées")
|
||||
assert.strictEqual(list.canEdit, false)
|
||||
let r = await ackReq(d, "/GUILD/PLAYLISTS/CREATE", { name: "Test" })
|
||||
assert.strictEqual(r.error, "DISABLED")
|
||||
r = await ackReq(d, "/OWNER/PLAYLISTS/SETTINGS", { enabled: true })
|
||||
assert.strictEqual(r.error, "FORBIDDEN", "un membre ne gère pas l'option")
|
||||
assert.ok(!sharedSettings.has("g1"), "aucun réglage écrit par un refus")
|
||||
|
||||
const pushed = new Promise(res => d.once("/GUILD/PLAYLISTS/UPDATE", res))
|
||||
r = await ackReq(c, "/OWNER/PLAYLISTS/SETTINGS", { enabled: true })
|
||||
assert.ok(r.ok && r.settings.enabled === true && r.settings.editors === "members", JSON.stringify(r))
|
||||
assert.deepStrictEqual(await pushed, { guildId: "g1" }, "poussée /GUILD/PLAYLISTS/UPDATE à la room du serveur")
|
||||
r = await ackReq(c, "/OWNER/PLAYLISTS/SETTINGS", { editors: "tous" })
|
||||
assert.strictEqual(r.error, "INVALID_PARAMS")
|
||||
|
||||
list = await ackReq(d, "/GUILD/PLAYLISTS/LIST")
|
||||
assert.ok(list.ok && list.canEdit === true && list.canManage === false && list.playlists.length === 1, JSON.stringify(list))
|
||||
r = await ackReq(d, "/GUILD/PLAYLISTS/CREATE", { name: " " })
|
||||
assert.strictEqual(r.error, "INVALID_PARAMS", "nom vide refusé")
|
||||
r = await ackReq(d, "/GUILD/PLAYLISTS/CREATE", { name: "x".repeat(101) })
|
||||
assert.strictEqual(r.error, "INVALID_PARAMS", "nom de plus de 100 caractères refusé")
|
||||
r = await ackReq(d, "/GUILD/PLAYLISTS/CREATE", { name: " De D " })
|
||||
assert.ok(r.ok && r.playlist.title === "De D" && r.playlist.createdBy.id === "D", JSON.stringify(r))
|
||||
const createdByD = r.playlist.playlistId
|
||||
|
||||
// Modérateurs seulement : D (simple membre) ne modifie plus, mais peut supprimer SA playlist
|
||||
r = await ackReq(c, "/OWNER/PLAYLISTS/SETTINGS", { editors: "mods" })
|
||||
assert.ok(r.ok && r.settings.editors === "mods" && r.settings.enabled === true)
|
||||
list = await ackReq(d, "/GUILD/PLAYLISTS/LIST")
|
||||
assert.strictEqual(list.canEdit, false)
|
||||
r = await ackReq(d, "/GUILD/PLAYLISTS/CREATE", { name: "Refusée" })
|
||||
assert.strictEqual(r.error, "FORBIDDEN")
|
||||
// Toutes les routes d'écriture sont refusées à un simple membre (vérifié avant le module)
|
||||
for (const [route, payload] of [
|
||||
["/GUILD/PLAYLISTS/RENAME", { id: "10", newName: "Piratée" }],
|
||||
["/GUILD/PLAYLISTS/ADD_SONG", { id: "10", song: { type: "youtube", id: "x", title: "x", url: "https://www.youtube.com/watch?v=x" } }],
|
||||
["/GUILD/PLAYLISTS/REMOVE_SONG", { id: "10", songId: "x" }],
|
||||
["/GUILD/PLAYLISTS/MOVE_SONG", { id: "10", from: 0, to: 1 }],
|
||||
["/GUILD/PLAYLISTS/REORDER", { order: ["10"] }],
|
||||
["/GUILD/PLAYLISTS/REFRESH", { id: "10" }],
|
||||
["/GUILD/PLAYLISTS/IMPORT", { playlistId: "1" }],
|
||||
]) {
|
||||
r = await ackReq(d, route, payload)
|
||||
assert.strictEqual(r.error, "FORBIDDEN", route + " refusé à un membre en mode modérateurs : " + JSON.stringify(r))
|
||||
}
|
||||
assert.strictEqual(guildLists.g1.find(p => p.playlistId === "10").title, "Commune", "aucune modification par un refus")
|
||||
r = await ackReq(d, "/GUILD/PLAYLISTS/DELETE", { id: "10" })
|
||||
assert.strictEqual(r.error, "FORBIDDEN", "playlist d'un autre membre")
|
||||
r = await ackReq(d, "/GUILD/PLAYLISTS/DELETE", { id: createdByD })
|
||||
assert.ok(r.ok, "le créateur supprime sa playlist : " + JSON.stringify(r))
|
||||
r = await ackReq(c, "/GUILD/PLAYLISTS/DELETE", { id: "inconnue" })
|
||||
assert.strictEqual(r.error, "NOT_FOUND")
|
||||
const ginfo = await request(d, "/GUILD/INFO")
|
||||
assert.deepStrictEqual(ginfo.sharedPlaylists, { enabled: true, editors: "mods", canEdit: false, canManage: false })
|
||||
// Copie d'une playlist du serveur dans mes playlists : simple lecture (D n'est pas éditeur)
|
||||
const personalPush = new Promise(res => d.once("/PLAYLISTS/UPDATE", res))
|
||||
r = await ackReq(d, "/GUILD/PLAYLISTS/COPY", { id: "10" })
|
||||
assert.ok(r.ok && r.playlist.title === "Commune", JSON.stringify(r))
|
||||
await personalPush
|
||||
|
||||
// --- Playlists personnelles : réponses par acquittement + poussée à toutes les sessions ---
|
||||
const cPush = new Promise(res => c.once("/PLAYLISTS/UPDATE", res))
|
||||
assert.strictEqual(await ackReq(c, "/PLAYLISTS/REORDER", { order: ["3", "inconnu", "1", "3"] }), true)
|
||||
await cPush
|
||||
assert.deepStrictEqual(personal.C.map(p => p.playlistId), ["3", "1", "2"], "ordre appliqué, inconnus ignorés, oubliés à la suite")
|
||||
assert.strictEqual(await ackReq(c, "/PLAYLISTS/REORDER", { order: "3,1" }), false)
|
||||
const listed = await ackReq(c, "/PLAYLISTS/LIST")
|
||||
assert.deepStrictEqual(listed.map(p => p.playlistId), ["3", "1", "2"])
|
||||
assert.ok(Array.isArray(await request(c, "/PLAYLISTS/LIST")), "ancien client (événement du même nom)")
|
||||
assert.strictEqual(await ackReq(c, "/PLAYLISTS/MOVE_SONG", { id: "1", from: 0, to: "x" }), false)
|
||||
assert.strictEqual(await ackReq(c, "/PLAYLISTS/CREATE", { name: "" }), false)
|
||||
|
||||
// --- Paroles ---
|
||||
r = await ackReq(c, "/PLAYER/LYRICS")
|
||||
assert.deepStrictEqual(r, { ok: false, error: "NO_MUSIC" }, "rien en cours de lecture")
|
||||
r = await ackReq(c, "/LYRICS/SEARCH", { q: " " })
|
||||
assert.strictEqual(r.error, "INVALID_PARAMS")
|
||||
r = await ackReq(c, "/LYRICS/SEARCH", { q: "x".repeat(201) })
|
||||
assert.strictEqual(r.error, "INVALID_PARAMS")
|
||||
r = await ackReq(c, "/LYRICS/SEARCH", { q: " daft punk one more time " })
|
||||
assert.ok(r.ok && r.results[0].track === "daft punk one more time", JSON.stringify(r))
|
||||
r = await ackReq(c, "/LYRICS/GET", { id: -4 })
|
||||
assert.strictEqual(r.error, "INVALID_PARAMS")
|
||||
r = await ackReq(c, "/LYRICS/GET", { id: 42 })
|
||||
assert.strictEqual(r.error, "UNAVAILABLE", "fonction absente du module : UNAVAILABLE")
|
||||
r = await request(c, "/SEARCH/LYRICS", { track: "Titre", artist: "Artiste", duration: 200 })
|
||||
assert.strictEqual(r.error, "NOT_FOUND", "ancienne route : résultat du module")
|
||||
assert.deepStrictEqual(lyricsCalls[lyricsCalls.length - 1], { track: "Titre", artist: "Artiste", duration: 200 })
|
||||
r = await ackReq(c, "/SEARCH/LYRICS", " artiste titre ")
|
||||
assert.strictEqual(lyricsCalls[lyricsCalls.length - 1], "artiste titre", "texte libre transmis tel quel (nettoyé)")
|
||||
r = await ackReq(c, "/SEARCH/LYRICS", { track: "", artist: "x" })
|
||||
assert.strictEqual(r.error, "INVALID_PARAMS")
|
||||
|
||||
// --- Statistiques publiques du serveur ---
|
||||
r = await ackReq(d, "/GUILD/STATS")
|
||||
assert.deepStrictEqual(r, { ok: true, musicPlayed: 12, secondsPlayed: 3600, commands: 0 })
|
||||
|
||||
await sleep(100)
|
||||
assert.deepStrictEqual(uncaught, [], "aucune exception non gérée")
|
||||
console.log("server-socket.test.js : OK")
|
||||
|
||||
Reference in new issue
Block a user