From f45bdc4e5a5510b2e7ac9d97f457be27ffac7ac9 Mon Sep 17 00:00:00 2001 From: tristan Date: Fri, 29 May 2026 15:12:20 +0200 Subject: [PATCH] test(arch) : garde-fou CI 'collections paginees obligatoires' --- .../CollectionsArePaginatedTest.php | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 tests/Architecture/CollectionsArePaginatedTest.php diff --git a/tests/Architecture/CollectionsArePaginatedTest.php b/tests/Architecture/CollectionsArePaginatedTest.php new file mode 100644 index 0000000..54c21a7 --- /dev/null +++ b/tests/Architecture/CollectionsArePaginatedTest.php @@ -0,0 +1,126 @@ + 'justification + reference ticket/spec'` + * + * @internal + */ +final class CollectionsArePaginatedTest extends TestCase +{ + /** + * Resources API Platform dont un `GetCollection` peut desactiver la pagination. + * + * Laisser vide au demarrage. Pour ajouter une exception : + * 'App\Module\Foo\Infrastructure\ApiPlatform\Resource\BarResource' + * => 'Referentiel statique < 50 items (types de contrat). Cf. ERP-XX.', + * + * @var array + */ + private const EXCLUDED = []; + + public function testAllGetCollectionOperationsHavePaginationEnabled(): void + { + $finder = new Finder() + ->files() + ->in(__DIR__.'/../../src') + ->name('*.php') + ->contains('#[ApiResource') + ; + + // Garde : si le scan ne trouve rien, le chemin est casse — le test + // deviendrait un faux positif vert. On verifie qu'il a du grain a moudre. + self::assertNotEmpty( + iterator_to_array($finder), + 'Aucun fichier #[ApiResource] trouve sous src/ : chemin invalide ou codebase vide.', + ); + + foreach ($finder as $file) { + $fqcn = $this->extractFqcn($file->getRealPath()); + if (null === $fqcn || !class_exists($fqcn)) { + continue; + } + + $reflection = new ReflectionClass($fqcn); + $apiResourceAttributes = $reflection->getAttributes(ApiResource::class); + + if ([] === $apiResourceAttributes) { + continue; + } + + foreach ($apiResourceAttributes as $attribute) { + /** @var ApiResource $apiResource */ + $apiResource = $attribute->newInstance(); + $operations = $apiResource->getOperations()?->getIterator() ?? []; + + foreach ($operations as $operation) { + if (!$operation instanceof GetCollection) { + continue; + } + + if (false !== $operation->getPaginationEnabled()) { + continue; + } + + // La pagination est explicitement desactivee : verifier + // que la resource est dans la whitelist EXCLUDED. + self::assertArrayHasKey( + $fqcn, + self::EXCLUDED, + sprintf( + "La resource %s desactive la pagination sur une operation GetCollection.\n" + ."Regle : toute collection API Platform doit etre paginee (cf. .claude/rules/backend.md).\n" + ."Si cette collection est structurellement bornee et que la desactivation est justifiee,\n" + .'ajouter une entree dans CollectionsArePaginatedTest::EXCLUDED avec une justification.', + $fqcn, + ), + ); + } + } + } + } + + /** + * Extrait le FQCN (namespace + classe) d'un fichier PHP par lecture du + * source, sans charger le fichier. + */ + private function extractFqcn(string $path): ?string + { + $source = file_get_contents($path); + if (false === $source) { + return null; + } + + if ( + 1 !== preg_match('/^namespace\s+([^;]+);/m', $source, $nsMatch) + || 1 !== preg_match('/^(?:final\s+|abstract\s+|readonly\s+)*class\s+(\w+)/m', $source, $classMatch) + ) { + return null; + } + + return trim($nsMatch[1]).'\\'.$classMatch[1]; + } +}