1
0
Files
breadcrumb-the-shire/tests/Architecture/ModuleUiContractTest.php

256 lines
11 KiB
PHP
Raw Normal View History

2026-03-19 18:30:14 +01:00
<?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);
}
}