// Routes "/ADMIN/YOUTUBE/*" de backend/src/server/Admin.js, appelées par un faux socket. // Dépendances lourdes (bot Discord, utilisateurs, players, bases...) simulées ; données YouTube isolées // dans le scratchpad (jamais backend/data). Aucun appel réseau : yt-dlp (run/update/check/test) est simulé. const Module = require('module'); const path = require('path'); const fs = require('fs'); const { BACKEND, isolateData, check, done, req, chopinTmpDirs } = require('./_setup'); const dataDir = isolateData('admin-yt'); process.env.YTDLP_AUTO_UPDATE = 'false'; process.env.YTDLP_COOKIES_ON_PROXY = 'false'; // verrouillé par l'environnement const SRC = path.join(BACKEND, 'src'); const norm = (p) => p.replace(/\\/g, '/').toLowerCase().replace(/\.js$/, ''); // --- Stubs --- const admin = { identity: { id: '111111', username: 'testadmin' }, labels: ['ADMIN'], isAdmin: () => true, isFullBanned: () => false }; const guilds = new Map([['222222', { id: '222222', name: 'Serveur Test' }]]); const stubs = { [norm(SRC + '/server/auth/User')]: { getUserById: (id) => id === '111111' ? admin : null, getUsers: () => [admin] }, [norm(SRC + '/server/auth/Session')]: { getSessionCount: () => 0 }, [norm(SRC + '/player/Player')]: { getAllPlayers: () => [], AllPlayers: new Map() }, [norm(SRC + '/discord/Bot')]: { getGuilds: () => guilds, getClient: () => null, isReady: () => false }, [norm(SRC + '/discord/ServerSettings')]: {}, [norm(SRC + '/playlists/PlaylistManager')]: {}, [norm(SRC + '/utils/SafeMetric')]: { getAll: () => [], get: () => 0 }, [norm(SRC + '/utils/LogReader')]: { init() {}, getCurrentName: () => 'x.log', isInterrupted: async () => false }, [norm(SRC + '/utils/Maintenance')]: {}, [norm(SRC + '/utils/Database/Configuration')]: { getMediaGuildId: () => null, getMediaChannelId: () => null }, [norm(SRC + '/utils/Database/Database')]: { getDatabase: () => null }, [norm(SRC + '/player/Method/Youtube')]: { activeProcesses: new Map([['222222', { pid: 1 }]]) }, }; const origLoad = Module._load; Module._load = function (request, parent) { if (request.startsWith('.') && parent) { const resolved = norm(path.resolve(path.dirname(parent.filename), request)); if (stubs[resolved]) return stubs[resolved]; } return origLoad.apply(this, arguments); }; const Admin = req('src/server/Admin.js'); const youtube = req('src/youtube'); // --- Faux socket / io --- const handlers = new Map(); const socket = { id: 'sock1', data: { userId: '111111' }, on: (name, fn) => handlers.set(name, fn), emit() {}, leave() {}, join() {} }; const emitted = []; const rooms = new Map([['ADMIN', new Set(['sock1'])]]); const fakeIo = { sockets: { adapter: { rooms }, sockets: new Map() }, to: (room) => ({ emit: (ev, data) => emitted.push({ room, ev, data }) }), emit() {}, of: () => ({ adapter: { on() {} } }) }; Admin.attach({ io: fakeIo, socket, socketUser: () => admin }); Admin.init(fakeIo); function call(name, payload) { return new Promise((resolve) => handlers.get(name)(payload, resolve)); } const sleep = (ms) => new Promise(r => setTimeout(r, ms)); // Cookies SYNTHÉTIQUES (valeurs factices), domaine YouTube + un domaine étranger const SECRET = 'FAKESECRET'; const exp = Math.floor(Date.now() / 1000) + 30 * 86400; const line = (d, n, v) => [d, 'TRUE', '/', 'TRUE', String(exp), n, v].join('\t'); const COOKIES_OK = ['# Netscape HTTP Cookie File', line('.youtube.com', 'LOGIN_INFO', SECRET + '_login'), line('.youtube.com', 'SAPISID', SECRET + '_sapisid'), line('.youtube.com', '__Secure-3PAPISID', SECRET + '_3p'), line('.youtube.com', 'SID', SECRET + '_sid'), line('.example.org', 'tracker', SECRET + '_other')].join('\r\n'); const COOKIES_ANON = ['# Netscape HTTP Cookie File', line('.youtube.com', 'YSC', SECRET + '_ysc')].join('\n'); (async () => { const expectedRoutes = ['STATUS', 'COOKIES/IMPORT', 'COOKIES/BROWSER', 'COOKIES/CHECK', 'COOKIES/DELETE', 'SETTINGS', 'TEST', 'UPDATE', 'DIAGNOSTICS/CLEAR']; check('routes enregistrées', expectedRoutes.every(r => handlers.has('/ADMIN/YOUTUBE/' + r))); // Vérification en direct simulée (pas de réseau) let checkCalls = 0; youtube.check.checkCookies = async () => { checkCalls++; const result = { at: new Date().toISOString(), ok: true, loggedIn: true, premium: false, message: 'Compte reconnu', cls: null, route: 'direct' }; youtube.cookies.setLastCheck(result); return { ...result, rotated: false, ms: 5, status: youtube.cookies.getStatus() }; }; console.log('# STATUS'); let r = await call('/ADMIN/YOUTUBE/STATUS', {}); check('STATUS ok', r.ok, r); check('STATUS forme', r.data && r.data.ytdlp && r.data.cookies && r.data.settings && r.data.diagnostics && Array.isArray(r.data.recent)); check('aucun cookie au départ', r.data.cookies.present === false); check('cookiesOnProxy verrouillé', r.data.settings.cookiesOnProxy.locked === true); console.log('# IMPORT'); r = await call('/ADMIN/YOUTUBE/COOKIES/IMPORT', { content: '' }); check('contenu vide refusé', !r.ok && r.error === 'INVALID_PARAMS', r); r = await call('/ADMIN/YOUTUBE/COOKIES/IMPORT', { content: 'x'.repeat(1000001) }); check('contenu > 1 000 000 refusé', !r.ok && r.error === 'INVALID_PARAMS'); r = await call('/ADMIN/YOUTUBE/COOKIES/IMPORT', { content: COOKIES_OK, source: 'browser:firefox' }); check('source interdite refusée', !r.ok && r.error === 'INVALID_PARAMS'); r = await call('/ADMIN/YOUTUBE/COOKIES/IMPORT', { content: 'du texte qui ne ressemble à rien' }); check('contenu illisible -> INVALID_PARAMS avec message FR', !r.ok && r.error === 'INVALID_PARAMS' && /cookie/i.test(r.message), r); r = await call('/ADMIN/YOUTUBE/COOKIES/IMPORT', { content: COOKIES_ANON, source: 'paste' }); check('import sans compte : ok + avertissement', r.ok && r.data.warnings.some(w => /connexion/i.test(w)) && r.data.checking === false, r.data?.warnings); r = await call('/ADMIN/YOUTUBE/COOKIES/IMPORT', { content: COOKIES_OK, source: 'file' }); check('import connecté ok', r.ok && r.data.status.cookies.loggedIn && r.data.status.cookies.valid, r); check('avertissement domaine étranger', r.ok && r.data.warnings.some(w => /autres sites/.test(w)), r.data?.warnings); check('vérification lancée', r.data.checking === true); check('source / auteur enregistrés', r.data.status.cookies.source === 'file' && r.data.status.cookies.importedBy?.username === 'testadmin'); check('aucune valeur de cookie dans la réponse', !JSON.stringify(r).includes(SECRET)); check('fichier maître dans le dossier isolé', fs.existsSync(path.join(dataDir, 'cookies.txt'))); await sleep(50); check('checkCookies appelé', checkCalls === 1); console.log('# Poussée'); emitted.length = 0; await sleep(700); const push = emitted.filter(e => e.ev === '/ADMIN/YOUTUBE/UPDATE'); check('poussée /ADMIN/YOUTUBE/UPDATE (coalescée)', push.length === 1 && push[0].room === 'ADMIN', emitted.map(e => e.ev)); check('poussée contient lastCheck', push[0]?.data?.cookies?.lastCheck?.ok === true); rooms.set('ADMIN', new Set()); emitted.length = 0; process.emit('YOUTUBE_UPDATE'); await sleep(700); check('pas de poussée si room ADMIN vide', emitted.length === 0); rooms.set('ADMIN', new Set(['sock1'])); console.log('# CHECK'); r = await call('/ADMIN/YOUTUBE/COOKIES/CHECK', {}); check('CHECK ok', r.ok && r.data.result.ok === true && r.data.result.status === undefined && r.data.status.cookies.lastCheck, r); console.log('# SETTINGS'); r = await call('/ADMIN/YOUTUBE/SETTINGS', { patch: { cookiesMode: 'always', playerClients: 'web_safari, tv' } }); check('SETTINGS appliqués', r.ok && r.data.settings.cookiesMode.value === 'always' && r.data.settings.playerClients.value === 'web_safari,tv', r); r = await call('/ADMIN/YOUTUBE/SETTINGS', { patch: { cookiesOnProxy: true } }); check('clé verrouillée refusée', !r.ok && r.error === 'INVALID_PARAMS' && /YTDLP_COOKIES_ON_PROXY/.test(r.message), r); r = await call('/ADMIN/YOUTUBE/SETTINGS', { patch: { seekStrategy: 'n importe quoi' } }); check('valeur invalide refusée', !r.ok && r.error === 'INVALID_PARAMS'); r = await call('/ADMIN/YOUTUBE/SETTINGS', { patch: { inconnu: 1 } }); check('clé inconnue refusée', !r.ok && r.error === 'INVALID_PARAMS'); r = await call('/ADMIN/YOUTUBE/SETTINGS', { patch: {} }); check('patch vide refusé', !r.ok && r.error === 'INVALID_PARAMS'); r = await call('/ADMIN/YOUTUBE/SETTINGS', { patch: [1] }); check('patch tableau refusé', !r.ok && r.error === 'INVALID_PARAMS'); // Retour aux valeurs par défaut r = await call('/ADMIN/YOUTUBE/SETTINGS', { patch: { cookiesMode: 'fallback', playerClients: '' } }); check('SETTINGS remis par défaut', r.ok && r.data.settings.cookiesMode.value === 'fallback' && r.data.settings.playerClients.value === ''); check('settings.json dans le dossier isolé', fs.existsSync(path.join(dataDir, 'settings.json'))); console.log('# TEST'); let testArgs = null; const realTest = youtube.check.testPlayback; youtube.check.testPlayback = async (opts) => { testArgs = opts; return { ok: true, route: 'direct', identity: 'anon', clients: '', cls: null, message: 'Extraction réussie', format: { id: '251' }, ms: 1, version: 'x' }; }; r = await call('/ADMIN/YOUTUBE/TEST', { route: 'direct', identity: 'anon', url: '', clients: '' }); check('TEST transmis (url / clients vides -> réglages)', r.ok && testArgs.route === 'direct' && testArgs.url === undefined && testArgs.clients === undefined, testArgs); r = await call('/ADMIN/YOUTUBE/TEST', { route: 'moon' }); check('route invalide', !r.ok && r.error === 'INVALID_PARAMS'); r = await call('/ADMIN/YOUTUBE/TEST', { identity: 'root' }); check('identité invalide', !r.ok && r.error === 'INVALID_PARAMS'); r = await call('/ADMIN/YOUTUBE/TEST', { url: 'javascript:alert(1)' }); check('url non http refusée', !r.ok && r.error === 'INVALID_PARAMS'); youtube.check.testPlayback = async () => { const e = new Error('Un test de lecture est déjà en cours'); e.code = 'BUSY'; throw e; }; r = await call('/ADMIN/YOUTUBE/TEST', {}); check('BUSY -> UNAVAILABLE', !r.ok && r.error === 'UNAVAILABLE', r); youtube.check.testPlayback = realTest; // Vraie fonction, sans réseau : clients invalides -> INVALID_PARAMS r = await call('/ADMIN/YOUTUBE/TEST', { clients: 'web;rm' }); check('clients invalides (vrai testPlayback) -> INVALID_PARAMS', !r.ok && r.error === 'INVALID_PARAMS', r); console.log('# UPDATE'); youtube.ytdlp.update = async ({ reason }) => ({ ok: true, updatable: true, before: '1', after: '2', message: 'Mise à jour : 1 → 2', output: '', at: new Date().toISOString(), reason }); r = await call('/ADMIN/YOUTUBE/UPDATE', {}); check('UPDATE ok', r.ok && r.data.result.after === '2' && /testadmin/.test(r.data.result.reason) && r.data.status.ytdlp, r); console.log('# BROWSER (yt-dlp simulé)'); const realRun = youtube.ytdlp.run; let runArgs = null; youtube.ytdlp.run = async (args) => { runArgs = args; const file = args[args.indexOf('--cookies') + 1]; fs.writeFileSync(file, COOKIES_OK + '\n' + line('.bank.example', 'session', SECRET + '_bank')); return { code: 1, stdout: '', stderr: 'ERROR: [youtube] x: Sign in to confirm you’re not a bot', timedOut: false }; }; r = await call('/ADMIN/YOUTUBE/COOKIES/BROWSER', { browser: 'firefox', profile: 'default-release' }); check('BROWSER importe malgré l\'échec d\'extraction', r.ok && r.data.status.cookies.source === 'browser:firefox' && r.data.status.cookies.loggedIn, r); check('arguments --cookies-from-browser', runArgs && runArgs[runArgs.indexOf('--cookies-from-browser') + 1] === 'firefox:default-release' && runArgs.includes('--skip-download')); const tmpDir = path.dirname(runArgs[runArgs.indexOf('--cookies') + 1]); check('fichier temporaire du navigateur supprimé', !fs.existsSync(tmpDir), tmpDir); check('cookies étrangers non stockés', !fs.readFileSync(path.join(dataDir, 'cookies.txt'), 'utf-8').includes('bank')); youtube.ytdlp.run = async () => ({ code: 1, stdout: '', stderr: 'ERROR: could not find firefox cookies database in /x --cookies /secret/path', timedOut: false }); r = await call('/ADMIN/YOUTUBE/COOKIES/BROWSER', { browser: 'firefox' }); check('échec navigateur -> UNAVAILABLE explicite', !r.ok && r.error === 'UNAVAILABLE' && /même machine/.test(r.message) && !r.message.includes('/secret/path'), r); youtube.ytdlp.run = async (args) => { fs.writeFileSync(args[args.indexOf('--cookies') + 1], '# Netscape HTTP Cookie File\n' + line('.bank.example', 'session', 'x')); return { code: 0, stdout: '', stderr: '' }; }; r = await call('/ADMIN/YOUTUBE/COOKIES/BROWSER', { browser: 'chrome' }); check('navigateur sans cookie YouTube -> INVALID_PARAMS', !r.ok && r.error === 'INVALID_PARAMS' && /chrome/.test(r.message), r); r = await call('/ADMIN/YOUTUBE/COOKIES/BROWSER', { browser: 'netscape' }); check('navigateur inconnu refusé', !r.ok && r.error === 'INVALID_PARAMS'); r = await call('/ADMIN/YOUTUBE/COOKIES/BROWSER', { browser: 'firefox', profile: 'a::b' }); check('profil invalide refusé', !r.ok && r.error === 'INVALID_PARAMS'); youtube.ytdlp.run = realRun; check('aucune copie chopin-ytc-* restante', chopinTmpDirs().length === 0, chopinTmpDirs()); console.log('# OVERVIEW'); r = await call('/ADMIN/OVERVIEW', {}); const yt = r.data?.ytdlp; check('overview.ytdlp compatible', r.ok && yt && 'version' in yt && yt.cookies.present === true && yt.cookies.updatedAt === yt.cookies.importedAt && yt.activeProcesses === 1, r.ok ? yt : r); check('overview.ytdlp enrichi', yt && yt.cookies.valid === true && 'recentFailureRate' in yt && 'lastFailure' in yt); console.log('# DIAGNOSTICS'); youtube.diagnostics.record({ guildId: '222222', songId: 's', title: 'Titre', route: 'direct', identity: 'anon', outcome: 'failed', cls: 'HTTP_403', message: 'HTTP Error 403', ms: 100 }); r = await call('/ADMIN/YOUTUBE/STATUS', {}); check('recent avec nom du serveur', r.data.recent[0]?.guildName === 'Serveur Test' && r.data.diagnostics.byClass.HTTP_403 === 1, r.data.recent[0]); r = await call('/ADMIN/YOUTUBE/DIAGNOSTICS/CLEAR', {}); check('CLEAR', r.ok && r.data.recent.length === 0 && r.data.diagnostics.total === 0); console.log('# DELETE'); r = await call('/ADMIN/YOUTUBE/COOKIES/DELETE', {}); check('DELETE', r.ok && r.data.cookies.present === false && !fs.existsSync(path.join(dataDir, 'cookies.txt'))); r = await call('/ADMIN/YOUTUBE/COOKIES/CHECK', {}); check('CHECK sans cookies -> UNAVAILABLE', !r.ok && r.error === 'UNAVAILABLE'); console.log('# Droits'); admin.labels = []; admin.isAdmin = () => false; r = await call('/ADMIN/YOUTUBE/STATUS', {}); check('non-admin refusé', !r.ok && r.error === 'FORBIDDEN'); admin.isAdmin = () => true; admin.labels = ['ADMIN']; // Journaux : aucune valeur de cookie await sleep(300); const logs = path.join(__dirname, 'logs'); let leaked = false; for (const f of fs.readdirSync(logs)) if (fs.readFileSync(path.join(logs, f), 'utf-8').includes(SECRET)) leaked = f; check('aucune valeur de cookie dans les logs', !leaked, leaked); const auditLines = fs.readdirSync(logs).flatMap(f => fs.readFileSync(path.join(logs, f), 'utf-8').split('\n')).filter(l => l.includes('[ADMIN] testadmin -> ') && /YouTube|yt-dlp/.test(l)); check('audits écrits', auditLines.length >= 6, auditLines.length); console.log(auditLines.slice(-12).map(l => ' ' + l.trim()).join('\n')); done(); setTimeout(() => process.exit(process.exitCode), 100); })().catch((e) => { console.error(e); process.exit(1); });