- 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.
424 lines
13 KiB
PHP
424 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\LiveComponents\Contracts\SupportsSlots;
|
|
use App\Framework\LiveComponents\SlotManager;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
|
use App\Framework\LiveComponents\ValueObjects\SlotContent;
|
|
use App\Framework\LiveComponents\ValueObjects\SlotContext;
|
|
use App\Framework\LiveComponents\ValueObjects\SlotDefinition;
|
|
|
|
describe('SlotManager', function () {
|
|
beforeEach(function () {
|
|
$this->slotManager = new SlotManager();
|
|
});
|
|
|
|
it('registers and retrieves slot contents', function () {
|
|
$componentId = ComponentId::generate();
|
|
$contents = [
|
|
SlotContent::named('header', '<h1>Header</h1>'),
|
|
SlotContent::named('body', '<p>Body</p>'),
|
|
];
|
|
|
|
$this->slotManager->registerSlotContents($componentId, $contents);
|
|
|
|
$retrieved = $this->slotManager->getSlotContents($componentId);
|
|
|
|
expect($retrieved)->toHaveCount(2);
|
|
expect($retrieved[0]->slotName)->toBe('header');
|
|
expect($retrieved[1]->slotName)->toBe('body');
|
|
});
|
|
|
|
it('resolves provided content over default content', function () {
|
|
$component = new class () implements SupportsSlots {
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::named('header', '<h2>Default Header</h2>'),
|
|
];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
return SlotContext::empty();
|
|
}
|
|
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
return $content;
|
|
}
|
|
|
|
public function validateSlots(array $providedSlots): array
|
|
{
|
|
return [];
|
|
}
|
|
};
|
|
|
|
$definition = SlotDefinition::named('header', '<h2>Default Header</h2>');
|
|
$providedContent = [
|
|
SlotContent::named('header', '<h1>Custom Header</h1>'),
|
|
];
|
|
|
|
$result = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definition,
|
|
$providedContent
|
|
);
|
|
|
|
expect($result)->toBe('<h1>Custom Header</h1>');
|
|
});
|
|
|
|
it('uses default content when no content provided', function () {
|
|
$component = new class () implements SupportsSlots {
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::named('header', '<h2>Default Header</h2>'),
|
|
];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
return SlotContext::empty();
|
|
}
|
|
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
return $content;
|
|
}
|
|
|
|
public function validateSlots(array $providedSlots): array
|
|
{
|
|
return [];
|
|
}
|
|
};
|
|
|
|
$definition = SlotDefinition::named('header', '<h2>Default Header</h2>');
|
|
$providedContent = [];
|
|
|
|
$result = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definition,
|
|
$providedContent
|
|
);
|
|
|
|
expect($result)->toBe('<h2>Default Header</h2>');
|
|
});
|
|
|
|
it('injects scoped context into slot content', function () {
|
|
$component = new class () implements SupportsSlots {
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::scoped('content', ['userId', 'userName']),
|
|
];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
return SlotContext::create([
|
|
'userId' => 123,
|
|
'userName' => 'John Doe',
|
|
]);
|
|
}
|
|
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
return $content;
|
|
}
|
|
|
|
public function validateSlots(array $providedSlots): array
|
|
{
|
|
return [];
|
|
}
|
|
};
|
|
|
|
$definition = SlotDefinition::scoped('content', ['userId', 'userName']);
|
|
$providedContent = [
|
|
SlotContent::named('content', '<p>User: {context.userName} (ID: {context.userId})</p>'),
|
|
];
|
|
|
|
$result = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definition,
|
|
$providedContent
|
|
);
|
|
|
|
expect($result)->toBe('<p>User: John Doe (ID: 123)</p>');
|
|
});
|
|
|
|
it('validates required slots', function () {
|
|
$component = new class () implements SupportsSlots {
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::named('body')->withRequired(true),
|
|
SlotDefinition::named('footer'),
|
|
];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
return SlotContext::empty();
|
|
}
|
|
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
return $content;
|
|
}
|
|
|
|
public function validateSlots(array $providedSlots): array
|
|
{
|
|
return [];
|
|
}
|
|
};
|
|
|
|
// No content provided
|
|
$errors = $this->slotManager->validateSlots($component, []);
|
|
|
|
expect($errors)->toContain("Required slot 'body' is not filled");
|
|
});
|
|
|
|
it('validates slots with content provided', function () {
|
|
$component = new class () implements SupportsSlots {
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::named('body')->withRequired(true),
|
|
];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
return SlotContext::empty();
|
|
}
|
|
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
return $content;
|
|
}
|
|
|
|
public function validateSlots(array $providedSlots): array
|
|
{
|
|
return [];
|
|
}
|
|
};
|
|
|
|
$providedContent = [
|
|
SlotContent::named('body', '<p>Body content</p>'),
|
|
];
|
|
|
|
$errors = $this->slotManager->validateSlots($component, $providedContent);
|
|
|
|
expect($errors)->toBeEmpty();
|
|
});
|
|
|
|
it('checks if component has specific slot', function () {
|
|
$component = new class () implements SupportsSlots {
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::named('header'),
|
|
SlotDefinition::named('body'),
|
|
];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
return SlotContext::empty();
|
|
}
|
|
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
return $content;
|
|
}
|
|
|
|
public function validateSlots(array $providedSlots): array
|
|
{
|
|
return [];
|
|
}
|
|
};
|
|
|
|
expect($this->slotManager->hasSlot($component, 'header'))->toBeTrue();
|
|
expect($this->slotManager->hasSlot($component, 'body'))->toBeTrue();
|
|
expect($this->slotManager->hasSlot($component, 'footer'))->toBeFalse();
|
|
});
|
|
|
|
it('gets slot definition by name', function () {
|
|
$component = new class () implements SupportsSlots {
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::named('header', '<h1>Default</h1>'),
|
|
];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
return SlotContext::empty();
|
|
}
|
|
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
return $content;
|
|
}
|
|
|
|
public function validateSlots(array $providedSlots): array
|
|
{
|
|
return [];
|
|
}
|
|
};
|
|
|
|
$definition = $this->slotManager->getSlotDefinition($component, 'header');
|
|
|
|
expect($definition)->not->toBeNull();
|
|
expect($definition->name)->toBe('header');
|
|
expect($definition->defaultContent)->toBe('<h1>Default</h1>');
|
|
});
|
|
|
|
it('returns null for unknown slot', function () {
|
|
$component = new class () implements SupportsSlots {
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
return SlotContext::empty();
|
|
}
|
|
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
return $content;
|
|
}
|
|
|
|
public function validateSlots(array $providedSlots): array
|
|
{
|
|
return [];
|
|
}
|
|
};
|
|
|
|
$definition = $this->slotManager->getSlotDefinition($component, 'unknown');
|
|
|
|
expect($definition)->toBeNull();
|
|
});
|
|
|
|
it('processes slot content through component hook', function () {
|
|
$component = new class () implements SupportsSlots {
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::named('body'),
|
|
];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
return SlotContext::empty();
|
|
}
|
|
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
// Wrap content in div
|
|
return $content->withContent('<div class="wrapper">' . $content->content . '</div>');
|
|
}
|
|
|
|
public function validateSlots(array $providedSlots): array
|
|
{
|
|
return [];
|
|
}
|
|
};
|
|
|
|
$definition = SlotDefinition::named('body');
|
|
$providedContent = [
|
|
SlotContent::named('body', '<p>Content</p>'),
|
|
];
|
|
|
|
$result = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definition,
|
|
$providedContent
|
|
);
|
|
|
|
expect($result)->toBe('<div class="wrapper"><p>Content</p></div>');
|
|
});
|
|
|
|
it('escapes HTML in scoped context values', function () {
|
|
$component = new class () implements SupportsSlots {
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::scoped('content', ['userInput']),
|
|
];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
return SlotContext::create([
|
|
'userInput' => '<script>alert("XSS")</script>',
|
|
]);
|
|
}
|
|
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
return $content;
|
|
}
|
|
|
|
public function validateSlots(array $providedSlots): array
|
|
{
|
|
return [];
|
|
}
|
|
};
|
|
|
|
$definition = SlotDefinition::scoped('content', ['userInput']);
|
|
$providedContent = [
|
|
SlotContent::named('content', '<p>{context.userInput}</p>'),
|
|
];
|
|
|
|
$result = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definition,
|
|
$providedContent
|
|
);
|
|
|
|
expect($result)->toContain('<script>');
|
|
expect($result)->not->toContain('<script>');
|
|
});
|
|
|
|
it('provides slot statistics', function () {
|
|
$componentId1 = ComponentId::generate();
|
|
$componentId2 = ComponentId::generate();
|
|
|
|
$this->slotManager->registerSlotContents($componentId1, [
|
|
SlotContent::named('header', '<h1>H1</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);
|
|
});
|
|
|
|
it('clears all registered slot contents', function () {
|
|
$componentId = ComponentId::generate();
|
|
|
|
$this->slotManager->registerSlotContents($componentId, [
|
|
SlotContent::named('header', '<h1>Header</h1>'),
|
|
]);
|
|
|
|
expect($this->slotManager->getSlotContents($componentId))->toHaveCount(1);
|
|
|
|
$this->slotManager->clear();
|
|
|
|
expect($this->slotManager->getSlotContents($componentId))->toBeEmpty();
|
|
});
|
|
});
|