test: add architecture contract tests for layering, sessions, and asset versioning
New contracts enforce repository layer isolation, no HTML in services, no DB calls in views, module session key prefixes, and asset versioning. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
120
tests/Architecture/RepositoryLayerIsolationContractTest.php
Normal file
120
tests/Architecture/RepositoryLayerIsolationContractTest.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?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('lib/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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user