1
0

Split module structure contracts

This commit is contained in:
2026-03-19 18:30:14 +01:00
parent 1e993e470c
commit ee5fdaaff7
8 changed files with 683 additions and 711 deletions

View File

@@ -0,0 +1,142 @@
<?php
namespace MintyPHP\Tests\Architecture;
final class ModuleManifestContractTest extends AbstractModuleStructureContractTestCase
{
public function testAtLeastOneModuleExists(): void
{
self::assertNotEmpty(self::$modules, 'Expected at least one module under modules/');
}
public function testEveryModuleHasManifest(): void
{
$modulesDir = dirname(__DIR__, 2) . '/modules';
if (!is_dir($modulesDir)) {
self::markTestSkipped('No modules/ directory');
}
$entries = scandir($modulesDir) ?: [];
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..' || !is_dir($modulesDir . '/' . $entry)) {
continue;
}
self::assertFileExists(
$modulesDir . '/' . $entry . '/module.php',
"Module directory '{$entry}' is missing module.php manifest"
);
}
}
public function testManifestIdMatchesDirectoryName(): void
{
foreach (self::$modules as $module) {
self::assertSame(
$module['id'],
$module['manifest']->id,
"Module directory '{$module['id']}' has mismatched manifest id '{$module['manifest']->id}'"
);
}
}
public function testManifestHasVersion(): void
{
foreach (self::$modules as $module) {
self::assertMatchesRegularExpression(
'/^\d+\.\d+\.\d+$/',
$module['manifest']->version,
"Module '{$module['id']}' must have a semver version (got '{$module['manifest']->version}')"
);
}
}
public function testManifestHasAllRequiredKeys(): void
{
$requiredKeys = [
'id', 'version', 'enabled_by_default', 'load_order',
'routes', 'public_paths', 'container_registrars',
'ui_slots', 'search_resources', 'asset_groups',
'scheduler_jobs', 'layout_context_providers',
'session_providers', 'permissions',
];
foreach (self::$modules as $module) {
foreach ($requiredKeys as $key) {
self::assertArrayHasKey(
$key,
$module['raw'],
"Module '{$module['id']}' manifest is missing required key '{$key}'"
);
}
}
}
public function testRequiresFieldContainsOnlyValidModuleIds(): void
{
foreach (self::$modules as $module) {
foreach ($module['manifest']->requires as $index => $requiredId) {
self::assertIsString($requiredId);
self::assertNotEmpty(
trim($requiredId),
"Module '{$module['id']}' requires[{$index}] must be a non-empty string"
);
self::assertMatchesRegularExpression(
'/^[a-z][a-z0-9_-]*$/',
$requiredId,
"Module '{$module['id']}' requires[{$index}] has invalid module id format: '{$requiredId}'"
);
}
}
self::addToAssertionCount(1);
}
public function testModuleHasLibDirectory(): void
{
foreach (self::$modules as $module) {
self::assertDirectoryExists(
$module['path'] . '/lib',
"Module '{$module['id']}' must have a lib/ directory for PHP classes"
);
}
}
public function testRoutesHavePathAndTarget(): void
{
foreach (self::$modules as $module) {
foreach ($module['manifest']->routes as $idx => $route) {
self::assertArrayHasKey(
'path',
$route,
"Module '{$module['id']}' route[{$idx}] is missing 'path'"
);
self::assertArrayHasKey(
'target',
$route,
"Module '{$module['id']}' route[{$idx}] is missing 'target'"
);
self::assertNotEmpty(
trim((string) $route['path']),
"Module '{$module['id']}' route[{$idx}] has empty 'path'"
);
self::assertNotEmpty(
trim((string) $route['target']),
"Module '{$module['id']}' route[{$idx}] has empty 'target'"
);
}
}
}
public function testModulesWithRoutesHavePagesDirectory(): void
{
foreach (self::$modules as $module) {
if (empty($module['manifest']->routes)) {
continue;
}
self::assertDirectoryExists(
$module['path'] . '/pages',
"Module '{$module['id']}' declares routes but has no pages/ directory"
);
}
}
}