173 lines
16 KiB
PHP
173 lines
16 KiB
PHP
<?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);
|