Import initial de Vivier depuis l'hebergement OVH
This commit is contained in:
commit
839e5e57c5
126 files changed
+22447
No files matched your search
@@ -0,0 +1 @@
|
||||
Require all denied
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
// Tests de l'aide intégrée : php tests/test_aide.php
|
||||
declare(strict_types=1);
|
||||
const VIVIER_RACINE = __DIR__ . '/..';
|
||||
const SENIORITES = ['junior' => 'Junior'];
|
||||
const STATUTS = ['recu' => 'Reçu'];
|
||||
require __DIR__ . '/../src/i18n.php';
|
||||
require __DIR__ . '/../src/Recherche.php';
|
||||
require __DIR__ . '/../src/markdown.php';
|
||||
require __DIR__ . '/../src/aide.php';
|
||||
|
||||
$ok = 0; $ko = 0;
|
||||
function verifier(string $nom, bool $cond): void {
|
||||
global $ok, $ko;
|
||||
if ($cond) { $ok++; } else { $ko++; echo "ECHEC : $nom\n"; }
|
||||
}
|
||||
|
||||
foreach (array_keys(DOCUMENTS_AIDE) as $guide) {
|
||||
$fr = aide($guide, 'fr');
|
||||
$en = aide($guide, 'en');
|
||||
verifier("$guide : existe en français et en anglais", $fr !== null && $en !== null && $fr['langue'] === 'fr' && $en['langue'] === 'en');
|
||||
verifier("$guide : même nombre de chapitres dans les deux langues", count($fr['chapitres']) === count($en['chapitres']) && count($fr['chapitres']) >= 10);
|
||||
foreach (['fr' => $fr, 'en' => $en] as $langue => $a) {
|
||||
preg_match_all('/ id="([^"]+)"/', $a['html'], $ids);
|
||||
verifier("$guide ($langue) : ancres uniques et non vides", count($ids[1]) === count(array_unique($ids[1])) && !in_array('', $ids[1], true));
|
||||
verifier("$guide ($langue) : chaque chapitre du sommaire a son ancre", !array_diff(array_column($a['chapitres'], 'id'), $ids[1]));
|
||||
verifier("$guide ($langue) : aucun balisage Markdown resté visible", !preg_match('/\*\*|^#|\|\s*---/m', strip_tags($a['html'])));
|
||||
}
|
||||
// Même structure : autant de sous-titres, de tableaux et de listes numérotées de part et d'autre
|
||||
foreach (['<h3' => 'sous-titres', '<table>' => 'tableaux', '<ol>' => 'listes numérotées', '<tr>' => 'lignes de tableau'] as $balise => $nom) {
|
||||
verifier("$guide : autant de $nom en français qu'en anglais", substr_count($fr['html'], $balise) === substr_count($en['html'], $balise));
|
||||
}
|
||||
}
|
||||
verifier('langue sans fichier : le français est servi', aide('guide', 'de')['langue'] === 'fr');
|
||||
verifier('guide inconnu ou nom piégé : rien', aide('autre', 'fr') === null && aide('../config/config', 'fr') === null && aide('guide', '../x') === null);
|
||||
verifier('ancre lisible, sans accent', aide('guide', 'fr')['chapitres'][1]['id'] === 'se-connecter');
|
||||
|
||||
echo "$ok réussis, $ko échoués\n";
|
||||
exit($ko > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
// Tests de la traduction : php tests/test_i18n.php
|
||||
declare(strict_types=1);
|
||||
const VIVIER_RACINE = __DIR__ . '/..';
|
||||
require __DIR__ . '/../src/i18n.php';
|
||||
|
||||
$ok = 0; $ko = 0;
|
||||
function verifier(string $nom, bool $cond): void {
|
||||
global $ok, $ko;
|
||||
if ($cond) { $ok++; } else { $ko++; echo "ECHEC : $nom\n"; }
|
||||
}
|
||||
|
||||
verifier('français : le texte est rendu tel quel', t('Profils') === 'Profils');
|
||||
verifier('français : variables', t('Conservation prolongée de %d mois.', 24) === 'Conservation prolongée de 24 mois.');
|
||||
verifier('français : accord', tn(1, '%d profil', '%d profils') === '1 profil' && tn(3, '%d profil', '%d profils') === '3 profils');
|
||||
verifier('français : nombre', nombre(9.10) === '9,1' && nombre(17.0) === '17' && annees(1) === '1 an' && annees(9.1) === '9,1 ans');
|
||||
|
||||
langue_courante('en');
|
||||
verifier('anglais : traduction', t('Profils') === 'Profiles');
|
||||
verifier('anglais : variables', t('Conservation prolongée de %d mois.', 24) === 'Retention extended by 24 months.');
|
||||
verifier('anglais : accord', tn(1, '%d profil', '%d profils') === '1 profile' && tn(3, '%d profil', '%d profils') === '3 profiles');
|
||||
verifier('anglais : nombre', nombre(9.10) === '9.1' && annees(9.1) === '9.1 years');
|
||||
verifier('anglais : clé absente rendue en français', t('Texte jamais traduit') === 'Texte jamais traduit');
|
||||
verifier('anglais : journal à partie variable', t_journal('Import de CV_Test.pdf') === 'Import of CV_Test.pdf' && t_journal('Traduction en anglais') === 'Translation into English');
|
||||
verifier('langue inconnue ignorée', langue_courante('xx') === 'en');
|
||||
verifier('allemand : libellés du CV', traduire('Compétences techniques', 'de') === 'Technische Kenntnisse' && annees(9.1, 'de') === '9,1 Jahre');
|
||||
|
||||
// Chaque traduction garde les mêmes variables que le texte français
|
||||
foreach (['en', 'de'] as $langue) {
|
||||
$ecarts = [];
|
||||
foreach (dictionnaire($langue) as $fr => $traduit) {
|
||||
preg_match_all('/%[sd]/', $fr, $a); preg_match_all('/%[sd]/', $traduit, $b);
|
||||
if ($a[0] !== $b[0]) { $ecarts[] = $fr; }
|
||||
}
|
||||
verifier("$langue : variables identiques dans chaque traduction" . ($ecarts ? ' (' . implode(' | ', $ecarts) . ')' : ''), !$ecarts);
|
||||
}
|
||||
|
||||
echo "$ok réussis, $ko échoués\n";
|
||||
exit($ko > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
// Tests de l'import : php tests/test_import.php
|
||||
declare(strict_types=1);
|
||||
const VIVIER_RACINE = __DIR__ . '/..';
|
||||
require __DIR__ . '/../src/i18n.php';
|
||||
require __DIR__ . '/../src/pays.php';
|
||||
require __DIR__ . '/../src/Import.php';
|
||||
|
||||
$ok = 0; $ko = 0;
|
||||
function verifier(string $nom, bool $cond): void {
|
||||
global $ok, $ko;
|
||||
if ($cond) { $ok++; } else { $ko++; echo "ECHEC : $nom\n"; }
|
||||
}
|
||||
|
||||
$exemple = json_decode(file_get_contents(__DIR__ . '/../claude/exemple-profil.json'), true);
|
||||
$auj = new DateTimeImmutable('2026-10-08');
|
||||
|
||||
verifier('exemple conforme', Import::valider($exemple) === []);
|
||||
|
||||
$p = $exemple; $p['seniorite']['niveau'] = 'senior';
|
||||
verifier('niveau inconnu rejeté', count(Import::valider($p)) === 1);
|
||||
$p = $exemple; $p['experiences'][0]['date_debut'] = '03/2020';
|
||||
verifier('date mal formée rejetée', count(Import::valider($p)) === 1);
|
||||
$p = $exemple; $p['age'] = 34;
|
||||
verifier('clé en trop rejetée', count(Import::valider($p)) === 1);
|
||||
$p = $exemple; unset($p['identite']['nom']);
|
||||
verifier('nom manquant rejeté', count(Import::valider($p)) === 1);
|
||||
$p = $exemple; $p['liens'][0]['url'] = 'javascript:alert(1)';
|
||||
verifier('lien non http rejeté', count(Import::valider($p)) === 1);
|
||||
$p = $exemple; $p['experiences'][1]['date_fin'] = '2016-01';
|
||||
verifier('fin avant début rejetée', count(Import::valider($p)) === 1);
|
||||
$p = $exemple; $p['schema_version'] = '2.0';
|
||||
verifier('version inconnue rejetée', count(Import::valider($p)) === 1);
|
||||
verifier('liste rejetée', Import::valider([1, 2]) !== []);
|
||||
verifier('texte rejeté', Import::valider('x') !== []);
|
||||
|
||||
// Format 1.1 : mots-clés et métadonnées
|
||||
$v10 = $exemple; $v10['schema_version'] = '1.0'; unset($v10['mots_cles'], $v10['metadonnees']);
|
||||
verifier('profil 1.0 toujours accepté', Import::valider($v10) === []);
|
||||
$p = $exemple; $p['metadonnees']['niveau_etudes'] = 'master';
|
||||
verifier('niveau d\'études inconnu rejeté', count(Import::valider($p)) === 1);
|
||||
$p = $exemple; $p['mots_cles'][] = 42;
|
||||
verifier('mot-clé non textuel rejeté', count(Import::valider($p)) === 1);
|
||||
$p = $exemple; $p['metadonnees']['encadrement'] = 'oui';
|
||||
verifier('encadrement non booléen rejeté', count(Import::valider($p)) === 1);
|
||||
$p = $exemple; $p['metadonnees']['salaire'] = 1;
|
||||
verifier('métadonnée inconnue rejetée', count(Import::valider($p)) === 1);
|
||||
|
||||
// Séniorité
|
||||
$s = Import::seniorite($exemple, $auj);
|
||||
verifier('exemple : 9,1 ans', $s['annees'] === 9.1);
|
||||
verifier('exemple : confirmé', $s['niveau'] === 'confirme');
|
||||
verifier('exemple : pas d\'écart', $s['ecart'] === false);
|
||||
|
||||
$exp = static fn (string $d, ?string $f, string $t = 'emploi', array $tech = []) =>
|
||||
['poste' => 'x', 'type' => $t, 'date_debut' => $d, 'date_fin' => $f, 'technologies' => $tech];
|
||||
|
||||
verifier('poste terminé : mois de fin compté', Import::moisExperience([$exp('2020-01', '2020-12')], $auj) === 12);
|
||||
verifier('chevauchement fusionné', Import::moisExperience([$exp('2020-01', '2020-12'), $exp('2020-07', '2021-06')], $auj) === 18);
|
||||
verifier('périodes disjointes additionnées', Import::moisExperience([$exp('2018-01', '2018-12'), $exp('2020-01', '2020-12')], $auj) === 24);
|
||||
verifier('stage exclu', Import::moisExperience([$exp('2020-01', '2020-06', 'stage')], $auj) === 0);
|
||||
verifier('alternance comptée', Import::moisExperience([$exp('2020-01', '2020-06', 'alternance')], $auj) === 6);
|
||||
verifier('poste en cours : jusqu\'au dernier mois complet', Import::moisExperience([$exp('2026-01', null)], $auj) === 9);
|
||||
verifier('fin dans le futur plafonnée', Import::moisExperience([$exp('2026-01', '2030-12')], $auj) === 10);
|
||||
verifier('année seule', Import::moisExperience([$exp('2015', '2016')], $auj) === 13);
|
||||
verifier('filtre par technologie, casse ignorée',
|
||||
Import::moisExperience([$exp('2020-01', '2020-12', 'emploi', ['Java']), $exp('2021-01', '2021-12', 'emploi', ['PHP'])], $auj, 'java') === 12);
|
||||
|
||||
verifier('3,9 ans : junior', Import::niveau(3.9) === 'junior');
|
||||
verifier('4 ans : intermédiaire', Import::niveau(4.0) === 'intermediaire');
|
||||
verifier('7,9 ans : intermédiaire', Import::niveau(7.9) === 'intermediaire');
|
||||
verifier('8 ans : confirmé', Import::niveau(8.0) === 'confirme');
|
||||
verifier('12 ans : expert', Import::niveau(12.0) === 'expert');
|
||||
|
||||
echo "$ok réussis, $ko échoués\n";
|
||||
exit($ko > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
// Tests de la recherche de photo dans un CV : php tests/test_photo.php
|
||||
declare(strict_types=1);
|
||||
const VIVIER_RACINE = __DIR__ . '/..';
|
||||
require __DIR__ . '/../src/i18n.php';
|
||||
require __DIR__ . '/../src/Photo.php';
|
||||
|
||||
$ok = 0; $ko = 0;
|
||||
function verifier(string $nom, bool $cond): void {
|
||||
global $ok, $ko;
|
||||
if ($cond) { $ok++; } else { $ko++; echo "ECHEC : $nom\n"; }
|
||||
}
|
||||
if (!Photo::disponible()) { echo "Extension GD absente : tests ignorés.\n"; exit(0); }
|
||||
|
||||
/** Image aux teintes variées, comme une photo. */
|
||||
function portrait(int $l, int $h, string $format = 'jpeg'): string {
|
||||
$img = imagecreatetruecolor($l, $h);
|
||||
mt_srand(7);
|
||||
for ($y = 0; $y < $h; $y += 4) {
|
||||
for ($x = 0; $x < $l; $x += 4) {
|
||||
imagefilledrectangle($img, $x, $y, $x + 3, $y + 3, imagecolorallocate($img, ($x * 255 / $l + mt_rand(0, 60)) % 256, ($y * 255 / $h + mt_rand(0, 60)) % 256, mt_rand(60, 200)));
|
||||
}
|
||||
}
|
||||
ob_start(); $format === 'png' ? imagepng($img) : imagejpeg($img, null, 90); return (string) ob_get_clean();
|
||||
}
|
||||
/** Image à deux couleurs, comme un logo. */
|
||||
function logo(int $l, int $h): string {
|
||||
$img = imagecreatetruecolor($l, $h);
|
||||
imagefill($img, 0, 0, imagecolorallocate($img, 255, 255, 255));
|
||||
imagefilledrectangle($img, (int) ($l / 4), (int) ($h / 4), (int) ($l * 3 / 4), (int) ($h * 3 / 4), imagecolorallocate($img, 206, 183, 59));
|
||||
ob_start(); imagepng($img); return (string) ob_get_clean();
|
||||
}
|
||||
function dimensions(?string $jpeg): array { $t = $jpeg ? getimagesizefromstring($jpeg) : [0, 0, 0]; return [$t[0], $t[1], $t[2]]; }
|
||||
|
||||
$tmp = sys_get_temp_dir() . '/vivier-test-' . bin2hex(random_bytes(4));
|
||||
mkdir($tmp);
|
||||
|
||||
// --- Word : la photo parmi un logo et un bandeau
|
||||
$zip = new ZipArchive(); $zip->open("$tmp/cv.docx", ZipArchive::CREATE);
|
||||
$zip->addFromString('word/document.xml', '<w:document/>');
|
||||
$zip->addFromString('word/media/image1.png', logo(400, 400));
|
||||
$zip->addFromString('word/media/image2.jpeg', portrait(600, 760));
|
||||
$zip->addFromString('word/media/image3.jpeg', portrait(900, 120));
|
||||
$zip->addFromString('word/media/image4.png', portrait(40, 50, 'png'));
|
||||
$zip->close();
|
||||
[$l, $h, $type] = dimensions(Photo::extraire("$tmp/cv.docx", 'docx'));
|
||||
verifier('Word : le portrait est retenu, pas le logo ni le bandeau', $type === IMAGETYPE_JPEG && abs($l / max($h, 1) - 600 / 760) < 0.02);
|
||||
verifier('Word : photo réduite à 480 px au plus', max($l, $h) === 480);
|
||||
|
||||
$zip = new ZipArchive(); $zip->open("$tmp/sans.docx", ZipArchive::CREATE);
|
||||
$zip->addFromString('word/document.xml', '<w:document/>');
|
||||
$zip->addFromString('word/media/image1.png', logo(400, 400));
|
||||
$zip->close();
|
||||
verifier('Word sans photo : rien n\'est retenu', Photo::extraire("$tmp/sans.docx", 'docx') === null);
|
||||
|
||||
// --- PDF : image JPEG
|
||||
$jpeg = portrait(300, 380);
|
||||
$pdf = "%PDF-1.4\n1 0 obj\n<< /Type /XObject /Subtype /Image /Width 300 /Height 380 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length " . strlen($jpeg) . " >>\nstream\n" . $jpeg . "\nendstream\nendobj\n%%EOF";
|
||||
file_put_contents("$tmp/jpeg.pdf", $pdf);
|
||||
[$l, $h] = dimensions(Photo::extraire("$tmp/jpeg.pdf", 'pdf'));
|
||||
verifier('PDF : image JPEG retrouvée', $l === 300 && $h === 380);
|
||||
|
||||
// --- PDF : image compressée sans perte, avec et sans prédicteur
|
||||
$img = imagecreatefromstring(portrait(200, 240));
|
||||
$brut = ''; $filtre = '';
|
||||
for ($y = 0; $y < 240; $y++) {
|
||||
$ligne = '';
|
||||
for ($x = 0; $x < 200; $x++) { $c = imagecolorat($img, $x, $y); $ligne .= chr(($c >> 16) & 255) . chr(($c >> 8) & 255) . chr($c & 255); }
|
||||
$brut .= $ligne; $filtre .= "\0" . $ligne;
|
||||
}
|
||||
$objet = static fn (string $donnees, string $plus = ''): string => "%PDF-1.4\n4 0 obj\n<< /Subtype /Image /Width 200 /Height 240 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode $plus/Length 9 0 R >>\nstream\r\n" . $donnees . "\r\nendstream\nendobj\n%%EOF";
|
||||
file_put_contents("$tmp/flate.pdf", $objet(gzcompress($brut)));
|
||||
[$l, $h] = dimensions(Photo::extraire("$tmp/flate.pdf", 'pdf'));
|
||||
verifier('PDF : image sans perte retrouvée, longueur indirecte', $l === 200 && $h === 240);
|
||||
file_put_contents("$tmp/predicteur.pdf", $objet(gzcompress($filtre), '/DecodeParms << /Predictor 15 /Colors 3 /Columns 200 >> '));
|
||||
[$l, $h] = dimensions(Photo::extraire("$tmp/predicteur.pdf", 'pdf'));
|
||||
verifier('PDF : image sans perte avec prédicteur retrouvée', $l === 200 && $h === 240);
|
||||
|
||||
file_put_contents("$tmp/texte.pdf", "%PDF-1.4\n1 0 obj\n<< /Length 20 >>\nstream\nBT (Bonjour) Tj ET\nendstream\nendobj\n%%EOF");
|
||||
verifier('PDF sans image : rien n\'est retenu', Photo::extraire("$tmp/texte.pdf", 'pdf') === null);
|
||||
file_put_contents("$tmp/casse.pdf", "%PDF-1.4 << /Subtype /Image /Width 300 /Height 300 /Filter /DCTDecode >> stream\nceci n'est pas une image");
|
||||
verifier('PDF endommagé : aucune erreur', Photo::extraire("$tmp/casse.pdf", 'pdf') === null);
|
||||
verifier('format sans image (txt) : rien', Photo::extraire(__FILE__, 'txt') === null);
|
||||
|
||||
// --- Normalisation
|
||||
[$l, $h, $type] = dimensions(Photo::normaliser(portrait(1200, 800, 'png')));
|
||||
verifier('chargement manuel : PNG converti en JPEG réduit', $type === IMAGETYPE_JPEG && $l === 480 && $h === 320);
|
||||
verifier('fichier qui n\'est pas une image : refusé', Photo::normaliser('<?php echo 1;') === null);
|
||||
|
||||
array_map('unlink', glob("$tmp/*")); rmdir($tmp);
|
||||
echo "$ok réussis, $ko échoués\n";
|
||||
exit($ko > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
// Tests du découpage en termes et du lecteur de rapports : php tests/test_recherche.php
|
||||
declare(strict_types=1);
|
||||
const SENIORITES = ['junior' => 'Junior', 'intermediaire' => 'Intermédiaire', 'confirme' => 'Confirmé', 'expert' => 'Expert'];
|
||||
const STATUTS = ['recu' => 'Reçu'];
|
||||
require __DIR__ . '/../src/Recherche.php';
|
||||
require __DIR__ . '/../src/markdown.php';
|
||||
|
||||
$ok = 0; $ko = 0;
|
||||
function verifier(string $nom, bool $cond): void {
|
||||
global $ok, $ko;
|
||||
if ($cond) { $ok++; } else { $ko++; echo "ECHEC : $nom\n"; }
|
||||
}
|
||||
$t = static fn (string $s, bool $d = true): array => Recherche::termes($s, $d);
|
||||
|
||||
verifier('minuscules et accents', $t('Développeur Sénior') === ['developpeur', 'senior']);
|
||||
verifier('c# conservé', in_array('c#', $t('C# et Java'), true));
|
||||
verifier('c++ conservé', in_array('c++', $t('Expert C++.'), true));
|
||||
verifier('.NET reconnu', in_array('dotnet', $t('ASP .NET Core'), true));
|
||||
verifier('node.js entier et décomposé', !array_diff(['node.js', 'node', 'js'], $t('Node.js')));
|
||||
verifier('ci/cd entier et décomposé', !array_diff(['ci/cd', 'ci', 'cd'], $t('CI/CD')));
|
||||
verifier('full-stack entier et décomposé', !array_diff(['full-stack', 'full', 'stack'], $t('Full-Stack')));
|
||||
verifier('requête non décomposée', $t('full-stack', false) === ['full-stack']);
|
||||
verifier('apostrophe séparatrice', $t("d'applications") === ['d', 'applications']);
|
||||
verifier('ponctuation finale retirée', $t('Java, Spring.') === ['java', 'spring']);
|
||||
verifier('lettre seule gardée', in_array('r', $t('R et Python'), true));
|
||||
verifier('œ développé', $t('Cœur') === ['coeur']);
|
||||
|
||||
$html = markdown_vers_html("# Titre\n\nTexte **gras** et <script>x</script>\n\n- un\n- deux\n\n| a | b |\n|---|---|\n| 1 | 2 |\n");
|
||||
verifier('titre décalé d\'un niveau', str_contains($html, '<h2>Titre</h2>'));
|
||||
verifier('gras', str_contains($html, '<strong>gras</strong>'));
|
||||
verifier('HTML du fichier neutralisé', !str_contains($html, '<script>') && str_contains($html, '<script>'));
|
||||
verifier('liste', str_contains($html, "<ul>\n<li>un</li>\n<li>deux</li>\n</ul>"));
|
||||
verifier('tableau', str_contains($html, '<th>a</th>') && str_contains($html, '<td>2</td>'));
|
||||
|
||||
echo "$ok réussis, $ko échoués\n";
|
||||
exit($ko > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
// Tests de la version 1.8 sans base de données : php tests/test_securite.php
|
||||
// Double authentification, blocages, nationalités, disponibilité, traduction, CV Europass, import 1.2.
|
||||
declare(strict_types=1);
|
||||
const VIVIER_RACINE = __DIR__ . '/..';
|
||||
const LONGUEUR_MOT_DE_PASSE = 12;
|
||||
const DISPONIBILITES = ['immediate' => 'Immédiate', '1_mois' => 'Sous 1 mois', '2_mois' => 'Sous 2 mois', '3_mois' => 'Sous 3 mois', 'date' => 'Date précise', 'a_preciser' => 'À préciser'];
|
||||
const TELETRAVAIL = ['non' => 'Sur site', 'partiel' => 'Télétravail partiel', 'complet' => 'Télétravail complet'];
|
||||
require __DIR__ . '/../src/i18n.php';
|
||||
require __DIR__ . '/../src/pays.php';
|
||||
require __DIR__ . '/../src/securite.php';
|
||||
require __DIR__ . '/../src/Import.php';
|
||||
require __DIR__ . '/../src/depot.php';
|
||||
require __DIR__ . '/../src/traduction.php';
|
||||
require __DIR__ . '/../src/europass.php';
|
||||
|
||||
// Copie de src/bootstrap.php, qui ne se charge pas sans configuration
|
||||
function date_fr(?string $date, bool $heure = false): string
|
||||
{
|
||||
return $date ? (new DateTimeImmutable($date))->format($heure ? 'd/m/Y H:i' : 'd/m/Y') : '';
|
||||
}
|
||||
|
||||
$ok = 0; $ko = 0;
|
||||
function verifier(string $nom, bool $cond): void {
|
||||
global $ok, $ko;
|
||||
if ($cond) { $ok++; } else { $ko++; echo "ECHEC : $nom\n"; }
|
||||
}
|
||||
|
||||
// --- Codes à usage unique : vecteurs de la RFC 6238 (SHA-1, clé « 12345678901234567890 »), six derniers chiffres
|
||||
$cle = Totp::base32('12345678901234567890');
|
||||
verifier('base32 : encodage de la clé de la RFC', $cle === 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ');
|
||||
verifier('base32 : aller-retour', Totp::base32Decoder($cle) === '12345678901234567890');
|
||||
verifier('base32 : espaces, tirets et minuscules ignorés', Totp::base32Decoder('gezd gnbv-gy3t qojq gezd gnbv gy3t qojq') === '12345678901234567890');
|
||||
verifier('base32 : caractère interdit', Totp::base32Decoder('GEZ1') === '');
|
||||
foreach ([59 => '287082', 1111111109 => '081804', 1111111111 => '050471', 1234567890 => '005924', 2000000000 => '279037', 20000000000 => '353130'] as $t => $attendu) {
|
||||
verifier("RFC 6238 : T = $t", Totp::code($cle, Totp::pas($t)) === $attendu);
|
||||
}
|
||||
$t = 1234567890;
|
||||
verifier('vérification : code courant accepté', Totp::verifier($cle, '005924', null, $t) === Totp::pas($t));
|
||||
verifier('vérification : espaces tolérés', Totp::verifier($cle, '005 924', null, $t) === Totp::pas($t));
|
||||
verifier('vérification : pas précédent accepté (horloge en retard)', Totp::verifier($cle, Totp::code($cle, Totp::pas($t) - 1), null, $t) === Totp::pas($t) - 1);
|
||||
verifier('vérification : pas suivant accepté (horloge en avance)', Totp::verifier($cle, Totp::code($cle, Totp::pas($t) + 1), null, $t) === Totp::pas($t) + 1);
|
||||
verifier('vérification : deux pas d\'écart refusés', Totp::verifier($cle, Totp::code($cle, Totp::pas($t) - 2), null, $t) === null);
|
||||
verifier('vérification : code rejoué refusé', Totp::verifier($cle, '005924', Totp::pas($t), $t) === null);
|
||||
verifier('vérification : code plus ancien que le dernier refusé', Totp::verifier($cle, Totp::code($cle, Totp::pas($t) - 1), Totp::pas($t), $t) === null);
|
||||
verifier('vérification : longueur fausse', Totp::verifier($cle, '05924', null, $t) === null && Totp::verifier($cle, '', null, $t) === null);
|
||||
$nouvelle = Totp::nouvelleCle();
|
||||
verifier('nouvelle clé : 32 caractères base32, 160 bits', strlen($nouvelle) === 32 && strlen(Totp::base32Decoder($nouvelle)) === 20);
|
||||
verifier('adresse otpauth', str_starts_with(Totp::adresse($cle, 'yann', 'Vivier'), 'otpauth://totp/Vivier%3Ayann?secret=' . $cle . '&issuer=Vivier'));
|
||||
verifier('clé lisible par groupes de 4', Totp::lisible('ABCDEFGH') === 'ABCD EFGH');
|
||||
|
||||
// --- Blocages : 5 échecs = 15 min, 10 = 30 min, 15 = 60 min ; adresse : 20 échecs dans l'heure
|
||||
$maintenant = 1_800_000_000;
|
||||
$serie = static fn (int $n, int $dernier): array => array_map(static fn ($i) => $dernier - ($n - 1 - $i) * 10, range(0, $n - 1));
|
||||
verifier('blocage : 4 échecs, pas de blocage', fin_blocage_compte($serie(4, $maintenant), $maintenant) === null);
|
||||
verifier('blocage : 5 échecs, 15 minutes', fin_blocage_compte($serie(5, $maintenant), $maintenant) === $maintenant + 900);
|
||||
verifier('blocage : levé après 15 minutes', fin_blocage_compte($serie(5, $maintenant - 901), $maintenant) === null);
|
||||
$m = array_merge($serie(5, $maintenant - 2000), $serie(5, $maintenant));
|
||||
verifier('blocage : 10 échecs, 30 minutes', fin_blocage_compte($m, $maintenant) === $maintenant + 1800);
|
||||
$m = array_merge($serie(10, $maintenant - 5000), $serie(5, $maintenant));
|
||||
verifier('blocage : 15 échecs, 60 minutes', fin_blocage_compte($m, $maintenant) === $maintenant + 3600);
|
||||
$m = array_merge($serie(15, $maintenant - 9000), $serie(5, $maintenant));
|
||||
verifier('blocage : au-delà, 60 minutes', fin_blocage_compte($m, $maintenant) === $maintenant + 3600);
|
||||
verifier('blocage : 6e échec après la fin du premier blocage, pas de nouveau blocage', fin_blocage_compte(array_merge($serie(5, $maintenant - 1000), [$maintenant]), $maintenant) === null);
|
||||
verifier('adresse : 19 échecs dans l\'heure, pas de blocage', fin_blocage_ip($serie(19, $maintenant), $maintenant) === null);
|
||||
$m = $serie(20, $maintenant);
|
||||
verifier('adresse : 20 échecs dans l\'heure, bloquée une heure après le premier', fin_blocage_ip($m, $maintenant) === $m[0] + 3600);
|
||||
verifier('adresse : échecs de plus d\'une heure ignorés', fin_blocage_ip($serie(20, $maintenant - 3700), $maintenant) === null);
|
||||
|
||||
// --- Réseau, navigateur
|
||||
verifier('réseau IPv4 : /24', reseau_ip('192.168.12.34') === '192.168.12.0/24');
|
||||
verifier('réseau IPv6 : /48', reseau_ip('2001:db8:abcd:12::1') === '2001:db8:abcd::/48');
|
||||
verifier('navigateur lisible', navigateur_lisible('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0 Safari/537.36') === 'Chrome, Windows');
|
||||
verifier('navigateur lisible : Edge sur Mac', navigateur_lisible('Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) Chrome/129.0 Safari/537.36 Edg/129.0') === 'Edge, macOS');
|
||||
|
||||
// --- Nationalités
|
||||
verifier('nationalités : codes ISO, doublons et inconnus écartés', codes_nationalites('fr, LU ;fr,XX') === ['FR', 'LU']);
|
||||
verifier('nationalités : 4 au plus', count(codes_nationalites(['FR', 'LU', 'BE', 'DE', 'IT'])) === 4);
|
||||
verifier('hors UE : aucune nationalité, inconnu', hors_ue([]) === null);
|
||||
verifier('hors UE : Union européenne', hors_ue(['FR']) === 0);
|
||||
verifier('hors UE : EEE et Suisse comptent comme UE', hors_ue(['NO']) === 0 && hors_ue(['CH']) === 0 && hors_ue(['IS']) === 0 && hors_ue(['LI']) === 0);
|
||||
verifier('hors UE : Maroc', hors_ue(['MA']) === 1);
|
||||
verifier('hors UE : double nationalité dont une européenne', hors_ue(['MA', 'FR']) === 0);
|
||||
verifier('hors UE : Royaume-Uni hors UE', hors_ue(['GB']) === 1);
|
||||
verifier('nom de pays : français et anglais', nom_pays('DE', 'fr') === 'Allemagne' && nom_pays('DE', 'en') === 'Germany');
|
||||
verifier('liste des pays : 27 pays de l\'UE plus EEE et Suisse', count(PAYS_LIBRE_CIRCULATION) === 31);
|
||||
|
||||
// --- Disponibilité
|
||||
$auj = new DateTimeImmutable('2026-10-15');
|
||||
verifier('disponibilité : date française', classer_disponibilite('Disponible le 01/12/2026', $auj) === ['code' => 'date', 'date' => '2026-12-01']);
|
||||
verifier('disponibilité : date ISO', classer_disponibilite('à partir du 2027-01-04', $auj) === ['code' => 'date', 'date' => '2027-01-04']);
|
||||
verifier('disponibilité : immédiate', classer_disponibilite('Immédiate', $auj)['code'] === 'immediate' && classer_disponibilite('Available now', $auj)['code'] === 'immediate');
|
||||
verifier('disponibilité : préavis de 2 mois', classer_disponibilite('Préavis de 2 mois', $auj) === ['code' => '2_mois', 'date' => '2026-12-15']);
|
||||
verifier('disponibilité : one month notice', classer_disponibilite('One month notice', $auj)['code'] === '1_mois');
|
||||
verifier('disponibilité : 3 mois', classer_disponibilite('trois mois de préavis', $auj)['code'] === '3_mois');
|
||||
verifier('disponibilité : 12 mois ne passe pas pour 2 mois', classer_disponibilite('12 mois', $auj)['code'] === 'a_preciser');
|
||||
verifier('disponibilité : texte flou', classer_disponibilite('à discuter', $auj) === ['code' => 'a_preciser', 'date' => null]);
|
||||
verifier('disponibilité : vide', classer_disponibilite(' ', $auj) === ['code' => null, 'date' => null]);
|
||||
verifier('libellé : sous 2 mois avec estimation', libelle_disponibilite(['disponibilite_code' => '2_mois', 'disponible_le' => '2026-12-15'], 'fr') === 'Sous 2 mois · vers le 15/12/2026');
|
||||
verifier('libellé : sans estimation sur un CV', libelle_disponibilite(['disponibilite_code' => '2_mois', 'disponible_le' => '2026-12-15'], 'en', false) === 'Within 2 months');
|
||||
verifier('libellé : date précise', libelle_disponibilite(['disponibilite_code' => 'date', 'disponible_le' => '2027-01-04'], 'de') === 'Ab 04/01/2027');
|
||||
verifier('libellé : aucun code', libelle_disponibilite(['disponibilite_code' => null, 'disponible_le' => null], 'fr') === '');
|
||||
|
||||
// --- Traduction
|
||||
verifier('langues du contenu : CV anglais', langues_du_contenu(['source_langue' => 'en', 'json_source' => '{"source":{"langue_cv":"en"}}']) !== []);
|
||||
$paquet = ['titre' => 'Développeur', 'experiences' => ['12' => ['poste' => 'Chef de projet', 'description' => 'Pilotage']], 'formations' => ['7' => 'Master']];
|
||||
[$fusion, $n] = fusionner_traduction($paquet, ['titre' => 'Developer', 'experiences' => ['12' => ['poste' => 'Project manager']], 'formations' => ['7' => ' '], 'pirate' => 'x']);
|
||||
verifier('fusion : textes traduits repris', $fusion['titre'] === 'Developer' && $fusion['experiences']['12']['poste'] === 'Project manager');
|
||||
verifier('fusion : texte manquant ou vide, original gardé', $fusion['experiences']['12']['description'] === 'Pilotage' && $fusion['formations']['7'] === 'Master');
|
||||
verifier('fusion : clé inconnue ignorée', !isset($fusion['pirate']));
|
||||
verifier('fusion : nombre de textes traduits', $n === 2);
|
||||
verifier('empreinte : les métiers ne comptent pas', empreinte_paquet($paquet) === empreinte_paquet($paquet + ['metiers' => ['développeur' => 'developer']]));
|
||||
verifier('empreinte : un texte changé la change', empreinte_paquet($paquet) !== empreinte_paquet(['titre' => 'Architecte'] + $paquet));
|
||||
|
||||
// --- Import au format 1.2
|
||||
$exemple = json_decode((string) file_get_contents(__DIR__ . '/../claude/exemple-profil.json'), true);
|
||||
verifier('import 1.2 : l\'exemple est valide', Import::valider($exemple) === []);
|
||||
$e = $exemple; $e['identite']['nationalites'] = ['FRA'];
|
||||
verifier('import 1.2 : nationalité sur trois lettres refusée', count(Import::valider($e)) === 1);
|
||||
$e = $exemple; $e['disponibilite_code'] = 'date'; $e['disponibilite_date'] = null;
|
||||
verifier('import 1.2 : date obligatoire pour « date »', count(Import::valider($e)) === 1);
|
||||
$e = $exemple; $e['disponibilite_code'] = 'bientot';
|
||||
verifier('import 1.2 : code de disponibilité inconnu', count(Import::valider($e)) === 1);
|
||||
$e = $exemple; $e['langues'][1]['cecrl'] = ['ecoute' => 'C1', 'lecture' => 'C2', 'production_orale' => 'B2', 'interaction_orale' => 'B2', 'ecriture' => 'C1'];
|
||||
verifier('import 1.2 : grille européenne acceptée', Import::valider($e) === []);
|
||||
$e['langues'][1]['cecrl']['ecoute'] = 'D1';
|
||||
verifier('import 1.2 : niveau de grille inconnu refusé', count(Import::valider($e)) === 1);
|
||||
$e = $exemple; $e['schema_version'] = '1.1'; unset($e['identite']['nationalites'], $e['disponibilite_code'], $e['disponibilite_date'], $e['benevolat'], $e['permis_conduire']);
|
||||
foreach ($e['experiences'] as &$x) { unset($x['projet'], $x['mission'], $x['charge']); } unset($x);
|
||||
foreach ($e['formations'] as &$f) { unset($f['date_debut'], $f['date_fin'], $f['lieu']); } unset($f);
|
||||
foreach ($e['langues'] as &$l) { unset($l['cecrl']); } unset($l);
|
||||
verifier('import 1.1 : toujours accepté', Import::valider($e) === []);
|
||||
$colonnes = static fn (array $p, DateTimeImmutable $d): array => (new ReflectionMethod(Import::class, 'colonnes'))->invoke(null, $p, $d);
|
||||
$col = $colonnes($exemple, $auj);
|
||||
verifier('import 1.2 : colonnes de nationalité', $col['nationalites'] === 'FR' && $col['hors_ue'] === 0);
|
||||
verifier('import 1.2 : disponibilité classée et date estimée', $col['disponibilite_code'] === '2_mois' && $col['disponible_le'] === '2026-12-15');
|
||||
verifier('import 1.2 : bénévolat et permis', str_contains((string) $col['benevolat'], 'ateliers') && $col['permis_conduire'] === 'B');
|
||||
$col = $colonnes($e, $auj);
|
||||
verifier('import 1.1 : disponibilité classée depuis le texte', $col['disponibilite_code'] === '2_mois' && $col['nationalites'] === null && $col['hors_ue'] === null);
|
||||
|
||||
// --- CV Europass
|
||||
$c = [
|
||||
'id' => 3, 'prenom' => 'Camille', 'nom' => 'EXEMPLE', 'titre' => 'Développeuse Java', 'resume' => "Camille conçoit des API.\nDix ans d'expérience.",
|
||||
'email' => 'camille@example.com', 'telephone' => '+352 1', 'rue' => '1 rue X', 'code_postal' => 'L-1000', 'ville' => 'Luxembourg', 'pays' => 'Luxembourg',
|
||||
'photo' => 'abc.jpg', 'liens' => [['type' => 'linkedin', 'url' => 'https://linkedin.com/in/c']], 'nationalites' => 'MA', 'permis_travail' => 'Titre salarié',
|
||||
'benevolat' => "Ateliers de code\nMaraudes", 'permis_conduire' => 'B, A',
|
||||
'experiences' => [['id' => 1, 'poste' => 'Développeuse', 'employeur' => 'ACME', 'client' => 'Banque X', 'lieu' => 'Luxembourg', 'date_debut' => '2020-03', 'date_fin' => null,
|
||||
'description' => "- Conçoit des API\n- Revue de code", 'technologies' => 'Java, Spring', 'projet' => 'Refonte', 'mission' => 'Développement back-end', 'charge' => '100 %']],
|
||||
'formations' => [['id' => 1, 'diplome' => 'Master Informatique', 'etablissement' => 'Université', 'annee_fin' => 2017, 'date_debut' => '2015-09', 'date_fin' => '2017-06', 'lieu' => 'Metz']],
|
||||
'certifications' => [['id' => 1, 'nom' => 'PSM I']], 'technologies' => [['nom' => 'Java', 'categorie' => 'langage'], ['nom' => 'PostgreSQL', 'categorie' => 'base_de_donnees']],
|
||||
'langues' => [['langue' => 'Français', 'niveau' => 'natif'], ['langue' => 'Anglais', 'niveau' => 'C1', 'ecoute' => null, 'lecture' => null, 'production_orale' => null, 'interaction_orale' => null, 'ecriture' => null]],
|
||||
];
|
||||
$d = donnees_europass($c, ['photo' => true, 'coordonnees' => true], 'fr');
|
||||
verifier('Europass : nom et coordonnées', $d['nom'] === 'Camille EXEMPLE' && count($d['coordonnees']) === 4 && $d['photo'] === 'abc.jpg');
|
||||
verifier('Europass : ni nationalité ni permis de travail', !str_contains(json_encode($d, JSON_UNESCAPED_UNICODE), 'Titre salarié') && !str_contains(json_encode($d), '"MA"'));
|
||||
verifier('Europass : dates au format mm/aaaa', $d['experiences'][0]['dates'] === '03/2020 – EN COURS' && $d['formations'][0]['dates'] === '09/2015 – 06/2017');
|
||||
verifier('Europass : projet, mission, charge', array_column($d['experiences'][0]['details'], 1) === ['Banque X', 'Refonte', 'Développement back-end', 'Java, Spring', '100 %']);
|
||||
verifier('Europass : réalisations sans puces', $d['experiences'][0]['realisations'] === ['Conçoit des API', 'Revue de code']);
|
||||
verifier('Europass : langue sans grille, niveau global', $d['avec_grille'] === false && $d['autres'][0]['niveau'] === 'C1' && $d['maternelles'] === ['FRANÇAIS']);
|
||||
verifier('Europass : compétences groupées', array_keys($d['competences']) === ['Certifications', 'Technique', 'Bases de données']);
|
||||
verifier('Europass : bénévolat et permis', $d['benevolat'] === ['Ateliers de code', 'Maraudes'] && $d['permis'] === ['B', 'A']);
|
||||
$a = donnees_europass($c, ['photo' => true, 'coordonnees' => true, 'anonyme' => true], 'en');
|
||||
verifier('Europass anonymisé : ni nom, ni photo, ni coordonnées', $a['nom'] === 'Profile ITC-0003' && $a['photo'] === null && $a['coordonnees'] === []);
|
||||
verifier('Europass anonymisé : prénom retiré des textes', !str_contains(implode(' ', $a['resume']), 'Camille'));
|
||||
verifier('Europass en anglais : intitulés', $a['libelles']['experience'] === 'Work experience' && $a['libelles']['grille']['ecoute'] === 'Listening');
|
||||
$c['langues'][1] = ['langue' => 'Anglais', 'niveau' => 'C1', 'ecoute' => 'C1', 'lecture' => 'C2', 'production_orale' => 'B2', 'interaction_orale' => 'B2', 'ecriture' => 'C1'];
|
||||
$g = donnees_europass($c, [], 'de');
|
||||
verifier('Europass : grille européenne', $g['avec_grille'] && $g['autres'][0]['grille']['lecture'] === 'C2' && $g['libelles']['langues'] === 'Sprachkenntnisse');
|
||||
verifier('Europass : sans coordonnées ni photo par défaut', $g['coordonnees'] === [] && $g['photo'] === null);
|
||||
|
||||
echo "$ok réussis, $ko échoués\n";
|
||||
exit($ko > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
// Tests du rapprochement, des doublons, de l'appariement du lot et de l'email de sauvegarde : php tests/test_suivi.php
|
||||
declare(strict_types=1);
|
||||
const VIVIER_RACINE = __DIR__ . '/..';
|
||||
const SENIORITES = ['junior' => '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 <vivier@itclub.lu>') && 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);
|
||||
Reference in new issue
Block a user