Files
michaelschiemer/tests/Framework/LiveComponents/Batch/BatchProcessorTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

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');
});
});