'Junior', 'intermediaire' => 'Intermédiaire', 'confirme' => 'Confirmé', 'expert' => 'Expert']; const STATUTS = ['recu' => 'Reçu']; const ETATS_BESOIN = ['ouvert' => 'Ouvert', 'pourvu' => 'Pourvu', 'ferme' => 'Fermé']; require __DIR__ . '/../src/i18n.php'; require __DIR__ . '/../src/Recherche.php'; require __DIR__ . '/../src/Import.php'; require __DIR__ . '/../src/besoins.php'; require __DIR__ . '/../src/Lot.php'; require __DIR__ . '/../src/Sauvegarde.php'; $ok = 0; $ko = 0; function verifier(string $nom, bool $cond): void { global $ok, $ko; if ($cond) { $ok++; } else { $ko++; echo "ECHEC : $nom\n"; } } // --- Listes saisies verifier('liste : virgules, points-virgules, doublons', liste_saisie(' Java, Spring Boot ; java,, PostgreSQL ') === ['Java', 'Spring Boot', 'PostgreSQL']); verifier('liste vide', liste_saisie(null) === [] && liste_saisie(' , ') === []); // --- Rapprochement $profil = static fn (array $technos, string $seniorite = 'confirme', array $plus = []): array => $plus + [ 'technologies' => $technos, 'termes' => array_fill_keys(array_merge(['developpeur', 'java'], array_keys($technos)), true), 'seniorite' => $seniorite, 'langues' => ['francais', 'anglais'], 'disponible_le' => null, 'tjm' => null, ]; $besoin = ['intitule' => 'Développeur Java', 'technologies' => 'Java, Spring Boot, PostgreSQL', 'seniorite_min' => null, 'langues' => null, 'date_debut' => null, 'tjm_max' => null]; $complet = noter_rapprochement($besoin, $profil(['java' => 9.0, 'spring boot' => 6.0, 'postgresql' => 5.0])); verifier('toutes les technologies, 5 ans et plus : 100', $complet['score'] === 100 && $complet['manquantes'] === [] && count($complet['trouvees']) === 3); $partiel = noter_rapprochement($besoin, $profil(['java' => 9.0])); verifier('une technologie sur trois : note plus basse, manquantes listées', $partiel['score'] < $complet['score'] && $partiel['manquantes'] === ['Spring Boot', 'PostgreSQL']); verifier('une sur trois à 5 ans : 55/3 + 45 = 63', $partiel['score'] === 63); $debutant = noter_rapprochement($besoin, $profil(['java' => 1.0, 'spring boot' => 1.0, 'postgresql' => 1.0])); verifier('mêmes technologies, moins d\'années : note plus basse', $debutant['score'] < $complet['score'] && $debutant['score'] > $partiel['score']); $sansAnnees = noter_rapprochement($besoin, $profil(['java' => null, 'spring boot' => null, 'postgresql' => null])); verifier('années inconnues : 60 % des points de technologie', $sansAnnees['score'] === 78); $horsSujet = noter_rapprochement($besoin, ['technologies' => ['cobol' => 20.0], 'termes' => ['cobol' => true], 'seniorite' => 'expert', 'langues' => [], 'disponible_le' => null, 'tjm' => null]); verifier('aucune technologie ni mot de l\'intitulé : profil écarté', $horsSujet['pertinent'] === false); // Technologie citée dans une description (index) mais absente de la liste des technologies $parIndex = noter_rapprochement(['intitule' => 'Consultant', 'technologies' => 'Spring Boot'] + $besoin, ['technologies' => [], 'termes' => ['spring' => true, 'boot' => true], 'seniorite' => 'junior', 'langues' => [], 'disponible_le' => null, 'tjm' => null]); verifier('technologie retrouvée par l\'index', $parIndex['pertinent'] && count($parIndex['trouvees']) === 1 && $parIndex['trouvees'][0]['annees'] === null); $b2 = ['seniorite_min' => 'expert'] + $besoin; $t = ['java' => 9.0, 'spring boot' => 6.0, 'postgresql' => 5.0]; verifier('séniorité un niveau en dessous : 4 points sur 10 et alerte', noter_rapprochement($b2, $profil($t, 'confirme'))['score'] === 94 && count(noter_rapprochement($b2, $profil($t, 'confirme'))['alertes']) === 1); verifier('séniorité deux niveaux en dessous : 0 point', noter_rapprochement($b2, $profil($t, 'intermediaire'))['score'] === 90); verifier('séniorité atteinte : pas d\'alerte', noter_rapprochement($b2, $profil($t, 'expert'))['alertes'] === []); $b3 = ['langues' => 'Français, Allemand'] + $besoin; $n = noter_rapprochement($b3, $profil($t)); verifier('une langue sur deux : 5 points de moins, langue nommée', $n['score'] === 95 && str_contains($n['alertes'][0], 'Allemand')); $b4 = ['date_debut' => '2026-11-01', 'tjm_max' => 600] + $besoin; verifier('disponibilité et tarif inconnus : moitié des points', noter_rapprochement($b4, $profil($t))['score'] === 95); verifier('disponible à temps, tarif dans le budget : 100', noter_rapprochement($b4, $profil($t, 'confirme', ['disponible_le' => '2026-10-15', 'tjm' => 550]))['score'] === 100); $tard = noter_rapprochement($b4, $profil($t, 'confirme', ['disponible_le' => '2026-12-01', 'tjm' => 700])); verifier('disponible trop tard, tarif trop haut : 90 et deux alertes', $tard['score'] === 90 && count($tard['alertes']) === 2); verifier('tarif 10 % au-dessus : 2 points', noter_rapprochement($b4, $profil($t, 'confirme', ['disponible_le' => '2026-10-15', 'tjm' => 650]))['score'] === 97); $sansTechno = ['technologies' => null] + $besoin; verifier('besoin sans technologie : l\'intitulé porte la note', noter_rapprochement($sansTechno, $profil([]))['score'] === 100); verifier('barème : total 100', array_sum(BAREME_RAPPROCHEMENT) === 100); // --- Doublons : clé de téléphone verifier('téléphone : indicatif et espaces ignorés', Import::cleTelephone('+352 621 37 37 95') === Import::cleTelephone('00352-621.373.795') && Import::cleTelephone('+33 6 12 34 56 78') === Import::cleTelephone('06 12 34 56 78')); verifier('téléphone trop court ou absent : pas de comparaison', Import::cleTelephone('12 34') === '' && Import::cleTelephone(null) === ''); // --- Lot : appariement des fichiers déposés ensemble $dossier = sys_get_temp_dir() . '/vivier-test-' . bin2hex(random_bytes(4)); mkdir($dossier); $fichier = static function (string $nom, string $contenu = 'x') use ($dossier): array { file_put_contents("$dossier/$nom", $contenu); return ['tmp' => "$dossier/$nom", 'nom' => $nom, 'ext' => strtolower(pathinfo($nom, PATHINFO_EXTENSION)), 'mime' => 'x', 'taille' => 1]; }; $json = static fn (string $cv, string $nom): string => (string) json_encode(['identite' => ['prenom' => 'A', 'nom' => $nom], 'source' => ['fichier_origine' => $cv]]); $r = Lot::apparier([ $fichier('profil-durand.json', $json('CV Durand.pdf', 'Durand')), $fichier('cv durand.PDF'), $fichier('rapport-durand.md'), $fichier('martin.json', $json('CV_Martin.docx', 'Martin')), $fichier('CV_Martin.docx'), $fichier('seul.json', $json('absent.pdf', 'Seul')), $fichier('orphelin.pdf'), $fichier('casse.json', '{pas du json'), ]); verifier('lot : deux paires formées', count($r['paires']) === 2); verifier('lot : CV associé sans tenir compte de la casse', $r['paires'][0]['cv']['nom'] === 'cv durand.PDF' && $r['paires'][1]['cv']['nom'] === 'CV_Martin.docx'); verifier('lot : rapport associé par le nom du candidat', ($r['paires'][0]['rapport']['nom'] ?? '') === 'rapport-durand.md' && $r['paires'][1]['rapport'] === null); verifier('lot : CV manquant, fichier orphelin et JSON illisible signalés', count($r['erreurs']) === 3); array_map('unlink', glob("$dossier/*")); rmdir($dossier); // --- Sauvegarde : composition de l'email [$sujet, $corps, $entetes] = Sauvegarde::composer('vivier@itclub.lu', 'Vivier : sauvegarde du 11/10/2026', "Bonjour,\r\ntexte accentué.", null, null); verifier('email simple : sujet encodé, texte en UTF-8', str_starts_with($sujet, '=?UTF-8?B?') && base64_decode(substr($sujet, 10, -2)) === 'Vivier : sauvegarde du 11/10/2026' && str_contains($entetes, 'From: Vivier ') && str_contains($entetes, 'text/plain; charset=UTF-8') && str_contains($corps, 'accentué')); $piece = sys_get_temp_dir() . '/vivier-test.zip'; file_put_contents($piece, random_bytes(5000)); [$sujet, $corps, $entetes] = Sauvegarde::composer('vivier@itclub.lu', 'Essai', 'Texte', $piece, 'vivier-base-2026-10-11-0600.zip'); preg_match('/boundary="([^"]+)"/', $entetes, $m); $parties = explode('--' . $m[1], $corps); verifier('email avec pièce : deux parties et une fin', count($parties) === 4 && trim($parties[3]) === '--'); verifier('pièce jointe : nom, type, contenu intact', str_contains($parties[2], 'filename="vivier-base-2026-10-11-0600.zip"') && str_contains($parties[2], 'application/zip') && base64_decode(preg_replace('/\s+/', '', explode("\r\n\r\n", $parties[2], 2)[1])) === file_get_contents($piece)); verifier('pièce jointe : lignes de 76 caractères au plus', max(array_map('strlen', explode("\r\n", $parties[2]))) <= 76); unlink($piece); verifier('taille lisible', Sauvegarde::taille(512) === '1 Ko' && Sauvegarde::taille(2621440) === '2,5 Mo'); echo "$ok réussis, $ko échoués\n"; exit($ko > 0 ? 1 : 0);