Split module registry contracts
This commit is contained in:
@@ -21,7 +21,8 @@
|
||||
<file>tests/Architecture/I18nKeyCompletenessContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleBoundaryContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleManifestContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleRegistryContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleRegistryBootstrapContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleRegistryFailFastContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleProviderContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleUiContractTest.php</file>
|
||||
<file>tests/Architecture/RepositoryInterfaceContractTest.php</file>
|
||||
@@ -66,7 +67,8 @@
|
||||
<testsuite name="ArchitectureModules">
|
||||
<file>tests/Architecture/AddressBookModuleIsolationContractTest.php</file>
|
||||
<file>tests/Architecture/BookmarksModuleIsolationContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleRegistryContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleRegistryBootstrapContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleRegistryFailFastContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleBoundaryContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleManifestContractTest.php</file>
|
||||
<file>tests/Architecture/ModuleProviderContractTest.php</file>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
abstract class AbstractModuleRegistryContractTestCase extends TestCase
|
||||
{
|
||||
protected string $fixturesDir;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->fixturesDir = sys_get_temp_dir() . '/corecore-module-architecture-' . uniqid('', true);
|
||||
mkdir($this->fixturesDir, 0777, true);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->removeDir($this->fixturesDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $manifest
|
||||
*/
|
||||
protected function createModuleManifest(string $dirName, array $manifest): void
|
||||
{
|
||||
$dir = $this->fixturesDir . '/' . $dirName;
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
|
||||
file_put_contents(
|
||||
$dir . '/module.php',
|
||||
'<?php return ' . var_export($manifest, true) . ';'
|
||||
);
|
||||
}
|
||||
|
||||
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());
|
||||
continue;
|
||||
}
|
||||
|
||||
unlink($item->getPathname());
|
||||
}
|
||||
|
||||
rmdir($dir);
|
||||
}
|
||||
}
|
||||
54
tests/Architecture/ModuleRegistryBootstrapContractTest.php
Normal file
54
tests/Architecture/ModuleRegistryBootstrapContractTest.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use MintyPHP\App\Module\ModuleRegistry;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ModuleRegistryBootstrapContractTest extends TestCase
|
||||
{
|
||||
public function testRegistryIsAvailableInContainer(): void
|
||||
{
|
||||
$registry = app(ModuleRegistry::class);
|
||||
|
||||
self::assertInstanceOf(ModuleRegistry::class, $registry);
|
||||
}
|
||||
|
||||
public function testAddressBookModuleLoadsExpectedRuntimeContributions(): void
|
||||
{
|
||||
$modulesDir = dirname(__DIR__, 2) . '/modules';
|
||||
$registry = ModuleRegistry::boot($modulesDir, ['addressbook']);
|
||||
|
||||
self::assertTrue($registry->hasModule('addressbook'));
|
||||
self::assertNotEmpty($registry->getPermissions());
|
||||
self::assertNotEmpty($registry->getSearchResources());
|
||||
self::assertNotEmpty($registry->getLayoutContextProviders());
|
||||
self::assertNotEmpty($registry->getSessionProviders());
|
||||
self::assertNotEmpty($registry->getSlotContributions('aside.tab_panel'));
|
||||
}
|
||||
|
||||
public function testEmptyEnvDisablesAllConfiguredModules(): void
|
||||
{
|
||||
$modulesDir = dirname(__DIR__, 2) . '/modules';
|
||||
|
||||
putenv('APP_ENABLED_MODULES=');
|
||||
|
||||
try {
|
||||
$enabledModules = ModuleRegistry::resolveEnabledModules([
|
||||
'enabled_modules' => ['addressbook', 'bookmarks'],
|
||||
]);
|
||||
$registry = ModuleRegistry::boot($modulesDir, $enabledModules);
|
||||
} finally {
|
||||
putenv('APP_ENABLED_MODULES');
|
||||
}
|
||||
|
||||
self::assertSame([], $enabledModules, 'APP_ENABLED_MODULES empty string must disable all modules.');
|
||||
self::assertSame([], $registry->getModules());
|
||||
self::assertSame([], $registry->getRoutes());
|
||||
self::assertSame([], $registry->getPermissions());
|
||||
self::assertSame([], $registry->getSearchResources());
|
||||
self::assertSame([], $registry->getLayoutContextProviders());
|
||||
self::assertSame([], $registry->getSessionProviders());
|
||||
self::assertSame([], $registry->getSlotContributions('aside.tab_panel'));
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use MintyPHP\App\Module\ModuleRegistry;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Architecture test: verifies module registry contract in both module-on and module-off modes.
|
||||
*/
|
||||
final class ModuleRegistryContractTest extends TestCase
|
||||
{
|
||||
public function testRegistryIsAvailableInContainer(): void
|
||||
{
|
||||
$registry = app(ModuleRegistry::class);
|
||||
|
||||
self::assertInstanceOf(ModuleRegistry::class, $registry);
|
||||
}
|
||||
|
||||
public function testAddressBookModuleLoadsWhenEnabled(): void
|
||||
{
|
||||
$modulesDir = dirname(__DIR__, 2) . '/modules';
|
||||
$registry = ModuleRegistry::boot($modulesDir, ['addressbook']);
|
||||
|
||||
self::assertTrue($registry->hasModule('addressbook'));
|
||||
self::assertNotEmpty($registry->getSearchResources());
|
||||
self::assertNotEmpty($registry->getLayoutContextProviders());
|
||||
self::assertNotEmpty($registry->getSessionProviders());
|
||||
}
|
||||
|
||||
public function testModuleOffModeReturnsEmptyRegistry(): void
|
||||
{
|
||||
$modulesDir = dirname(__DIR__, 2) . '/modules';
|
||||
|
||||
putenv('APP_ENABLED_MODULES=');
|
||||
try {
|
||||
$enabledModules = ModuleRegistry::resolveEnabledModules([
|
||||
'enabled_modules' => ['addressbook', 'bookmarks'],
|
||||
]);
|
||||
self::assertSame([], $enabledModules, 'APP_ENABLED_MODULES empty string must disable all modules.');
|
||||
$registry = ModuleRegistry::boot($modulesDir, $enabledModules);
|
||||
} finally {
|
||||
putenv('APP_ENABLED_MODULES');
|
||||
}
|
||||
|
||||
self::assertSame([], $registry->getModules());
|
||||
self::assertSame([], $registry->getRoutes());
|
||||
self::assertSame([], $registry->getPermissions());
|
||||
self::assertSame([], $registry->getSearchResources());
|
||||
self::assertSame([], $registry->getLayoutContextProviders());
|
||||
self::assertSame([], $registry->getSessionProviders());
|
||||
self::assertSame([], $registry->getSlotContributions('aside.tab_panel'));
|
||||
}
|
||||
|
||||
public function testAddressBookModuleManifestIsValid(): void
|
||||
{
|
||||
$modulesDir = dirname(__DIR__, 2) . '/modules';
|
||||
$manifestFile = $modulesDir . '/addressbook/module.php';
|
||||
|
||||
self::assertFileExists($manifestFile, 'Address book module manifest must exist');
|
||||
|
||||
$raw = require $manifestFile;
|
||||
self::assertIsArray($raw);
|
||||
self::assertSame('addressbook', $raw['id']);
|
||||
self::assertSame([[
|
||||
'key' => 'address_book.view',
|
||||
'description' => 'Can view address book',
|
||||
'active' => true,
|
||||
'is_system' => true,
|
||||
]], $raw['permissions']);
|
||||
self::assertArrayHasKey('address-book', $raw['asset_groups']);
|
||||
self::assertSame(
|
||||
['modules/addressbook/css/pages/address-book-view.css'],
|
||||
$raw['asset_groups']['address-book']
|
||||
);
|
||||
self::assertNotEmpty($raw['search_resources']);
|
||||
self::assertNotEmpty($raw['layout_context_providers']);
|
||||
self::assertNotEmpty($raw['session_providers']);
|
||||
self::assertArrayHasKey('aside.tab_panel', $raw['ui_slots']);
|
||||
self::assertArrayHasKey('search.resource_item', $raw['ui_slots']);
|
||||
self::assertArrayHasKey('user.edit.aside_action', $raw['ui_slots']);
|
||||
|
||||
$routes = is_array($raw['routes'] ?? null) ? $raw['routes'] : [];
|
||||
$routeByPath = [];
|
||||
foreach ($routes as $route) {
|
||||
if (!is_array($route)) {
|
||||
continue;
|
||||
}
|
||||
$routeByPath[(string) ($route['path'] ?? '')] = (string) ($route['target'] ?? '');
|
||||
}
|
||||
self::assertSame('address-book', $routeByPath['address-book'] ?? null);
|
||||
self::assertSame('address-book/data', $routeByPath['address-book/data'] ?? null);
|
||||
self::assertSame('address-book', $routeByPath['addressbook'] ?? null);
|
||||
self::assertSame('address-book', $routeByPath['adressbook'] ?? null);
|
||||
}
|
||||
|
||||
public function testBookmarksModuleManifestIsValid(): void
|
||||
{
|
||||
$modulesDir = dirname(__DIR__, 2) . '/modules';
|
||||
$manifestFile = $modulesDir . '/bookmarks/module.php';
|
||||
|
||||
self::assertFileExists($manifestFile, 'Bookmarks module manifest must exist');
|
||||
|
||||
$raw = require $manifestFile;
|
||||
self::assertIsArray($raw);
|
||||
self::assertSame('bookmarks', $raw['id']);
|
||||
self::assertArrayHasKey('aside.tab_panel', $raw['ui_slots']);
|
||||
self::assertArrayHasKey('topbar.right_item', $raw['ui_slots']);
|
||||
self::assertArrayHasKey('layout.body_end_template', $raw['ui_slots']);
|
||||
self::assertArrayHasKey('layout.head_style', $raw['ui_slots']);
|
||||
self::assertArrayHasKey('runtime.component', $raw['ui_slots']);
|
||||
}
|
||||
|
||||
public function testModuleProviderClassesExist(): void
|
||||
{
|
||||
self::assertTrue(
|
||||
class_exists(\MintyPHP\Module\AddressBook\Providers\AddressBookSearchProvider::class),
|
||||
'AddressBookSearchProvider class must be autoloadable'
|
||||
);
|
||||
self::assertTrue(
|
||||
class_exists(\MintyPHP\Module\AddressBook\Providers\AddressBookLayoutProvider::class),
|
||||
'AddressBookLayoutProvider class must be autoloadable'
|
||||
);
|
||||
self::assertTrue(
|
||||
class_exists(\MintyPHP\Module\AddressBook\Providers\AddressBookSessionProvider::class),
|
||||
'AddressBookSessionProvider class must be autoloadable'
|
||||
);
|
||||
}
|
||||
|
||||
public function testProviderClassesImplementContracts(): void
|
||||
{
|
||||
self::assertInstanceOf(
|
||||
\MintyPHP\App\Module\Contracts\SearchResourceProvider::class,
|
||||
new \MintyPHP\Module\AddressBook\Providers\AddressBookSearchProvider()
|
||||
);
|
||||
self::assertInstanceOf(
|
||||
\MintyPHP\App\Module\Contracts\LayoutContextProvider::class,
|
||||
new \MintyPHP\Module\AddressBook\Providers\AddressBookLayoutProvider()
|
||||
);
|
||||
self::assertInstanceOf(
|
||||
\MintyPHP\App\Module\Contracts\SessionProvider::class,
|
||||
new \MintyPHP\Module\AddressBook\Providers\AddressBookSessionProvider()
|
||||
);
|
||||
}
|
||||
|
||||
public function testMissingDependencyThrowsException(): void
|
||||
{
|
||||
$fixturesDir = sys_get_temp_dir() . '/module-dep-test-' . uniqid();
|
||||
mkdir($fixturesDir . '/mod-dep', 0777, true);
|
||||
|
||||
file_put_contents($fixturesDir . '/mod-dep/module.php', '<?php return ' . var_export([
|
||||
'id' => 'mod-dep',
|
||||
'requires' => ['nonexistent-module'],
|
||||
], true) . ';');
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage("requires module 'nonexistent-module' which is not enabled");
|
||||
|
||||
try {
|
||||
ModuleRegistry::boot($fixturesDir, ['mod-dep']);
|
||||
} finally {
|
||||
@unlink($fixturesDir . '/mod-dep/module.php');
|
||||
@rmdir($fixturesDir . '/mod-dep');
|
||||
@rmdir($fixturesDir);
|
||||
}
|
||||
}
|
||||
|
||||
public function testCircularDependencyThrowsException(): void
|
||||
{
|
||||
$fixturesDir = sys_get_temp_dir() . '/module-circular-test-' . uniqid();
|
||||
mkdir($fixturesDir . '/mod-x', 0777, true);
|
||||
mkdir($fixturesDir . '/mod-y', 0777, true);
|
||||
|
||||
file_put_contents($fixturesDir . '/mod-x/module.php', '<?php return ' . var_export([
|
||||
'id' => 'mod-x',
|
||||
'requires' => ['mod-y'],
|
||||
], true) . ';');
|
||||
file_put_contents($fixturesDir . '/mod-y/module.php', '<?php return ' . var_export([
|
||||
'id' => 'mod-y',
|
||||
'requires' => ['mod-x'],
|
||||
], true) . ';');
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Circular module dependency');
|
||||
|
||||
try {
|
||||
ModuleRegistry::boot($fixturesDir, ['mod-x', 'mod-y']);
|
||||
} finally {
|
||||
@unlink($fixturesDir . '/mod-x/module.php');
|
||||
@unlink($fixturesDir . '/mod-y/module.php');
|
||||
@rmdir($fixturesDir . '/mod-x');
|
||||
@rmdir($fixturesDir . '/mod-y');
|
||||
@rmdir($fixturesDir);
|
||||
}
|
||||
}
|
||||
|
||||
public function testConflictDetectionIsStrict(): void
|
||||
{
|
||||
$fixturesDir = sys_get_temp_dir() . '/module-arch-test-' . uniqid();
|
||||
mkdir($fixturesDir . '/mod-a', 0777, true);
|
||||
mkdir($fixturesDir . '/mod-b', 0777, true);
|
||||
|
||||
file_put_contents($fixturesDir . '/mod-a/module.php', '<?php return ' . var_export([
|
||||
'id' => 'mod-a',
|
||||
'permissions' => [['key' => 'shared.perm', 'description' => 'Shared']],
|
||||
], true) . ';');
|
||||
file_put_contents($fixturesDir . '/mod-b/module.php', '<?php return ' . var_export([
|
||||
'id' => 'mod-b',
|
||||
'permissions' => [['key' => 'shared.perm', 'description' => 'Shared duplicate']],
|
||||
], true) . ';');
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Permission conflict');
|
||||
|
||||
try {
|
||||
ModuleRegistry::boot($fixturesDir, ['mod-a', 'mod-b']);
|
||||
} finally {
|
||||
array_map('unlink', glob($fixturesDir . '/mod-a/*') ?: []);
|
||||
array_map('unlink', glob($fixturesDir . '/mod-b/*') ?: []);
|
||||
rmdir($fixturesDir . '/mod-a');
|
||||
rmdir($fixturesDir . '/mod-b');
|
||||
rmdir($fixturesDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
tests/Architecture/ModuleRegistryFailFastContractTest.php
Normal file
56
tests/Architecture/ModuleRegistryFailFastContractTest.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use MintyPHP\App\Module\ModuleRegistry;
|
||||
use RuntimeException;
|
||||
|
||||
final class ModuleRegistryFailFastContractTest extends AbstractModuleRegistryContractTestCase
|
||||
{
|
||||
public function testMissingDependencyThrowsException(): void
|
||||
{
|
||||
$this->createModuleManifest('mod-dep', [
|
||||
'id' => 'mod-dep',
|
||||
'requires' => ['nonexistent-module'],
|
||||
]);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage("requires module 'nonexistent-module' which is not enabled");
|
||||
|
||||
ModuleRegistry::boot($this->fixturesDir, ['mod-dep']);
|
||||
}
|
||||
|
||||
public function testCircularDependencyThrowsException(): void
|
||||
{
|
||||
$this->createModuleManifest('mod-x', [
|
||||
'id' => 'mod-x',
|
||||
'requires' => ['mod-y'],
|
||||
]);
|
||||
$this->createModuleManifest('mod-y', [
|
||||
'id' => 'mod-y',
|
||||
'requires' => ['mod-x'],
|
||||
]);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Circular module dependency');
|
||||
|
||||
ModuleRegistry::boot($this->fixturesDir, ['mod-x', 'mod-y']);
|
||||
}
|
||||
|
||||
public function testPermissionConflictThrowsException(): void
|
||||
{
|
||||
$this->createModuleManifest('mod-a', [
|
||||
'id' => 'mod-a',
|
||||
'permissions' => [['key' => 'shared.perm', 'description' => 'Shared']],
|
||||
]);
|
||||
$this->createModuleManifest('mod-b', [
|
||||
'id' => 'mod-b',
|
||||
'permissions' => [['key' => 'shared.perm', 'description' => 'Shared duplicate']],
|
||||
]);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Permission conflict');
|
||||
|
||||
ModuleRegistry::boot($this->fixturesDir, ['mod-a', 'mod-b']);
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ ohne dass jede Aenderung immer die komplette Suite im Kopf behalten muss.
|
||||
|
||||
## Naechste Refactor-Kandidaten
|
||||
|
||||
- `tests/Architecture/ModuleRegistryContractTest.php`
|
||||
- `tests/Architecture/DetailValidationSummaryContractTest.php`
|
||||
- `tests/Architecture/DetailActionPolicyContractTest.php`
|
||||
- `tests/Architecture/ContainerFactoryContractTest.php`
|
||||
- `tests/Architecture/SecurityLoggingContractTest.php`
|
||||
|
||||
Reference in New Issue
Block a user