createAdminClient(); $this->seedClient('Export Alpha'); $response = $client->request('GET', self::EXPORT_URL); self::assertResponseIsSuccessful(); $headers = $response->getHeaders(false); self::assertStringContainsString(self::XLSX_MIME, $headers['content-type'][0] ?? ''); $disposition = $headers['content-disposition'][0] ?? ''; self::assertStringContainsString('attachment; filename="repertoire-clients-', $disposition); self::assertMatchesRegularExpression( '/filename="repertoire-clients-\d{8}\.xlsx"/', $disposition, ); // Le binaire est un XLSX relisible dont la 1re ligne porte les en-tetes. $grid = $this->gridFromResponse($response->getContent()); $headers = $grid[0]; self::assertSame('Nom entreprise', $headers[0]); self::assertContains('Catégories', $headers); self::assertContains('Sites', $headers); self::assertContains('Date de création', $headers); } public function testExportExcludesArchivedByDefault(): void { $client = $this->createAdminClient(); $this->seedClient('Active One'); $this->seedClient('Archived One', true); $names = $this->companyNames($client->request('GET', self::EXPORT_URL)->getContent()); self::assertContains('ACTIVE ONE', $names); self::assertNotContains('ARCHIVED ONE', $names); } public function testExportRespectsSearchFilter(): void { $client = $this->createAdminClient(); $this->seedClient('Searchable Alpha'); $this->seedClient('Other Beta'); $names = $this->companyNames( $client->request('GET', self::EXPORT_URL.'?search=alpha')->getContent(), ); self::assertContains('SEARCHABLE ALPHA', $names); self::assertNotContains('OTHER BETA', $names); } public function testExportRespectsCategoryTypeFilter(): void { $client = $this->createAdminClient(); $this->seedClient('Distrib Co', false, 'DISTRIBUTEUR'); $this->seedClient('Secteur Co', false, 'SECTEUR'); $names = $this->companyNames( $client->request('GET', self::EXPORT_URL.'?categoryType=DISTRIBUTEUR')->getContent(), ); self::assertContains('DISTRIB CO', $names); self::assertNotContains('SECTEUR CO', $names); } public function testSirenColumnPresentWithAccountingView(): void { // L'admin bypass le RBAC : il a donc accounting.view -> colonne SIREN. $client = $this->createAdminClient(); $seed = $this->seedClient('Siren Co'); $em = $this->getEm(); $seed->setSiren('123456789'); $em->flush(); $grid = $this->gridFromResponse($client->request('GET', self::EXPORT_URL)->getContent()); self::assertContains('SIREN', $grid[0]); self::assertStringContainsString('123456789', $this->flatten($grid)); } public function testSirenColumnAbsentWithoutAccountingView(): void { // Seed via admin, puis relecture par un user qui n'a QUE clients.view. $admin = $this->createAdminClient(); $seed = $this->seedClient('No Siren Co'); $em = $this->getEm(); $seed->setSiren('987654321'); $em->flush(); $creds = $this->createUserWithPermission('commercial.clients.view'); $viewer = $this->authenticatedClient($creds['username'], $creds['password']); $grid = $this->gridFromResponse($viewer->request('GET', self::EXPORT_URL)->getContent()); self::assertNotContains('SIREN', $grid[0]); self::assertStringNotContainsString('987654321', $this->flatten($grid)); } public function testForbiddenWithoutClientsViewPermission(): void { $creds = $this->createUserWithPermission('core.users.view'); $client = $this->authenticatedClient($creds['username'], $creds['password']); $client->request('GET', self::EXPORT_URL); self::assertResponseStatusCodeSame(403); } public function testUnauthorizedWhenAnonymous(): void { $client = self::createClient(); $client->request('GET', self::EXPORT_URL); self::assertResponseStatusCodeSame(401); } /** * Relit le binaire XLSX d'une reponse et renvoie la grille de cellules. * * @return array> */ private function gridFromResponse(string $binary): array { $tmp = tempnam(sys_get_temp_dir(), 'xlsx_export_test_'); self::assertIsString($tmp); file_put_contents($tmp, $binary); try { return IOFactory::load($tmp)->getActiveSheet()->toArray(); } finally { @unlink($tmp); } } /** * Extrait la colonne « Nom entreprise » (1re colonne) des lignes de donnees. * * @return list */ private function companyNames(string $binary): array { $grid = $this->gridFromResponse($binary); $rows = array_slice($grid, 1); // saute l'en-tete return array_values(array_map(static fn (array $row): string => (string) ($row[0] ?? ''), $rows)); } /** * Aplatit toute la grille en une chaine, pour les assertions de presence. * * @param array> $grid */ private function flatten(array $grid): string { return implode('|', array_map( static fn (array $row): string => implode('|', array_map(static fn ($cell): string => (string) $cell, $row)), $grid, )); } }