lthn.io/app/Core/Headers/Testing/HeaderAssertions.php
Claude 41a90cbff8
feat: lthn.io API serving live chain data
Fixed: basePath self→static binding, namespace detection, event wiring,
SQLite cache, file cache driver. All Mod Boot classes converted to
$listens pattern for lifecycle event discovery.

Working endpoints:
- /v1/explorer/info — live chain height, difficulty, aliases
- /v1/explorer/stats — formatted chain statistics
- /v1/names/directory — alias directory grouped by type
- /v1/names/available/{name} — name availability check
- /v1/names/lookup/{name} — name details

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 17:17:42 +01:00

477 lines
16 KiB
PHP

<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Headers\Testing;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Assert;
/**
* Testing utilities for HTTP security headers.
*
* Provides assertion methods and helpers for testing security headers
* in your application's HTTP responses.
*
* ## Usage in Test Classes
*
* ```php
* use Core\Headers\Testing\HeaderAssertions;
*
* class MySecurityTest extends TestCase
* {
* use HeaderAssertions;
*
* public function test_security_headers_are_present(): void
* {
* $response = $this->get('/');
*
* $this->assertHasSecurityHeaders($response);
* $this->assertHasHstsHeader($response);
* $this->assertHasCspHeader($response);
* }
* }
* ```
*
* ## Available Assertions
*
* | Method | Description |
* |--------|-------------|
* | `assertHasSecurityHeaders()` | Assert all standard security headers present |
* | `assertHasHstsHeader()` | Assert HSTS header present with valid config |
* | `assertHasCspHeader()` | Assert CSP header present |
* | `assertCspContainsDirective()` | Assert CSP contains a specific directive |
* | `assertCspContainsSource()` | Assert CSP directive contains a source |
* | `assertCspDoesNotContainSource()` | Assert CSP directive does not contain source |
* | `assertHasPermissionsPolicy()` | Assert Permissions-Policy header present |
* | `assertPermissionsPolicyFeature()` | Assert specific feature in Permissions-Policy |
* | `assertHasXFrameOptions()` | Assert X-Frame-Options header present |
* | `assertHasXContentTypeOptions()` | Assert X-Content-Type-Options header present |
* | `assertHasReferrerPolicy()` | Assert Referrer-Policy header present |
* | `assertHasCspNonce()` | Assert CSP contains a nonce directive |
* | `assertNoCspUnsafeInline()` | Assert CSP does not use unsafe-inline |
*/
trait HeaderAssertions
{
/**
* Assert that all standard security headers are present.
*
* Checks for:
* - X-Content-Type-Options: nosniff
* - X-Frame-Options
* - X-XSS-Protection
* - Referrer-Policy
*
* @param TestResponse $response The HTTP response to check
* @return $this
*/
public function assertHasSecurityHeaders(TestResponse $response): self
{
$response->assertHeader('X-Content-Type-Options');
$response->assertHeader('X-Frame-Options');
$response->assertHeader('X-XSS-Protection');
$response->assertHeader('Referrer-Policy');
return $this;
}
/**
* Assert that HSTS header is present and properly configured.
*
* @param TestResponse $response The HTTP response to check
* @param int|null $minMaxAge Minimum max-age value (optional)
* @param bool|null $includeSubdomains Whether includeSubDomains should be present (optional)
* @param bool|null $preload Whether preload should be present (optional)
* @return $this
*/
public function assertHasHstsHeader(
TestResponse $response,
?int $minMaxAge = null,
?bool $includeSubdomains = null,
?bool $preload = null
): self {
$response->assertHeader('Strict-Transport-Security');
$hsts = $response->headers->get('Strict-Transport-Security');
Assert::assertNotNull($hsts, 'HSTS header should not be null');
// Check max-age
if ($minMaxAge !== null) {
preg_match('/max-age=(\d+)/', $hsts, $matches);
Assert::assertNotEmpty($matches, 'HSTS should contain max-age directive');
Assert::assertGreaterThanOrEqual($minMaxAge, (int) $matches[1], "HSTS max-age should be at least {$minMaxAge}");
}
// Check includeSubDomains
if ($includeSubdomains === true) {
Assert::assertStringContainsString('includeSubDomains', $hsts, 'HSTS should include subdomains');
} elseif ($includeSubdomains === false) {
Assert::assertStringNotContainsString('includeSubDomains', $hsts, 'HSTS should not include subdomains');
}
// Check preload
if ($preload === true) {
Assert::assertStringContainsString('preload', $hsts, 'HSTS should have preload flag');
} elseif ($preload === false) {
Assert::assertStringNotContainsString('preload', $hsts, 'HSTS should not have preload flag');
}
return $this;
}
/**
* Assert that CSP header is present.
*
* @param TestResponse $response The HTTP response to check
* @param bool $reportOnly Whether to check for report-only header
* @return $this
*/
public function assertHasCspHeader(TestResponse $response, bool $reportOnly = false): self
{
$headerName = $reportOnly
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-Policy';
$response->assertHeader($headerName);
return $this;
}
/**
* Assert that CSP contains a specific directive.
*
* @param TestResponse $response The HTTP response to check
* @param string $directive The CSP directive to check (e.g., 'default-src', 'script-src')
* @param bool $reportOnly Whether to check report-only header
* @return $this
*/
public function assertCspContainsDirective(
TestResponse $response,
string $directive,
bool $reportOnly = false
): self {
$csp = $this->getCspHeader($response, $reportOnly);
Assert::assertStringContainsString($directive, $csp, "CSP should contain '{$directive}' directive");
return $this;
}
/**
* Assert that a CSP directive contains a specific source.
*
* @param TestResponse $response The HTTP response to check
* @param string $directive The CSP directive (e.g., 'script-src')
* @param string $source The source to check for (e.g., "'self'", 'https://example.com')
* @param bool $reportOnly Whether to check report-only header
* @return $this
*/
public function assertCspContainsSource(
TestResponse $response,
string $directive,
string $source,
bool $reportOnly = false
): self {
$directives = $this->parseCspDirectives($response, $reportOnly);
Assert::assertArrayHasKey($directive, $directives, "CSP should contain '{$directive}' directive");
Assert::assertContains(
$source,
$directives[$directive],
"CSP directive '{$directive}' should contain source '{$source}'"
);
return $this;
}
/**
* Assert that a CSP directive does not contain a specific source.
*
* @param TestResponse $response The HTTP response to check
* @param string $directive The CSP directive (e.g., 'script-src')
* @param string $source The source that should not be present
* @param bool $reportOnly Whether to check report-only header
* @return $this
*/
public function assertCspDoesNotContainSource(
TestResponse $response,
string $directive,
string $source,
bool $reportOnly = false
): self {
$directives = $this->parseCspDirectives($response, $reportOnly);
if (isset($directives[$directive])) {
Assert::assertNotContains(
$source,
$directives[$directive],
"CSP directive '{$directive}' should not contain source '{$source}'"
);
}
return $this;
}
/**
* Assert that CSP contains a nonce directive.
*
* @param TestResponse $response The HTTP response to check
* @param string $directive The directive to check for nonce (default: 'script-src')
* @param bool $reportOnly Whether to check report-only header
* @return $this
*/
public function assertHasCspNonce(
TestResponse $response,
string $directive = 'script-src',
bool $reportOnly = false
): self {
$csp = $this->getCspHeader($response, $reportOnly);
$pattern = "/{$directive}[^;]*'nonce-[A-Za-z0-9+\/=]+'/";
Assert::assertMatchesRegularExpression(
$pattern,
$csp,
"CSP '{$directive}' should contain a nonce directive"
);
return $this;
}
/**
* Assert that CSP does not use 'unsafe-inline' in specified directive.
*
* @param TestResponse $response The HTTP response to check
* @param string $directive The directive to check (default: 'script-src')
* @param bool $reportOnly Whether to check report-only header
* @return $this
*/
public function assertNoCspUnsafeInline(
TestResponse $response,
string $directive = 'script-src',
bool $reportOnly = false
): self {
return $this->assertCspDoesNotContainSource($response, $directive, "'unsafe-inline'", $reportOnly);
}
/**
* Assert that CSP does not use 'unsafe-eval' in specified directive.
*
* @param TestResponse $response The HTTP response to check
* @param string $directive The directive to check (default: 'script-src')
* @param bool $reportOnly Whether to check report-only header
* @return $this
*/
public function assertNoCspUnsafeEval(
TestResponse $response,
string $directive = 'script-src',
bool $reportOnly = false
): self {
return $this->assertCspDoesNotContainSource($response, $directive, "'unsafe-eval'", $reportOnly);
}
/**
* Assert that Permissions-Policy header is present.
*
* @param TestResponse $response The HTTP response to check
* @return $this
*/
public function assertHasPermissionsPolicy(TestResponse $response): self
{
$response->assertHeader('Permissions-Policy');
return $this;
}
/**
* Assert that Permissions-Policy contains a specific feature setting.
*
* @param TestResponse $response The HTTP response to check
* @param string $feature The feature name (e.g., 'geolocation', 'camera')
* @param array<string> $allowList Expected allow list (empty array for '()')
* @return $this
*/
public function assertPermissionsPolicyFeature(
TestResponse $response,
string $feature,
array $allowList = []
): self {
$policy = $response->headers->get('Permissions-Policy');
Assert::assertNotNull($policy, 'Permissions-Policy header should be present');
if (empty($allowList)) {
// Feature should be disabled: feature=()
Assert::assertMatchesRegularExpression(
"/{$feature}=\(\)/",
$policy,
"Permissions-Policy should disable '{$feature}'"
);
} else {
// Feature should have specific origins
Assert::assertStringContainsString(
"{$feature}=",
$policy,
"Permissions-Policy should contain '{$feature}' feature"
);
}
return $this;
}
/**
* Assert that X-Frame-Options header is present with expected value.
*
* @param TestResponse $response The HTTP response to check
* @param string|null $expected Expected value ('DENY', 'SAMEORIGIN', etc.)
* @return $this
*/
public function assertHasXFrameOptions(TestResponse $response, ?string $expected = null): self
{
$response->assertHeader('X-Frame-Options');
if ($expected !== null) {
$actual = $response->headers->get('X-Frame-Options');
Assert::assertSame($expected, $actual, "X-Frame-Options should be '{$expected}'");
}
return $this;
}
/**
* Assert that X-Content-Type-Options header is present with 'nosniff'.
*
* @param TestResponse $response The HTTP response to check
* @return $this
*/
public function assertHasXContentTypeOptions(TestResponse $response): self
{
$response->assertHeader('X-Content-Type-Options', 'nosniff');
return $this;
}
/**
* Assert that Referrer-Policy header is present with expected value.
*
* @param TestResponse $response The HTTP response to check
* @param string|null $expected Expected value (e.g., 'strict-origin-when-cross-origin')
* @return $this
*/
public function assertHasReferrerPolicy(TestResponse $response, ?string $expected = null): self
{
$response->assertHeader('Referrer-Policy');
if ($expected !== null) {
$actual = $response->headers->get('Referrer-Policy');
Assert::assertSame($expected, $actual, "Referrer-Policy should be '{$expected}'");
}
return $this;
}
/**
* Assert that X-XSS-Protection header is present.
*
* @param TestResponse $response The HTTP response to check
* @param string|null $expected Expected value (e.g., '1; mode=block')
* @return $this
*/
public function assertHasXssProtection(TestResponse $response, ?string $expected = null): self
{
$response->assertHeader('X-XSS-Protection');
if ($expected !== null) {
$actual = $response->headers->get('X-XSS-Protection');
Assert::assertSame($expected, $actual, "X-XSS-Protection should be '{$expected}'");
}
return $this;
}
/**
* Assert that a header is NOT present.
*
* @param TestResponse $response The HTTP response to check
* @param string $headerName The header name to check
* @return $this
*/
public function assertHeaderMissing(TestResponse $response, string $headerName): self
{
$response->assertHeaderMissing($headerName);
return $this;
}
/**
* Get the CSP header value from response.
*
* @param TestResponse $response The HTTP response
* @param bool $reportOnly Whether to get report-only header
* @return string The CSP header value
*/
protected function getCspHeader(TestResponse $response, bool $reportOnly = false): string
{
$headerName = $reportOnly
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-Policy';
$csp = $response->headers->get($headerName);
Assert::assertNotNull($csp, "{$headerName} header should be present");
return $csp;
}
/**
* Parse CSP header into directives array.
*
* @param TestResponse $response The HTTP response
* @param bool $reportOnly Whether to parse report-only header
* @return array<string, array<string>> Map of directive to sources
*/
protected function parseCspDirectives(TestResponse $response, bool $reportOnly = false): array
{
$csp = $this->getCspHeader($response, $reportOnly);
$directives = [];
foreach (explode(';', $csp) as $part) {
$part = trim($part);
if (empty($part)) {
continue;
}
$tokens = preg_split('/\s+/', $part);
$directiveName = array_shift($tokens);
$directives[$directiveName] = $tokens;
}
return $directives;
}
/**
* Extract the nonce value from a CSP header.
*
* @param TestResponse $response The HTTP response
* @param string $directive The directive to extract nonce from
* @param bool $reportOnly Whether to check report-only header
* @return string|null The nonce value or null if not found
*/
public function extractCspNonce(
TestResponse $response,
string $directive = 'script-src',
bool $reportOnly = false
): ?string {
$csp = $this->getCspHeader($response, $reportOnly);
// Match nonce in the specified directive
if (preg_match("/{$directive}[^;]*'nonce-([A-Za-z0-9+\/=]+)'/", $csp, $matches)) {
return $matches[1];
}
return null;
}
}