feat(js): harden global HTTP and module import contracts
This commit is contained in:
@@ -8,17 +8,17 @@ final class FrontendJsContractsTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
public function testNoWindowAlertCallsInCoreAndHelpdeskJs(): void
|
||||
public function testNoWindowAlertCallsInFrontendJsScope(): void
|
||||
{
|
||||
$violations = array_merge(
|
||||
$this->findPatternMatchesInFiles('web/js', '/window\.alert\s*\(/', ['js']),
|
||||
$this->findPatternMatchesInFiles('modules/helpdesk/web/js', '/window\.alert\s*\(/', ['js'])
|
||||
);
|
||||
$violations = $this->findPatternMatchesInFiles('web/js', '/window\.alert\s*\(/', ['js']);
|
||||
foreach ($this->moduleJsDirectories() as $moduleJsDir) {
|
||||
$violations = array_merge($violations, $this->findPatternMatchesInFiles($moduleJsDir, '/window\.alert\s*\(/', ['js']));
|
||||
}
|
||||
|
||||
$violations = array_values(array_unique($violations));
|
||||
sort($violations);
|
||||
|
||||
$this->assertSame([], $violations, "window.alert usage found in JS scope:\n" . implode("\n", $violations));
|
||||
$this->assertSame([], $violations, "window.alert usage found in frontend JS scope:\n" . implode("\n", $violations));
|
||||
}
|
||||
|
||||
public function testHelpdeskRuntimeComponentsDoNotAutoInitialize(): void
|
||||
@@ -39,53 +39,55 @@ final class FrontendJsContractsTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testHelpdeskJsUsesCentralHttpLayer(): void
|
||||
public function testFrontendJsUsesCentralHttpLayer(): void
|
||||
{
|
||||
$violations = $this->findPatternMatchesInFiles('modules/helpdesk/web/js', '/\bfetch\s*\(/', ['js']);
|
||||
$this->assertSame([], $violations, "Direct fetch(...) found in helpdesk JS:\n" . implode("\n", $violations));
|
||||
$violations = $this->findDirectFetchCallsInFrontendScope();
|
||||
$this->assertSame([], $violations, "Direct fetch(...) found outside approved HTTP infrastructure:\n" . implode("\n", $violations));
|
||||
}
|
||||
|
||||
public function testHelpdeskImportPolicyForCoreAndSiblingModules(): void
|
||||
public function testModuleImportPolicyForCoreAndSiblingModules(): void
|
||||
{
|
||||
$root = $this->projectRootPath();
|
||||
$base = $root . '/modules/helpdesk/web/js';
|
||||
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($base, \FilesystemIterator::SKIP_DOTS));
|
||||
|
||||
$violations = [];
|
||||
|
||||
/** @var \SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if (!$file->isFile() || $file->getExtension() !== 'js') {
|
||||
continue;
|
||||
}
|
||||
foreach ($this->moduleJsDirectories() as $moduleJsDir) {
|
||||
$base = $root . '/' . $moduleJsDir;
|
||||
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($base, \FilesystemIterator::SKIP_DOTS));
|
||||
|
||||
$content = file_get_contents($file->getPathname());
|
||||
$this->assertNotFalse($content, 'Could not read file: ' . $file->getPathname());
|
||||
$relativePath = str_replace($root . '/', '', $file->getPathname());
|
||||
|
||||
preg_match_all('/^\s*import\s+(?:[^\"\']+\s+from\s+)?[\"\']([^\"\']+)[\"\']/m', $content, $matches);
|
||||
foreach ($matches[1] as $importPathRaw) {
|
||||
$importPath = (string) $importPathRaw;
|
||||
|
||||
if (str_starts_with($importPath, '/js/')) {
|
||||
/** @var \SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if (!$file->isFile() || $file->getExtension() !== 'js') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($importPath, './') || str_starts_with($importPath, '../')) {
|
||||
if (preg_match('#(?:^|/)js/(core|components|pages)/#', $importPath) === 1) {
|
||||
$violations[] = $relativePath . ' => relative core import: ' . $importPath;
|
||||
$content = file_get_contents($file->getPathname());
|
||||
$this->assertNotFalse($content, 'Could not read file: ' . $file->getPathname());
|
||||
$relativePath = str_replace($root . '/', '', $file->getPathname());
|
||||
|
||||
preg_match_all('/^\s*import\s+(?:[^\"\']+\s+from\s+)?[\"\']([^\"\']+)[\"\']/m', $content, $matches);
|
||||
foreach ($matches[1] as $importPathRaw) {
|
||||
$importPath = (string) $importPathRaw;
|
||||
|
||||
if (str_starts_with($importPath, '/js/')) {
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$violations[] = $relativePath . ' => invalid import path: ' . $importPath;
|
||||
if (str_starts_with($importPath, './') || str_starts_with($importPath, '../')) {
|
||||
if (preg_match('#(?:^|/)js/(core|components|pages)/#', $importPath) === 1) {
|
||||
$violations[] = $relativePath . ' => relative core import: ' . $importPath;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$violations[] = $relativePath . ' => invalid import path: ' . $importPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$violations = array_values(array_unique($violations));
|
||||
sort($violations);
|
||||
|
||||
$this->assertSame([], $violations, "Helpdesk import policy violations:\n" . implode("\n", $violations));
|
||||
$this->assertSame([], $violations, "Module import policy violations:\n" . implode("\n", $violations));
|
||||
}
|
||||
|
||||
public function testLookupFieldComponentExportsRuntimeInitializerWithoutAutoInit(): void
|
||||
@@ -113,4 +115,70 @@ final class FrontendJsContractsTest extends TestCase
|
||||
'modules/helpdesk/web/js/handover-schema-editor.js',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function moduleJsDirectories(): array
|
||||
{
|
||||
$root = $this->projectRootPath();
|
||||
$modulesRoot = $root . '/modules';
|
||||
$this->assertDirectoryExists($modulesRoot, 'Modules root not found.');
|
||||
|
||||
$dirs = [];
|
||||
$entries = scandir($modulesRoot);
|
||||
$this->assertNotFalse($entries, 'Could not read modules directory.');
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry === '.' || $entry === '..') {
|
||||
continue;
|
||||
}
|
||||
$jsPath = $modulesRoot . '/' . $entry . '/web/js';
|
||||
if (is_dir($jsPath)) {
|
||||
$dirs[] = 'modules/' . $entry . '/web/js';
|
||||
}
|
||||
}
|
||||
|
||||
sort($dirs);
|
||||
return $dirs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function findDirectFetchCallsInFrontendScope(): array
|
||||
{
|
||||
$root = $this->projectRootPath();
|
||||
$allowed = [
|
||||
'web/js/core/app-http.js',
|
||||
'web/js/core/app-telemetry.js',
|
||||
];
|
||||
$scanRoots = array_merge(['web/js'], $this->moduleJsDirectories());
|
||||
$violations = [];
|
||||
|
||||
foreach ($scanRoots as $scanRoot) {
|
||||
$base = $root . '/' . $scanRoot;
|
||||
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($base, \FilesystemIterator::SKIP_DOTS));
|
||||
/** @var \SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if (!$file->isFile() || $file->getExtension() !== 'js') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relativePath = str_replace($root . '/', '', $file->getPathname());
|
||||
if (in_array($relativePath, $allowed, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = file_get_contents($file->getPathname());
|
||||
$this->assertNotFalse($content, 'Could not read file: ' . $file->getPathname());
|
||||
if (preg_match('/\bfetch\s*\(/', $content) === 1) {
|
||||
$violations[] = $relativePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$violations = array_values(array_unique($violations));
|
||||
sort($violations);
|
||||
return $violations;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user