parser -> DBAL). * * @internal */ final class SyncIdtfCommandTest extends KernelTestCase { private Connection $connection; protected function setUp(): void { self::bootKernel(); /** @var Connection $connection */ $connection = self::getContainer()->get('doctrine.dbal.default_connection'); $this->connection = $connection; $this->purge(); } protected function tearDown(): void { $this->purge(); parent::tearDown(); } public function testSyncParsesXlsxUpsertsAndLogs(): void { $path = $this->makeXlsx([ ['Export date: 12-6-2026'], ['Avertissement preambule'], [], ['Numéro IDTF', 'Product Group', 'Nom de la marchandise', 'Régime de nettoyage', 'Exigences importantes', 'Date d’application obligatoire', 'Produits apparentés', 'Formule', 'Code EURAL', 'Numéro CAS', 'Annotations'], ['30748', 'Inorganiques', 'Argiles régime C', 'C', 'Exig X', '02-04-2026', 'Poudre', 'Al2O3', '01 01 01', '7631-86-9 ; 1344-28-1', 'Note'], ['', '', '', '', '', '', '', '', '', '', ''], ['30744', '', 'Additifs', 'A', '', '', '', '', '', '', ''], ]); $tester = $this->runSync($path); $tester->assertCommandIsSuccessful(); self::assertSame(2, $this->countRows('SELECT COUNT(*) FROM idtf_product')); $row = $this->connection->fetchAssociative("SELECT * FROM idtf_product WHERE idtf_number = 30748 AND schema = 'road'"); self::assertNotFalse($row); self::assertSame('Argiles régime C', $row['name']); self::assertSame('C', $row['cleaning_regime']); self::assertSame('2026-04-02', $row['mandatory_date']); self::assertSame('2026-06-12', $row['source_export_date']); self::assertSame(['7631-86-9', '1344-28-1'], json_decode((string) $row['cas_numbers'], true)); $log = $this->connection->fetchAssociative('SELECT * FROM idtf_sync_log ORDER BY id DESC LIMIT 1'); self::assertNotFalse($log); self::assertSame('road', $log['schema']); self::assertSame('2026-06-12', $log['export_date']); self::assertSame(2, (int) $log['rows_total']); self::assertSame(2, (int) $log['rows_upserted']); self::assertSame(0, (int) $log['rows_deactivated']); } public function testSecondSyncSoftDeletesMissing(): void { $header = ['Numéro IDTF', 'Nom de la marchandise', 'Régime de nettoyage']; $this->runSync($this->makeXlsx([ ['Export date: 1-6-2026'], $header, ['100', 'Produit 100', 'A'], ['200', 'Produit 200', 'B'], ]))->assertCommandIsSuccessful(); self::assertSame(2, $this->countRows('SELECT COUNT(*) FROM idtf_product WHERE is_active = TRUE')); // 2e export sans 200 -> soft-delete de 200, mise a jour de 100. $tester = $this->runSync($this->makeXlsx([ ['Export date: 2-6-2026'], $header, ['100', 'Produit 100 maj', 'C'], ])); $tester->assertCommandIsSuccessful(); self::assertSame(2, $this->countRows('SELECT COUNT(*) FROM idtf_product')); self::assertSame(1, $this->countRows('SELECT COUNT(*) FROM idtf_product WHERE is_active = TRUE')); self::assertSame(1, $this->countRows('SELECT COUNT(*) FROM idtf_product WHERE idtf_number = 200 AND is_active = FALSE')); $row100 = $this->connection->fetchAssociative('SELECT * FROM idtf_product WHERE idtf_number = 100'); self::assertNotFalse($row100); self::assertSame('Produit 100 maj', $row100['name']); self::assertSame('C', $row100['cleaning_regime']); $log = $this->connection->fetchAssociative('SELECT * FROM idtf_sync_log ORDER BY id DESC LIMIT 1'); self::assertNotFalse($log); self::assertSame(1, (int) $log['rows_upserted']); self::assertSame(1, (int) $log['rows_deactivated']); } public function testInvalidSchemaIsRejected(): void { $path = $this->makeXlsx([ ['Numéro IDTF', 'Nom de la marchandise', 'Régime de nettoyage'], ['1', 'X', 'A'], ]); $application = new Application(self::$kernel); $tester = new CommandTester($application->find('app:idtf:sync')); $exitCode = $tester->execute(['--file' => $path, '--schema' => 'air']); @unlink($path); self::assertSame(2, $exitCode); // Command::INVALID self::assertSame(0, $this->countRows('SELECT COUNT(*) FROM idtf_product')); } /** * @param array> $matrix */ private function makeXlsx(array $matrix): string { $spreadsheet = new Spreadsheet(); $spreadsheet->getActiveSheet()->fromArray($matrix, null, 'A1', true); $path = tempnam(sys_get_temp_dir(), 'idtf_').'.xlsx'; new Xlsx($spreadsheet)->save($path); $spreadsheet->disconnectWorksheets(); return $path; } private function runSync(string $path): CommandTester { $application = new Application(self::$kernel); $tester = new CommandTester($application->find('app:idtf:sync')); $tester->execute(['--file' => $path, '--schema' => 'road']); @unlink($path); return $tester; } private function countRows(string $sql): int { return (int) $this->connection->fetchOne($sql); } private function purge(): void { $this->connection->executeStatement('DELETE FROM idtf_product'); $this->connection->executeStatement('DELETE FROM idtf_sync_log'); } }