diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f3a574e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: Symfony + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + +permissions: + contents: read + +jobs: + symfony-tests: + runs-on: ubuntu-latest + steps: + - uses: shivammathur/setup-php@v2 + with: + php-version: "8.2" + - uses: actions/checkout@v4 + - name: Copy .env.test.local + run: php -r "file_exists('.env.test.local') || copy('.env.test', '.env.test.local');" + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + - name: Install Dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + - name: Execute tests (Unit and Feature tests) via PHPUnit + run: vendor/bin/phpunit tests diff --git a/.gitignore b/.gitignore index 57872d0..3a9875b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /vendor/ +composer.lock diff --git a/composer.json b/composer.json index 3ecd49f..0f6d47b 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,17 @@ "require-dev": { "phpstan/phpstan": "^1.8", "friendsofphp/php-cs-fixer": "^3.9", - "api-platform/symfony": "^3.0" + "api-platform/symfony": "^3.0", + "phpunit/phpunit": "^10", + "nyholm/symfony-bundle-test": "^2.0", + "symfony/expression-language": "^6.3", + "doctrine/doctrine-bundle": "^2.10", + "doctrine/orm": "^2.16", + "symfony/security-bundle": "^6.3", + "phpdocumentor/reflection-docblock": "^5.3", + "phpstan/phpdoc-parser": "^1.24", + "symfony/property-access": "^6.3", + "symfony/property-info": "^6.3", + "symfony/serializer": "^6.3" } } diff --git a/tests/BundleInitializationTest.php b/tests/BundleInitializationTest.php new file mode 100644 index 0000000..eccb517 --- /dev/null +++ b/tests/BundleInitializationTest.php @@ -0,0 +1,106 @@ +assertTrue($container->has('theodo_accent.access_control_checker_command')); + $service = $container->get('theodo_accent.access_control_checker_command'); + $this->assertInstanceOf(Command::class, $service); + } + + public function testExposedRoutes(): void + { + $container = self::getContainer(); + + $this->assertTrue($container->has('theodo_accent.accent_report_factory')); + + /** @var AccentReportFactory $reportFactory */ + $reportFactory = $container->get('theodo_accent.accent_report_factory'); + $accentReport = $reportFactory->createAccentReport(); + + // 3 from API Platform _api_validation_errors routes + 2 from the Book ressource + // entrypoint and docs are present and not protected (by default). + // 3 routes for API Platform _api_validation_errors is not protected either. + $this->assertEquals(5, $accentReport->getUnprotectedRoutesCount()); + + $resourceRelatedRoutes = array_filter( + $accentReport->getRouteAccessControlList(), + fn ($route) => RouteAccessControlData::RESOURCE_UNRELATED_ROUTE !== $route->getExpression() + // Excluding the 3 _api_validation_errors_* routes that are generated by API Platform but not really exposed + // (trying to GET them generates an NotExposedHttpException => 404) + && !preg_match('/_api_validation_errors_*/', $route->getRouteName()) + ); + + $this->assertCount(3, $resourceRelatedRoutes); + $checkedRoutes = []; + foreach ($resourceRelatedRoutes as $routeData) { + $checkedRoutes[$routeData->getRouteName()] = $routeData->getExpression(); + } + + $expectedRoutes = [ + '_api_/books/{id}{._format}_get' => "is_granted('ROLE_USER_GET')", + '_api_/books{._format}_post' => "is_granted('ROLE_USER_DEFAULT')", + '_api_/books/{id}{._format}_patch' => "is_granted('ROLE_USER_PATCH')", + ]; + + $this->assertEquals($expectedRoutes, $checkedRoutes); + } + + protected static function getKernelClass(): string + { + return TestKernel::class; + } + + /** + * @param array{environment?: string, debug?: string} $options + */ + protected static function createKernel(array $options = []): KernelInterface + { + /** + * @var TestKernel $kernel + */ + $kernel = parent::createKernel($options); + $kernel->addTestBundle(DoctrineBundle::class); + $kernel->addTestBundle(ApiPlatformBundle::class); + $kernel->addTestBundle(TheodoAccentBundle::class); + $kernel->addTestConfig(__DIR__.'/config/packages/doctrine.yaml'); + $kernel->addTestConfig(__DIR__.'/config/packages/api_platform.yaml'); + $kernel->addTestRoutingFile(__DIR__.'/config/routes.yaml'); + + // Unused, non public services are removed during kernel boot. + // We force them as public during test. + $kernel->addTestCompilerPass(new class implements CompilerPassInterface { + public function process(ContainerBuilder $container): void + { + $services = $container->getDefinitions() + $container->getAliases(); + + foreach ($services as $id => $definition) { + if (0 === mb_stripos($id, 'theodo_accent')) { + $definition->setPublic(true); + } + } + } + }); + + $kernel->handleOptions($options); + + return $kernel; + } +} diff --git a/tests/Entity/Book.php b/tests/Entity/Book.php new file mode 100644 index 0000000..c9ea925 --- /dev/null +++ b/tests/Entity/Book.php @@ -0,0 +1,23 @@ +