Files
chopin/tests/dev/cookies.test.js
T

184 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('fs');
const path = require('path');
const { BACKEND, __glob, isolateData, check, done, req, chopinTmpDirs } = require('./_setup');
const dataDir = isolateData('cookies');
const cookies = req('src/youtube/cookies');
const NOW = 1791360000; // 2026-10-07
const FUT = 1830000000; // 2027-12
const PAST = 1700000000;
const T = (...f) => f.join('\t');
let events = 0;
process.on('YOUTUBE_UPDATE', () => events++);
function throwsInvalid(fn) {
try { fn(); return false; } catch (e) { return e.code === 'INVALID_COOKIES' && /[a-zé]/.test(e.message); }
}
console.log('# Netscape');
{
const loggedIn = [
'# Netscape HTTP Cookie File',
'# commentaire',
'',
T('.youtube.com', 'TRUE', '/', 'TRUE', FUT, 'LOGIN_INFO', 'AAA'),
T('.youtube.com', 'TRUE', '/', 'TRUE', FUT, 'SAPISID', 'BBB'),
'#HttpOnly_' + T('.youtube.com', 'TRUE', '/', 'TRUE', FUT, '__Secure-3PSID', 'CCC'),
T('.youtube.com', 'TRUE', '/', 'FALSE', 0, 'PREF', 'f6=40000000'),
T('.youtube.com', 'TRUE', '/', 'TRUE', PAST, 'GPS', '1'),
T('.example.com', 'TRUE', '/', 'FALSE', FUT, 'tracker', 'x'),
T('accounts.google.com', 'FALSE', '/', 'TRUE', FUT, '__Host-GAPS', 'g'),
'ligne cassée sans tabulation',
T('.youtube.com', 'MAYBE', '/', 'TRUE', FUT, 'X', 'y'),
].join('\r\n');
const r = cookies.parseCookies(loggedIn, { now: NOW });
const names = r.cookies.map(c => c.name);
check('CRLF + en-tête : cookies valides lus', names.includes('LOGIN_INFO') && names.includes('SAPISID') && names.includes('PREF'), names);
check('#HttpOnly_ reconnu', r.cookies.find(c => c.name === '__Secure-3PSID')?.httpOnly === true);
check('cookie de session (0) gardé', r.cookies.find(c => c.name === 'PREF')?.expires === 0);
check('expiré retiré', !names.includes('GPS'));
check('domaine étranger retiré', !names.includes('tracker'));
check('google.com gardé (hostOnly)', r.cookies.find(c => c.name === '__Host-GAPS')?.includeSubdomains === false);
check('avertissements (lignes, domaines, expirés)', r.warnings.length === 3, r.warnings);
const a = cookies.analyze(r.cookies);
check('analyze : loggedIn', a.loggedIn === true);
check('analyze : jamais de valeur', !JSON.stringify(a).includes('AAA') && !JSON.stringify(a).includes('BBB'));
check('analyze : authNames', a.authNames.includes('LOGIN_INFO') && a.authNames.includes('SAPISID') && a.authNames.includes('__Secure-3PSID'), a.authNames);
check('analyze : earliestAuthExpiry ISO', a.earliestAuthExpiry === new Date(FUT * 1000).toISOString());
// Sans en-tête, LF, valeur vide, tabulations converties en espaces
const noHeader = [
T('.youtube.com', 'TRUE', '/', 'TRUE', FUT, 'LOGIN_INFO', 'AAA'),
'.youtube.com TRUE / TRUE ' + FUT + ' __Secure-3PAPISID ZZZ',
T('.youtube.com', 'TRUE', '/', 'TRUE', FUT, 'EMPTY', ''),
].join('\n');
const r2 = cookies.parseCookies(noHeader, { now: NOW });
check('sans en-tête + espaces au lieu de tabulations', r2.cookies.length === 3, r2.cookies.map(c => c.name));
check('valeur vide acceptée', r2.cookies.find(c => c.name === 'EMPTY')?.value === '');
check('loggedIn via __Secure-3PAPISID', cookies.analyze(r2.cookies).loggedIn === true);
// Règle yt-dlp : LOGIN_INFO seul ou SAPISID seul ne suffit pas
const onlySapisid = T('.youtube.com', 'TRUE', '/', 'TRUE', FUT, '__Secure-3PAPISID', 'x');
check('pas connecté sans LOGIN_INFO', cookies.analyze(cookies.parseCookies(onlySapisid, { now: NOW }).cookies).loggedIn === false);
const onlyLogin = T('.youtube.com', 'TRUE', '/', 'TRUE', FUT, 'LOGIN_INFO', 'x');
const aLogin = cookies.analyze(cookies.parseCookies(onlyLogin, { now: NOW }).cookies);
check('pas connecté sans SAPISID', aLogin.loggedIn === false && aLogin.missing.includes('SAPISID'));
// Cookies google.com seuls : ne comptent pas pour youtube.com
const googleOnly = [T('.google.com', 'TRUE', '/', 'TRUE', FUT, 'LOGIN_INFO', 'x'), T('.google.com', 'TRUE', '/', 'TRUE', FUT, 'SAPISID', 'x')].join('\n');
check('LOGIN_INFO sur google.com ne compte pas', cookies.analyze(cookies.parseCookies(googleOnly, { now: NOW }).cookies).loggedIn === false);
// Sérialisation : relue à l'identique, LF, en-tête
const text = cookies.serializeCookies(r.cookies);
check('sérialisation : en-tête Netscape + LF', text.startsWith('# Netscape HTTP Cookie File\n') && !text.includes('\r'));
const back = cookies.parseCookies(text, { now: NOW }).cookies;
check('sérialisation : aller-retour', JSON.stringify(back) === JSON.stringify(r.cookies));
}
console.log('# JSON');
{
const arr = [
{ domain: '.youtube.com', expirationDate: FUT + 0.512229, hostOnly: false, httpOnly: true, name: 'LOGIN_INFO', path: '/', sameSite: 'no_restriction', secure: true, session: false, storeId: '0', value: 'AAA', id: 1 },
{ domain: '.youtube.com', expirationDate: FUT + 0.9, hostOnly: false, httpOnly: false, name: 'SAPISID', path: '/', secure: true, session: false, value: 'BBB' },
{ domain: 'www.youtube.com', hostOnly: true, httpOnly: false, name: 'ST-xyz', path: '/', secure: false, session: true, value: 'S' },
{ domain: 'youtube.com', hostOnly: false, name: 'NODOT', path: '/', secure: true, expires: (FUT * 1000), value: 'ms' },
{ domain: '.youtube.com', name: 'OLD', expirationDate: PAST, value: 'x' },
{ domain: '.facebook.com', name: 'fb', expirationDate: FUT, value: 'x' },
{ nope: true },
];
const r = cookies.parseCookies(JSON.stringify(arr), { now: NOW });
const by = Object.fromEntries(r.cookies.map(c => [c.name, c]));
check('JSON : expirationDate décimal arrondi', by.LOGIN_INFO?.expires === FUT);
check('JSON : session -> 0', by['ST-xyz']?.expires === 0);
check('JSON : hostOnly -> sans sous-domaines', by['ST-xyz']?.includeSubdomains === false && by['ST-xyz']?.domain === 'www.youtube.com');
check('JSON : domaine non hostOnly sans point -> .youtube.com', by.NODOT?.domain === '.youtube.com' && by.NODOT?.includeSubdomains === true);
check('JSON : expires en millisecondes', by.NODOT?.expires === FUT);
check('JSON : httpOnly', by.LOGIN_INFO?.httpOnly === true && by.SAPISID?.httpOnly === false);
check('JSON : expiré, étranger et invalide retirés', !by.OLD && !by.fb && r.warnings.length === 3, r.warnings);
check('JSON : loggedIn', cookies.analyze(r.cookies).loggedIn === true);
check('JSON : hasSessionOnly faux (le cookie de session n\'est pas un cookie de connexion)', cookies.analyze(r.cookies).hasSessionOnly === false);
const wrapped = cookies.parseCookies(JSON.stringify({ url: 'https://www.youtube.com', cookies: arr.slice(0, 2) }), { now: NOW });
check('{cookies:[...]} accepté', wrapped.cookies.length === 2 && cookies.analyze(wrapped.cookies).loggedIn);
const withBom = cookies.parseCookies('' + JSON.stringify(arr.slice(0, 2)), { now: NOW });
check('BOM ignoré', withBom.cookies.length === 2);
const expiry = cookies.parseCookies(JSON.stringify([{ domain: '.youtube.com', name: 'A', value: 'b', expiry: FUT }]), { now: NOW });
check('champ expiry (Selenium)', expiry.cookies[0].expires === FUT);
const sessionAuth = cookies.parseCookies(JSON.stringify([{ domain: '.youtube.com', name: 'LOGIN_INFO', value: 'b', session: true }]), { now: NOW });
check('hasSessionOnly vrai pour un cookie de connexion de session', cookies.analyze(sessionAuth.cookies).hasSessionOnly === true);
}
console.log('# Contenus invalides');
check('vide', throwsInvalid(() => cookies.parseCookies(' ')));
check('non-chaîne', throwsInvalid(() => cookies.parseCookies(null)));
check('texte quelconque', throwsInvalid(() => cookies.parseCookies('bonjour\nceci n\'est pas un fichier de cookies')));
check('JSON tronqué', throwsInvalid(() => cookies.parseCookies('[{"domain": ".youtube.com", "name": ')));
check('JSON objet sans cookies', throwsInvalid(() => cookies.parseCookies('{"a": 1}')));
check('JSON liste vide', throwsInvalid(() => cookies.parseCookies('[]')));
check('HTML', throwsInvalid(() => cookies.parseCookies('<html><body>Login</body></html>')));
check('commentaires seuls', throwsInvalid(() => cookies.parseCookies('# Netscape HTTP Cookie File\n# rien\n')));
check('valeur avec retour à la ligne refusée', throwsInvalid(() => cookies.parseCookies(JSON.stringify([{ domain: '.youtube.com', name: 'A', value: 'x\ny' }]))));
console.log('# Stockage');
const goodText = [T('.youtube.com', 'TRUE', '/', 'TRUE', FUT, 'LOGIN_INFO', 'AAA'), T('.youtube.com', 'TRUE', '/', 'TRUE', FUT, 'SAPISID', 'BBB')].join('\n');
let threw = false;
try { cookies.importCookies(T('.google.com', 'TRUE', '/', 'TRUE', FUT, 'SID', 'x')); } catch (e) { threw = e.code === 'INVALID_COOKIES'; }
check('import refusé sans cookie youtube.com', threw && !fs.existsSync(__glob.YOUTUBE_COOKIES));
const before = events;
const res = cookies.importCookies(goodText, { source: 'paste', by: { id: '42', username: 'raphix' } });
check('import : statut valide', res.status.valid && res.status.loggedIn && res.status.count === 2 && res.status.source === 'paste', res.status);
check('import : importedBy', res.status.importedBy?.username === 'raphix');
check('import : YOUTUBE_UPDATE émis', events > before);
check('import : fichier maître LF + en-tête', fs.readFileSync(__glob.YOUTUBE_COOKIES, 'utf8').startsWith('# Netscape HTTP Cookie File\n'));
check('statut : jamais de valeur', !JSON.stringify(cookies.getStatus()).includes('AAA'));
const notLogged = cookies.importCookies(T('.youtube.com', 'TRUE', '/', 'TRUE', FUT, 'PREF', 'x'), { source: 'file' });
check('import non connecté : stocké + avertissement', notLogged.status.present && !notLogged.status.loggedIn && notLogged.warnings.some(w => /connexion/.test(w)), notLogged.warnings);
cookies.importCookies(goodText, { source: 'paste' });
check('markInvalid', cookies.markInvalid('test') === true && cookies.getStatus().valid === false && cookies.getStatus().invalidReason === 'test');
check('markInvalid idempotent', cookies.markInvalid('encore') === false && cookies.getStatus().invalidReason === 'test');
cookies.setLastCheck({ at: new Date().toISOString(), ok: true, loggedIn: true, premium: false, message: 'ok' });
check('vérification réussie : invalidation levée', cookies.getStatus().valid === true);
cookies.reload();
check('rechargement depuis le disque', cookies.getStatus().valid === true && cookies.getStatus().count === 2);
(async () => {
const copy = await cookies.acquireCopy();
check('acquireCopy : fichier identique au maître', fs.readFileSync(copy.path, 'utf8') === fs.readFileSync(__glob.YOUTUBE_COOKIES, 'utf8'));
check('acquireCopy : dossier chopin-ytc-', path.basename(path.dirname(copy.path)).startsWith('chopin-ytc-') && cookies.liveCopyCount() === 1);
copy.release(); copy.release();
check('release : dossier supprimé', !fs.existsSync(path.dirname(copy.path)) && cookies.liveCopyCount() === 0);
const del = cookies.deleteCookies();
check('deleteCookies', !del.present && !fs.existsSync(__glob.YOUTUBE_COOKIES));
check('acquireCopy sans cookies -> null', (await cookies.acquireCopy()) === null);
check('aucune copie temporaire restante', chopinTmpDirs().length === 0, chopinTmpDirs());
console.log('# Fichiers réels (lecture seule, analyse sans valeurs)');
const realTxt = path.join(BACKEND, 'data', 'cookies.txt');
const realJson = path.join(BACKEND, 'data', 'cookies.json');
const mtimes = () => [realTxt, realJson].map(f => fs.existsSync(f) ? fs.statSync(f).mtimeMs : null).join(',');
const mtimesBefore = mtimes();
for (const file of [realTxt, realJson]) {
if (!fs.existsSync(file)) { console.log(` (absent) ${path.basename(file)}`); continue; }
try {
const r = cookies.parseCookies(fs.readFileSync(file, 'utf8'));
console.log(` ${path.basename(file)} :`, JSON.stringify(cookies.analyze(r.cookies)), '| avertissements :', JSON.stringify(r.warnings));
} catch (e) {
console.log(` ${path.basename(file)} : erreur ${e.code} ${e.message}`);
}
}
// Migration : les anciens fichiers réels servent de source, la destination est le dossier isolé
cookies.reload();
__glob.COOKIES = realTxt;
__glob.COOKIES_JSON = realJson;
const mig = cookies.migrateLegacy();
const st = cookies.getStatus();
console.log(' migration :', JSON.stringify(mig), '| statut :', JSON.stringify({ present: st.present, loggedIn: st.loggedIn, valid: st.valid, source: st.source, count: st.count, authNames: st.authNames, missing: st.missing, earliestAuthExpiry: st.earliestAuthExpiry }));
check('migration : fichiers d\'origine non modifiés', mtimes() === mtimesBefore);
check('migration : pas de seconde migration', cookies.migrateLegacy().migrated === false);
fs.rmSync(dataDir, { recursive: true, force: true });
done();
})();