php-framework/tests/Feature/AdminMenuRegistryTest.php

285 lines
9.1 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace Core\Tests\Feature;
use Core\Front\Admin\AdminMenuRegistry;
Fix critical and high severity issues from code review Security fixes: - Fix XSS in JSON-LD output via JSON_HEX_TAG (Seo module) - Fix SQL injection via LIKE wildcards (Config module) - Fix regex injection in env updates (Console module) - Fix weak token hashing with HMAC-SHA256 (CDN module) - Mask database credentials in install output (Console module) New features: - Add MakeModCommand, MakePlugCommand, MakeWebsiteCommand scaffolds - Add event prioritization via array syntax in $listens - Add EventAuditLog for tracking handler execution and failures - Add ServiceVersion with semver and deprecation support - Add HealthCheckable interface with HealthCheckResult - Add ServiceStatus enum for service health states - Add DynamicMenuProvider for uncached menu items - Add LangServiceProvider with auto-discovery and fallback chains Improvements: - Add retry logic with exponential backoff (CDN uploads) - Add file size validation before uploads (100MB default) - Add key rotation mechanism for LthnHash - Add Unicode NFC normalization to Sanitiser - Add configurable filter rules per field (Input) - Add menu caching with configurable TTL (Admin) - Add Redis fallback alerting via events (Storage) - Add Predis support alongside phpredis (Storage) - Add memory safety checks for image processing (Media) - Add SchemaValidator for schema.org validation (SEO) - Add translation key validation in dev environments Bug fixes: - Fix nested array filtering returning null (Sanitiser) - Fix race condition in EmailShieldStat increment - Fix stack overflow on deep JSON nesting (ConfigResolver) - Fix missing table existence check (BlocklistService) - Fix missing class_exists guards (Search, Media) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 20:20:14 +00:00
use Core\Front\Admin\Concerns\HasMenuPermissions;
use Core\Front\Admin\Contracts\AdminMenuProvider;
use Core\Tenant\Services\EntitlementService;
use Core\Tests\TestCase;
use Mockery;
class AdminMenuRegistryTest extends TestCase
{
protected AdminMenuRegistry $registry;
protected EntitlementService $entitlements;
protected function setUp(): void
{
parent::setUp();
$this->entitlements = Mockery::mock(EntitlementService::class);
$this->registry = new AdminMenuRegistry($this->entitlements);
Fix critical and high severity issues from code review Security fixes: - Fix XSS in JSON-LD output via JSON_HEX_TAG (Seo module) - Fix SQL injection via LIKE wildcards (Config module) - Fix regex injection in env updates (Console module) - Fix weak token hashing with HMAC-SHA256 (CDN module) - Mask database credentials in install output (Console module) New features: - Add MakeModCommand, MakePlugCommand, MakeWebsiteCommand scaffolds - Add event prioritization via array syntax in $listens - Add EventAuditLog for tracking handler execution and failures - Add ServiceVersion with semver and deprecation support - Add HealthCheckable interface with HealthCheckResult - Add ServiceStatus enum for service health states - Add DynamicMenuProvider for uncached menu items - Add LangServiceProvider with auto-discovery and fallback chains Improvements: - Add retry logic with exponential backoff (CDN uploads) - Add file size validation before uploads (100MB default) - Add key rotation mechanism for LthnHash - Add Unicode NFC normalization to Sanitiser - Add configurable filter rules per field (Input) - Add menu caching with configurable TTL (Admin) - Add Redis fallback alerting via events (Storage) - Add Predis support alongside phpredis (Storage) - Add memory safety checks for image processing (Media) - Add SchemaValidator for schema.org validation (SEO) - Add translation key validation in dev environments Bug fixes: - Fix nested array filtering returning null (Sanitiser) - Fix race condition in EmailShieldStat increment - Fix stack overflow on deep JSON nesting (ConfigResolver) - Fix missing table existence check (BlocklistService) - Fix missing class_exists guards (Search, Media) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 20:20:14 +00:00
$this->registry->setCachingEnabled(false);
}
public function test_build_returns_empty_array_when_no_providers_registered(): void
{
$menu = $this->registry->build(null);
$this->assertIsArray($menu);
$this->assertEmpty($menu);
}
public function test_register_adds_provider(): void
{
$provider = $this->createMockProvider([
[
'group' => 'services',
'priority' => 10,
'item' => fn () => ['label' => 'Test Service', 'icon' => 'cog', 'href' => '/test'],
],
]);
$this->registry->register($provider);
$menu = $this->registry->build(null);
$this->assertNotEmpty($menu);
}
public function test_build_groups_items_into_predefined_groups(): void
{
$provider = $this->createMockProvider([
[
'group' => 'dashboard',
'priority' => 10,
'item' => fn () => ['label' => 'Dashboard', 'icon' => 'home', 'href' => '/'],
],
[
'group' => 'services',
'priority' => 10,
'item' => fn () => ['label' => 'Commerce', 'icon' => 'cart', 'href' => '/commerce'],
],
]);
$this->registry->register($provider);
$menu = $this->registry->build(null);
// Dashboard is standalone, so items appear directly
// Services becomes a dropdown parent
$this->assertNotEmpty($menu);
}
public function test_build_sorts_items_by_priority(): void
{
$provider = $this->createMockProvider([
[
'group' => 'dashboard',
'priority' => 30,
'item' => fn () => ['label' => 'Third', 'icon' => 'cog', 'href' => '/third'],
],
[
'group' => 'dashboard',
'priority' => 10,
'item' => fn () => ['label' => 'First', 'icon' => 'home', 'href' => '/first'],
],
[
'group' => 'dashboard',
'priority' => 20,
'item' => fn () => ['label' => 'Second', 'icon' => 'star', 'href' => '/second'],
],
]);
$this->registry->register($provider);
$menu = $this->registry->build(null);
// Dashboard is standalone, items should be sorted by priority
$labels = array_column($menu, 'label');
$this->assertEquals(['First', 'Second', 'Third'], $labels);
}
public function test_build_skips_items_returning_null(): void
{
$provider = $this->createMockProvider([
[
'group' => 'dashboard',
'priority' => 10,
'item' => fn () => ['label' => 'Visible', 'icon' => 'eye', 'href' => '/visible'],
],
[
'group' => 'dashboard',
'priority' => 20,
'item' => fn () => null,
],
]);
$this->registry->register($provider);
$menu = $this->registry->build(null);
$this->assertCount(1, $menu);
$this->assertEquals('Visible', $menu[0]['label']);
}
public function test_get_groups_returns_predefined_group_keys(): void
{
$groups = $this->registry->getGroups();
$this->assertIsArray($groups);
$this->assertContains('dashboard', $groups);
$this->assertContains('workspaces', $groups);
$this->assertContains('services', $groups);
$this->assertContains('settings', $groups);
$this->assertContains('admin', $groups);
}
public function test_get_group_config_returns_config_for_known_group(): void
{
$config = $this->registry->getGroupConfig('settings');
$this->assertIsArray($config);
$this->assertArrayHasKey('label', $config);
$this->assertEquals('Account', $config['label']);
}
public function test_get_group_config_returns_empty_for_unknown_group(): void
{
$config = $this->registry->getGroupConfig('nonexistent');
$this->assertIsArray($config);
$this->assertEmpty($config);
}
public function test_multiple_providers_can_be_registered(): void
{
$provider1 = $this->createMockProvider([
[
'group' => 'dashboard',
'priority' => 10,
'item' => fn () => ['label' => 'From Provider 1', 'icon' => 'one', 'href' => '/one'],
],
]);
$provider2 = $this->createMockProvider([
[
'group' => 'dashboard',
'priority' => 20,
'item' => fn () => ['label' => 'From Provider 2', 'icon' => 'two', 'href' => '/two'],
],
]);
$this->registry->register($provider1);
$this->registry->register($provider2);
$menu = $this->registry->build(null);
$labels = array_column($menu, 'label');
$this->assertContains('From Provider 1', $labels);
$this->assertContains('From Provider 2', $labels);
}
public function test_build_uses_default_priority_when_not_specified(): void
{
$provider = $this->createMockProvider([
[
'group' => 'dashboard',
'item' => fn () => ['label' => 'No Priority', 'icon' => 'default', 'href' => '/default'],
],
[
'group' => 'dashboard',
'priority' => 100,
'item' => fn () => ['label' => 'Low Priority', 'icon' => 'low', 'href' => '/low'],
],
[
'group' => 'dashboard',
'priority' => 10,
'item' => fn () => ['label' => 'High Priority', 'icon' => 'high', 'href' => '/high'],
],
]);
$this->registry->register($provider);
$menu = $this->registry->build(null);
// Default priority is 50, so order should be: High (10), No (50), Low (100)
$labels = array_column($menu, 'label');
$this->assertEquals(['High Priority', 'No Priority', 'Low Priority'], $labels);
}
public function test_build_adds_dividers_between_groups(): void
{
$provider = $this->createMockProvider([
[
'group' => 'dashboard',
'priority' => 10,
'item' => fn () => ['label' => 'Dashboard Item', 'icon' => 'home', 'href' => '/'],
],
[
'group' => 'services',
'priority' => 10,
'item' => fn () => ['label' => 'Service Item', 'icon' => 'cog', 'href' => '/service'],
],
]);
$this->registry->register($provider);
$menu = $this->registry->build(null);
// Should have a divider between dashboard and services
$hasDivider = false;
foreach ($menu as $item) {
if (isset($item['divider']) && $item['divider'] === true) {
$hasDivider = true;
break;
}
}
$this->assertTrue($hasDivider);
}
public function test_build_creates_dropdown_for_non_standalone_groups(): void
{
$provider = $this->createMockProvider([
[
'group' => 'settings',
'priority' => 10,
'item' => fn () => ['label' => 'Profile', 'icon' => 'user', 'href' => '/profile'],
],
[
'group' => 'settings',
'priority' => 20,
'item' => fn () => ['label' => 'Security', 'icon' => 'lock', 'href' => '/security'],
],
]);
$this->registry->register($provider);
$menu = $this->registry->build(null);
// Settings is not standalone, should create a dropdown
$settingsDropdown = null;
foreach ($menu as $item) {
if (isset($item['label']) && $item['label'] === 'Account') {
$settingsDropdown = $item;
break;
}
}
$this->assertNotNull($settingsDropdown);
$this->assertArrayHasKey('children', $settingsDropdown);
$this->assertCount(2, $settingsDropdown['children']);
}
protected function createMockProvider(array $items): AdminMenuProvider
{
return new class($items) implements AdminMenuProvider
{
Fix critical and high severity issues from code review Security fixes: - Fix XSS in JSON-LD output via JSON_HEX_TAG (Seo module) - Fix SQL injection via LIKE wildcards (Config module) - Fix regex injection in env updates (Console module) - Fix weak token hashing with HMAC-SHA256 (CDN module) - Mask database credentials in install output (Console module) New features: - Add MakeModCommand, MakePlugCommand, MakeWebsiteCommand scaffolds - Add event prioritization via array syntax in $listens - Add EventAuditLog for tracking handler execution and failures - Add ServiceVersion with semver and deprecation support - Add HealthCheckable interface with HealthCheckResult - Add ServiceStatus enum for service health states - Add DynamicMenuProvider for uncached menu items - Add LangServiceProvider with auto-discovery and fallback chains Improvements: - Add retry logic with exponential backoff (CDN uploads) - Add file size validation before uploads (100MB default) - Add key rotation mechanism for LthnHash - Add Unicode NFC normalization to Sanitiser - Add configurable filter rules per field (Input) - Add menu caching with configurable TTL (Admin) - Add Redis fallback alerting via events (Storage) - Add Predis support alongside phpredis (Storage) - Add memory safety checks for image processing (Media) - Add SchemaValidator for schema.org validation (SEO) - Add translation key validation in dev environments Bug fixes: - Fix nested array filtering returning null (Sanitiser) - Fix race condition in EmailShieldStat increment - Fix stack overflow on deep JSON nesting (ConfigResolver) - Fix missing table existence check (BlocklistService) - Fix missing class_exists guards (Search, Media) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 20:20:14 +00:00
use HasMenuPermissions;
public function __construct(private array $items) {}
public function adminMenuItems(): array
{
return $this->items;
}
};
}
}