Files
2026-10-08 10:06:46 +02:00

391 lines
26 KiB
JavaScript
Raw Permalink 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.
// Tests hors réseau de backend/src/lyrics/Lyrics.js (node lyrics.test.js) : fetch global remplacé par un faux
const { check, done, req } = require("./_setup");
const L = req("src/lyrics/Lyrics.js");
const realFetch = globalThis.fetch;
let calls = [];
let route = () => json(404, { message: "not found" });
function json(status, body) {
return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
}
function mock(handler) {
calls = [];
route = handler;
L.clearCache();
}
globalThis.fetch = async (input, options = {}) => {
const url = new URL(String(input));
calls.push({ url, options });
return route(url, options);
};
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
const LRC = "[ar:Daft Punk]\n[ti:One More Time]\n[00:30.75] One more time\n[00:33.18] \n[00:46.35] Celebrate\n[00:50.00] Oh yeah\n";
const PLAIN = "One more time\n\nCelebrate\nOh yeah";
function record(over = {}) {
return {
id: 1, trackName: "One More Time", artistName: "Daft Punk", albumName: "Discovery", duration: 320,
instrumental: false, plainLyrics: PLAIN, syncedLyrics: LRC, ...over,
};
}
(async () => {
console.log("parseLrc");
{
check("texte non chaîne → []", eq(L.parseLrc(null), []) && eq(L.parseLrc(42), []) && eq(L.parseLrc(""), []));
const lines = L.parseLrc(LRC);
check("métadonnées ignorées, lignes vides gardées", eq(lines, [
{ t: 30750, text: "One more time" }, { t: 33180, text: "" }, { t: 46350, text: "Celebrate" }, { t: 50000, text: "Oh yeah" },
]), lines);
check("[mm:ss] sans fraction", eq(L.parseLrc("[01:02]Bye"), [{ t: 62000, text: "Bye" }]));
check("fractions .x / .xx / .xxx / :xx", eq(L.parseLrc("[00:01.5]a\n[00:02.25]b\n[00:03.125]c\n[00:04:50]d").map(l => l.t), [1500, 2250, 3125, 4500]));
const multi = L.parseLrc("[00:20.00][00:05.00]Refrain\n[00:10.00]Couplet");
check("plusieurs horodatages par ligne + tri", eq(multi, [
{ t: 5000, text: "Refrain" }, { t: 10000, text: "Couplet" }, { t: 20000, text: "Refrain" },
]), multi);
check("[offset:+500] avance les paroles", eq(L.parseLrc("[offset:+500]\n[00:10.00]a").map(l => l.t), [9500]));
check("[offset:-250] retarde les paroles", eq(L.parseLrc("[offset:-250]\n[00:10.00]a").map(l => l.t), [10250]));
check("offset ne rend pas de temps négatif", eq(L.parseLrc("[offset:2000]\n[00:01.00]a").map(l => l.t), [0]));
check("CRLF, BOM et horodatages par mot", eq(L.parseLrc("[00:01.00]Hello <00:01.20>world\r\n[00:02.00] x "), [
{ t: 1000, text: "Hello world" }, { t: 2000, text: "x" },
]));
check("lignes sans horodatage ignorées, secondes invalides ignorées", eq(L.parseLrc("texte libre\n[00:75.00]x\n[00:03.00]ok"), [{ t: 3000, text: "ok" }]));
check("égalité de temps : ordre d'origine conservé", eq(L.parseLrc("[00:01.00]a\n[00:01.00]b").map(l => l.text), ["a", "b"]));
}
console.log("cleanTitle / cleanArtist");
{
check("Official Video + [HD]", L.cleanTitle("One More Time (Official Video) [HD]") === "One More Time");
check("Clip officiel / Lyrics", L.cleanTitle("Alors on danse (Clip Officiel)") === "Alors on danse" && L.cleanTitle("Hello (Lyrics)") === "Hello");
check("feat. retiré", L.cleanTitle("Get Lucky (feat. Pharrell Williams)") === "Get Lucky" && L.cleanTitle("Get Lucky ft. Pharrell") === "Get Lucky");
check("suffixe « - Remastered 2011 » retiré", L.cleanTitle("Bohemian Rhapsody - Remastered 2011") === "Bohemian Rhapsody");
check("suffixe « | … » retiré", L.cleanTitle("Titre | Clip officiel") === "Titre");
check("parenthèse de titre conservée", L.cleanTitle("(Don't Fear) The Reaper") === "(Don't Fear) The Reaper");
check("« (Remix) » conservé", L.cleanTitle("Song (Remix)") === "Song (Remix)");
check("« Video » dans un vrai titre conservé", L.cleanTitle("The Buggles - Video Killed the Radio Star") === "The Buggles - Video Killed the Radio Star");
check("titre entièrement parasite → original", L.cleanTitle("(Official Video)") === "(Official Video)");
check("vide / null", L.cleanTitle("") === "" && L.cleanTitle(null) === "");
check("Artist - Topic", L.cleanArtist("Stromae - Topic") === "Stromae");
check("ArtistVEVO → espaces", L.cleanArtist("DaftPunkVEVO") === "Daft Punk" && L.cleanArtist("Adele VEVO") === "Adele");
check("Official / feat.", L.cleanArtist("Daft Punk Official") === "Daft Punk" && L.cleanArtist("Daft Punk feat. Pharrell") === "Daft Punk");
check("Auteur inconnu → vide", L.cleanArtist("Auteur inconnu") === "");
check("AC/DC intact", L.cleanArtist("AC/DC") === "AC/DC");
check("longueur bornée à 200", L.cleanTitle("a".repeat(500)).length === 200);
}
console.log("guessQuery");
{
const g = (title, author, extra = {}) => L.guessQuery({ title, author, type: "youtube", ...extra });
check("Artiste - Titre (Official Video)", eq(g("Daft Punk - One More Time (Official Video)", "Daft Punk", { duration: 320.4 }), { track: "One More Time", artist: "Daft Punk", duration: 320 }));
check("chaîne VEVO sans tiret", eq(g("Never Gonna Give You Up", "RickAstleyVEVO"), { track: "Never Gonna Give You Up", artist: "Rick Astley", duration: 0 }));
check("chaîne de label → artiste du titre", eq(g("The Buggles - Video Killed the Radio Star", "Island Records"), { track: "Video Killed the Radio Star", artist: "The Buggles", duration: 0 }));
check("Titre - Artiste", eq(g("Shape of You - Ed Sheeran", "Ed Sheeran"), { track: "Shape of You", artist: "Ed Sheeran", duration: 0 }));
check("suffixe de version Spotify", eq(g("Hello - Live at Wembley", "Adele"), { track: "Hello", artist: "Adele", duration: 0 }));
check("feat. dans la partie artiste", eq(g("Daft Punk ft. Pharrell Williams - Get Lucky [HD] | Clip officiel", "Some Label"), { track: "Get Lucky", artist: "Daft Punk", duration: 0 }));
check("Artiste « Titre »", eq(g('Stromae "Alors on danse" (Clip officiel)', "StromaeVEVO"), { track: "Alors on danse", artist: "Stromae", duration: 0 }));
check("guillemets dans un titre Spotify", eq(g('Theme from "Shaft"', "Isaac Hayes"), { track: "Theme from Shaft", artist: "Isaac Hayes", duration: 0 }));
check("fichier envoyé", eq(L.guessQuery({ title: "01 - Muse - Uprising.mp3", author: "Auteur inconnu", type: "attachment" }), { track: "Uprising", artist: "Muse", duration: 0 }));
check("valeurs par défaut du Song → vide", eq(L.guessQuery({ title: "Aucun titre", author: "Auteur inconnu" }), { track: "", artist: "", duration: 0 }));
check("durée invalide → 0", L.guessQuery({ title: "x", duration: "abc" }).duration === 0 && L.guessQuery({ title: "x", duration: -3 }).duration === 0);
check("pas d'objet → vide", eq(L.guessQuery(null), { track: "", artist: "", duration: 0 }));
}
console.log("songKeyOf");
{
check("type:id", L.songKeyOf({ type: "youtube", id: "abc", url: "u" }) === "youtube:abc");
check("repli sur l'URL", L.songKeyOf({ type: "youtube", url: "https://x" }) === "https://x");
check("rien → null", L.songKeyOf({}) === null && L.songKeyOf(null) === null);
}
console.log("pickBestCandidate");
{
const q = { track: "One More Time", artist: "Daft Punk", duration: 320 };
const list = [
record({ id: 10, duration: 234, albumName: "Charts" }),
record({ id: 11, duration: 321, syncedLyrics: null }),
record({ id: 12, duration: 319 }),
record({ id: 13, trackName: "Around the World", duration: 320 }),
record({ id: 14, artistName: "Someone Else", duration: 330 }),
];
check("synchronisé et durée proche", L.pickBestCandidate(list, q)?.id === 12);
check("titre différent rejeté", L.pickBestCandidate([list[3]], q) === null);
check("autre artiste rejeté", L.pickBestCandidate([list[4]], q) === null);
// Chaîne de label : titre identique et durée quasi exacte suffisent
check("autre artiste accepté si titre et durée exacts", L.pickBestCandidate([record({ id: 19, artistName: "Ed Banger Records", duration: 321 })], q)?.id === 19);
check("hors fenêtre seulement si rien de mieux", L.pickBestCandidate([list[0]], q)?.id === 10 && L.pickBestCandidate([list[0], list[1]], q)?.id === 11);
check("sans contenu ignoré", L.pickBestCandidate([record({ id: 15, syncedLyrics: "", plainLyrics: "" })], q) === null);
const upload = record({ id: 16, trackName: "Daft Punk - One More Time (Official Video)" });
check("saisie propre préférée à « Artiste - Titre (Official Video) »", L.pickBestCandidate([upload, record({ id: 17 })], q)?.id === 17);
const free = { track: "daft punk one more time", artist: "", duration: 0 };
check("texte libre : bon candidat", L.pickBestCandidate([list[3], record({ id: 18 })], free)?.id === 18);
check("texte libre : titre absent de la requête rejeté", L.pickBestCandidate([list[3]], free) === null);
}
console.log("getLyricsForSong (fetch simulé)");
{
const song = { type: "youtube", id: "FGBhQbmPwH8", title: "Daft Punk - One More Time (Official Video)", author: "Daft Punk", duration: 320, url: "https://www.youtube.com/watch?v=FGBhQbmPwH8" };
mock((url) => url.pathname === "/api/get" ? json(200, record()) : json(404, {}));
let r = await L.getLyricsForSong(song);
check("LRCLIB /get → synchronisées", r.ok && r.source === "lrclib" && r.synced && r.lines.length === 4 && r.lines[0].t === 30750, r);
check("songKey renvoyé", r.songKey === "youtube:FGBhQbmPwH8");
check("plain et match remplis", r.plain === PLAIN && eq(r.match, { id: 1, track: "One More Time", artist: "Daft Punk", album: "Discovery", duration: 320 }) && r.instrumental === false);
const p = calls[0].url.searchParams;
check("paramètres /get nettoyés", calls[0].url.host === "lrclib.net" && p.get("artist_name") === "Daft Punk" && p.get("track_name") === "One More Time" && p.get("duration") === "320", calls[0].url.href);
check("User-Agent avec la version", /^Subsonics-Chopin\/\S+ \(https:\/\/git\.raphix\.fr\/subsonics\)$/.test(calls[0].options.headers["User-Agent"]) && L.USER_AGENT.includes(require("../../package.json").version));
check("délai d'attente fourni", calls[0].options.signal instanceof AbortSignal);
const before = calls.length;
const again = await L.getLyricsForSong({ ...song, id: "autreVideo" });
check("cache par requête (aucun nouvel appel), songKey propre", calls.length === before && again.songKey === "youtube:autreVideo" && again.synced);
mock((url) => url.pathname === "/api/get" ? json(200, record()) : json(404, {}));
r = await L.getLyricsForSong({ ...song, duration: 0 });
check("durée 0 → pas de paramètre duration", r.ok && !calls[0].url.searchParams.has("duration"));
mock((url) => {
if (url.pathname === "/api/get") return json(404, { message: "Failed to find specified track" });
if (url.pathname === "/api/search" && url.searchParams.get("track_name")) {
return json(200, [record({ id: 30, duration: 210 }), record({ id: 31, duration: 321 }), record({ id: 32, trackName: "Digital Love", duration: 320 })]);
}
return json(200, []);
});
r = await L.getLyricsForSong(song);
check("/get 404 → recherche par champs, bon candidat", r.ok && r.match.id === 31 && r.synced, r.match);
check("recherche libre évitée si un candidat convient", !calls.some(c => c.url.searchParams.has("q")));
mock((url) => {
if (url.pathname === "/api/get") return json(404, {});
if (url.searchParams.has("q")) return json(200, [record({ id: 40, duration: 320 })]);
return json(200, []);
});
r = await L.getLyricsForSong(song);
check("puis recherche libre « artiste titre »", r.ok && r.match.id === 40 && calls.find(c => c.url.searchParams.has("q"))?.url.searchParams.get("q") === "Daft Punk One More Time");
mock((url) => url.pathname === "/api/get" ? json(200, record({ duration: 260 })) : json(200, []));
r = await L.getLyricsForSong(song);
check("durée trop différente → texte seul", r.ok && r.synced === false && r.lines === null && r.plain === PLAIN, r);
mock((url) => url.pathname === "/api/get" ? json(200, record({ duration: 260, plainLyrics: null })) : json(200, []));
r = await L.getLyricsForSong(song);
check("texte seul reconstruit depuis le LRC", r.ok && !r.synced && r.plain === PLAIN, r.plain);
mock((url) => url.pathname === "/api/get" ? json(200, record({ instrumental: true, plainLyrics: null, syncedLyrics: null })) : json(404, {}));
r = await L.getLyricsForSong(song);
check("instrumental", r.ok && r.instrumental && !r.synced && r.lines === null && r.plain === null);
// Entrée d'essai réellement présente sur LRCLIB (« [00:00.00]probe ») : ignorée au profit de la recherche
mock((url) => {
if (url.pathname === "/api/get") return json(200, record({ id: 50, plainLyrics: "probe", syncedLyrics: "[00:00.00]probe" }));
if (url.pathname === "/api/search") return json(200, [record({ id: 50, plainLyrics: "probe", syncedLyrics: "[00:00.00]probe" }), record({ id: 51 })]);
return json(404, {});
});
r = await L.getLyricsForSong(song);
check("entrée vandalisée ignorée", r.ok && r.match.id === 51 && r.synced, r.match);
// 503 passager : une seule nouvelle tentative
let flaky = 0;
mock((url) => {
if (url.pathname === "/api/get") return ++flaky === 1 ? json(503, {}) : json(200, record());
return json(404, {});
});
r = await L.getLyricsForSong(song);
check("503 passager → nouvelle tentative réussie", r.ok && r.synced && flaky === 2);
mock((url) => {
if (url.host === "lrclib.net") return url.pathname === "/api/get" ? json(404, {}) : json(200, []);
if (url.host === "api.deezer.com") return json(200, { data: [
{ id: 1, title: "Harder, Better, Faster, Stronger", artist: { name: "Daft Punk" }, album: { title: "Discovery" }, duration: 224 },
{ id: 2, title: "One More Time (Radio Edit)", title_short: "One More Time", artist: { name: "Daft Punk" }, album: { title: "Discovery" }, duration: 320 },
] });
if (url.host === "api.lyrics.ovh") return json(200, { lyrics: "Paroles de la chanson One More Time par Daft Punk\r\nOne more time\r\n\r\n\r\nCelebrate\r\nOh yeah" });
return json(404, {});
});
r = await L.getLyricsForSong(song);
check("repli Deezer + lyrics.ovh", r.ok && r.source === "lyrics.ovh" && !r.synced && r.lines === null && r.plain === PLAIN, r);
const ovh = calls.find(c => c.url.host === "api.lyrics.ovh");
check("lyrics.ovh interrogé avec le bon candidat Deezer", ovh && decodeURIComponent(ovh.url.pathname) === "/v1/Daft Punk/One More Time", ovh?.url.pathname);
check("Deezer en https", calls.find(c => c.url.host === "api.deezer.com")?.url.protocol === "https:");
mock(() => json(404, { error: "No lyrics found" }));
r = await L.getLyricsForSong(song);
check("rien nulle part → NOT_FOUND + songKey", !r.ok && r.error === "NOT_FOUND" && r.songKey === "youtube:FGBhQbmPwH8", r);
const n = calls.length;
await L.getLyricsForSong(song);
check("échec mis en cache", calls.length === n);
mock((url) => {
if (url.host === "lrclib.net") throw new TypeError("fetch failed");
return json(404, {});
});
r = await L.getLyricsForSong(song);
check("LRCLIB injoignable → repli puis UNAVAILABLE", !r.ok && r.error === "UNAVAILABLE" && calls.filter(c => c.url.host === "lrclib.net").length === 2 && calls.every(c => c.url.pathname !== "/api/search"), calls.map(c => c.url.href));
mock((url) => url.host === "lrclib.net" ? json(500, {}) : json(404, {}));
r = await L.getLyricsForSong(song);
check("HTTP 500 → UNAVAILABLE", !r.ok && r.error === "UNAVAILABLE");
mock(() => json(200, record()));
check("pas de morceau → NO_MUSIC", eq(await L.getLyricsForSong(null), { ok: false, error: "NO_MUSIC" }));
r = await L.getLyricsForSong({ type: "youtube", id: "x", title: "Aucun titre", author: "Auteur inconnu" });
check("titre vide → INVALID_PARAMS", !r.ok && r.error === "INVALID_PARAMS" && r.songKey === "youtube:x" && calls.length === 0);
}
console.log("searchCandidates / getById / findLyrics / getLyrics");
{
mock(() => json(200, record()));
for (const bad of [undefined, null, 42, "", " ", "x".repeat(201), { q: "x" }]) {
const r = await L.searchCandidates(bad);
check(`searchCandidates(${JSON.stringify(bad)?.slice(0, 20)}) → INVALID_PARAMS`, eq(r, { ok: false, error: "INVALID_PARAMS" }));
}
check("aucune requête pour une entrée invalide", calls.length === 0);
mock(() => json(200, [record({ id: 5 }), record({ id: 6, syncedLyrics: null, instrumental: true, albumName: null }), { bad: true }]));
let r = await L.searchCandidates(" daft punk one more time ");
check("candidats mis en forme", r.ok && eq(r.results, [
{ id: 5, track: "One More Time", artist: "Daft Punk", album: "Discovery", duration: 320, synced: true, instrumental: false },
{ id: 6, track: "One More Time", artist: "Daft Punk", album: "", duration: 320, synced: false, instrumental: true },
]), r);
check("requête q transmise", calls[0].url.pathname === "/api/search" && calls[0].url.searchParams.get("q") === "daft punk one more time");
mock(() => { throw new TypeError("fetch failed"); });
r = await L.searchCandidates("abc");
check("réseau en panne → UNAVAILABLE", eq(r, { ok: false, error: "UNAVAILABLE" }));
route = () => json(200, []);
r = await L.searchCandidates("abc");
check("UNAVAILABLE non mis en cache", r.ok && eq(r.results, []));
mock((url) => url.pathname === "/api/get/77" ? json(200, record({ id: 77, duration: 100 })) : json(404, {}));
for (const bad of [0, -1, 1.5, "abc", null, {}, Number.MAX_SAFE_INTEGER + 2]) {
check(`getById(${JSON.stringify(bad)}) → INVALID_PARAMS`, (await L.getById(bad)).error === "INVALID_PARAMS");
}
r = await L.getById(77);
check("getById garde la synchro (choix manuel)", r.ok && r.synced && r.match.id === 77 && r.songKey === null, r);
check("getById accepte une chaîne numérique", (await L.getById("77")).ok);
check("getById inconnu → NOT_FOUND", (await L.getById(78)).error === "NOT_FOUND");
mock((url) => url.pathname === "/api/get" ? json(200, record()) : json(404, {}));
r = await L.findLyrics({ track: "One More Time (Official Video)", artist: "DaftPunkVEVO", duration: 320 });
check("findLyrics objet", r.ok && r.synced && r.songKey === null && calls[0].url.searchParams.get("artist_name") === "Daft Punk");
check("findLyrics invalide", (await L.findLyrics({ track: "" })).error === "INVALID_PARAMS"
&& (await L.findLyrics({ track: "a", artist: 3 })).error === "INVALID_PARAMS"
&& (await L.findLyrics({ track: "a", duration: "x" })).error === "INVALID_PARAMS"
&& (await L.findLyrics(["a"])).error === "INVALID_PARAMS"
&& (await L.findLyrics("x".repeat(201))).error === "INVALID_PARAMS");
mock((url) => url.searchParams.has("q") ? json(200, [record({ id: 90, trackName: "Time" }), record({ id: 91 })]) : json(404, {}));
r = await L.findLyrics("daft punk one more time");
check("findLyrics texte libre", r.ok && r.match.track === "One More Time", r);
mock((url) => url.pathname === "/api/get" ? json(200, record()) : json(404, {}));
const text = await L.getLyrics("Daft Punk - One More Time");
check("getLyrics (compat) → texte brut", text === PLAIN, text);
mock(() => json(404, {}));
check("getLyrics (compat) → null si rien", (await L.getLyrics("zzz introuvable")) === null && (await L.getLyrics(null)) === null);
}
console.log("titres YouTube supplémentaires");
{
const g = (title, author, extra = {}) => L.guessQuery({ title, author, type: "youtube", ...extra });
check("Artiste - Titre (Official Video) [4K]", eq(g("Daft Punk - Around the World (Official Video) [4K]", "Daft Punk"), { track: "Around the World", artist: "Daft Punk", duration: 0 }));
check("Titre (Official Music Video) 4K sans tiret", L.cleanTitle("Blinding Lights (Official Music Video) 4K") === "Blinding Lights");
check("chaîne « - Topic » + feat. dans le titre", eq(g("Instant Crush (feat. Julian Casablancas)", "Daft Punk - Topic", { duration: 337 }), { track: "Instant Crush", artist: "Daft Punk", duration: 337 }));
check("ft. en fin de titre", L.cleanTitle("Stay ft. Justin Bieber") === "Stay");
check("Titre [Lyrics] / (Audio)", L.cleanTitle("Bad Guy [Lyrics]") === "Bad Guy" && L.cleanTitle("Lose Yourself (Audio)") === "Lose Yourself");
check("« (Official Video) » + émoji", L.cleanTitle("🔥 Alors on danse (Official Video) 🔥") === "Alors on danse");
check("suffixe « - Live » conservé comme version : titre seul", eq(g("Hey Jude - Live", "The Beatles"), { track: "Hey Jude", artist: "The Beatles", duration: 0 }));
check("chaîne Official + titre sans tiret", eq(g("Bohemian Rhapsody (Official Video Remastered)", "Queen Official"), { track: "Bohemian Rhapsody", artist: "Queen", duration: 0 }));
}
console.log("similarity / rankCandidates");
{
check("identique → 1", L.similarity("One More Time", "one more time") === 1);
check("accents et apostrophes ignorés", L.similarity("Je t'aime", "Je taime") === 1 && L.similarity("Éléphant", "elephant") === 1);
check("espaces ignorés → 0.95", L.similarity("AC DC", "ACDC") === 0.95);
check("inclusion → ≥ 0.6", L.similarity("One More Time", "One More Time Radio Edit") >= 0.6);
check("rien en commun → 0", L.similarity("Hello", "Goodbye") === 0 && L.similarity("", "x") === 0);
const ranked = L.rankCandidates([
record({ id: 1, trackName: "Something Else", artistName: "Nobody" }),
record({ id: 2, trackName: "Daft Punk - One More Time (Official Video)" }),
record({ id: 3, syncedLyrics: null }),
record({ id: 4 }),
], "daft punk one more time");
check("classement : synchro propre, synchro bruitée, texte seul, hors sujet", eq(ranked.map(r => r.id), [4, 2, 3, 1]), ranked.map(r => r.id));
check("rankCandidates ne modifie pas l'entrée", (() => { const list = [record({ id: 9 })]; L.rankCandidates(list, "x"); return list.length === 1; })());
}
console.log("durées de cache (horloge simulée)");
{
const MIN = 60 * 1000;
const HOUR = 60 * MIN;
check("ttl trouvé 6 h", L.ttlForResult({ ok: true }) === 6 * HOUR);
check("ttl introuvable 20 min", L.ttlForResult({ ok: false, error: "NOT_FOUND" }) === 20 * MIN);
check("ttl indisponible 1 min", L.ttlForResult({ ok: false, error: "UNAVAILABLE" }) === MIN);
check("ttl autre erreur / rien → 0", L.ttlForResult({ ok: false, error: "INVALID_PARAMS" }) === 0 && L.ttlForResult(null) === 0);
const realNow = Date.now;
// Horloge monotone : le limiteur retient l'heure du dernier démarrage, reculer le bloquerait
let offset = 0;
let base = 0;
const at = (ms) => { offset = base + ms; };
const nextScenario = () => { base = offset + HOUR; offset = base; };
Date.now = () => realNow() + offset;
try {
const song = { type: "youtube", id: "ttl", title: "Daft Punk - One More Time", author: "Daft Punk", duration: 320 };
const lrclibCalls = () => calls.filter(c => c.url.host === "lrclib.net").length;
mock((url) => url.pathname === "/api/get" ? json(200, record()) : json(404, {}));
await L.getLyricsForSong(song);
let n = lrclibCalls();
at(6 * HOUR - MIN);
await L.getLyricsForSong(song);
check("trouvé : encore en cache à 5 h 59", lrclibCalls() === n);
at(6 * HOUR + MIN);
await L.getLyricsForSong(song);
check("trouvé : redemandé après 6 h", lrclibCalls() > n);
nextScenario();
mock(() => json(404, {}));
await L.getLyricsForSong(song);
n = calls.length;
at(19 * MIN);
await L.getLyricsForSong(song);
check("introuvable : en cache à 19 min", calls.length === n);
at(21 * MIN);
await L.getLyricsForSong(song);
check("introuvable : redemandé après 20 min", calls.length > n);
nextScenario();
mock((url) => { if (url.host === "lrclib.net") throw new TypeError("fetch failed"); return json(404, {}); });
await L.getLyricsForSong(song);
n = calls.length;
at(30 * 1000);
await L.getLyricsForSong(song);
check("indisponible : en cache 30 s", calls.length === n);
at(61 * 1000);
await L.getLyricsForSong(song);
check("indisponible : redemandé après 1 min", calls.length > n);
nextScenario();
mock(() => json(200, [record({ id: 5 })]));
await L.searchCandidates("daft punk");
n = calls.length;
at(9 * MIN);
await L.searchCandidates("Daft Punk");
check("recherche manuelle : cache 10 min (requête normalisée)", calls.length === n);
at(11 * MIN);
await L.searchCandidates("daft punk");
check("recherche manuelle : redemandée après 10 min", calls.length > n);
} finally {
Date.now = realNow;
}
}
globalThis.fetch = realFetch;
done();
})().catch((error) => {
globalThis.fetch = realFetch;
console.error(error);
process.exitCode = 1;
});