228 lines
8.5 KiB
PHP
228 lines
8.5 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace MintyPHP\Tests\Console;
|
||
|
|
|
||
|
|
use MintyPHP\Console\Commands\Module\ValidateCommand;
|
||
|
|
use PHPUnit\Framework\TestCase;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Tests for module:validate enhancements:
|
||
|
|
* - AuthorizationPolicy container-registration check (check 20)
|
||
|
|
* - Event listener unknown event warning (check 21)
|
||
|
|
*
|
||
|
|
* Uses reflection to call the private validateModule() method directly,
|
||
|
|
* avoiding bootstrapApp() and STDOUT capture issues.
|
||
|
|
*/
|
||
|
|
final class ValidateCommandTest extends TestCase
|
||
|
|
{
|
||
|
|
private string $modulesDir;
|
||
|
|
|
||
|
|
/** @var list<string> Module IDs to clean up */
|
||
|
|
private array $cleanup = [];
|
||
|
|
|
||
|
|
private \ReflectionMethod $validateModule;
|
||
|
|
private ValidateCommand $command;
|
||
|
|
|
||
|
|
protected function setUp(): void
|
||
|
|
{
|
||
|
|
$this->modulesDir = dirname(__DIR__, 2) . '/modules';
|
||
|
|
$this->command = new ValidateCommand();
|
||
|
|
$this->validateModule = new \ReflectionMethod($this->command, 'validateModule');
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function tearDown(): void
|
||
|
|
{
|
||
|
|
foreach ($this->cleanup as $id) {
|
||
|
|
$this->removeDir($this->modulesDir . '/' . $id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testValidateWarnsOnUnknownEventListenerKey(): void
|
||
|
|
{
|
||
|
|
$id = 'ztest-unknown-event';
|
||
|
|
$this->scaffoldModule($id, <<<'PHP'
|
||
|
|
<?php
|
||
|
|
return [
|
||
|
|
'id' => 'ztest-unknown-event',
|
||
|
|
'version' => '0.1.0',
|
||
|
|
'event_listeners' => [
|
||
|
|
'user.created' => [
|
||
|
|
['class' => 'NonExistent\Listener', 'method' => 'handle'],
|
||
|
|
],
|
||
|
|
'totally.unknown.event' => [
|
||
|
|
['class' => 'NonExistent\Listener', 'method' => 'handle'],
|
||
|
|
],
|
||
|
|
],
|
||
|
|
];
|
||
|
|
PHP);
|
||
|
|
|
||
|
|
[$errors, $warnings] = $this->validateModule->invoke($this->command, $this->modulesDir, $id);
|
||
|
|
|
||
|
|
$warningText = implode("\n", $warnings);
|
||
|
|
self::assertStringContainsString('totally.unknown.event', $warningText);
|
||
|
|
self::assertStringContainsString('not defined in ModuleEvents', $warningText);
|
||
|
|
// user.created is a known event — should NOT trigger a warning
|
||
|
|
self::assertStringNotContainsString("unknown event 'user.created'", $warningText);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testValidateDoesNotWarnOnKnownEventKeys(): void
|
||
|
|
{
|
||
|
|
$id = 'ztest-known-events';
|
||
|
|
$this->scaffoldModule($id, <<<'PHP'
|
||
|
|
<?php
|
||
|
|
return [
|
||
|
|
'id' => 'ztest-known-events',
|
||
|
|
'version' => '0.1.0',
|
||
|
|
'event_listeners' => [
|
||
|
|
'user.created' => [
|
||
|
|
['class' => 'NonExistent\Listener', 'method' => 'handle'],
|
||
|
|
],
|
||
|
|
'scheduler.job_failed' => [
|
||
|
|
['class' => 'NonExistent\Listener', 'method' => 'handle'],
|
||
|
|
],
|
||
|
|
],
|
||
|
|
];
|
||
|
|
PHP);
|
||
|
|
|
||
|
|
[$errors, $warnings] = $this->validateModule->invoke($this->command, $this->modulesDir, $id);
|
||
|
|
|
||
|
|
$warningText = implode("\n", $warnings);
|
||
|
|
self::assertStringNotContainsString('not defined in ModuleEvents', $warningText);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testValidateWarnsWhenPolicyNotReferencedInRegistrar(): void
|
||
|
|
{
|
||
|
|
$id = 'ztest-policy-missing';
|
||
|
|
$moduleDir = $this->modulesDir . '/' . $id;
|
||
|
|
mkdir($moduleDir . '/lib/Module/ZtestPolicyMissing', 0777, true);
|
||
|
|
$this->cleanup[] = $id;
|
||
|
|
|
||
|
|
file_put_contents(
|
||
|
|
$moduleDir . '/lib/Module/ZtestPolicyMissing/ZtestPolicyMissingContainerRegistrar.php',
|
||
|
|
<<<'PHP'
|
||
|
|
<?php
|
||
|
|
namespace MintyPHP\Module\ZtestPolicyMissing;
|
||
|
|
use MintyPHP\App\AppContainer;
|
||
|
|
use MintyPHP\App\Container\ContainerRegistrar;
|
||
|
|
final class ZtestPolicyMissingContainerRegistrar implements ContainerRegistrar {
|
||
|
|
public function register(AppContainer $container): void {}
|
||
|
|
}
|
||
|
|
PHP
|
||
|
|
);
|
||
|
|
|
||
|
|
file_put_contents(
|
||
|
|
$moduleDir . '/lib/Module/ZtestPolicyMissing/ZtestPolicyMissingAuthorizationPolicy.php',
|
||
|
|
<<<'PHP'
|
||
|
|
<?php
|
||
|
|
namespace MintyPHP\Module\ZtestPolicyMissing;
|
||
|
|
use MintyPHP\Service\Access\AuthorizationDecision;
|
||
|
|
use MintyPHP\Service\Access\AuthorizationPolicyInterface;
|
||
|
|
final class ZtestPolicyMissingAuthorizationPolicy implements AuthorizationPolicyInterface {
|
||
|
|
public function supports(string $ability): bool { return false; }
|
||
|
|
public function authorize(string $ability, array $context = []): AuthorizationDecision {
|
||
|
|
return AuthorizationDecision::deny(403, 'forbidden');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
PHP
|
||
|
|
);
|
||
|
|
|
||
|
|
file_put_contents($moduleDir . '/module.php', <<<'PHP'
|
||
|
|
<?php
|
||
|
|
return [
|
||
|
|
'id' => 'ztest-policy-missing',
|
||
|
|
'version' => '0.1.0',
|
||
|
|
'container_registrars' => [\MintyPHP\Module\ZtestPolicyMissing\ZtestPolicyMissingContainerRegistrar::class],
|
||
|
|
'authorization_policies' => [\MintyPHP\Module\ZtestPolicyMissing\ZtestPolicyMissingAuthorizationPolicy::class],
|
||
|
|
];
|
||
|
|
PHP);
|
||
|
|
|
||
|
|
[$errors, $warnings] = $this->validateModule->invoke($this->command, $this->modulesDir, $id);
|
||
|
|
|
||
|
|
$warningText = implode("\n", $warnings);
|
||
|
|
self::assertStringContainsString('ZtestPolicyMissingAuthorizationPolicy', $warningText);
|
||
|
|
self::assertStringContainsString('not referenced in any ContainerRegistrar', $warningText);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testValidateNoWarningWhenPolicyReferencedInRegistrar(): void
|
||
|
|
{
|
||
|
|
$id = 'ztest-policy-ok';
|
||
|
|
$moduleDir = $this->modulesDir . '/' . $id;
|
||
|
|
mkdir($moduleDir . '/lib/Module/ZtestPolicyOk', 0777, true);
|
||
|
|
$this->cleanup[] = $id;
|
||
|
|
|
||
|
|
file_put_contents(
|
||
|
|
$moduleDir . '/lib/Module/ZtestPolicyOk/ZtestPolicyOkContainerRegistrar.php',
|
||
|
|
<<<'PHP'
|
||
|
|
<?php
|
||
|
|
namespace MintyPHP\Module\ZtestPolicyOk;
|
||
|
|
use MintyPHP\App\AppContainer;
|
||
|
|
use MintyPHP\App\Container\ContainerRegistrar;
|
||
|
|
final class ZtestPolicyOkContainerRegistrar implements ContainerRegistrar {
|
||
|
|
public function register(AppContainer $container): void {
|
||
|
|
$container->set(ZtestPolicyOkAuthorizationPolicy::class, static fn () => new ZtestPolicyOkAuthorizationPolicy());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
PHP
|
||
|
|
);
|
||
|
|
|
||
|
|
file_put_contents(
|
||
|
|
$moduleDir . '/lib/Module/ZtestPolicyOk/ZtestPolicyOkAuthorizationPolicy.php',
|
||
|
|
<<<'PHP'
|
||
|
|
<?php
|
||
|
|
namespace MintyPHP\Module\ZtestPolicyOk;
|
||
|
|
use MintyPHP\Service\Access\AuthorizationDecision;
|
||
|
|
use MintyPHP\Service\Access\AuthorizationPolicyInterface;
|
||
|
|
final class ZtestPolicyOkAuthorizationPolicy implements AuthorizationPolicyInterface {
|
||
|
|
public function supports(string $ability): bool { return false; }
|
||
|
|
public function authorize(string $ability, array $context = []): AuthorizationDecision {
|
||
|
|
return AuthorizationDecision::deny(403, 'forbidden');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
PHP
|
||
|
|
);
|
||
|
|
|
||
|
|
file_put_contents($moduleDir . '/module.php', <<<'PHP'
|
||
|
|
<?php
|
||
|
|
return [
|
||
|
|
'id' => 'ztest-policy-ok',
|
||
|
|
'version' => '0.1.0',
|
||
|
|
'container_registrars' => [\MintyPHP\Module\ZtestPolicyOk\ZtestPolicyOkContainerRegistrar::class],
|
||
|
|
'authorization_policies' => [\MintyPHP\Module\ZtestPolicyOk\ZtestPolicyOkAuthorizationPolicy::class],
|
||
|
|
];
|
||
|
|
PHP);
|
||
|
|
|
||
|
|
[$errors, $warnings] = $this->validateModule->invoke($this->command, $this->modulesDir, $id);
|
||
|
|
|
||
|
|
self::assertSame([], $errors);
|
||
|
|
$warningText = implode("\n", $warnings);
|
||
|
|
self::assertStringNotContainsString('not referenced', $warningText);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function scaffoldModule(string $id, string $manifestContent): void
|
||
|
|
{
|
||
|
|
$moduleDir = $this->modulesDir . '/' . $id;
|
||
|
|
mkdir($moduleDir . '/lib', 0777, true);
|
||
|
|
file_put_contents($moduleDir . '/module.php', $manifestContent);
|
||
|
|
$this->cleanup[] = $id;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function removeDir(string $dir): void
|
||
|
|
{
|
||
|
|
if (!is_dir($dir)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
$items = new \RecursiveIteratorIterator(
|
||
|
|
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
|
||
|
|
\RecursiveIteratorIterator::CHILD_FIRST
|
||
|
|
);
|
||
|
|
foreach ($items as $item) {
|
||
|
|
if ($item->isDir()) {
|
||
|
|
rmdir($item->getPathname());
|
||
|
|
} else {
|
||
|
|
unlink($item->getPathname());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
rmdir($dir);
|
||
|
|
}
|
||
|
|
}
|