1
0
Files
breadcrumb-the-shire/tests/Architecture/RepositoryLayerIsolationContractTest.php

121 lines
3.9 KiB
PHP
Raw Normal View History

<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
/**
* Repository layer must not access HTTP superglobals or emit HTTP responses.
* Repositories are pure data access no session, request, or response handling.
*/
class RepositoryLayerIsolationContractTest extends TestCase
{
use ProjectFileAssertionSupport;
/** @var array<string, string> */
private const FORBIDDEN_PATTERNS = [
'/\$_SESSION\b/' => '$_SESSION',
'/\$_GET\b/' => '$_GET',
'/\$_POST\b/' => '$_POST',
'/\$_COOKIE\b/' => '$_COOKIE',
'/\$_REQUEST\b/' => '$_REQUEST',
'/\bheader\s*\(/' => 'header()',
'/\bhttp_response_code\s*\(/' => 'http_response_code()',
'/\bsetcookie\s*\(/' => 'setcookie()',
'/\bsession_start\s*\(/' => 'session_start()',
];
public function testCoreRepositoriesDoNotAccessHttpSuperglobals(): void
{
$this->assertRepositoryIsolation('core/Repository');
}
public function testModuleRepositoriesDoNotAccessHttpSuperglobals(): void
{
$root = $this->projectRootPath();
$modulesDir = $root . '/modules';
if (!is_dir($modulesDir)) {
self::markTestSkipped('modules/ directory missing.');
}
foreach (scandir($modulesDir) ?: [] as $entry) {
if ($entry === '.' || $entry === '..' || !is_dir($modulesDir . '/' . $entry)) {
continue;
}
$libRel = 'modules/' . $entry . '/lib';
if (!is_dir($root . '/' . $libRel)) {
continue;
}
// Scan only Repository directories within module lib/
$repoDir = $libRel . '/Repository';
if (is_dir($root . '/' . $repoDir)) {
$this->assertRepositoryIsolation($repoDir);
}
// Also scan files named *Repository.php anywhere in module lib/
$this->assertRepositoryFilesIsolated($libRel);
}
}
private function assertRepositoryIsolation(string $relativeDirectory): void
{
$violations = [];
foreach (self::FORBIDDEN_PATTERNS as $pattern => $label) {
foreach ($this->findPatternMatchesInPhpFiles($relativeDirectory, $pattern) as $file) {
$violations[] = "{$file} uses {$label}";
}
}
self::assertSame(
[],
$violations,
"Repository layer must not access HTTP state. Violations:\n" . implode("\n", $violations)
);
}
private function assertRepositoryFilesIsolated(string $relativeDirectory): void
{
$root = $this->projectRootPath();
$basePath = $root . '/' . $relativeDirectory;
$violations = [];
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($basePath, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $file) {
/** @var \SplFileInfo $file */
if ($file->getExtension() !== 'php') {
continue;
}
if (!str_ends_with($file->getFilename(), 'Repository.php')) {
continue;
}
if (str_ends_with($file->getFilename(), 'RepositoryInterface.php')) {
continue;
}
$content = file_get_contents($file->getPathname());
if ($content === false) {
continue;
}
$relativePath = str_replace($root . '/', '', $file->getPathname());
foreach (self::FORBIDDEN_PATTERNS as $pattern => $label) {
if (preg_match($pattern, $content)) {
$violations[] = "{$relativePath} uses {$label}";
}
}
}
self::assertSame(
[],
$violations,
"Repository files must not access HTTP state. Violations:\n" . implode("\n", $violations)
);
}
}