1
0
Files
breadcrumb-the-shire/tests/Architecture/PostRedirectGetContractTest.php
fs f403a6704f feat(tests): automate 5 remaining guards as contract tests
Extract findPatternMatchesIn*Files() to ProjectFileAssertionSupport
trait for reuse. Add contract tests for:
- GR-SEC-005: Encryption centralized in Crypto.php
- GR-SEC-006: File storage in storage/, not web/
- GR-SEC-002: No PII in error_log() calls
- GR-CORE-012: POST-Redirect-GET pattern enforcement
- GR-UI-015: i18n key completeness de ↔ en

Brings automated guard coverage from 24 to 29 test files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 21:20:08 +01:00

84 lines
2.5 KiB
PHP

<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
/**
* GR-CORE-012: POST handlers that mutate state MUST redirect (PRG pattern)
* or return JSON. API/data endpoints and multi-step wizards are exempt.
*/
class PostRedirectGetContractTest extends TestCase
{
use ProjectFileAssertionSupport;
/** Patterns for endpoints exempt from PRG (JSON, data, wizards). */
private const PRG_EXEMPT_PATTERNS = [
'#/api/v1/#',
'#/data\(\)\.php$#',
'#/ingest\(\)\.php$#',
'#/imports/index\(\)\.php$#', // multi-step import wizard renders stages inline
];
public function testAllPostHandlersUseRedirectAfterMutation(): void
{
$pagesDir = $this->projectRootPath() . '/pages';
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($pagesDir));
$missing = [];
/** @var \SplFileInfo $file */
foreach ($iterator as $file) {
if (!$file->isFile() || $file->getExtension() !== 'php') {
continue;
}
$relativePath = 'pages/' . ltrim(str_replace($pagesDir, '', $file->getPathname()), '/');
if ($this->isPrgExempt($relativePath)) {
continue;
}
$content = (string) file_get_contents($file->getPathname());
if (!$this->handlesPostData($content)) {
continue;
}
// Router::redirect (PRG) or Router::json (AJAX) are both valid responses.
if (!str_contains($content, 'Router::redirect') && !str_contains($content, 'Router::json')) {
$missing[] = $relativePath;
}
}
sort($missing);
$this->assertSame(
[],
$missing,
"POST handlers missing Router::redirect() — PRG pattern required (GR-CORE-012):\n" . implode("\n", $missing)
);
}
private function isPrgExempt(string $path): bool
{
foreach (self::PRG_EXEMPT_PATTERNS as $pattern) {
if (preg_match($pattern, $path)) {
return true;
}
}
return false;
}
private function handlesPostData(string $content): bool
{
return str_contains($content, "isMethod('POST')")
|| str_contains($content, 'hasBody(')
|| str_contains($content, 'bodyAll()')
|| preg_match('/->body\(/', $content) === 1;
}
}