setAccessible(true); $tmpDir = sys_get_temp_dir() . '/annu-kute-cache-test-' . getmypid(); if (!is_dir($tmpDir)) { mkdir($tmpDir, 0755, true); } $cacheDirProperty->setValue($cache, $tmpDir); // --- get sur un cache vide -------------------------------------------------- assertNull($cache->get('videos'), 'get retourne null quand aucune entrée n\'existe'); // --- set puis get ----------------------------------------------------------- $content = ['data' => ['video1', 'video2']]; $cache->set('videos', ['count' => 6], $content); assertEquals($content, $cache->get('videos', ['count' => 6]), 'get retourne le contenu après set'); // Le fichier stocké contient bien content / expires / created $files = glob($tmpDir . '/cache_*.json'); assertEquals(1, count($files), 'set crée exactement un fichier de cache'); $stored = json_decode(file_get_contents($files[0]), true); assertEquals($content, $stored['content'], 'le fichier stocke le contenu sous la clé "content"'); assertTrue(isset($stored['expires']) && $stored['expires'] > time(), 'le fichier stocke une expiration future'); assertTrue(isset($stored['created']), 'le fichier stocke la date de création'); // --- Ordre des paramètres --------------------------------------------------- // ksort() dans getCacheKey : l'ordre des paramètres ne change pas la clé $cache->set('videos', ['b' => 2, 'a' => 1], 'ordonné'); assertEquals( 'ordonné', $cache->get('videos', ['a' => 1, 'b' => 2]), 'get retrouve l\'entrée malgré un ordre de paramètres différent' ); assertNull( $cache->get('videos', ['a' => 9]), 'get retourne null pour des paramètres différents' ); // --- Expiration ------------------------------------------------------------- $cache->set('videos', ['old' => 1], 'périmé', -1); // TTL négatif : déjà expiré $filesBefore = count(glob($tmpDir . '/cache_*.json')); assertNull($cache->get('videos', ['old' => 1]), 'get retourne null pour une entrée expirée'); assertEquals( $filesBefore - 1, count(glob($tmpDir . '/cache_*.json')), 'get supprime le fichier expiré' ); // --- cleanup ---------------------------------------------------------------- $cache->set('cle-valide', [], 'valide', 300); $cache->set('cle-perimee', [], 'périmée', -10); $cleaned = $cache->cleanup(); assertEquals(1, $cleaned, 'cleanup supprime uniquement les entrées expirées'); assertEquals('valide', $cache->get('cle-valide'), 'cleanup conserve les entrées valides'); assertNull($cache->get('cle-perimee'), 'cleanup a bien supprimé l\'entrée expirée'); // --- Écriture verrouillée (LOCK_EX) ------------------------------------------ // Verrou structurel : set() écrit avec un verrou exclusif $cacheSource = file_get_contents(dirname(__DIR__, 2) . '/includes/simple-cache.php'); assertContains('LOCK_EX', $cacheSource, 'set() écrit avec file_put_contents et LOCK_EX'); // Intégrité d'écriture : un contenu volumineux est relu sans troncature $bigContent = ['payload' => str_repeat('données-€-', 5000)]; $cache->set('lock-test', [], $bigContent); assertEquals($bigContent, $cache->get('lock-test'), 'une écriture volumineuse est relue intégralement'); // --- getPeerTubeCacheTtl() : correspondance exacte puis préfixe -------------- assertEquals(3600, getPeerTubeCacheTtl('videos/categories'), 'catégories : TTL 1 heure'); assertEquals(600, getPeerTubeCacheTtl('videos'), 'liste des vidéos : TTL 10 minutes'); assertEquals(600, getPeerTubeCacheTtl('search/videos'), 'recherche : TTL 10 minutes (match exact)'); assertEquals(900, getPeerTubeCacheTtl('wp-posts'), 'WordPress : TTL 15 minutes'); assertEquals(300, getPeerTubeCacheTtl('accounts'), 'comptes : TTL 5 minutes'); assertEquals( 300, getPeerTubeCacheTtl('accounts/membre/videos'), 'vidéos d\'un compte (lives) : TTL 5 minutes — l\'ancien strpos capturait « videos » à 10 minutes' ); assertEquals(600, getPeerTubeCacheTtl('videos/9cf2e3a1-abc'), 'détail vidéo : TTL 10 minutes'); assertEquals(600, getPeerTubeCacheTtl('videos/9cf2e3a1-abc/comment-threads'), 'commentaires : TTL 10 minutes'); assertEquals(600, getPeerTubeCacheTtl('video-channels/ma-chaine/videos'), 'vidéos d\'une chaîne : TTL 10 minutes'); assertEquals(300, getPeerTubeCacheTtl('endpoint-inconnu'), 'endpoint inconnu : TTL par défaut de 5 minutes'); // --- clear() : purge complète ------------------------------------------------- $cache->clear(); // repartir d'un dossier vide $cache->set('purge-1', [], 'un'); $cache->set('purge-2', [], 'deux', 300); assertEquals(2, count(glob($tmpDir . '/cache_*.json')), 'deux entrées présentes avant purge'); $deleted = $cache->clear(); assertEquals(2, $deleted, 'clear() supprime toutes les entrées, même valides'); assertEquals(0, count(glob($tmpDir . '/cache_*.json')), 'le dossier est vide après clear()'); assertNull($cache->get('purge-1'), 'une entrée purgée n\'est plus lisible'); // --- CACHE_ENABLED honoré par callPeerTubeApiCached --------------------------- // Impossible de redéfinir une constante dans ce processus (CACHE_ENABLED=true // dans bootstrap.php) : on teste dans un sous-processus PHP avec un stub de // callPeerTubeApiOriginal qui compte les appels réels. $root = dirname(__DIR__, 2); $runCacheSnippet = function (string $code): string { $runner = tempnam(sys_get_temp_dir(), 'cache-runner-') . '.php'; file_put_contents($runner, "&1'); unlink($runner); return $output ?? ''; }; $flagTestDir = sys_get_temp_dir() . '/annu-kute-cache-flag-' . getmypid(); mkdir($flagTestDir, 0755, true); $snippetTemplate = <<<'PHP' define('CACHE_ENABLED', %s); require %s; $GLOBALS['simple_api_cache'] = new SimpleAPICache(%s); $GLOBALS['api_calls'] = 0; function callPeerTubeApiOriginal($endpoint, $params = []) { $GLOBALS['api_calls']++; return ['data' => ['appel-' . $GLOBALS['api_calls']]]; } callPeerTubeApiCached('videos-test-flag'); callPeerTubeApiCached('videos-test-flag'); echo $GLOBALS['api_calls']; PHP; $simpleCachePath = var_export($root . '/includes/simple-cache.php', true); $flagDirExport = var_export($flagTestDir, true); // Cache désactivé : deux appels API réels, aucune écriture de cache $outDisabled = trim($runCacheSnippet(sprintf($snippetTemplate, 'false', $simpleCachePath, $flagDirExport))); assertEquals('2', $outDisabled, 'CACHE_ENABLED=false : chaque appel va à l\'API (pas de lecture cache)'); assertEquals(0, count(glob($flagTestDir . '/cache_*.json')), 'CACHE_ENABLED=false : aucune écriture de cache'); // Cache activé : le second appel est servi par le cache $outEnabled = trim($runCacheSnippet(sprintf($snippetTemplate, 'true', $simpleCachePath, $flagDirExport))); assertEquals('1', $outEnabled, 'CACHE_ENABLED=true : le second appel est servi par le cache'); assertEquals(1, count(glob($flagTestDir . '/cache_*.json')), 'CACHE_ENABLED=true : une entrée écrite en cache'); foreach (glob($flagTestDir . '/cache_*.json') as $file) { unlink($file); } rmdir($flagTestDir); // --- scripts/purge-cache.php --------------------------------------------------- $purgeDir = sys_get_temp_dir() . '/annu-kute-purge-' . getmypid(); mkdir($purgeDir, 0755, true); file_put_contents($purgeDir . '/cache_a.json', '{}'); file_put_contents($purgeDir . '/cache_b.json', '{}'); file_put_contents($purgeDir . '/cache_c.json', '{}'); file_put_contents($purgeDir . '/autre.txt', 'conservé'); // ne doit pas être supprimé $purgeOutput = shell_exec( escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/scripts/purge-cache.php') . ' ' . escapeshellarg($purgeDir) . ' 2>&1' ) ?? ''; assertContains('Cache purgé : 3', $purgeOutput, 'le script rapporte les 3 entrées supprimées'); assertEquals(0, count(glob($purgeDir . '/cache_*.json')), 'le script supprime tous les fichiers de cache'); assertTrue(file_exists($purgeDir . '/autre.txt'), 'le script conserve les fichiers hors cache'); unlink($purgeDir . '/autre.txt'); rmdir($purgeDir); // --- Nettoyage du dossier temporaire ---------------------------------------- foreach (glob($tmpDir . '/cache_*.json') as $file) { unlink($file); } rmdir($tmpDir);