- 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.
269 lines
10 KiB
PHP
269 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\LiveComponents\Batch\BatchOperation;
|
|
use App\Framework\LiveComponents\Batch\BatchProcessor;
|
|
use App\Framework\LiveComponents\Batch\BatchRequest;
|
|
use App\Framework\LiveComponents\ComponentRegistry;
|
|
use App\Framework\LiveComponents\Contracts\LiveComponent;
|
|
use App\Framework\LiveComponents\LiveComponentHandler;
|
|
use App\Framework\LiveComponents\Rendering\FragmentRenderer;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentData;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentUpdate;
|
|
use App\Framework\View\Rendering\FragmentCollection;
|
|
|
|
describe('BatchProcessor', function () {
|
|
beforeEach(function () {
|
|
$this->componentRegistry = Mockery::mock(ComponentRegistry::class);
|
|
$this->handler = Mockery::mock(LiveComponentHandler::class);
|
|
$this->fragmentRenderer = Mockery::mock(FragmentRenderer::class);
|
|
|
|
$this->processor = new BatchProcessor(
|
|
$this->componentRegistry,
|
|
$this->handler,
|
|
$this->fragmentRenderer
|
|
);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
it('processes single operation successfully', function () {
|
|
$operation = new BatchOperation('counter:demo', 'increment', ['amount' => 5]);
|
|
$request = new BatchRequest($operation);
|
|
|
|
$component = Mockery::mock(LiveComponent::class);
|
|
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo'));
|
|
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['count' => 5]));
|
|
|
|
$this->componentRegistry
|
|
->shouldReceive('resolve')
|
|
->once()
|
|
->andReturn($component);
|
|
|
|
$update = new ComponentUpdate(
|
|
component: $component,
|
|
state: ComponentData::fromArray(['count' => 10]),
|
|
events: [['type' => 'incremented']]
|
|
);
|
|
|
|
$this->handler
|
|
->shouldReceive('handleAction')
|
|
->once()
|
|
->andReturn($update);
|
|
|
|
$this->componentRegistry
|
|
->shouldReceive('render')
|
|
->once()
|
|
->with($component)
|
|
->andReturn('<div>Counter: 10</div>');
|
|
|
|
$response = $this->processor->process($request);
|
|
|
|
expect($response->totalOperations)->toBe(1);
|
|
expect($response->successCount)->toBe(1);
|
|
expect($response->failureCount)->toBe(0);
|
|
expect($response->results[0]->success)->toBeTrue();
|
|
expect($response->results[0]->html)->toBe('<div>Counter: 10</div>');
|
|
expect($response->results[0]->state)->toBe(['count' => 10]);
|
|
expect($response->results[0]->events)->toBe([['type' => 'incremented']]);
|
|
});
|
|
|
|
it('processes multiple operations successfully', function () {
|
|
$op1 = new BatchOperation('counter:demo', 'increment');
|
|
$op2 = new BatchOperation('stats:user', 'refresh');
|
|
$request = new BatchRequest($op1, $op2);
|
|
|
|
$component1 = Mockery::mock(LiveComponent::class);
|
|
$component1->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo'));
|
|
$component1->shouldReceive('getData')->andReturn(ComponentData::fromArray(['count' => 1]));
|
|
|
|
$component2 = Mockery::mock(LiveComponent::class);
|
|
$component2->shouldReceive('getId')->andReturn(ComponentId::fromString('stats:user'));
|
|
$component2->shouldReceive('getData')->andReturn(ComponentData::fromArray(['views' => 100]));
|
|
|
|
$this->componentRegistry
|
|
->shouldReceive('resolve')
|
|
->twice()
|
|
->andReturn($component1, $component2);
|
|
|
|
$update1 = new ComponentUpdate(
|
|
component: $component1,
|
|
state: ComponentData::fromArray(['count' => 2]),
|
|
events: []
|
|
);
|
|
|
|
$update2 = new ComponentUpdate(
|
|
component: $component2,
|
|
state: ComponentData::fromArray(['views' => 101]),
|
|
events: []
|
|
);
|
|
|
|
$this->handler
|
|
->shouldReceive('handleAction')
|
|
->twice()
|
|
->andReturn($update1, $update2);
|
|
|
|
$this->componentRegistry
|
|
->shouldReceive('render')
|
|
->twice()
|
|
->andReturn('<div>Counter: 2</div>', '<div>Views: 101</div>');
|
|
|
|
$response = $this->processor->process($request);
|
|
|
|
expect($response->totalOperations)->toBe(2);
|
|
expect($response->successCount)->toBe(2);
|
|
expect($response->failureCount)->toBe(0);
|
|
});
|
|
|
|
it('handles operation failure with error isolation', function () {
|
|
$op1 = new BatchOperation('counter:demo', 'increment');
|
|
$op2 = new BatchOperation('invalid:component', 'action');
|
|
$op3 = new BatchOperation('stats:user', 'refresh');
|
|
$request = new BatchRequest($op1, $op2, $op3);
|
|
|
|
$component1 = Mockery::mock(LiveComponent::class);
|
|
$component1->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo'));
|
|
$component1->shouldReceive('getData')->andReturn(ComponentData::fromArray(['count' => 1]));
|
|
|
|
$component3 = Mockery::mock(LiveComponent::class);
|
|
$component3->shouldReceive('getId')->andReturn(ComponentId::fromString('stats:user'));
|
|
$component3->shouldReceive('getData')->andReturn(ComponentData::fromArray(['views' => 100]));
|
|
|
|
$this->componentRegistry
|
|
->shouldReceive('resolve')
|
|
->times(3)
|
|
->andReturnUsing(function ($id) use ($component1, $component3) {
|
|
if ($id->toString() === 'invalid:component') {
|
|
throw new InvalidArgumentException('Unknown component: invalid:component');
|
|
}
|
|
|
|
return $id->toString() === 'counter:demo' ? $component1 : $component3;
|
|
});
|
|
|
|
$update1 = new ComponentUpdate(
|
|
component: $component1,
|
|
state: ComponentData::fromArray(['count' => 2]),
|
|
events: []
|
|
);
|
|
|
|
$update3 = new ComponentUpdate(
|
|
component: $component3,
|
|
state: ComponentData::fromArray(['views' => 101]),
|
|
events: []
|
|
);
|
|
|
|
$this->handler
|
|
->shouldReceive('handleAction')
|
|
->twice()
|
|
->andReturn($update1, $update3);
|
|
|
|
$this->componentRegistry
|
|
->shouldReceive('render')
|
|
->twice()
|
|
->andReturn('<div>Counter: 2</div>', '<div>Views: 101</div>');
|
|
|
|
$response = $this->processor->process($request);
|
|
|
|
expect($response->totalOperations)->toBe(3);
|
|
expect($response->successCount)->toBe(2);
|
|
expect($response->failureCount)->toBe(1);
|
|
|
|
// First operation succeeds
|
|
expect($response->results[0]->success)->toBeTrue();
|
|
|
|
// Second operation fails
|
|
expect($response->results[1]->success)->toBeFalse();
|
|
expect($response->results[1]->error)->toContain('Unknown component');
|
|
expect($response->results[1]->errorCode)->toBe('COMPONENT_NOT_FOUND');
|
|
|
|
// Third operation succeeds (error isolation works)
|
|
expect($response->results[2]->success)->toBeTrue();
|
|
});
|
|
|
|
it('processes fragment-based operation', function () {
|
|
$operation = new BatchOperation(
|
|
componentId: 'counter:demo',
|
|
method: 'increment',
|
|
fragments: ['counter-display']
|
|
);
|
|
$request = new BatchRequest($operation);
|
|
|
|
$component = Mockery::mock(LiveComponent::class);
|
|
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo'));
|
|
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['count' => 5]));
|
|
|
|
$this->componentRegistry
|
|
->shouldReceive('resolve')
|
|
->once()
|
|
->andReturn($component);
|
|
|
|
$update = new ComponentUpdate(
|
|
component: $component,
|
|
state: ComponentData::fromArray(['count' => 10]),
|
|
events: []
|
|
);
|
|
|
|
$this->handler
|
|
->shouldReceive('handleAction')
|
|
->once()
|
|
->andReturn($update);
|
|
|
|
$fragmentCollection = FragmentCollection::fromArray([
|
|
'counter-display' => '<span>10</span>',
|
|
]);
|
|
|
|
$this->fragmentRenderer
|
|
->shouldReceive('renderFragments')
|
|
->once()
|
|
->with($component, ['counter-display'])
|
|
->andReturn($fragmentCollection);
|
|
|
|
$response = $this->processor->process($request);
|
|
|
|
expect($response->successCount)->toBe(1);
|
|
expect($response->results[0]->fragments)->toBe(['counter-display' => '<span>10</span>']);
|
|
expect($response->results[0]->html)->toBeNull();
|
|
});
|
|
|
|
it('preserves operation ids in results', function () {
|
|
$op1 = new BatchOperation('counter:demo', 'increment', operationId: 'op-1');
|
|
$op2 = new BatchOperation('stats:user', 'refresh', operationId: 'op-2');
|
|
$request = new BatchRequest($op1, $op2);
|
|
|
|
$component1 = Mockery::mock(LiveComponent::class);
|
|
$component1->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo'));
|
|
$component1->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));
|
|
|
|
$component2 = Mockery::mock(LiveComponent::class);
|
|
$component2->shouldReceive('getId')->andReturn(ComponentId::fromString('stats:user'));
|
|
$component2->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));
|
|
|
|
$this->componentRegistry
|
|
->shouldReceive('resolve')
|
|
->twice()
|
|
->andReturn($component1, $component2);
|
|
|
|
$this->handler
|
|
->shouldReceive('handleAction')
|
|
->twice()
|
|
->andReturn(
|
|
new ComponentUpdate($component1, ComponentData::fromArray([]), []),
|
|
new ComponentUpdate($component2, ComponentData::fromArray([]), [])
|
|
);
|
|
|
|
$this->componentRegistry
|
|
->shouldReceive('render')
|
|
->twice()
|
|
->andReturn('<div>1</div>', '<div>2</div>');
|
|
|
|
$response = $this->processor->process($request);
|
|
|
|
expect($response->results[0]->operationId)->toBe('op-1');
|
|
expect($response->results[1]->operationId)->toBe('op-2');
|
|
});
|
|
});
|