From a6d7a36b06aef1c12985bf9d197800890f6ee6e0 Mon Sep 17 00:00:00 2001 From: Ferror Date: Thu, 21 Dec 2023 23:17:39 +0100 Subject: [PATCH] Initial Commit --- .gitattributes | 6 ++ .github/workflows/ci.yml | 36 ++++++++++ .gitignore | 3 + Dockerfile | 15 +++++ Makefile | 28 ++++++++ composer.json | 34 ++++++++++ docker-compose.yaml | 16 +++++ phpstan.neon | 11 ++++ phpunit.xml | 34 ++++++++++ src/Symfony/Bundle.php | 38 +++++++++++ src/Symfony/Console/CheckCoverageCommand.php | 65 +++++++++++++++++++ .../Integration/CheckCoverageCommandTest.php | 26 ++++++++ tests/Integration/Service/Kernel.php | 22 +++++++ tests/Integration/Service/config/library.yaml | 3 + 14 files changed, 337 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 composer.json create mode 100644 docker-compose.yaml create mode 100644 phpstan.neon create mode 100644 phpunit.xml create mode 100644 src/Symfony/Bundle.php create mode 100644 src/Symfony/Console/CheckCoverageCommand.php create mode 100644 tests/Integration/CheckCoverageCommandTest.php create mode 100644 tests/Integration/Service/Kernel.php create mode 100644 tests/Integration/Service/config/library.yaml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8478769 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/.github export-ignore +/tests export-ignore +.gitignore export-ignore +phpstan.neon export-ignore +phpunit.xml export-ignore +README.md export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..096de8c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: ~ + workflow_dispatch: ~ + +jobs: + tests: + runs-on: ubuntu-latest + + steps: + - # Copies the repository files to the Action Runner + name: Checkout Repository + uses: actions/checkout@v3 + + - # Installs PHP and other necessary tools + name: Setup PHP + uses: shivammathur/setup-php@2.25.4 + with: + php-version: 8.3 + + - # Installs and caches PHP dependencies + name: Install Dependencies + uses: ramsey/composer-install@2.2.0 + + - # Validates composer.json structure and required fields + name: Validate composer.json + run: composer validate --ansi --strict --no-check-publish + + - # Runs code quality tools, like phpstan etc. + name: Run Code Quality Tools + run: composer analyse + + - # Runs unit and integration tests, like phpspec, phpunit etc. + name: Run Tests + run: composer test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6408043 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/var/ +/vendor/ +/composer.lock diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f73f7ec --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +ARG COMPOSER_VERSION +ARG PHP_VERSION +ARG EXTENSION_INSTALLER_VERSION + +FROM composer:${COMPOSER_VERSION} AS composer +FROM mlocati/php-extension-installer:${EXTENSION_INSTALLER_VERSION} AS extensions + +FROM php:${PHP_VERSION}-cli-alpine AS php + +COPY --from=composer /usr/bin/composer /usr/bin/composer +COPY --from=extensions /usr/bin/install-php-extensions /usr/bin/install-php-extensions + +RUN install-php-extensions xdebug + +WORKDIR /app/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0ca47a1 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +APP := @docker compose run --rm app + +.PHONY: +ci: analyse test + +.PHONY: +install: + $(APP) composer install + +.PHONY: +update: + $(APP) composer update + +.PHONY: +analyse: + $(APP) composer analyse + +.PHONY: +test: + $(APP) composer test + +.PHONY: +coverage: + $(APP) composer coverage + +.PHONY: +sh: + $(APP) sh diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e8f4ac6 --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "ferror/openapi-coverage", + "type": "library", + "license": "MIT", + "require": { + "php": "^8.2", + "nelmio/api-doc-bundle": "^4.12", + "symfony/console": "^6.4", + "symfony/dependency-injection": "^6.4", + "symfony/http-kernel": "^6.4", + "symfony/routing": "^6.4" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-symfony": "^1.3", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^10.5" + }, + "autoload": { + "psr-4": { + "Ferror\\OpenapiCoverage\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Ferror\\OpenapiCoverage\\": "tests/" + } + }, + "scripts": { + "test": "vendor/bin/phpunit", + "coverage": "vendor/bin/phpunit --coverage-html=var/coverage", + "analyse": "vendor/bin/phpstan analyse" + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..e215ca9 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,16 @@ +services: + app: + build: + context: . + target: php + args: + PHP_VERSION: 8.2 + COMPOSER_VERSION: 2.5 + EXTENSION_INSTALLER_VERSION: 2.1 + command: ["composer", "install", "--no-interaction"] + environment: + XDEBUG_MODE: coverage +# XDEBUG_MODE: debug +# XDEBUG_CONFIG: "profiler_enable=on idekey=PHPSTORM client_host=host.docker.internal" + volumes: + - ./:/app:delegate \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..ffca23c --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,11 @@ +includes: + - vendor/phpstan/phpstan-symfony/extension.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-symfony/rules.neon + - vendor/phpstan/phpstan-phpunit/rules.neon + +parameters: + level: 6 + paths: + - src + - tests diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..d95da42 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,34 @@ + + + + + tests + + + + + + + + + + src + + + diff --git a/src/Symfony/Bundle.php b/src/Symfony/Bundle.php new file mode 100644 index 0000000..d2a3e05 --- /dev/null +++ b/src/Symfony/Bundle.php @@ -0,0 +1,38 @@ +register('ferror.openapi_coverage.console', CheckCoverageCommand::class) + ->addArgument(new Reference('router')) + ->addArgument(new Reference('nelmio_api_doc.render_docs')) + ->addTag('console.command') + ; + } + + public function configure(DefinitionConfigurator $definition): void + { + $definition->rootNode() + ->children() + ->arrayNode('excluded_routes') + ->scalarPrototype()->end() + ->end() + ->end() + ; + } +} diff --git a/src/Symfony/Console/CheckCoverageCommand.php b/src/Symfony/Console/CheckCoverageCommand.php new file mode 100644 index 0000000..28c1a73 --- /dev/null +++ b/src/Symfony/Console/CheckCoverageCommand.php @@ -0,0 +1,65 @@ +setDescription('Check OpenAPI coverage against Symfony routes') + ->addArgument('openapi-schema', InputArgument::OPTIONAL, 'Path to OpenAPI schema YAML file') + ; + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $symfonyPaths = $this->router->getRouteCollection(); + $symfonyPaths = array_map(fn (Route $route) => $route->getPath(), $symfonyPaths->all()); + $symfonyPaths = array_values($symfonyPaths); + $symfonyPaths = array_filter($symfonyPaths, fn (string $route) => str_starts_with($route, '/risk/v1')); + $symfonyPaths = array_map(fn (string $route) => str_replace('/risk/v1', '', $route), $symfonyPaths); + $symfonyPaths = array_filter($symfonyPaths, fn(string $route) => !in_array($route, $this->excludedPaths, true)); + + $openApi = $this->renderOpenApi->render('json', 'default'); + $openApi = json_decode($openApi, true, 512, JSON_THROW_ON_ERROR); + + $openApiPaths = array_keys($openApi['paths']); + + $missingPaths = array_diff($symfonyPaths, $openApiPaths); + + $missingPathCoverage = count($openApiPaths) / count($symfonyPaths); + $output->writeln('Open API coverage: ' . round($missingPathCoverage * 100, 2) . '%'); + + if (empty($missingPaths)) { + $output->writeln('OpenAPI schema covers all Symfony routes. Good job!'); + } else { + $output->writeln('Missing paths in OpenAPI schema:'); + foreach ($missingPaths as $path) { + $output->writeln("- $path"); + } + } + + return Command::SUCCESS; + } +} diff --git a/tests/Integration/CheckCoverageCommandTest.php b/tests/Integration/CheckCoverageCommandTest.php new file mode 100644 index 0000000..e3a47e3 --- /dev/null +++ b/tests/Integration/CheckCoverageCommandTest.php @@ -0,0 +1,26 @@ +find('ferror:check-openapi-coverage'); + $commandTester = new CommandTester($command); + $commandTester->execute([]); + + $commandTester->assertCommandIsSuccessful(); + + $display = $commandTester->getDisplay(); + } +} diff --git a/tests/Integration/Service/Kernel.php b/tests/Integration/Service/Kernel.php new file mode 100644 index 0000000..95a4be0 --- /dev/null +++ b/tests/Integration/Service/Kernel.php @@ -0,0 +1,22 @@ +load(__DIR__ . '/config/library.yaml'); + } +} diff --git a/tests/Integration/Service/config/library.yaml b/tests/Integration/Service/config/library.yaml new file mode 100644 index 0000000..d35b45a --- /dev/null +++ b/tests/Integration/Service/config/library.yaml @@ -0,0 +1,3 @@ +ferror_openapi_coverage: + excluded_routes: + - rest-docs.json