Files
michaelschiemer/tests/Unit/Framework/Sse/SseBroadcasterTest.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

504 lines
17 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\Sse;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\Router\Result\SseEvent;
use App\Framework\Sse\SseBroadcaster;
use App\Framework\Sse\SseConnectionPool;
use App\Framework\Sse\ValueObjects\ConnectionId;
use App\Framework\Sse\ValueObjects\SseChannel;
use App\Framework\Sse\ValueObjects\SseConnection;
use Tests\Support\MockSseStream;
describe('SseBroadcaster', function () {
beforeEach(function () {
$this->pool = new SseConnectionPool();
$this->broadcaster = new SseBroadcaster($this->pool);
});
describe('Basic Broadcasting', function () {
it('broadcasts raw SSE event to channel', function () {
$channel = SseChannel::forUser('user123');
$stream = new MockSseStream();
$connection = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream,
channels: [$channel],
userId: 'user123'
);
$this->pool->add($connection);
$event = new SseEvent(
data: json_encode(['message' => 'test']),
event: 'custom-event'
);
$sent = $this->broadcaster->broadcast($event, $channel);
expect($sent)->toBe(1);
expect($stream->getSentEventsCount())->toBe(1);
});
it('broadcasts to multiple channels at once', function () {
$userChannel = SseChannel::forUser('user123');
$systemChannel = SseChannel::system();
$stream1 = new MockSseStream();
$connection1 = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream1,
channels: [$userChannel]
);
$stream2 = new MockSseStream();
$connection2 = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream2,
channels: [$systemChannel]
);
$this->pool->add($connection1);
$this->pool->add($connection2);
$event = new SseEvent(data: 'test', event: 'test');
$sent = $this->broadcaster->broadcastToMultipleChannels(
$event,
[$userChannel, $systemChannel]
);
expect($sent)->toBe(2);
});
it('broadcasts JSON data', function () {
$channel = SseChannel::system();
$stream = new MockSseStream();
$connection = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream,
channels: [$channel]
);
$this->pool->add($connection);
$sent = $this->broadcaster->broadcastJson(
['foo' => 'bar', 'value' => 123],
$channel,
'data-event'
);
expect($sent)->toBe(1);
$events = $stream->getSentEventsByType('data-event');
expect($events)->toHaveCount(1);
$eventArray = array_values($events); // Re-index array
$data = json_decode($eventArray[0]->data, true);
expect($data['foo'])->toBe('bar');
expect($data['value'])->toBe(123);
});
});
describe('Event Batching', function () {
it('enables event batching with configuration', function () {
$this->broadcaster->enableBatching(maxBatchSize: 5, maxBatchDelayMs: 50);
$channel = SseChannel::system();
$stream = new MockSseStream();
$connection = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream,
channels: [$channel]
);
$this->pool->add($connection);
// Send 3 events (below batch size)
for ($i = 0; $i < 3; $i++) {
$event = new SseEvent(data: "event-{$i}", event: 'test');
$this->broadcaster->broadcast($event, $channel);
}
// Events should be batched, not sent yet
expect($stream->getSentEventsCount())->toBe(0);
// Flush manually
$this->broadcaster->flushAll();
// Now should have 1 batch event
expect($stream->getSentEventsCount())->toBe(1);
$batchEvents = $stream->getSentEventsByType('batch');
expect($batchEvents)->toHaveCount(1);
$eventArray = array_values($batchEvents);
$batchData = json_decode($eventArray[0]->data, true);
expect($batchData['type'])->toBe('batch');
expect($batchData['count'])->toBe(3);
expect($batchData['events'])->toHaveCount(3);
});
it('auto-flushes when batch size reached', function () {
$this->broadcaster->enableBatching(maxBatchSize: 3, maxBatchDelayMs: 1000);
$channel = SseChannel::system();
$stream = new MockSseStream();
$connection = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream,
channels: [$channel]
);
$this->pool->add($connection);
// Send exactly batch size
for ($i = 0; $i < 3; $i++) {
$event = new SseEvent(data: "event-{$i}", event: 'test');
$sent = $this->broadcaster->broadcast($event, $channel);
if ($i < 2) {
expect($sent)->toBe(0); // Not flushed yet
} else {
expect($sent)->toBe(1); // Auto-flushed on 3rd event
}
}
expect($stream->getSentEventsCount())->toBe(1);
});
it('disables batching and flushes pending events', function () {
$this->broadcaster->enableBatching(maxBatchSize: 10, maxBatchDelayMs: 100);
$channel = SseChannel::system();
$stream = new MockSseStream();
$connection = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream,
channels: [$channel]
);
$this->pool->add($connection);
// Add some events to batch
for ($i = 0; $i < 3; $i++) {
$event = new SseEvent(data: "event-{$i}", event: 'test');
$this->broadcaster->broadcast($event, $channel);
}
expect($stream->getSentEventsCount())->toBe(0);
// Disable batching - should flush
$this->broadcaster->disableBatching();
expect($stream->getSentEventsCount())->toBe(1);
// New events should be sent immediately
$event = new SseEvent(data: 'immediate', event: 'test');
$sent = $this->broadcaster->broadcast($event, $channel);
expect($sent)->toBe(1);
expect($stream->getSentEventsCount())->toBe(2);
});
});
describe('Component Updates', function () {
it('broadcasts component update', function () {
$componentId = ComponentId::fromString('todo-list:instance-123');
$channel = SseChannel::forComponent($componentId);
$stream = new MockSseStream();
$connection = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream,
channels: [$channel]
);
$this->pool->add($connection);
$sent = $this->broadcaster->broadcastComponentUpdate(
$componentId,
state: ['count' => 5],
html: '<div>Updated HTML</div>',
events: ['item-added']
);
expect($sent)->toBe(1);
$events = $stream->getSentEventsByType('component-update');
expect($events)->toHaveCount(1);
$eventArray = array_values($events);
$data = json_decode($eventArray[0]->data, true);
expect($data['componentId'])->toBe('todo-list:instance-123');
expect($data['state']['count'])->toBe(5);
expect($data['html'])->toBe('<div>Updated HTML</div>');
expect($data['events'])->toBe(['item-added']);
});
it('broadcasts component fragments', function () {
$componentId = ComponentId::fromString('todo-list:instance-123');
$channel = SseChannel::forComponent($componentId);
$stream = new MockSseStream();
$connection = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream,
channels: [$channel]
);
$this->pool->add($connection);
$sent = $this->broadcaster->broadcastComponentFragments(
$componentId,
fragments: [
'header' => '<h1>Updated</h1>',
'footer' => '<p>New footer</p>'
],
state: ['count' => 10],
events: []
);
expect($sent)->toBe(1);
$events = $stream->getSentEventsByType('component-fragments');
expect($events)->toHaveCount(1);
$eventArray = array_values($events);
$data = json_decode($eventArray[0]->data, true);
expect($data['componentId'])->toBe('todo-list:instance-123');
expect($data['fragments']['header'])->toBe('<h1>Updated</h1>');
expect($data['fragments']['footer'])->toBe('<p>New footer</p>');
});
});
describe('Presence Broadcasting', function () {
it('broadcasts presence update', function () {
$channel = SseChannel::presence('room-123');
$stream = new MockSseStream();
$connection = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream,
channels: [$channel]
);
$this->pool->add($connection);
$sent = $this->broadcaster->broadcastPresence(
room: 'room-123',
userId: 'user456',
status: 'online',
metadata: ['name' => 'John Doe']
);
expect($sent)->toBe(1);
$events = $stream->getSentEventsByType('presence');
expect($events)->toHaveCount(1);
$eventArray = array_values($events);
$data = json_decode($eventArray[0]->data, true);
expect($data['userId'])->toBe('user456');
expect($data['status'])->toBe('online');
expect($data['metadata']['name'])->toBe('John Doe');
expect($data['timestamp'])->toBeInt();
});
});
describe('Notification Broadcasting', function () {
it('broadcasts notification to user', function () {
$channel = SseChannel::forUser('user123');
$stream = new MockSseStream();
$connection = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream,
channels: [$channel],
userId: 'user123'
);
$this->pool->add($connection);
$sent = $this->broadcaster->broadcastNotification(
userId: 'user123',
title: 'New Message',
message: 'You have a new message',
type: 'info',
data: ['messageId' => 789]
);
expect($sent)->toBe(1);
$events = $stream->getSentEventsByType('notification');
expect($events)->toHaveCount(1);
$eventArray = array_values($events);
$data = json_decode($eventArray[0]->data, true);
expect($data['title'])->toBe('New Message');
expect($data['message'])->toBe('You have a new message');
expect($data['type'])->toBe('info');
expect($data['data']['messageId'])->toBe(789);
});
});
describe('Progress Broadcasting', function () {
it('broadcasts progress update', function () {
$channel = SseChannel::forUser('user123');
$stream = new MockSseStream();
$connection = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream,
channels: [$channel],
userId: 'user123'
);
$this->pool->add($connection);
$sent = $this->broadcaster->broadcastProgress(
userId: 'user123',
taskId: 'export-123',
percent: 75,
message: 'Exporting data...',
data: ['rows_processed' => 7500]
);
expect($sent)->toBe(1);
$events = $stream->getSentEventsByType('progress');
expect($events)->toHaveCount(1);
$eventArray = array_values($events);
$data = json_decode($eventArray[0]->data, true);
expect($data['taskId'])->toBe('export-123');
expect($data['percent'])->toBe(75);
expect($data['message'])->toBe('Exporting data...');
expect($data['data']['rows_processed'])->toBe(7500);
});
});
describe('System Announcements', function () {
it('broadcasts system announcement to all clients', function () {
$channel = SseChannel::system();
// Create multiple connections
$streams = [];
for ($i = 0; $i < 3; $i++) {
$stream = new MockSseStream();
$streams[] = $stream;
$connection = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream,
channels: [$channel]
);
$this->pool->add($connection);
}
$sent = $this->broadcaster->broadcastSystemAnnouncement(
message: 'System maintenance in 10 minutes',
level: 'warning',
data: ['scheduled_time' => '2024-01-15T10:00:00Z']
);
expect($sent)->toBe(3);
foreach ($streams as $stream) {
$events = $stream->getSentEventsByType('system-announcement');
expect($events)->toHaveCount(1);
$eventArray = array_values($events);
$data = json_decode($eventArray[0]->data, true);
expect($data['message'])->toBe('System maintenance in 10 minutes');
expect($data['level'])->toBe('warning');
}
});
});
describe('Statistics', function () {
it('retrieves connection pool stats', function () {
$stream = new MockSseStream();
$connection = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream,
channels: [SseChannel::system()],
userId: 'user123'
);
$this->pool->add($connection);
$stats = $this->broadcaster->getStats();
expect($stats['total_connections'])->toBe(1);
expect($stats['authenticated_connections'])->toBe(1);
expect($stats['anonymous_connections'])->toBe(0);
});
});
describe('Edge Cases', function () {
it('handles broadcasting to channel with no connections', function () {
$channel = SseChannel::forUser('nonexistent');
$event = new SseEvent(data: 'test', event: 'test');
$sent = $this->broadcaster->broadcast($event, $channel);
expect($sent)->toBe(0);
});
it('broadcasts to multiple users with different channels', function () {
$user1Channel = SseChannel::forUser('user1');
$user2Channel = SseChannel::forUser('user2');
$stream1 = new MockSseStream();
$connection1 = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream1,
channels: [$user1Channel],
userId: 'user1'
);
$stream2 = new MockSseStream();
$connection2 = new SseConnection(
connectionId: ConnectionId::generate(),
stream: $stream2,
channels: [$user2Channel],
userId: 'user2'
);
$this->pool->add($connection1);
$this->pool->add($connection2);
// Broadcast to user1
$sent1 = $this->broadcaster->broadcastNotification(
'user1',
'User 1 Notification',
'Message for user 1'
);
expect($sent1)->toBe(1);
expect($stream1->getSentEventsCount())->toBe(1);
expect($stream2->getSentEventsCount())->toBe(0);
// Broadcast to user2
$sent2 = $this->broadcaster->broadcastNotification(
'user2',
'User 2 Notification',
'Message for user 2'
);
expect($sent2)->toBe(1);
expect($stream1->getSentEventsCount())->toBe(1); // Still 1
expect($stream2->getSentEventsCount())->toBe(1); // Now 1
});
});
});