test(php): add native unit tests for security, cache, format, markdown
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/**
|
||||
* Bootstrap des tests unitaires PHP.
|
||||
*
|
||||
* Définit des constantes de test puis charge les includes du projet
|
||||
* SANS aucun appel réseau : PEERTUBE_URL pointe volontairement sur
|
||||
* 127.0.0.1, ce qui fait échouer isValidPeerTubeUrl() et court-circuite
|
||||
* callPeerTubeApiOriginal() (retour [] immédiat, jamais de cURL). De ce
|
||||
* fait, initCategories() — appelée au chargement de config.php — est
|
||||
* neutralisée et PEERTUBE_CATEGORIES vaut un tableau vide.
|
||||
*
|
||||
* Les constantes sont définies AVANT le chargement de config.php pour
|
||||
* primer sur les valeurs de config.default.php (protégées par !defined()).
|
||||
*/
|
||||
|
||||
define('APP_HOST_NAME', 'test.local');
|
||||
define('SITE_NAME', 'Site de test');
|
||||
define('CSRF_SECRET', 'secret-csrf-pour-tests-unitaires');
|
||||
define('PEERTUBE_URL', 'http://127.0.0.1'); // URL privée volontaire : bloque tout appel API réel
|
||||
define('CACHE_ENABLED', true);
|
||||
define('CACHE_DURATION', 3600);
|
||||
define('DEFAULT_TIMEZONE', 'UTC');
|
||||
|
||||
// Ne pas polluer la sortie des tests avec les error_log() de sécurité
|
||||
// (ex. "Invalid PeerTube URL", attendu avec l'URL factice ci-dessus)
|
||||
ini_set('error_log', sys_get_temp_dir() . '/annu-kute-tests-errors.log');
|
||||
|
||||
$includes_dir = dirname(__DIR__, 2) . '/includes';
|
||||
|
||||
require_once $includes_dir . '/security.php';
|
||||
require_once $includes_dir . '/simple-cache.php';
|
||||
require_once $includes_dir . '/lib/markdown.php';
|
||||
require_once $includes_dir . '/structured-data.php';
|
||||
require_once $includes_dir . '/config.php'; // fournit les fonctions de formatage
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests unitaires pour la classe SimpleAPICache (includes/simple-cache.php).
|
||||
*
|
||||
* Le répertoire de cache est redirigé vers un dossier temporaire via
|
||||
* réflexion afin de ne jamais toucher au cache réel (cache/api).
|
||||
*/
|
||||
|
||||
$cache = new SimpleAPICache();
|
||||
$cacheDirProperty = new ReflectionProperty(SimpleAPICache::class, 'cacheDir');
|
||||
$cacheDirProperty->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');
|
||||
|
||||
// --- Nettoyage du dossier temporaire ----------------------------------------
|
||||
|
||||
foreach (glob($tmpDir . '/cache_*.json') as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
rmdir($tmpDir);
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests unitaires pour les fonctions de formatage de includes/config.php
|
||||
* (formatDuration, formatViewCount, formatDate, formatVideosData)
|
||||
* et truncateText de includes/structured-data.php
|
||||
*/
|
||||
|
||||
// --- formatDuration ---------------------------------------------------------
|
||||
|
||||
assertEquals('0:00', formatDuration(0), 'formatDuration(0) donne 0:00');
|
||||
assertEquals('0:59', formatDuration(59), 'formatDuration formate les secondes');
|
||||
assertEquals('1:05', formatDuration(65), 'formatDuration formate minutes:secondes avec zéro');
|
||||
assertEquals('1:00:00', formatDuration(3600), 'formatDuration passe au format heures');
|
||||
assertEquals('1:01:01', formatDuration(3661), 'formatDuration formate heures:minutes:secondes');
|
||||
|
||||
// --- formatViewCount --------------------------------------------------------
|
||||
|
||||
assertEquals(999, formatViewCount(999), 'formatViewCount laisse les valeurs < 1000 intactes');
|
||||
assertEquals('1K', formatViewCount(1000), 'formatViewCount abrège en K à partir de 1000');
|
||||
assertEquals('1.5K', formatViewCount(1500), 'formatViewCount arrondit les K à une décimale');
|
||||
assertEquals('1M', formatViewCount(1000000), 'formatViewCount abrège en M à partir de 1 million');
|
||||
assertEquals('2.5M', formatViewCount(2500000), 'formatViewCount arrondit les M à une décimale');
|
||||
|
||||
// --- formatDate -------------------------------------------------------------
|
||||
|
||||
assertEquals('Aujourd\'hui', formatDate(date('Y-m-d H:i:s')), 'formatDate affiche Aujourd\'hui');
|
||||
assertEquals('Hier', formatDate(date('Y-m-d H:i:s', time() - 86400)), 'formatDate affiche Hier');
|
||||
assertEquals(
|
||||
'Il y a 3 jours',
|
||||
formatDate(date('Y-m-d H:i:s', time() - 3 * 86400)),
|
||||
'formatDate affiche les jours sous une semaine'
|
||||
);
|
||||
assertEquals(
|
||||
'Il y a 1 semaine',
|
||||
formatDate(date('Y-m-d H:i:s', time() - 10 * 86400)),
|
||||
'formatDate affiche la semaine au singulier'
|
||||
);
|
||||
assertEquals(
|
||||
'Il y a 2 semaines',
|
||||
formatDate(date('Y-m-d H:i:s', time() - 20 * 86400)),
|
||||
'formatDate affiche les semaines au pluriel'
|
||||
);
|
||||
assertEquals(
|
||||
'Il y a 2 mois',
|
||||
formatDate(date('Y-m-d H:i:s', time() - 60 * 86400)),
|
||||
'formatDate affiche les mois'
|
||||
);
|
||||
assertEquals(
|
||||
'Il y a 1 an',
|
||||
formatDate(date('Y-m-d H:i:s', time() - 400 * 86400)),
|
||||
'formatDate affiche l\'année au singulier'
|
||||
);
|
||||
assertEquals(
|
||||
'Il y a 2 ans',
|
||||
formatDate(date('Y-m-d H:i:s', time() - 800 * 86400)),
|
||||
'formatDate affiche les années au pluriel'
|
||||
);
|
||||
|
||||
// --- formatVideosData -------------------------------------------------------
|
||||
|
||||
$rawVideos = [
|
||||
[
|
||||
'uuid' => 'abc-123',
|
||||
'name' => 'Ma vidéo',
|
||||
'previewPath' => '/lazy/abc.jpg',
|
||||
'duration' => 125,
|
||||
'channel' => [
|
||||
'displayName' => 'Chaîne',
|
||||
'avatars' => [['path' => '/avatars/a.png']]
|
||||
],
|
||||
'views' => 42,
|
||||
'publishedAt' => '2024-01-01T00:00:00.000Z',
|
||||
'aspectRatio' => 1.78,
|
||||
'description' => 'Une description',
|
||||
'tags' => ['musique'],
|
||||
'isLive' => true,
|
||||
],
|
||||
[
|
||||
// Vidéo minimale : sans vignette, avatar, description, tags ni isLive
|
||||
'uuid' => 'def-456',
|
||||
'name' => 'Minimale',
|
||||
'duration' => 5,
|
||||
'channel' => ['displayName' => 'Autre chaîne'],
|
||||
'views' => 0,
|
||||
'publishedAt' => '2024-01-02T00:00:00.000Z',
|
||||
'aspectRatio' => 1.78,
|
||||
],
|
||||
];
|
||||
|
||||
$videos = formatVideosData($rawVideos);
|
||||
|
||||
assertEquals(2, count($videos), 'formatVideosData retourne une entrée par vidéo');
|
||||
|
||||
assertEquals('abc-123', $videos[0]['id'], 'formatVideosData mappe uuid vers id');
|
||||
assertEquals('Ma vidéo', $videos[0]['title'], 'formatVideosData mappe name vers title');
|
||||
assertEquals(
|
||||
PEERTUBE_URL . '/lazy/abc.jpg',
|
||||
$videos[0]['thumbnail'],
|
||||
'formatVideosData préfixe la vignette avec PEERTUBE_URL'
|
||||
);
|
||||
assertEquals(
|
||||
PEERTUBE_URL . '/avatars/a.png',
|
||||
$videos[0]['channelAvatar'],
|
||||
'formatVideosData préfixe l\'avatar avec PEERTUBE_URL'
|
||||
);
|
||||
assertEquals(125, $videos[0]['duration'], 'formatVideosData conserve la durée');
|
||||
assertEquals('Chaîne', $videos[0]['channel'], 'formatVideosData mappe le nom de la chaîne');
|
||||
assertEquals(42, $videos[0]['views'], 'formatVideosData conserve les vues');
|
||||
assertEquals('2024-01-01T00:00:00.000Z', $videos[0]['date'], 'formatVideosData mappe publishedAt vers date');
|
||||
assertEquals('Une description', $videos[0]['description'], 'formatVideosData conserve la description');
|
||||
assertEquals(['musique'], $videos[0]['tags'], 'formatVideosData conserve les tags');
|
||||
assertTrue($videos[0]['isLive'], 'formatVideosData conserve isLive');
|
||||
|
||||
assertEquals(
|
||||
'img/default-thumbnail.jpg',
|
||||
$videos[1]['thumbnail'],
|
||||
'formatVideosData utilise une vignette par défaut si absente'
|
||||
);
|
||||
assertEquals(
|
||||
'img/default-avatar.png',
|
||||
$videos[1]['channelAvatar'],
|
||||
'formatVideosData utilise un avatar par défaut si absent'
|
||||
);
|
||||
assertEquals('', $videos[1]['description'], 'formatVideosData met une description vide par défaut');
|
||||
assertEquals([], $videos[1]['tags'], 'formatVideosData met des tags vides par défaut');
|
||||
assertFalse($videos[1]['isLive'], 'formatVideosData met isLive à false par défaut');
|
||||
|
||||
// --- truncateText (includes/structured-data.php) ----------------------------
|
||||
|
||||
assertEquals('court', truncateText('court', 200), 'truncateText laisse un texte court intact');
|
||||
assertEquals('exact', truncateText('exact', 5), 'truncateText laisse un texte à la limite exacte intact');
|
||||
|
||||
$textWithSpace = str_repeat('a', 150) . ' ' . str_repeat('b', 100); // 251 caractères
|
||||
assertEquals(
|
||||
str_repeat('a', 150) . '...',
|
||||
truncateText($textWithSpace, 200),
|
||||
'truncateText coupe au dernier espace avant la limite'
|
||||
);
|
||||
|
||||
$textNoSpace = str_repeat('a', 250);
|
||||
assertEquals(
|
||||
str_repeat('a', 200) . '...',
|
||||
truncateText($textNoSpace, 200),
|
||||
'truncateText coupe à la limite quand il n\'y a pas d\'espace'
|
||||
);
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests unitaires pour markdown_to_html (includes/lib/markdown.php)
|
||||
*
|
||||
* Les sorties attendues reflètent le comportement actuel de la fonction,
|
||||
* y compris ses particularités (voir la note sur les listes à puces).
|
||||
*/
|
||||
|
||||
// --- Échappement XSS ---------------------------------------------------------
|
||||
|
||||
assertEquals(
|
||||
'<script>alert("x")</script>',
|
||||
markdown_to_html('<script>alert("x")</script>'),
|
||||
'markdown_to_html échappe le HTML brut'
|
||||
);
|
||||
assertNotContains(
|
||||
'<script>',
|
||||
markdown_to_html('<script>alert("x")</script>'),
|
||||
'markdown_to_html ne laisse passer aucune balise script'
|
||||
);
|
||||
|
||||
// --- Gras et italique --------------------------------------------------------
|
||||
|
||||
assertEquals(
|
||||
'<strong>gras</strong>',
|
||||
markdown_to_html('**gras**'),
|
||||
'markdown_to_html convertit **texte** en <strong>'
|
||||
);
|
||||
assertEquals(
|
||||
'<em>ital</em>',
|
||||
markdown_to_html('*ital*'),
|
||||
'markdown_to_html convertit *texte* en <em>'
|
||||
);
|
||||
|
||||
// --- Liens Markdown ----------------------------------------------------------
|
||||
|
||||
assertEquals(
|
||||
'<a href="https://exemple.com" target="_blank" rel="noopener noreferrer">exemple</a>',
|
||||
markdown_to_html('[exemple](https://exemple.com)'),
|
||||
'markdown_to_html convertit un lien Markdown avec attributs de sécurité'
|
||||
);
|
||||
assertEquals(
|
||||
'<a href="http://exemple.com" target="_blank" rel="noopener noreferrer">site</a>',
|
||||
markdown_to_html('[site](exemple.com)'),
|
||||
'markdown_to_html ajoute http:// aux liens sans protocole'
|
||||
);
|
||||
|
||||
// --- URLs brutes -------------------------------------------------------------
|
||||
|
||||
assertEquals(
|
||||
'Visite <a href="https://exemple.com/page" target="_blank" rel="noopener noreferrer">https://exemple.com/page</a> pour info',
|
||||
markdown_to_html('Visite https://exemple.com/page pour info'),
|
||||
'markdown_to_html rend les URLs http(s) brutes cliquables'
|
||||
);
|
||||
assertEquals(
|
||||
'va sur <a href="http://o-k-i.net" target="_blank" rel="noopener noreferrer">o-k-i.net</a> maintenant',
|
||||
markdown_to_html('va sur o-k-i.net maintenant'),
|
||||
'markdown_to_html rend les domaines nus cliquables (avec http://)'
|
||||
);
|
||||
assertEquals(
|
||||
'version v1.2 dispo',
|
||||
markdown_to_html('version v1.2 dispo'),
|
||||
'markdown_to_html ne transforme pas un numéro de version en lien'
|
||||
);
|
||||
|
||||
// --- Listes ------------------------------------------------------------------
|
||||
|
||||
// Note : la passe des listes numérotées s'applique aussi aux <li> déjà
|
||||
// produits par la passe des puces, d'où un double enveloppement <ul><ol>.
|
||||
// C'est le comportement actuel, verrouillé ici contre toute régression.
|
||||
assertEquals(
|
||||
"<ul><ol><li>a</li>\n<li>b</li></ol></ul>",
|
||||
markdown_to_html("- a\n- b"),
|
||||
'markdown_to_html convertit les listes à puces (double enveloppement actuel)'
|
||||
);
|
||||
assertEquals(
|
||||
"<ol><li>a</li>\n<li>b</li></ol>",
|
||||
markdown_to_html("1. a\n2. b"),
|
||||
'markdown_to_html convertit les listes numérotées en <ol>'
|
||||
);
|
||||
|
||||
// --- Retours à la ligne ------------------------------------------------------
|
||||
|
||||
assertEquals(
|
||||
"ligne1<br />\nligne2",
|
||||
markdown_to_html("ligne1\nligne2"),
|
||||
'markdown_to_html convertit les sauts de ligne en <br />'
|
||||
);
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/**
|
||||
* Mini-lanceur de tests unitaires PHP (aucune dépendance externe).
|
||||
*
|
||||
* Usage : php tests/php/run.php
|
||||
*
|
||||
* Exécute tous les fichiers tests/php/*-test.php puis affiche un résumé.
|
||||
* Code de sortie : 0 si tous les tests passent, 1 sinon.
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
|
||||
// --- Assertions -----------------------------------------------------------
|
||||
|
||||
$GLOBALS['tests_passed'] = 0;
|
||||
$GLOBALS['tests_failed'] = 0;
|
||||
$GLOBALS['tests_failures'] = [];
|
||||
|
||||
/**
|
||||
* Formate une valeur pour l'affichage dans les messages d'échec
|
||||
*/
|
||||
function test_export($value) {
|
||||
if (is_array($value)) {
|
||||
return json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
return var_export($value, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre le résultat d'une assertion et affiche la ligne correspondante
|
||||
*/
|
||||
function test_record($ok, $label, $detail = '') {
|
||||
if ($ok) {
|
||||
$GLOBALS['tests_passed']++;
|
||||
echo " [OK] $label\n";
|
||||
} else {
|
||||
$GLOBALS['tests_failed']++;
|
||||
$GLOBALS['tests_failures'][] = $label;
|
||||
echo " [ÉCHEC] $label" . ($detail !== '' ? " — $detail" : '') . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
function assertEquals($expected, $actual, $label) {
|
||||
test_record(
|
||||
$expected == $actual,
|
||||
$label,
|
||||
'attendu ' . test_export($expected) . ', obtenu ' . test_export($actual)
|
||||
);
|
||||
}
|
||||
|
||||
function assertTrue($actual, $label) {
|
||||
test_record($actual === true, $label, 'attendu true, obtenu ' . test_export($actual));
|
||||
}
|
||||
|
||||
function assertFalse($actual, $label) {
|
||||
test_record($actual === false, $label, 'attendu false, obtenu ' . test_export($actual));
|
||||
}
|
||||
|
||||
function assertNull($actual, $label) {
|
||||
test_record($actual === null, $label, 'attendu null, obtenu ' . test_export($actual));
|
||||
}
|
||||
|
||||
function assertContains($needle, $haystack, $label) {
|
||||
test_record(
|
||||
strpos($haystack, $needle) !== false,
|
||||
$label,
|
||||
test_export($needle) . ' introuvable dans ' . test_export($haystack)
|
||||
);
|
||||
}
|
||||
|
||||
function assertNotContains($needle, $haystack, $label) {
|
||||
test_record(
|
||||
strpos($haystack, $needle) === false,
|
||||
$label,
|
||||
test_export($needle) . ' trouvé dans ' . test_export($haystack)
|
||||
);
|
||||
}
|
||||
|
||||
// --- Exécution des fichiers de test ---------------------------------------
|
||||
|
||||
$testFiles = glob(__DIR__ . '/*-test.php');
|
||||
sort($testFiles);
|
||||
|
||||
if (empty($testFiles)) {
|
||||
echo "Aucun fichier de test trouvé dans " . __DIR__ . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
foreach ($testFiles as $file) {
|
||||
echo "\n== " . basename($file) . " ==\n";
|
||||
try {
|
||||
require $file;
|
||||
} catch (Throwable $e) {
|
||||
test_record(false, basename($file) . ' : exception non interceptée — ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// --- Résumé ----------------------------------------------------------------
|
||||
|
||||
$passed = $GLOBALS['tests_passed'];
|
||||
$failed = $GLOBALS['tests_failed'];
|
||||
|
||||
echo "\n----------------------------------------\n";
|
||||
echo "Résumé : $passed réussi(s), $failed échoué(s).\n";
|
||||
|
||||
if ($failed > 0) {
|
||||
echo "Tests en échec :\n";
|
||||
foreach ($GLOBALS['tests_failures'] as $failure) {
|
||||
echo " - $failure\n";
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests unitaires pour includes/security.php
|
||||
* (et les validateurs d'URL de includes/config.php)
|
||||
*/
|
||||
|
||||
// --- validateVideoId --------------------------------------------------------
|
||||
|
||||
assertEquals(
|
||||
'9c5de94d-8e1a-4b3c-9d2e-1234567890ab',
|
||||
validateVideoId('9c5de94d-8e1a-4b3c-9d2e-1234567890ab'),
|
||||
'validateVideoId accepte un UUID valide'
|
||||
);
|
||||
assertEquals(
|
||||
'9C5DE94D-8E1A-4B3C-9D2E-1234567890AB',
|
||||
validateVideoId('9C5DE94D-8E1A-4B3C-9D2E-1234567890AB'),
|
||||
'validateVideoId accepte un UUID en majuscules'
|
||||
);
|
||||
assertEquals(
|
||||
'9c5de94d-8e1a-4b3c-9d2e-1234567890ab',
|
||||
validateVideoId(' 9c5de94d-8e1a-4b3c-9d2e-1234567890ab '),
|
||||
'validateVideoId nettoie les espaces autour de l\'UUID'
|
||||
);
|
||||
assertFalse(validateVideoId(''), 'validateVideoId refuse une chaîne vide');
|
||||
assertFalse(validateVideoId('pas-un-uuid'), 'validateVideoId refuse une chaîne quelconque');
|
||||
assertFalse(
|
||||
validateVideoId('9c5de94d-8e1a-4b3c-1234567890ab'),
|
||||
'validateVideoId refuse un UUID tronqué'
|
||||
);
|
||||
|
||||
// --- validateSearchQuery ----------------------------------------------------
|
||||
|
||||
assertEquals('indépendance', validateSearchQuery('indépendance'), 'validateSearchQuery conserve les accents');
|
||||
assertEquals(
|
||||
'script',
|
||||
validateSearchQuery('<script>'),
|
||||
'validateSearchQuery supprime les chevrons'
|
||||
);
|
||||
assertEquals(
|
||||
'recherche btest/b',
|
||||
validateSearchQuery('recherche <b>test</b>'),
|
||||
'validateSearchQuery supprime les caractères dangereux <>"\''
|
||||
);
|
||||
assertEquals('terme', validateSearchQuery(' terme '), 'validateSearchQuery nettoie les espaces');
|
||||
assertFalse(validateSearchQuery(''), 'validateSearchQuery refuse une chaîne vide');
|
||||
assertFalse(
|
||||
validateSearchQuery(str_repeat('a', 201)),
|
||||
'validateSearchQuery refuse plus de 200 caractères'
|
||||
);
|
||||
assertEquals(
|
||||
str_repeat('a', 200),
|
||||
validateSearchQuery(str_repeat('a', 200)),
|
||||
'validateSearchQuery accepte exactement 200 caractères'
|
||||
);
|
||||
|
||||
// --- validateCategoryId -----------------------------------------------------
|
||||
|
||||
assertEquals(1, validateCategoryId(1), 'validateCategoryId accepte la borne basse 1');
|
||||
assertEquals(20, validateCategoryId(20), 'validateCategoryId accepte la borne haute 20');
|
||||
assertEquals(5, validateCategoryId('5'), 'validateCategoryId convertit une chaîne numérique');
|
||||
assertFalse(validateCategoryId(0), 'validateCategoryId refuse 0');
|
||||
assertFalse(validateCategoryId(21), 'validateCategoryId refuse 21');
|
||||
assertFalse(validateCategoryId(-3), 'validateCategoryId refuse un nombre négatif');
|
||||
assertFalse(validateCategoryId('abc'), 'validateCategoryId refuse une chaîne non numérique');
|
||||
|
||||
// --- validatePageNumber -----------------------------------------------------
|
||||
|
||||
assertEquals(1, validatePageNumber(0), 'validatePageNumber remonte 0 à 1');
|
||||
assertEquals(1, validatePageNumber(-5), 'validatePageNumber remonte un négatif à 1');
|
||||
assertEquals(3, validatePageNumber(3), 'validatePageNumber conserve une page valide');
|
||||
assertEquals(2, validatePageNumber('2'), 'validatePageNumber convertit une chaîne numérique');
|
||||
assertEquals(1, validatePageNumber('abc'), 'validatePageNumber ramène une chaîne non numérique à 1');
|
||||
|
||||
// --- generateCSRFToken / validateCSRFToken ----------------------------------
|
||||
|
||||
$token = generateCSRFToken();
|
||||
assertContains(':', $token, 'generateCSRFToken produit le format "timestamp:hash"');
|
||||
|
||||
[$timestamp, $hash] = explode(':', $token, 2);
|
||||
assertTrue(ctype_digit($timestamp), 'generateCSRFToken : la partie timestamp est numérique');
|
||||
assertEquals(
|
||||
hash_hmac('sha256', $timestamp, CSRF_SECRET),
|
||||
$hash,
|
||||
'generateCSRFToken : le hash est un HMAC-SHA256 du timestamp avec CSRF_SECRET'
|
||||
);
|
||||
assertTrue(validateCSRFToken($token), 'validateCSRFToken accepte un token fraîchement généré');
|
||||
|
||||
assertFalse(validateCSRFToken(''), 'validateCSRFToken refuse une chaîne vide');
|
||||
assertFalse(validateCSRFToken('sans-deux-points'), 'validateCSRFToken refuse un token sans séparateur');
|
||||
assertFalse(validateCSRFToken('abc:' . $hash), 'validateCSRFToken refuse un timestamp non numérique');
|
||||
assertFalse(
|
||||
validateCSRFToken($timestamp . ':' . hash_hmac('sha256', $timestamp, 'autre-secret')),
|
||||
'validateCSRFToken refuse un hash signé avec un autre secret'
|
||||
);
|
||||
assertFalse(
|
||||
validateCSRFToken($timestamp . ':0' . $hash),
|
||||
'validateCSRFToken refuse un hash modifié'
|
||||
);
|
||||
|
||||
// Token expiré : horodatage d'il y a 2 heures (limite : 1 heure)
|
||||
$oldTimestamp = (string) (time() - 7200);
|
||||
$oldToken = $oldTimestamp . ':' . hash_hmac('sha256', $oldTimestamp, CSRF_SECRET);
|
||||
assertFalse(validateCSRFToken($oldToken), 'validateCSRFToken refuse un token expiré (> 1 heure)');
|
||||
|
||||
// Timestamp trop loin dans le futur (l'écart absolu est également plafonné)
|
||||
$futureTimestamp = (string) (time() + 7200);
|
||||
$futureToken = $futureTimestamp . ':' . hash_hmac('sha256', $futureTimestamp, CSRF_SECRET);
|
||||
assertFalse(validateCSRFToken($futureToken), 'validateCSRFToken refuse un timestamp trop futur');
|
||||
|
||||
// --- isValidPeerTubeUrl (includes/config.php) -------------------------------
|
||||
|
||||
assertTrue(isValidPeerTubeUrl('https://peertube.example.com'), 'isValidPeerTubeUrl accepte une URL HTTPS publique');
|
||||
assertTrue(isValidPeerTubeUrl('http://peertube.example.com'), 'isValidPeerTubeUrl accepte HTTP (développement)');
|
||||
assertTrue(isValidPeerTubeUrl('https://8.8.8.8'), 'isValidPeerTubeUrl accepte une IP publique');
|
||||
assertFalse(isValidPeerTubeUrl('ftp://example.com'), 'isValidPeerTubeUrl refuse un schéma non HTTP(S)');
|
||||
assertFalse(isValidPeerTubeUrl('pas-une-url'), 'isValidPeerTubeUrl refuse une chaîne mal formée');
|
||||
assertFalse(isValidPeerTubeUrl('https://localhost'), 'isValidPeerTubeUrl refuse localhost');
|
||||
assertFalse(isValidPeerTubeUrl('http://127.0.0.1'), 'isValidPeerTubeUrl refuse 127.0.0.1');
|
||||
assertFalse(isValidPeerTubeUrl('http://192.168.1.1'), 'isValidPeerTubeUrl refuse une IP privée');
|
||||
assertFalse(isValidPeerTubeUrl('http://10.0.0.5'), 'isValidPeerTubeUrl refuse une IP privée (10.x)');
|
||||
assertFalse(
|
||||
isValidPeerTubeUrl('https://metadata.google.internal'),
|
||||
'isValidPeerTubeUrl refuse metadata.google.internal'
|
||||
);
|
||||
|
||||
// --- isValidApiEndpoint (includes/config.php) -------------------------------
|
||||
|
||||
assertTrue(isValidApiEndpoint('videos'), 'isValidApiEndpoint accepte "videos"');
|
||||
assertTrue(isValidApiEndpoint('videos/categories'), 'isValidApiEndpoint accepte "videos/categories"');
|
||||
assertTrue(isValidApiEndpoint('search/videos'), 'isValidApiEndpoint accepte "search/videos"');
|
||||
assertTrue(isValidApiEndpoint('accounts'), 'isValidApiEndpoint accepte "accounts"');
|
||||
assertTrue(
|
||||
isValidApiEndpoint('videos/9c5de94d-8e1a-4b3c-9d2e-1234567890ab'),
|
||||
'isValidApiEndpoint accepte un endpoint vidéo dynamique'
|
||||
);
|
||||
assertTrue(
|
||||
isValidApiEndpoint('videos/abc/comment-threads'),
|
||||
'isValidApiEndpoint accepte les fils de commentaires'
|
||||
);
|
||||
assertTrue(
|
||||
isValidApiEndpoint('accounts/annu_kute_ced/videos'),
|
||||
'isValidApiEndpoint accepte les vidéos d\'un compte'
|
||||
);
|
||||
assertFalse(isValidApiEndpoint('../config'), 'isValidApiEndpoint refuse le path traversal');
|
||||
assertFalse(isValidApiEndpoint('videos/../x'), 'isValidApiEndpoint refuse ".." dans le chemin');
|
||||
assertFalse(isValidApiEndpoint('videos//categories'), 'isValidApiEndpoint refuse un double slash');
|
||||
assertFalse(isValidApiEndpoint('wp-admin'), 'isValidApiEndpoint refuse un endpoint hors liste blanche');
|
||||
assertFalse(isValidApiEndpoint('videos.json'), 'isValidApiEndpoint refuse les caractères non autorisés (.)');
|
||||
assertFalse(isValidApiEndpoint('videos?count=1'), 'isValidApiEndpoint refuse une query string');
|
||||
assertFalse(isValidApiEndpoint(''), 'isValidApiEndpoint refuse une chaîne vide');
|
||||
|
||||
// --- getCspNonce ------------------------------------------------------------
|
||||
|
||||
$nonce = getCspNonce();
|
||||
assertTrue(is_string($nonce) && $nonce !== '', 'getCspNonce retourne une chaîne non vide');
|
||||
assertEquals(16, strlen(base64_decode($nonce, true)), 'getCspNonce est un base64 de 16 octets aléatoires');
|
||||
assertEquals($nonce, getCspNonce(), 'getCspNonce retourne le même nonce durant toute la requête');
|
||||
Reference in New Issue
Block a user