diff --git a/.github/workflows/cs-tests.yml b/.github/workflows/cs-tests.yml new file mode 100644 index 0000000..3da9965 --- /dev/null +++ b/.github/workflows/cs-tests.yml @@ -0,0 +1,46 @@ +on: + - push + +name: Run phpcs checks + +jobs: + mutation: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.1" + - "8.2" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: composer:v2, cs2pr + coverage: none + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + - name: Install dependencies with composer + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run phpcs checks + run: vendor/bin/phpcs diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..74550fc --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,46 @@ +on: + - push + +name: Run static analysis + +jobs: + mutation: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.1" + - "8.2" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: composer:v2, cs2pr + coverage: none + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + - name: Install dependencies with composer + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run static analysis + run: vendor/bin/psalm --no-cache --output-format=github --show-info=false --threads=4 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..d2ab8e7 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,47 @@ +on: + - push + +name: Run PHPUnit tests + +jobs: + mutation: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.1" + - "8.2" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: composer:v2, cs2pr + coverage: none + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + + - name: Install dependencies with composer + run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run PHPUnit tests + run: vendor/bin/phpunit --colors=always diff --git a/.gitignore b/.gitignore index 485dee6..948af93 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ +composer.phar +/vendor/ +.phpcs-cache .idea +composer.lock +.phpunit.result.cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 3486233..d3870ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,4 +31,4 @@ * Nothing ### Fixed -* Nothing \ No newline at end of file +* Nothing diff --git a/README.md b/README.md index 366be66..9adf98a 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,23 @@ # dot-response-header ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-response-header) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-response-header/3.1.0) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-response-header/3.2.0) [![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-response-header)](https://github.com/dotkernel/dot-response-header/issues) [![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-response-header)](https://github.com/dotkernel/dot-response-header/network) [![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-response-header)](https://github.com/dotkernel/dot-response-header/stargazers) -[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-response-header)](https://github.com/dotkernel/response-header/LICENSE.md) +[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-response-header)](https://github.com/dotkernel/dot-response-header/blob/3.0/LICENSE) + +[![Build Static](https://github.com/dotkernel/dot-response-header/actions/workflows/static-analysis.yml/badge.svg?branch=3.0)](https://github.com/dotkernel/dot-response-header/actions/workflows/static-analysis.yml) + +[![SymfonyInsight](https://insight.symfony.com/projects/dce88959-bd29-40ef-b1e7-d12815145438/big.svg)](https://insight.symfony.com/projects/dce88959-bd29-40ef-b1e7-d12815145438) + Middleware for setting and overwriting custom response headers. ### Requirements -- PHP >= 7.4 +- PHP >= 8.1 ### Installation diff --git a/composer.json b/composer.json index 644afff..95b630a 100644 --- a/composer.json +++ b/composer.json @@ -19,15 +19,42 @@ "mezzio" ], "require": { - "php": "~7.4.0 || ~8.0.0 || ~8.1.0" + "php": "~8.1.0 || ~8.2.0", + "mezzio/mezzio-router": "^3.16", + "psr/http-client": "^1.0", + "psr/http-message": "^2.0" }, "require-dev": { - "phpunit/phpunit": "^9.5.20", - "squizlabs/php_codesniffer": "^3.6.2" + "laminas/laminas-coding-standard": "^2.5", + "laminas/laminas-diactoros": "^3.0", + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.13" }, "autoload": { "psr-4": { - "Dot\\ResponseHeader\\": "src" + "Dot\\ResponseHeader\\": "src/" } + }, + "autoload-dev": { + "psr-4": { + "DotTest\\ResponseHeader\\": "test/" + } + }, + "scripts": { + "check": [ + "@cs-check", + "@test" + ], + "cs-check": "phpcs", + "cs-fix": "phpcbf", + "test": "phpunit --colors=always", + "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", + "static-analysis": "psalm --shepherd --stats" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "sort-packages": true } -} \ No newline at end of file +} diff --git a/config/response-header.global.php.dist b/config/response-header.global.php.dist index 79562ab..4c7a0e1 100644 --- a/config/response-header.global.php.dist +++ b/config/response-header.global.php.dist @@ -18,4 +18,4 @@ return [ ] ] ] -]; \ No newline at end of file +]; diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..0d4c1dc --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + config + src + test + + + + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..cf5ba32 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,14 @@ + + + + + ./test + + + + + + ./src + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..30f61eb --- /dev/null +++ b/psalm.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index a0a9e96..aa45e00 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -1,20 +1,14 @@ has('config')) { + throw new Exception(self::MESSAGE_MISSING_CONFIG); + } + $config = $container->get('config'); + + if ( + ! array_key_exists('dot_response_headers', $config) + || ! is_array($config['dot_response_headers']) + || empty($config['dot_response_headers']) + ) { + throw new Exception(self::MESSAGE_MISSING_PACKAGE_CONFIG); + } + return new ResponseHeaderMiddleware($container->get('config')['dot_response_headers'] ?? []); } -} \ No newline at end of file +} diff --git a/src/Middleware/ResponseHeaderMiddleware.php b/src/Middleware/ResponseHeaderMiddleware.php index 975b1d7..3f3d6ca 100644 --- a/src/Middleware/ResponseHeaderMiddleware.php +++ b/src/Middleware/ResponseHeaderMiddleware.php @@ -1,36 +1,28 @@ config = $config; } - /** - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface - */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $response = $handler->handle($request); @@ -38,7 +30,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $response; } - $response = $this->addHeaders($response, self::ALL_ROUTES); + $response = $this->addHeaders($response, self::ALL_ROUTES); $routeResult = $request->getAttribute(RouteResult::class); if ($routeResult instanceof RouteResult && $routeResult->isSuccess()) { $response = $this->addHeaders($response, $routeResult->getMatchedRouteName()); @@ -46,24 +38,22 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $response; } - /** - * @param ResponseInterface $response - * @param string $route - * @return ResponseInterface - */ - private function addHeaders(ResponseInterface $response, string $route): ResponseInterface + public function addHeaders(ResponseInterface $response, string $route): ResponseInterface { if (array_key_exists($route, $this->config)) { foreach ($this->config[$route] as $header => $data) { if (! array_key_exists('value', $data)) { continue; } - $overwrite = (isset($data['overwrite']) && $data['overwrite'] === true) ? true : false; - if ($overwrite) { + $addHeader = true; + if ($response->hasHeader($header)) { + $addHeader = isset($data['overwrite']) && $data['overwrite'] === true; + } + if ($addHeader === true) { $response = $response->withHeader($header, $data['value']); } } } return $response; } -} \ No newline at end of file +} diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php new file mode 100644 index 0000000..23a3255 --- /dev/null +++ b/test/ConfigProviderTest.php @@ -0,0 +1,32 @@ +configProvider = new ConfigProvider(); + } + + public function testInvoke() + { + $data = $this->configProvider->__invoke(); + + $this->assertIsArray($data); + } + + public function testGetDependencies() + { + $data = $this->configProvider->getDependencies(); + + $this->assertIsArray($data); + } +} diff --git a/test/Factory/ResponseHeaderMiddlewareFactoryTest.php b/test/Factory/ResponseHeaderMiddlewareFactoryTest.php new file mode 100644 index 0000000..7995468 --- /dev/null +++ b/test/Factory/ResponseHeaderMiddlewareFactoryTest.php @@ -0,0 +1,60 @@ +createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(false); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage(ResponseHeaderMiddlewareFactory::MESSAGE_MISSING_CONFIG); + (new ResponseHeaderMiddlewareFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateApplicationWithoutPackageConfig(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn([ + 'test', + ]); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage(ResponseHeaderMiddlewareFactory::MESSAGE_MISSING_PACKAGE_CONFIG); + (new ResponseHeaderMiddlewareFactory())($container); + } +} diff --git a/test/Middleware/ResponseHeaderMiddlewareTest.php b/test/Middleware/ResponseHeaderMiddlewareTest.php new file mode 100644 index 0000000..5e929bb --- /dev/null +++ b/test/Middleware/ResponseHeaderMiddlewareTest.php @@ -0,0 +1,177 @@ +responseHeader = new ResponseHeaderMiddleware([]); + $this->serverRequest = $this->createMock(ServerRequestInterface::class); + $this->requestHandler = $this->createMock(RequestHandlerInterface::class); + } + + public function testProcess() + { + $data = $this->responseHeader->process($this->serverRequest, $this->requestHandler); + + $this->assertInstanceOf(ResponseInterface::class, $data); + $this->assertInstanceOf(StreamInterface::class, $data->getBody()); + $this->assertNotEmpty($data->getBody()); + $this->assertIsArray($data->getHeaders()); + $this->assertIsInt($data->getStatusCode()); + $this->assertIsString($data->getProtocolVersion()); + $this->assertIsString($data->getReasonPhrase()); + } + + public function testWillNotAddHeadersWithoutCommonWithoutRouteSpecificHeadersConfigured(): void + { + $responseHeader = new ResponseHeaderMiddleware([]); + + $response = new Response(); + $response = $responseHeader->addHeaders($response, 'test'); + + $this->assertEmpty($response->getHeaders()); + } + + public function testWillAddHeadersWithCommonWithoutRouteSpecificHeadersConfigured(): void + { + $config = [ + '*' => [ + 'CustomHeader1' => [ + 'value' => 'CustomHeader1-Value', + 'overwrite' => true, + ], + 'CustomHeader2' => [ + 'value' => 'CustomHeader2-Value', + 'overwrite' => false, + ], + ], + ]; + $responseHeader = new ResponseHeaderMiddleware($config); + + $response = new Response(); + + $response = $responseHeader->addHeaders($response, 'test'); + $this->assertEmpty($response->getHeaders()); + $this->assertFalse($response->hasHeader('CustomHeader1')); + $this->assertFalse($response->hasHeader('CustomHeader2')); + + $response = $responseHeader->addHeaders($response, '*'); + $this->assertCount(2, $response->getHeaders()); + $this->assertTrue($response->hasHeader('CustomHeader1')); + $this->assertTrue($response->hasHeader('CustomHeader2')); + $this->assertSame($config['*']['CustomHeader1']['value'], $response->getHeaderLine('CustomHeader1')); + $this->assertSame($config['*']['CustomHeader2']['value'], $response->getHeaderLine('CustomHeader2')); + } + + public function testWillAddHeadersWithCommonWithRouteSpecificHeadersConfiguredWhenNoRouteMatched(): void + { + $config = [ + '*' => [ + 'CustomHeader1' => [ + 'value' => 'CustomHeader1-Value', + 'overwrite' => true, + ], + 'CustomHeader2' => [ + 'value' => 'CustomHeader2-Value', + 'overwrite' => false, + ], + ], + 'home' => [ + 'CustomHeader' => [ + 'value' => 'header3', + ], + ], + ]; + $responseHeader = new ResponseHeaderMiddleware($config); + + $response = new Response(); + + $response = $responseHeader->addHeaders($response, 'test'); + $this->assertFalse($response->hasHeader('CustomHeader')); + $this->assertEmpty($response->getHeaders()); + + $response = $responseHeader->addHeaders($response, '*'); + $this->assertCount(2, $response->getHeaders()); + $this->assertTrue($response->hasHeader('CustomHeader1')); + $this->assertTrue($response->hasHeader('CustomHeader2')); + $this->assertSame($config['*']['CustomHeader1']['value'], $response->getHeaderLine('CustomHeader1')); + $this->assertSame($config['*']['CustomHeader2']['value'], $response->getHeaderLine('CustomHeader2')); + } + + public function testWillAddHeadersWithCommonWithRouteSpecificHeadersConfiguredWhenRouteMatched(): void + { + $config = [ + '*' => [ + 'CustomHeader1' => [ + 'value' => 'CustomHeader1-Value', + 'overwrite' => true, + ], + 'CustomHeader2' => [ + 'value' => 'CustomHeader2-Value', + 'overwrite' => true, + ], + ], + 'home' => [ + 'CustomHeader' => [ + 'value' => 'header3', + ], + 'CustomHeader1' => [ + 'value' => 'CustomHeader1-Overwritten-Value', + 'overwrite' => true, + ], + 'CustomHeader2' => [ + 'value' => 'CustomHeader2-Overwritten-Value', + 'overwrite' => false, + ], + ], + ]; + $responseHeader = new ResponseHeaderMiddleware($config); + + $response = new Response(); + + $this->assertFalse($response->hasHeader('CustomHeader1')); + $this->assertFalse($response->hasHeader('CustomHeader2')); + $this->assertFalse($response->hasHeader('CustomHeader')); + $this->assertEmpty($response->getHeaders()); + + $response = $responseHeader->addHeaders($response, '*'); + $this->assertCount(2, $response->getHeaders()); + $this->assertTrue($response->hasHeader('CustomHeader1')); + $this->assertTrue($response->hasHeader('CustomHeader2')); + $this->assertSame($config['*']['CustomHeader1']['value'], $response->getHeaderLine('CustomHeader1')); + $this->assertSame($config['*']['CustomHeader2']['value'], $response->getHeaderLine('CustomHeader2')); + + $response = $responseHeader->addHeaders($response, 'home'); + $this->assertCount(3, $response->getHeaders()); + $this->assertTrue($response->hasHeader('CustomHeader')); + $this->assertTrue($response->hasHeader('CustomHeader1')); + $this->assertTrue($response->hasHeader('CustomHeader2')); + $this->assertSame($config['home']['CustomHeader']['value'], $response->getHeaderLine('CustomHeader')); + $this->assertSame($config['home']['CustomHeader1']['value'], $response->getHeaderLine('CustomHeader1')); + $this->assertSame($config['*']['CustomHeader2']['value'], $response->getHeaderLine('CustomHeader2')); + $this->assertNotSame($config['home']['CustomHeader2']['value'], $response->getHeaderLine('CustomHeader2')); + } +}