- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
332 lines
12 KiB
PHP
332 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Application\LiveComponents\CardComponent;
|
|
use App\Application\LiveComponents\ContainerComponent;
|
|
use App\Application\LiveComponents\LayoutComponent;
|
|
use App\Application\LiveComponents\ModalComponent;
|
|
use App\Framework\LiveComponents\SlotManager;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentState;
|
|
use App\Framework\LiveComponents\ValueObjects\SlotContent;
|
|
|
|
describe('Slot System Integration', function () {
|
|
beforeEach(function () {
|
|
$this->slotManager = new SlotManager();
|
|
});
|
|
|
|
describe('CardComponent', function () {
|
|
it('renders with custom header, body and footer slots', function () {
|
|
$component = new CardComponent(
|
|
id: ComponentId::generate(),
|
|
state: ComponentState::fromArray([])
|
|
);
|
|
|
|
$providedSlots = [
|
|
SlotContent::named('header', '<h2>User Profile</h2>'),
|
|
SlotContent::named('body', '<p>User details...</p>'),
|
|
SlotContent::named('footer', '<button>Edit</button>'),
|
|
];
|
|
|
|
// Validate slots
|
|
$errors = $this->slotManager->validateSlots($component, $providedSlots);
|
|
expect($errors)->toBeEmpty();
|
|
|
|
// Resolve each slot
|
|
$definitions = $component->getSlotDefinitions();
|
|
|
|
$headerContent = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definitions[0], // header
|
|
$providedSlots
|
|
);
|
|
|
|
$bodyContent = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definitions[1], // body
|
|
$providedSlots
|
|
);
|
|
|
|
$footerContent = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definitions[2], // footer
|
|
$providedSlots
|
|
);
|
|
|
|
expect($headerContent)->toContain('User Profile');
|
|
expect($bodyContent)->toContain('User details');
|
|
expect($footerContent)->toContain('Edit');
|
|
});
|
|
|
|
it('validates that body slot is required', function () {
|
|
$component = new CardComponent(
|
|
id: ComponentId::generate(),
|
|
state: ComponentState::fromArray([])
|
|
);
|
|
|
|
// Only provide header, skip required body
|
|
$providedSlots = [
|
|
SlotContent::named('header', '<h2>Header</h2>'),
|
|
];
|
|
|
|
$errors = $this->slotManager->validateSlots($component, $providedSlots);
|
|
|
|
expect($errors)->toContain("Required slot 'body' is not filled");
|
|
});
|
|
|
|
it('uses default header when not provided', function () {
|
|
$component = new CardComponent(
|
|
id: ComponentId::generate(),
|
|
state: ComponentState::fromArray([])
|
|
);
|
|
|
|
$providedSlots = [
|
|
SlotContent::named('body', '<p>Body content</p>'),
|
|
];
|
|
|
|
$definitions = $component->getSlotDefinitions();
|
|
|
|
$headerContent = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definitions[0], // header
|
|
$providedSlots
|
|
);
|
|
|
|
expect($headerContent)->toContain('card-header-default');
|
|
});
|
|
});
|
|
|
|
describe('ModalComponent', function () {
|
|
it('renders with scoped context in content and actions slots', function () {
|
|
$modalId = ComponentId::generate();
|
|
|
|
$component = new ModalComponent(
|
|
id: $modalId,
|
|
state: ComponentState::fromArray(['isOpen' => true])
|
|
);
|
|
|
|
$providedSlots = [
|
|
SlotContent::named('title', '<h3>Confirm</h3>'),
|
|
SlotContent::named('content', '<p>Modal ID: {context.modalId}</p>'),
|
|
SlotContent::named('actions', '<button onclick="{context.closeFunction}">Close</button>'),
|
|
];
|
|
|
|
$definitions = $component->getSlotDefinitions();
|
|
|
|
$contentSlot = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definitions[1], // content (scoped)
|
|
$providedSlots
|
|
);
|
|
|
|
$actionsSlot = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definitions[2], // actions (scoped)
|
|
$providedSlots
|
|
);
|
|
|
|
// Check that context was injected
|
|
expect($contentSlot)->toContain($modalId->toString());
|
|
expect($actionsSlot)->toContain("closeModal('{$modalId->toString()}')");
|
|
});
|
|
|
|
it('validates that content slot is required', function () {
|
|
$component = new ModalComponent(
|
|
id: ComponentId::generate(),
|
|
state: ComponentState::fromArray([])
|
|
);
|
|
|
|
// Only provide title, skip required content
|
|
$providedSlots = [
|
|
SlotContent::named('title', '<h3>Title</h3>'),
|
|
];
|
|
|
|
$errors = $this->slotManager->validateSlots($component, $providedSlots);
|
|
|
|
expect($errors)->toContain("Required slot 'content' is not filled");
|
|
});
|
|
});
|
|
|
|
describe('LayoutComponent', function () {
|
|
it('renders with sidebar, main, header and footer slots', function () {
|
|
$component = new LayoutComponent(
|
|
id: ComponentId::generate(),
|
|
state: ComponentState::fromArray([
|
|
'sidebarWidth' => '300px',
|
|
'sidebarCollapsed' => false,
|
|
])
|
|
);
|
|
|
|
$providedSlots = [
|
|
SlotContent::named('header', '<header>App Header</header>'),
|
|
SlotContent::named('sidebar', '<nav>Sidebar (width: {context.sidebarWidth})</nav>'),
|
|
SlotContent::named('main', '<main>Main content</main>'),
|
|
SlotContent::named('footer', '<footer>Footer</footer>'),
|
|
];
|
|
|
|
$errors = $this->slotManager->validateSlots($component, $providedSlots);
|
|
expect($errors)->toBeEmpty();
|
|
|
|
$definitions = $component->getSlotDefinitions();
|
|
|
|
$sidebarContent = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definitions[0], // sidebar (scoped)
|
|
$providedSlots
|
|
);
|
|
|
|
// Check scoped context injection
|
|
expect($sidebarContent)->toContain('300px');
|
|
});
|
|
|
|
it('validates that main slot is required', function () {
|
|
$component = new LayoutComponent(
|
|
id: ComponentId::generate(),
|
|
state: ComponentState::fromArray([])
|
|
);
|
|
|
|
$providedSlots = [
|
|
SlotContent::named('sidebar', '<nav>Sidebar</nav>'),
|
|
];
|
|
|
|
$errors = $this->slotManager->validateSlots($component, $providedSlots);
|
|
|
|
expect($errors)->toContain("Required slot 'main' is not filled");
|
|
});
|
|
});
|
|
|
|
describe('ContainerComponent', function () {
|
|
it('renders with default slot and actions', function () {
|
|
$component = new ContainerComponent(
|
|
id: ComponentId::generate(),
|
|
state: ComponentState::fromArray(['padding' => 'large'])
|
|
);
|
|
|
|
$providedSlots = [
|
|
SlotContent::default('<h1>Welcome</h1><p>Content</p>'),
|
|
SlotContent::named('title', '<h2>Container</h2>'),
|
|
SlotContent::named('actions', '<button>Save</button>'),
|
|
];
|
|
|
|
$errors = $this->slotManager->validateSlots($component, $providedSlots);
|
|
expect($errors)->toBeEmpty();
|
|
|
|
$definitions = $component->getSlotDefinitions();
|
|
|
|
$defaultContent = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definitions[0], // default
|
|
$providedSlots
|
|
);
|
|
|
|
expect($defaultContent)->toContain('Welcome');
|
|
expect($defaultContent)->toContain('container-padding-large');
|
|
});
|
|
|
|
it('uses default content when default slot is not provided', function () {
|
|
$component = new ContainerComponent(
|
|
id: ComponentId::generate(),
|
|
state: ComponentState::fromArray([])
|
|
);
|
|
|
|
$providedSlots = [
|
|
SlotContent::named('title', '<h2>Container</h2>'),
|
|
];
|
|
|
|
$definitions = $component->getSlotDefinitions();
|
|
|
|
$defaultContent = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definitions[0], // default
|
|
$providedSlots
|
|
);
|
|
|
|
expect($defaultContent)->toContain('empty-container');
|
|
});
|
|
});
|
|
|
|
describe('Slot Content Processing', function () {
|
|
it('processes slot content through component hook', function () {
|
|
$component = new CardComponent(
|
|
id: ComponentId::generate(),
|
|
state: ComponentState::fromArray([])
|
|
);
|
|
|
|
$providedSlots = [
|
|
SlotContent::named('header', '<h2>Header</h2>'),
|
|
SlotContent::named('body', '<p>Body</p>'),
|
|
];
|
|
|
|
$definitions = $component->getSlotDefinitions();
|
|
|
|
// Header gets wrapped in div.card-header
|
|
$headerContent = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definitions[0],
|
|
$providedSlots
|
|
);
|
|
|
|
expect($headerContent)->toContain('<div class="card-header">');
|
|
expect($headerContent)->toContain('</div>');
|
|
|
|
// Body gets wrapped in div.card-body
|
|
$bodyContent = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definitions[1],
|
|
$providedSlots
|
|
);
|
|
|
|
expect($bodyContent)->toContain('<div class="card-body">');
|
|
});
|
|
});
|
|
|
|
describe('XSS Protection', function () {
|
|
it('escapes HTML in scoped context values', function () {
|
|
$component = new ModalComponent(
|
|
id: ComponentId::generate(),
|
|
state: ComponentState::fromArray([])
|
|
);
|
|
|
|
// Try to inject script via slot content
|
|
$providedSlots = [
|
|
SlotContent::named('content', '<p>{context.modalId}</p>'),
|
|
];
|
|
|
|
$definitions = $component->getSlotDefinitions();
|
|
|
|
$content = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definitions[1], // content (scoped)
|
|
$providedSlots
|
|
);
|
|
|
|
// modalId is a ComponentId, which gets htmlspecialchars treatment
|
|
// Check that HTML entities are properly escaped
|
|
expect($content)->not->toContain('<script>');
|
|
});
|
|
});
|
|
|
|
describe('Slot Statistics', function () {
|
|
it('tracks slot registration statistics', function () {
|
|
$componentId1 = ComponentId::generate();
|
|
$componentId2 = ComponentId::generate();
|
|
|
|
$this->slotManager->registerSlotContents($componentId1, [
|
|
SlotContent::named('header', '<h1>Header</h1>'),
|
|
SlotContent::named('body', '<p>Body</p>'),
|
|
]);
|
|
|
|
$this->slotManager->registerSlotContents($componentId2, [
|
|
SlotContent::named('footer', '<footer>Footer</footer>'),
|
|
]);
|
|
|
|
$stats = $this->slotManager->getStats();
|
|
|
|
expect($stats['total_components_with_slots'])->toBe(2);
|
|
expect($stats['total_slot_contents'])->toBe(3);
|
|
expect($stats['avg_slots_per_component'])->toBe(1.5);
|
|
});
|
|
});
|
|
});
|