- 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.
504 lines
17 KiB
PHP
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
|
|
});
|
|
});
|
|
});
|