| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Unit\Http;
- use App\Http\CspMiddleware;
- use PHPUnit\Framework\TestCase;
- use Psr\Http\Message\ResponseInterface;
- use Psr\Http\Message\ServerRequestInterface;
- use Psr\Http\Server\RequestHandlerInterface;
- use Slim\Psr7\Factory\ResponseFactory;
- use Slim\Psr7\Factory\ServerRequestFactory;
- final class CspMiddlewareTest extends TestCase
- {
- public function testGeneratedNoncesAreUniqueAndUrlSafe(): void
- {
- $seen = [];
- for ($i = 0; $i < 50; $i++) {
- $nonce = CspMiddleware::generateNonce();
- self::assertMatchesRegularExpression('/^[A-Za-z0-9_-]+$/', $nonce);
- self::assertFalse(in_array($nonce, $seen, true), 'nonce should be unique');
- $seen[] = $nonce;
- }
- }
- public function testPolicyContainsNonceAndDropsUnsafeDirectives(): void
- {
- $nonce = 'TESTNONCE123';
- $policy = CspMiddleware::policy($nonce);
- self::assertStringContainsString("'nonce-{$nonce}'", $policy);
- self::assertStringNotContainsString("'unsafe-eval'", $policy);
- // `script-src` directive itself must not list `'unsafe-inline'`.
- $scriptSrc = self::extractDirective($policy, 'script-src');
- self::assertNotSame('', $scriptSrc, 'script-src directive missing');
- self::assertStringNotContainsString("'unsafe-inline'", $scriptSrc);
- self::assertStringContainsString("'self'", $scriptSrc);
- self::assertStringContainsString("'nonce-{$nonce}'", $scriptSrc);
- // Defence-in-depth: frame-ancestors / form-action / base-uri locked down.
- self::assertStringContainsString("frame-ancestors 'none'", $policy);
- self::assertStringContainsString("form-action 'self'", $policy);
- self::assertStringContainsString("base-uri 'self'", $policy);
- }
- private static function extractDirective(string $csp, string $name): string
- {
- foreach (explode(';', $csp) as $part) {
- $part = trim($part);
- if (str_starts_with($part, $name . ' ')) {
- return $part;
- }
- }
- return '';
- }
- public function testProcessSetsHeaderAndExposesNonceOnRequestAttribute(): void
- {
- $middleware = new CspMiddleware();
- $request = (new ServerRequestFactory())->createServerRequest('GET', '/');
- $rf = new ResponseFactory();
- $captured = null;
- $handler = new class ($rf, $captured) implements RequestHandlerInterface {
- public function __construct(
- private readonly ResponseFactory $rf,
- public ?string $captured,
- ) {
- }
- public function handle(ServerRequestInterface $request): ResponseInterface
- {
- $this->captured = $request->getAttribute(CspMiddleware::ATTR_NONCE);
- return $this->rf->createResponse(200);
- }
- };
- $response = $middleware->process($request, $handler);
- self::assertNotNull($handler->captured);
- self::assertNotSame('', $handler->captured);
- self::assertStringContainsString(
- "'nonce-{$handler->captured}'",
- $response->getHeaderLine('Content-Security-Policy'),
- );
- }
- }
|