Split module structure contracts
This commit is contained in:
255
tests/Architecture/ModuleUiContractTest.php
Normal file
255
tests/Architecture/ModuleUiContractTest.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
final class ModuleUiContractTest extends AbstractModuleStructureContractTestCase
|
||||
{
|
||||
public function testUiSlotAbilitiesMatchPolicySupports(): void
|
||||
{
|
||||
foreach (self::$modules as $module) {
|
||||
$supportedAbilities = [];
|
||||
foreach ($module['manifest']->authorizationPolicies as $policyClass) {
|
||||
if (!class_exists($policyClass)) {
|
||||
continue;
|
||||
}
|
||||
$reflection = new \ReflectionClass($policyClass);
|
||||
foreach ($reflection->getConstants() as $constValue) {
|
||||
if (is_string($constValue)) {
|
||||
$supportedAbilities[] = $constValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($supportedAbilities === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($module['manifest']->uiSlots as $slotName => $contributions) {
|
||||
$contributionList = is_array($contributions) ? $contributions : [$contributions];
|
||||
foreach ($contributionList as $idx => $contribution) {
|
||||
if (!is_array($contribution)) {
|
||||
continue;
|
||||
}
|
||||
$ability = trim((string) ($contribution['permission'] ?? ''));
|
||||
if ($ability === '') {
|
||||
continue;
|
||||
}
|
||||
self::assertContains(
|
||||
$ability,
|
||||
$supportedAbilities,
|
||||
"Module '{$module['id']}' ui_slots['{$slotName}'][{$idx}] permission '{$ability}' "
|
||||
. "is not declared as a constant on any module authorization policy"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testPermissionsUseObjectContract(): void
|
||||
{
|
||||
foreach (self::$modules as $module) {
|
||||
foreach ($module['manifest']->permissions as $index => $permission) {
|
||||
self::assertArrayHasKey('key', $permission, "Module '{$module['id']}' permissions[{$index}] missing key");
|
||||
self::assertArrayHasKey('description', $permission, "Module '{$module['id']}' permissions[{$index}] missing description");
|
||||
self::assertNotEmpty(trim((string) $permission['key']), "Module '{$module['id']}' permissions[{$index}] key must be non-empty");
|
||||
self::assertNotEmpty(trim((string) $permission['description']), "Module '{$module['id']}' permissions[{$index}] description must be non-empty");
|
||||
self::assertContains((int) $permission['active'], [0, 1], "Module '{$module['id']}' permissions[{$index}] active must be 0|1");
|
||||
self::assertContains((int) $permission['is_system'], [0, 1], "Module '{$module['id']}' permissions[{$index}] is_system must be 0|1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testSchedulerJobsUseFullContractSchema(): void
|
||||
{
|
||||
foreach (self::$modules as $module) {
|
||||
foreach ($module['manifest']->schedulerJobs as $index => $job) {
|
||||
$required = [
|
||||
'job_key',
|
||||
'handler',
|
||||
'label',
|
||||
'description',
|
||||
'default_enabled',
|
||||
'default_timezone',
|
||||
'default_schedule_type',
|
||||
'default_schedule_interval',
|
||||
'default_schedule_time',
|
||||
'default_schedule_weekdays_csv',
|
||||
'default_catchup_once',
|
||||
'allowed_schedule_types',
|
||||
];
|
||||
|
||||
foreach ($required as $requiredKey) {
|
||||
self::assertArrayHasKey(
|
||||
$requiredKey,
|
||||
$job,
|
||||
"Module '{$module['id']}' scheduler_jobs[{$index}] missing '{$requiredKey}'"
|
||||
);
|
||||
}
|
||||
|
||||
self::assertNotEmpty(trim((string) $job['job_key']), "Module '{$module['id']}' scheduler_jobs[{$index}] job_key must be non-empty");
|
||||
self::assertNotEmpty(trim((string) $job['handler']), "Module '{$module['id']}' scheduler_jobs[{$index}] handler must be non-empty");
|
||||
self::assertNotEmpty(trim((string) $job['label']), "Module '{$module['id']}' scheduler_jobs[{$index}] label must be non-empty");
|
||||
self::assertContains(
|
||||
(string) $job['default_schedule_type'],
|
||||
['hourly', 'daily', 'weekly'],
|
||||
"Module '{$module['id']}' scheduler_jobs[{$index}] default_schedule_type invalid"
|
||||
);
|
||||
|
||||
$allowedTypes = array_values($job['allowed_schedule_types']);
|
||||
self::assertNotEmpty($allowedTypes, "Module '{$module['id']}' scheduler_jobs[{$index}] allowed_schedule_types must be non-empty");
|
||||
foreach ($allowedTypes as $allowedType) {
|
||||
self::assertContains(
|
||||
(string) $allowedType,
|
||||
['hourly', 'daily', 'weekly'],
|
||||
"Module '{$module['id']}' scheduler_jobs[{$index}] allowed_schedule_types entry invalid"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self::addToAssertionCount(1);
|
||||
}
|
||||
|
||||
public function testUiSlotContributionsHaveRequiredKeys(): void
|
||||
{
|
||||
$slotKeyRequirements = [
|
||||
'aside.tab_panel' => ['key', 'label', 'icon', 'permission', 'panel_template'],
|
||||
'search.resource_item' => ['key', 'label', 'base_url', 'permission'],
|
||||
'user.edit.aside_action' => ['key', 'type', 'label', 'permission'],
|
||||
'topbar.right_item' => ['key', 'template'],
|
||||
'layout.body_end_template' => ['key', 'template'],
|
||||
'layout.head_style' => ['key', 'path'],
|
||||
'runtime.component' => ['key', 'script'],
|
||||
];
|
||||
|
||||
foreach (self::$modules as $module) {
|
||||
foreach ($module['manifest']->uiSlots as $slotName => $contributions) {
|
||||
if (!isset($slotKeyRequirements[$slotName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$required = $slotKeyRequirements[$slotName];
|
||||
$contributionList = is_array($contributions) ? $contributions : [$contributions];
|
||||
|
||||
foreach ($contributionList as $idx => $contribution) {
|
||||
if (!is_array($contribution)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($required as $requiredKey) {
|
||||
self::assertArrayHasKey(
|
||||
$requiredKey,
|
||||
$contribution,
|
||||
"Module '{$module['id']}' ui_slots['{$slotName}'][{$idx}] is missing required key '{$requiredKey}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testPanelTemplatesExist(): void
|
||||
{
|
||||
foreach (self::$modules as $module) {
|
||||
$templateSlots = [
|
||||
'aside.tab_panel' => 'panel_template',
|
||||
'topbar.right_item' => 'template',
|
||||
'layout.body_end_template' => 'template',
|
||||
];
|
||||
|
||||
foreach ($templateSlots as $slotName => $templateKey) {
|
||||
$contributions = $module['manifest']->uiSlots[$slotName] ?? [];
|
||||
if (!is_array($contributions)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($contributions as $contribution) {
|
||||
if (!is_array($contribution)) {
|
||||
continue;
|
||||
}
|
||||
$template = $contribution[$templateKey] ?? null;
|
||||
if ($template === null) {
|
||||
continue;
|
||||
}
|
||||
$templatePath = (string) $template;
|
||||
if (!str_starts_with($templatePath, '/')) {
|
||||
$templatePath = $module['manifest']->basePath . '/' . $templatePath;
|
||||
}
|
||||
self::assertFileExists(
|
||||
$templatePath,
|
||||
"Module '{$module['id']}' {$slotName} {$templateKey} does not exist: {$templatePath}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testRuntimeComponentPhasesAreValid(): void
|
||||
{
|
||||
foreach (self::$modules as $module) {
|
||||
$runtimeComponents = $module['manifest']->uiSlots['runtime.component'] ?? [];
|
||||
if (!is_array($runtimeComponents)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($runtimeComponents as $index => $contribution) {
|
||||
if (!is_array($contribution)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$phase = strtolower(trim((string) ($contribution['phase'] ?? 'late')));
|
||||
self::assertContains(
|
||||
$phase,
|
||||
['early', 'default', 'late'],
|
||||
"Module '{$module['id']}' runtime.component[{$index}] has invalid phase '{$phase}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testSessionProvidersUseModulePrefixedKeys(): void
|
||||
{
|
||||
foreach (self::$modules as $module) {
|
||||
foreach ($module['manifest']->sessionProviders as $providerClass) {
|
||||
if (!class_exists($providerClass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reflection = new \ReflectionClass($providerClass);
|
||||
$file = $reflection->getFileName();
|
||||
if ($file === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = file_get_contents($file);
|
||||
if ($content === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$expectedPrefix = 'module.' . $module['id'] . '.';
|
||||
|
||||
if (preg_match_all('/\$_SESSION\[\'([^\']+)\'\]/', $content, $matches)) {
|
||||
foreach ($matches[1] as $key) {
|
||||
self::assertStringStartsWith(
|
||||
$expectedPrefix,
|
||||
$key,
|
||||
"Module '{$module['id']}' session provider '{$providerClass}' uses session key "
|
||||
. "'{$key}' which does not follow the '{$expectedPrefix}*' convention"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match_all('/->set\(\s*\'([^\']+)\'/', $content, $matches)) {
|
||||
foreach ($matches[1] as $key) {
|
||||
self::assertStringStartsWith(
|
||||
$expectedPrefix,
|
||||
$key,
|
||||
"Module '{$module['id']}' session provider '{$providerClass}' uses session store key "
|
||||
. "'{$key}' which does not follow the '{$expectedPrefix}*' convention"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self::addToAssertionCount(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user