diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 482deb0..b28e0ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,7 +119,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: '8.3' extensions: mbstring, intl coverage: none tools: phive diff --git a/CHANGELOG.md b/CHANGELOG.md index e65462e..960f164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] + +### Added + +- `BroadcastingTrait` for table classes to provide broadcasting functionality +- `BroadcastingTraitInterface` for type-safe broadcasting method contracts +- `TestQueueAdapter` for testing queued broadcasts in test suites +- Queue assertion methods to `BroadcastingTrait` test helper (`assertBroadcastQueued`, `assertBroadcastQueuedToChannel`, `assertNoBroadcastsQueued`, etc.) +- Comprehensive test suite for `BroadcastingTrait` with full coverage + +### Changed + +- **BREAKING**: `BroadcastingBehavior` now requires tables to use `BroadcastingTrait` and implement `BroadcastingTraitInterface` +- Broadcasting logic moved from `BroadcastingBehavior` to `BroadcastingTrait` for better separation of concerns +- Updated PHPUnit from 10.5 to 12.5 + ## [1.0.3] ### Fixed diff --git a/composer.json b/composer.json index b3ef532..93cc21a 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "cakephp/cakephp": "^5.1", "cakephp/authentication": "*", "cakephp/authorization": "*", @@ -24,7 +24,7 @@ "symfony/config": "^6.0|^7.0" }, "require-dev": { - "phpunit/phpunit": "^10.5", + "phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.5.0", "ratchet/pawl": "^0.4", "react/http": "^1.7", "cakephp/cakephp-codesniffer": "^5.0", @@ -45,13 +45,21 @@ } }, "scripts": { + "check": [ + "@cs-check", + "@test", + "@analyse" + ], + "analyse": [ + "@stan" + ], "cs-check": "phpcs -p --standard=phpcs.xml src/ tests/", "cs-fix": "phpcbf --standard=phpcs.xml src/ tests/", "test": "phpunit", "test:unit": "phpunit --testsuite=Unit", "test:integration": "phpunit --testsuite=Integration", "test:coverage": "phpunit --coverage-html coverage", - "stan": "tools/phpstan analyse src/", + "stan": "tools/phpstan analyse src/ tests/", "psalm": "tools/psalm --show-info=false", "stan-tests": "tools/phpstan analyze -c tests/phpstan.neon", "stan-baseline": "tools/phpstan --generate-baseline" diff --git a/docs/index.md b/docs/index.md index e26c545..8dbeca4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1128,7 +1128,7 @@ However, if you are not using these events for any other purposes in your applic ### Model Broadcasting Behavior -To get started, your ORM Table should use the `Broadcasting.Broadcasting` behavior. The behavior should define which events should be broadcast and how: +To get started, your ORM Table must use the `BroadcastingTrait` and implement `BroadcastingTraitInterface`, then add the `Broadcasting.Broadcasting` behavior. The behavior handles event mapping while the trait provides broadcasting functionality: ```php addBehavior('Broadcasting.Broadcasting', [ + $this->addBehavior('Crustum/Broadcasting.Broadcasting', [ 'events' => [ 'Model.afterSave' => 'saved', 'Model.afterDelete' => 'deleted', ], - 'channels' => function ($entity, $event) { - return ['posts.' . $entity->id]; - }, - 'payload' => function ($entity, $event) { - return [ - 'post' => $entity->toArray(), - ]; - }, ]); + + $this->setBroadcastChannels(function ($entity, $event) { + return ['posts.' . $entity->id]; + }); + + $this->setBroadcastPayload(function ($entity, $event) { + return [ + 'post' => $entity->toArray(), + ]; + }); } } ``` -Once your model includes this behavior and defines its broadcast configuration, it will begin automatically broadcasting events when a model instance is created, updated, or deleted. +Once your model includes the trait, implements the interface, and adds the behavior, it will begin automatically broadcasting events when a model instance is created, updated, or deleted. + +You can also configure broadcasting options via the behavior configuration: + +```php +$this->addBehavior('Crustum/Broadcasting.Broadcasting', [ + 'events' => [ + 'Model.afterSave' => 'saved', + 'Model.afterDelete' => 'deleted', + ], + 'enabled' => true, + 'connection' => 'default', + 'queue' => 'broadcasts', + 'channels' => function ($entity, $event) { + return ['posts.' . $entity->id]; + }, + 'payload' => function ($entity, $event) { + return ['post' => $entity->toArray()]; + }, + 'eventName' => 'PostUpdated', + 'broadcastEvents' => [ + 'created' => true, + 'updated' => true, + 'deleted' => false, + ], +]); +``` #### Default Channel Name Generation @@ -1226,28 +1258,35 @@ $this->addBehavior('Broadcasting.Broadcasting', [ #### Configuration Options -The behavior supports several configuration options: +The behavior supports several configuration options that are passed to the trait methods: - `events`: Maps CakePHP model events to broadcast event names (default: `['Model.afterSave' => 'saved', 'Model.afterDelete' => 'deleted']`) -- `broadcastEvents`: Controls which event types are enabled (default: `['created' => true, 'updated' => true, 'deleted' => true]`) -- `channels`: Callback or array defining which channels to broadcast to (default: entity-based channel naming) -- `payload`: Callback or array defining the data to broadcast (default: `$entity->toArray()` with `event_type`) -- `eventName`: Callback or string defining the broadcast event name (default: `{EntityClass}{EventType}`) -- `connection`: Broadcasting connection to use (default: `'default'`) -- `queue`: Queue name for async broadcasting (default: `null` for synchronous) -- `enabled`: Whether broadcasting is enabled (default: `true`) +- `enabled`: Whether broadcasting is enabled (default: `true`) - calls `enableBroadcasting()` or `disableBroadcasting()` +- `connection`: Broadcasting connection to use (default: `'default'`) - calls `setBroadcastConnection()` +- `queue`: Queue name for async broadcasting (default: `null` for synchronous) - calls `setBroadcastQueue()` +- `channels`: Callback or array defining which channels to broadcast to (default: entity-based channel naming) - calls `setBroadcastChannels()` +- `payload`: Callback or array defining the data to broadcast (default: `$entity->toArray()` with `event_type`) - calls `setBroadcastPayload()` +- `eventName`: Callback or string defining the broadcast event name (default: `{EntityClass}{EventType}`) - calls `setBroadcastEventName()` +- `broadcastEvents`: Controls which event types are enabled (default: `['created' => true, 'updated' => true, 'deleted' => true]`) - calls `setBroadcastEvents()` -You may have noticed that the behavior receives a string `$event` argument in the callbacks. This argument contains the type of event that has occurred on the model and will have a value of `created`, `updated`, `deleted`, etc. By inspecting the value of this variable, you may determine which channels (if any) the model should broadcast to for a particular event: +You can also configure these options programmatically using the trait methods: ```php -'channels' => function ($entity, $event) { +$this->setBroadcastChannels(function ($entity, $event) { return match ($event) { 'deleted' => [], default => ['posts.' . $entity->id, 'posts'], }; -} +}); + +$this->setBroadcastQueue('broadcasts'); +$this->setBroadcastConnection('pusher'); +$this->enableBroadcastEvent('created'); +$this->disableBroadcastEvent('deleted'); ``` +The `$event` argument in callbacks contains the type of event that has occurred on the model and will have a value of `created`, `updated`, `deleted`, etc. By inspecting the value of this variable, you may determine which channels (if any) the model should broadcast to for a particular event. + ### Listening for Model Broadcasts @@ -1635,9 +1674,8 @@ class PostTest extends TestCase public function testPostBroadcastsOnSave(): void { $postsTable = $this->getTableLocator()->get('Posts'); - $postsTable->addBehavior('Broadcasting.Broadcasting', [ - 'channels' => fn($entity) => ['posts.' . $entity->id], - ]); + $postsTable->addBehavior('Crustum/Broadcasting.Broadcasting'); + $postsTable->setBroadcastChannels(fn($entity) => ['posts.' . $entity->id]); $post = $postsTable->newEntity(['title' => 'Test Post']); $postsTable->save($post); @@ -1649,7 +1687,7 @@ class PostTest extends TestCase public function testPostBroadcastCustomization(): void { $postsTable = $this->getTableLocator()->get('Posts'); - $postsTable->addBehavior('Broadcasting.Broadcasting', [ + $postsTable->addBehavior('Crustum/Broadcasting.Broadcasting', [ 'channels' => ['posts', 'admin'], 'eventName' => 'post.created', 'payload' => function ($entity) { @@ -1667,6 +1705,21 @@ class PostTest extends TestCase $this->assertBroadcastSentToChannels(['posts', 'admin'], 'post.created'); $this->assertBroadcastPayloadContains('post.created', 'title', 'Test Post'); } + + public function testQueuedBroadcasts(): void + { + $postsTable = $this->getTableLocator()->get('Posts'); + $postsTable->addBehavior('Crustum/Broadcasting.Broadcasting'); + $postsTable->setBroadcastQueue('broadcasts'); + $postsTable->setBroadcastChannels(['posts']); + + $post = $postsTable->newEntity(['title' => 'Test Post']); + $postsTable->save($post); + + $this->assertBroadcastQueued('PostCreated'); + $this->assertBroadcastQueuedToChannel('posts', 'PostCreated'); + $this->assertNoBroadcastsSent(); + } } ``` @@ -1691,6 +1744,10 @@ The `BroadcastingTrait` provides the following assertion methods for your tests: | `assertBroadcastToChannelTimes(string $channel, string $event, int $times)` | Assert a broadcast to channel was sent N times | | `assertBroadcastCount(int $count)` | Assert the total number of broadcasts sent | | `assertNoBroadcastsSent()` | Assert no broadcasts were sent | +| `assertBroadcastQueued(string $event)` | Assert a broadcast was queued | +| `assertBroadcastQueuedToChannel(string $channel, string $event)` | Assert a broadcast was queued to a channel | +| `assertNoBroadcastsQueued()` | Assert no broadcasts were queued | +| `assertBroadcastQueuedCount(int $count)` | Assert the total number of queued broadcasts | Helper methods for retrieving captured broadcasts: @@ -1700,3 +1757,5 @@ Helper methods for retrieving captured broadcasts: | `getBroadcastsByEvent(string $event)` | Get broadcasts of a specific event | | `getBroadcastsToChannel(string $channel)` | Get broadcasts sent to a channel | | `getBroadcastsByConnection(string $connection)` | Get broadcasts sent via a connection | +| `getQueuedJobs()` | Get all queued jobs | +| `getQueuedBroadcastsByEvent(string $event)` | Get queued broadcasts of a specific event | diff --git a/src/Model/Behavior/BroadcastingBehavior.php b/src/Model/Behavior/BroadcastingBehavior.php index 17be9e9..f26a76d 100644 --- a/src/Model/Behavior/BroadcastingBehavior.php +++ b/src/Model/Behavior/BroadcastingBehavior.php @@ -6,13 +6,37 @@ use Cake\Datasource\EntityInterface; use Cake\Event\EventInterface; use Cake\ORM\Behavior; -use Crustum\Broadcasting\Broadcasting; +use Crustum\Broadcasting\Model\Interface\BroadcastingTraitInterface; +use LogicException; /** * Broadcasting Behavior * * Enables automatic broadcasting of model lifecycle events. * + * The table class must use BroadcastingTrait and implement BroadcastingTraitInterface. + * + * Usage: + * ``` + * use Crustum\Broadcasting\Model\Interface\BroadcastingTraitInterface; + * use Crustum\Broadcasting\Model\Trait\BroadcastingTrait; + * + * class UsersTable extends Table implements BroadcastingTraitInterface + * { + * use BroadcastingTrait; + * + * public function initialize(array $config): void + * { + * $this->addBehavior('Crustum/Broadcasting.Broadcasting', [ + * 'events' => [ + * 'Model.afterSave' => 'saved', + * 'Model.afterDelete' => 'deleted', + * ] + * ]); + * } + * } + * ``` + * * Examples: * * Basic usage: @@ -79,34 +103,10 @@ class BroadcastingBehavior extends Behavior */ protected array $_defaultConfig = [ 'implementedFinders' => [], - 'implementedMethods' => [ - 'broadcastEvent' => 'broadcastEvent', - 'enableBroadcasting' => 'enableBroadcasting', - 'disableBroadcasting' => 'disableBroadcasting', - 'isBroadcastingEnabled' => 'isBroadcastingEnabled', - 'setBroadcastChannels' => 'setBroadcastChannels', - 'setBroadcastPayload' => 'setBroadcastPayload', - 'setBroadcastConnection' => 'setBroadcastConnection', - 'setBroadcastQueue' => 'setBroadcastQueue', - 'setBroadcastEventName' => 'setBroadcastEventName', - 'setBroadcastEvents' => 'setBroadcastEvents', - 'enableBroadcastEvent' => 'enableBroadcastEvent', - 'disableBroadcastEvent' => 'disableBroadcastEvent', - ], 'events' => [ 'Model.afterSave' => 'saved', 'Model.afterDelete' => 'deleted', ], - 'broadcastEvents' => [ - 'created' => true, - 'updated' => true, - 'deleted' => true, - ], - 'connection' => 'default', - 'queue' => null, - 'channels' => null, - 'payload' => null, - 'enabled' => true, ]; /** @@ -114,328 +114,88 @@ class BroadcastingBehavior extends Behavior * * @param array $config Configuration options * @return void + * @throws \LogicException If table does not implement BroadcastingTraitInterface */ public function initialize(array $config): void { if (isset($config['events'])) { $this->setConfig('events', $config['events'], false); } - } - - /** - * Get the list of events this behavior is interested in - * - * @return array - */ - public function implementedEvents(): array - { - return array_fill_keys(array_keys($this->_config['events']), 'handleEvent'); - } - - /** - * Handle model lifecycle events - * - * @param \Cake\Event\EventInterface<\Cake\ORM\Table> $event The model event - * @param \Cake\Datasource\EntityInterface $entity The entity - * @return void - */ - public function handleEvent(EventInterface $event, EntityInterface $entity): void - { - if (!$this->getConfig('enabled')) { - return; - } - - $eventName = $event->getName(); - - if ($eventName === 'Model.afterSave') { - $broadcastEvent = $entity->isNew() ? 'created' : 'updated'; - } else { - $broadcastEvent = $this->_config['events'][$eventName]; - } - - $broadcastEvents = $this->getConfig('broadcastEvents', []); - if (isset($broadcastEvents[$broadcastEvent]) && !$broadcastEvents[$broadcastEvent]) { - return; - } - - $this->broadcastEvent($entity, $broadcastEvent); - } - /** - * Broadcast a model event - * - * @param \Cake\Datasource\EntityInterface $entity The entity - * @param string $event The broadcast event name - * @return void - */ - public function broadcastEvent(EntityInterface $entity, string $event): void - { - if (!$this->getConfig('enabled')) { - return; + if (!$this->_table instanceof BroadcastingTraitInterface) { + throw new LogicException( + sprintf( + 'Table %s must use BroadcastingTrait and implement BroadcastingTraitInterface.', + get_class($this->_table), + ), + ); } - $channels = $this->getBroadcastChannels($entity, $event); - $payload = $this->getBroadcastPayload($entity, $event); - $connection = $this->getBroadcastConnection($entity); + /** @var \Cake\ORM\Table&\Crustum\Broadcasting\Model\Interface\BroadcastingTraitInterface $table */ + $table = $this->_table; - if (!empty($channels)) { - $eventName = $this->getEventName($entity, $event); - $connectionName = $connection ?? 'default'; - $queue = $this->getConfig('queue'); - - $pending = Broadcasting::to($channels) - ->event($eventName) - ->data($payload) - ->connection($connectionName); - - if ($queue !== null) { - $pending->queue($queue); + if (isset($config['enabled'])) { + if ($config['enabled']) { + $table->enableBroadcasting(); } else { - $pending->send(); + $table->disableBroadcasting(); } } - } - - /** - * Get the channels to broadcast on - * - * @param \Cake\Datasource\EntityInterface $entity The entity - * @param string $event The event name - * @return array Array of channels - */ - protected function getBroadcastChannels(EntityInterface $entity, string $event): array - { - $channels = $this->getConfig('channels'); - if (is_callable($channels)) { - $result = $channels($entity, $event); - $channelsArray = is_array($result) ? $result : [$result]; - } elseif (is_array($channels)) { - $channelsArray = $channels; - } else { - $channelsArray = [$entity]; + if (isset($config['channels'])) { + $table->setBroadcastChannels($config['channels']); } - return $this->convertEntitiesToChannelNames($channelsArray); - } - - /** - * Convert entity instances to Laravel-style channel names - * - * @param array $channels Array of channels or entities - * @return array Array of channel names - */ - protected function convertEntitiesToChannelNames(array $channels): array - { - $converted = []; - foreach ($channels as $channel) { - if ($channel instanceof EntityInterface) { - $entityClass = get_class($channel); - $channelName = str_replace('\\', '.', $entityClass); - $id = $channel->get('id'); - if ($id !== null) { - $channelName .= '.' . $id; - } - $converted[] = $channelName; - } else { - $converted[] = $channel; - } + if (isset($config['payload'])) { + $table->setBroadcastPayload($config['payload']); } - return $converted; - } - - /** - * Get the payload to broadcast - * - * @param \Cake\Datasource\EntityInterface $entity The entity - * @param string $event The event name - * @return array - */ - protected function getBroadcastPayload(EntityInterface $entity, string $event): array - { - $payload = $this->getConfig('payload'); - - if (is_callable($payload)) { - return $payload($entity, $event); + if (isset($config['connection'])) { + $table->setBroadcastConnection($config['connection']); } - if (is_array($payload)) { - return $payload; - } - if ($payload instanceof EntityInterface) { - return $payload->toArray(); - } - $data = $entity->toArray(); - $data['event_type'] = $event; - - return $data; - } - - /** - * Get the broadcast connection - * - * @param \Cake\Datasource\EntityInterface $entity The entity - * @return string|null - */ - protected function getBroadcastConnection(EntityInterface $entity): ?string - { - $connection = $this->getConfig('connection'); - - if (is_callable($connection)) { - return $connection($entity); + if (isset($config['queue'])) { + $table->setBroadcastQueue($config['queue']); } - return $connection; - } - - /** - * Get the event name for broadcasting - * - * @param \Cake\Datasource\EntityInterface $entity The entity - * @param string $event The event name - * @return string - */ - protected function getEventName(EntityInterface $entity, string $event): string - { - $eventNameConfig = $this->getConfig('eventName'); - - if (is_callable($eventNameConfig)) { - $result = $eventNameConfig($entity, $event); - if ($result !== null) { - return $result; - } + if (isset($config['eventName'])) { + $table->setBroadcastEventName($config['eventName']); } - if (is_string($eventNameConfig)) { - return $eventNameConfig; + if (isset($config['broadcastEvents'])) { + $table->setBroadcastEvents($config['broadcastEvents']); } - - $entityClass = get_class($entity); - $className = substr($entityClass, strrpos($entityClass, '\\') + 1); - - return $className . ucfirst($event); - } - - /** - * Enable broadcasting - * - * @return void - */ - public function enableBroadcasting(): void - { - $this->setConfig('enabled', true); - } - - /** - * Disable broadcasting - * - * @return void - */ - public function disableBroadcasting(): void - { - $this->setConfig('enabled', false); - } - - /** - * Check if broadcasting is enabled - * - * @return bool - */ - public function isBroadcastingEnabled(): bool - { - return $this->getConfig('enabled'); - } - - /** - * Set custom channels for broadcasting - * - * @param callable|array|null $channels Channels configuration - * @return void - */ - public function setBroadcastChannels(callable|array|null $channels): void - { - $this->setConfig('channels', $channels); - } - - /** - * Set custom payload for broadcasting - * - * @param callable|array|null $payload Payload configuration - * @return void - */ - public function setBroadcastPayload(callable|array|null $payload): void - { - $this->setConfig('payload', $payload); - } - - /** - * Set broadcast connection - * - * @param callable|string|null $connection Connection name or callback - * @return void - */ - public function setBroadcastConnection(string|callable|null $connection): void - { - $this->setConfig('connection', $connection); - } - - /** - * Set custom event name for broadcasting - * - * @param callable|string|null $eventName Event name or callback - * @return void - */ - public function setBroadcastEventName(callable|string|null $eventName): void - { - $this->setConfig('eventName', $eventName); } /** - * Set which broadcast events are enabled + * Get the list of events this behavior is interested in * - * @param array $events Event configuration - * @return void + * @return array */ - public function setBroadcastEvents(array $events): void + public function implementedEvents(): array { - $this->setConfig('broadcastEvents', $events); + return array_fill_keys(array_keys($this->_config['events']), 'handleEvent'); } /** - * Enable a specific broadcast event + * Handle model lifecycle events * - * @param string $event Event name (created, updated, deleted, etc.) + * @param \Cake\Event\EventInterface<\Cake\ORM\Table> $event The model event + * @param \Cake\Datasource\EntityInterface $entity The entity * @return void */ - public function enableBroadcastEvent(string $event): void + public function handleEvent(EventInterface $event, EntityInterface $entity): void { - $broadcastEvents = $this->getConfig('broadcastEvents', []); - $broadcastEvents[$event] = true; - $this->setConfig('broadcastEvents', $broadcastEvents); - } + $eventName = $event->getName(); - /** - * Disable a specific broadcast event - * - * @param string $event Event name (created, updated, deleted, etc.) - * @return void - */ - public function disableBroadcastEvent(string $event): void - { - $broadcastEvents = $this->getConfig('broadcastEvents', []); - $broadcastEvents[$event] = false; - $this->setConfig('broadcastEvents', $broadcastEvents); - } + if ($eventName === 'Model.afterSave') { + $broadcastEvent = $entity->isNew() ? 'created' : 'updated'; + } else { + $broadcastEvent = $this->_config['events'][$eventName]; + } - /** - * Set broadcast queue - * - * @param string|null $queue Queue name - * @return void - */ - public function setBroadcastQueue(?string $queue): void - { - $this->setConfig('queue', $queue); + /** @var \Cake\ORM\Table&\Crustum\Broadcasting\Model\Interface\BroadcastingTraitInterface $table */ + $table = $event->getSubject(); + $table->broadcastEvent($entity, $broadcastEvent); } } diff --git a/src/Model/Interface/BroadcastingTraitInterface.php b/src/Model/Interface/BroadcastingTraitInterface.php new file mode 100644 index 0000000..bb52b71 --- /dev/null +++ b/src/Model/Interface/BroadcastingTraitInterface.php @@ -0,0 +1,110 @@ +|null $channels Channels configuration + * @return void + */ + public function setBroadcastChannels(Closure|array|null $channels): void; + + /** + * Set custom payload for broadcasting + * + * @param \Cake\Datasource\EntityInterface|\Closure|array|null $payload Payload configuration + * @return void + */ + public function setBroadcastPayload(Closure|array|EntityInterface|null $payload): void; + + /** + * Set broadcast connection + * + * @param \Closure|string|null $connection Connection name or callback + * @return void + */ + public function setBroadcastConnection(string|Closure|null $connection): void; + + /** + * Set broadcast queue + * + * @param string|null $queue Queue name + * @return void + */ + public function setBroadcastQueue(?string $queue): void; + + /** + * Set custom event name for broadcasting + * + * @param \Closure|string|null $eventName Event name or callback + * @return void + */ + public function setBroadcastEventName(Closure|string|null $eventName): void; + + /** + * Set which broadcast events are enabled + * + * @param array $events Event configuration + * @return void + */ + public function setBroadcastEvents(array $events): void; + + /** + * Enable a specific broadcast event + * + * @param string $event Event name (created, updated, deleted, etc.) + * @return void + */ + public function enableBroadcastEvent(string $event): void; + + /** + * Disable a specific broadcast event + * + * @param string $event Event name (created, updated, deleted, etc.) + * @return void + */ + public function disableBroadcastEvent(string $event): void; +} diff --git a/src/Model/Trait/BroadcastingTrait.php b/src/Model/Trait/BroadcastingTrait.php new file mode 100644 index 0000000..941e5f0 --- /dev/null +++ b/src/Model/Trait/BroadcastingTrait.php @@ -0,0 +1,367 @@ +addBehavior('Crustum/Broadcasting.Broadcasting'); + * } + * } + * ``` + */ +trait BroadcastingTrait +{ + /** + * Broadcasting enabled flag + * + * @var bool + */ + private bool $_broadcastingEnabled = true; + + /** + * Broadcasting channels configuration + * + * @var \Closure|array|null + */ + private Closure|array|null $_broadcastingChannels = null; + + /** + * Broadcasting payload configuration + * + * @var \Cake\Datasource\EntityInterface|\Closure|array|null + */ + private Closure|array|EntityInterface|null $_broadcastingPayload = null; + + /** + * Broadcasting connection configuration + * + * @var \Closure|string|null + */ + private Closure|string|null $_broadcastingConnection = 'default'; + + /** + * Broadcasting queue configuration + * + * @var string|null + */ + private ?string $_broadcastingQueue = null; + + /** + * Broadcasting event name configuration + * + * @var \Closure|string|null + */ + private Closure|string|null $_broadcastingEventName = null; + + /** + * Broadcasting events configuration + * + * @var array + */ + private array $_broadcastingEvents = [ + 'created' => true, + 'updated' => true, + 'deleted' => true, + ]; + + /** + * Broadcast a model event + * + * @param \Cake\Datasource\EntityInterface $entity The entity + * @param string $event The broadcast event name + * @return void + */ + public function broadcastEvent(EntityInterface $entity, string $event): void + { + if (!$this->_broadcastingEnabled) { + return; + } + + if (isset($this->_broadcastingEvents[$event]) && !$this->_broadcastingEvents[$event]) { + return; + } + + $channels = $this->getBroadcastChannels($entity, $event); + $payload = $this->getBroadcastPayload($entity, $event); + $connection = $this->getBroadcastConnection($entity); + + if (!empty($channels)) { + $eventName = $this->getEventName($entity, $event); + $connectionName = $connection ?? 'default'; + $queue = $this->_broadcastingQueue; + + $pending = Broadcasting::to($channels) + ->event($eventName) + ->data($payload) + ->connection($connectionName); + + if ($queue !== null) { + $pending->queue($queue); + } else { + $pending->send(); + } + } + } + + /** + * Enable broadcasting + * + * @return void + */ + public function enableBroadcasting(): void + { + $this->_broadcastingEnabled = true; + } + + /** + * Disable broadcasting + * + * @return void + */ + public function disableBroadcasting(): void + { + $this->_broadcastingEnabled = false; + } + + /** + * Check if broadcasting is enabled + * + * @return bool + */ + public function isBroadcastingEnabled(): bool + { + return $this->_broadcastingEnabled; + } + + /** + * Set custom channels for broadcasting + * + * @param \Closure|array|null $channels Channels configuration + * @return void + */ + public function setBroadcastChannels(Closure|array|null $channels): void + { + $this->_broadcastingChannels = $channels; + } + + /** + * Set custom payload for broadcasting + * + * @param \Cake\Datasource\EntityInterface|\Closure|array|null $payload Payload configuration + * @return void + */ + public function setBroadcastPayload(Closure|array|EntityInterface|null $payload): void + { + $this->_broadcastingPayload = $payload; + } + + /** + * Set broadcast connection + * + * @param \Closure|string|null $connection Connection name or callback + * @return void + */ + public function setBroadcastConnection(string|Closure|null $connection): void + { + $this->_broadcastingConnection = $connection; + } + + /** + * Set broadcast queue + * + * @param string|null $queue Queue name + * @return void + */ + public function setBroadcastQueue(?string $queue): void + { + $this->_broadcastingQueue = $queue; + } + + /** + * Set custom event name for broadcasting + * + * @param \Closure|string|null $eventName Event name or callback + * @return void + */ + public function setBroadcastEventName(Closure|string|null $eventName): void + { + $this->_broadcastingEventName = $eventName; + } + + /** + * Set which broadcast events are enabled + * + * @param array $events Event configuration + * @return void + */ + public function setBroadcastEvents(array $events): void + { + $this->_broadcastingEvents = $events; + } + + /** + * Enable a specific broadcast event + * + * @param string $event Event name (created, updated, deleted, etc.) + * @return void + */ + public function enableBroadcastEvent(string $event): void + { + $this->_broadcastingEvents[$event] = true; + } + + /** + * Disable a specific broadcast event + * + * @param string $event Event name (created, updated, deleted, etc.) + * @return void + */ + public function disableBroadcastEvent(string $event): void + { + $this->_broadcastingEvents[$event] = false; + } + + /** + * Get the channels to broadcast on + * + * @param \Cake\Datasource\EntityInterface $entity The entity + * @param string $event The event name + * @return array Array of channels + */ + private function getBroadcastChannels(EntityInterface $entity, string $event): array + { + $channels = $this->_broadcastingChannels; + + if ($channels instanceof Closure) { + $result = $channels($entity, $event); + $channelsArray = is_array($result) ? $result : [$result]; + } elseif (is_array($channels)) { + $channelsArray = $channels; + } else { + $channelsArray = [$entity]; + } + + return $this->convertEntitiesToChannelNames($channelsArray); + } + + /** + * Convert entity instances to Laravel-style channel names + * + * @param array $channels Array of channels or entities + * @return array Array of channel names + */ + private function convertEntitiesToChannelNames(array $channels): array + { + $converted = []; + foreach ($channels as $channel) { + if ($channel instanceof EntityInterface) { + $entityClass = get_class($channel); + $channelName = str_replace('\\', '.', $entityClass); + $id = $channel->get('id'); + if ($id !== null) { + $channelName .= '.' . $id; + } + $converted[] = $channelName; + } else { + $converted[] = $channel; + } + } + + return $converted; + } + + /** + * Get the payload to broadcast + * + * @param \Cake\Datasource\EntityInterface $entity The entity + * @param string $event The event name + * @return array + */ + private function getBroadcastPayload(EntityInterface $entity, string $event): array + { + $payload = $this->_broadcastingPayload; + + if ($payload instanceof Closure) { + return $payload($entity, $event); + } + + if (is_array($payload)) { + return $payload; + } + + if ($payload !== null) { + return $payload->toArray(); + } + $data = $entity->toArray(); + $data['event_type'] = $event; + + return $data; + } + + /** + * Get the broadcast connection + * + * @param \Cake\Datasource\EntityInterface $entity The entity + * @return string|null + */ + private function getBroadcastConnection(EntityInterface $entity): ?string + { + $connection = $this->_broadcastingConnection; + + if ($connection instanceof Closure) { + return $connection($entity); + } + + return $connection; + } + + /** + * Get the event name for broadcasting + * + * @param \Cake\Datasource\EntityInterface $entity The entity + * @param string $event The event name + * @return string + */ + private function getEventName(EntityInterface $entity, string $event): string + { + $eventNameConfig = $this->_broadcastingEventName; + + if ($eventNameConfig instanceof Closure) { + $result = $eventNameConfig($entity, $event); + if ($result !== null) { + return $result; + } + } + + if (is_string($eventNameConfig)) { + return $eventNameConfig; + } + + $entityClass = get_class($entity); + $className = substr($entityClass, strrpos($entityClass, '\\') + 1); + + return $className . ucfirst($event); + } +} diff --git a/src/TestSuite/BroadcastingTrait.php b/src/TestSuite/BroadcastingTrait.php index f4415ed..0e6dbca 100644 --- a/src/TestSuite/BroadcastingTrait.php +++ b/src/TestSuite/BroadcastingTrait.php @@ -52,7 +52,7 @@ trait BroadcastingTrait public function setupTestBroadcaster(): void { foreach (Broadcasting::configured() as $config) { - Broadcasting::drop($config); + Broadcasting::drop((string)$config); } Broadcasting::setConfig('default', [ @@ -62,6 +62,8 @@ public function setupTestBroadcaster(): void Broadcasting::getRegistry()->reset(); TestBroadcaster::clearBroadcasts(); + TestQueueAdapter::replaceQueueAdapter(); + TestQueueAdapter::clearQueuedJobs(); } /** @@ -75,6 +77,7 @@ public function setupTestBroadcaster(): void public function cleanupBroadcastingTrait(): void { TestBroadcaster::clearBroadcasts(); + TestQueueAdapter::clearQueuedJobs(); Broadcasting::getRegistry()->reset(); } @@ -391,4 +394,101 @@ public function getBroadcastsByConnection(string $connection): array { return TestBroadcaster::getBroadcastsByConnection($connection); } + + /** + * Assert a broadcast was queued + * + * @param string $event Event name + * @param string $message Optional assertion message + * @return void + */ + public function assertBroadcastQueued(string $event, string $message = ''): void + { + $queued = TestQueueAdapter::getQueuedBroadcastsByEvent($event); + $this->assertNotEmpty( + $queued, + $message ?: "Broadcast {$event} was not queued", + ); + } + + /** + * Assert a broadcast was queued to a specific channel + * + * @param string $channel Channel name + * @param string $event Event name + * @param string $message Optional assertion message + * @return void + */ + public function assertBroadcastQueuedToChannel( + string $channel, + string $event, + string $message = '', + ): void { + $queued = TestQueueAdapter::getQueuedBroadcastsByChannel($channel); + $found = false; + foreach ($queued as $job) { + if (isset($job['data']['eventName']) && $job['data']['eventName'] === $event) { + $found = true; + break; + } + } + $this->assertTrue( + $found, + $message ?: "Broadcast {$event} was not queued to channel {$channel}", + ); + } + + /** + * Assert no broadcasts were queued + * + * @param string $message Optional assertion message + * @return void + */ + public function assertNoBroadcastsQueued(string $message = ''): void + { + $count = TestQueueAdapter::getQueuedJobCount(); + $this->assertEquals( + 0, + $count, + $message ?: "Expected no broadcasts to be queued, but {$count} were queued", + ); + } + + /** + * Assert a specific count of broadcasts were queued + * + * @param int $count Expected queued count + * @param string $message Optional assertion message + * @return void + */ + public function assertBroadcastQueuedCount(int $count, string $message = ''): void + { + $actualCount = TestQueueAdapter::getQueuedJobCount(); + $this->assertEquals( + $count, + $actualCount, + $message ?: "Expected {$count} broadcasts to be queued, but {$actualCount} were queued", + ); + } + + /** + * Get all queued jobs + * + * @return array> + */ + public function getQueuedJobs(): array + { + return TestQueueAdapter::getQueuedJobs(); + } + + /** + * Get queued broadcasts by event + * + * @param string $event Event name + * @return array> + */ + public function getQueuedBroadcastsByEvent(string $event): array + { + return TestQueueAdapter::getQueuedBroadcastsByEvent($event); + } } diff --git a/src/TestSuite/TestQueueAdapter.php b/src/TestSuite/TestQueueAdapter.php new file mode 100644 index 0000000..63885fa --- /dev/null +++ b/src/TestSuite/TestQueueAdapter.php @@ -0,0 +1,161 @@ +event('OrderCreated')->queue(); + * + * // Make assertions + * $queued = TestQueueAdapter::getQueuedJobs(); + * ``` + */ +class TestQueueAdapter implements QueueAdapterInterface +{ + /** + * Captured queued jobs + * + * @var array> + */ + protected static array $queuedJobs = []; + + /** + * Push a job onto the queue (capture it instead) + * + * @param string $jobClass Job class name + * @param array $data Job data + * @param array $options Job options + * @return void + */ + public function push(string $jobClass, array $data = [], array $options = []): void + { + static::$queuedJobs[] = [ + 'jobClass' => $jobClass, + 'data' => $data, + 'options' => $options, + 'timestamp' => time(), + ]; + } + + /** + * Generate a unique ID for a job + * + * @param string $eventName Event name + * @param string $type Job type + * @param array $data Job data + * @return string Unique job ID + */ + public function getUniqueId(string $eventName, string $type, array $data = []): string + { + return md5($eventName . $type . serialize($data)); + } + + /** + * Replace the queue adapter with test adapter + * + * @return void + */ + public static function replaceQueueAdapter(): void + { + Broadcasting::setQueueAdapter(new self()); + } + + /** + * Get all queued jobs + * + * @return array> + */ + public static function getQueuedJobs(): array + { + return static::$queuedJobs; + } + + /** + * Get queued jobs by job class + * + * @param string $jobClass Job class name + * @return array> + */ + public static function getQueuedJobsByClass(string $jobClass): array + { + $filtered = array_filter(static::$queuedJobs, function ($job) use ($jobClass) { + return $job['jobClass'] === $jobClass; + }); + + return array_values($filtered); + } + + /** + * Get queued broadcast jobs by event name + * + * @param string $eventName Event name + * @return array> + */ + public static function getQueuedBroadcastsByEvent(string $eventName): array + { + $broadcastJobs = static::getQueuedJobsByClass(BroadcastJob::class); + + $filtered = array_filter($broadcastJobs, function ($job) use ($eventName) { + return isset($job['data']['eventName']) && $job['data']['eventName'] === $eventName; + }); + + return array_values($filtered); + } + + /** + * Get queued broadcast jobs by channel + * + * @param string $channel Channel name + * @return array> + */ + public static function getQueuedBroadcastsByChannel(string $channel): array + { + $broadcastJobs = static::getQueuedJobsByClass(BroadcastJob::class); + + $filtered = array_filter($broadcastJobs, function ($job) use ($channel) { + $channels = $job['data']['channels'] ?? []; + if (is_array($channels)) { + return in_array($channel, $channels); + } + + return $channels === $channel; + }); + + return array_values($filtered); + } + + /** + * Clear all queued jobs + * + * @return void + */ + public static function clearQueuedJobs(): void + { + static::$queuedJobs = []; + } + + /** + * Get count of queued jobs + * + * @return int + */ + public static function getQueuedJobCount(): int + { + return count(static::$queuedJobs); + } +} diff --git a/tests/Fixture/OrdersFixture.php b/tests/Fixture/OrdersFixture.php index 911ac7b..af2aabf 100644 --- a/tests/Fixture/OrdersFixture.php +++ b/tests/Fixture/OrdersFixture.php @@ -32,5 +32,21 @@ class OrdersFixture extends TestFixture 'created' => '2024-01-02 00:00:00', 'modified' => '2024-01-02 00:00:00', ], + [ + 'id' => 123, + 'user_id' => 1, + 'total' => 100.00, + 'status' => 'paid', + 'created' => '2024-01-03 00:00:00', + 'modified' => '2024-01-03 00:00:00', + ], + [ + 'id' => 124, + 'user_id' => 999, + 'total' => 200.00, + 'status' => 'paid', + 'created' => '2024-01-04 00:00:00', + 'modified' => '2024-01-04 00:00:00', + ], ]; } diff --git a/tests/Fixture/RoomsFixture.php b/tests/Fixture/RoomsFixture.php new file mode 100644 index 0000000..7177f0a --- /dev/null +++ b/tests/Fixture/RoomsFixture.php @@ -0,0 +1,50 @@ + + */ + public array $fields = [ + 'id' => ['type' => 'integer', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null], + 'user_id' => ['type' => 'integer', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], + ], + ]; + + /** + * Init method + * + * @return void + */ + public function init(): void + { + $this->records = [ + [ + 'id' => 456, + 'user_id' => 1, + ], + ]; + parent::init(); + } +} diff --git a/tests/TestApp/Broadcasting/InvalidChannel.php b/tests/TestApp/Broadcasting/InvalidChannel.php index 349580c..367de31 100644 --- a/tests/TestApp/Broadcasting/InvalidChannel.php +++ b/tests/TestApp/Broadcasting/InvalidChannel.php @@ -5,7 +5,7 @@ class InvalidChannel { - public function join($user, $model): bool + public function join(mixed $user, mixed $model): bool { return true; } diff --git a/tests/TestApp/Controller/OrdersController.php b/tests/TestApp/Controller/OrdersController.php index 020dd9c..05109da 100644 --- a/tests/TestApp/Controller/OrdersController.php +++ b/tests/TestApp/Controller/OrdersController.php @@ -37,7 +37,7 @@ public function create(): Response return $this->response ->withType('application/json') - ->withStringBody(json_encode([ + ->withStringBody((string)json_encode([ 'success' => true, 'order_id' => $orderId, ])); @@ -61,7 +61,7 @@ public function update(): Response return $this->response ->withType('application/json') - ->withStringBody(json_encode(['success' => true])); + ->withStringBody((string)json_encode(['success' => true])); } /** @@ -82,6 +82,6 @@ public function broadcastWithConnection(): Response return $this->response ->withType('application/json') - ->withStringBody(json_encode(['success' => true])); + ->withStringBody((string)json_encode(['success' => true])); } } diff --git a/tests/TestApp/Event/TestBroadcastableClass.php b/tests/TestApp/Event/TestBroadcastableClass.php index b3ebcd2..77d9b5c 100644 --- a/tests/TestApp/Event/TestBroadcastableClass.php +++ b/tests/TestApp/Event/TestBroadcastableClass.php @@ -10,8 +10,20 @@ class TestBroadcastableClass implements BroadcastableInterface, ConditionalInterface, QueueableInterface { + /** + * Channels array + * + * @var array<\Crustum\Broadcasting\Channel\Channel> + */ protected array $channels = []; + protected ?string $eventName = null; + + /** + * Data array + * + * @var array|null + */ protected ?array $data = null; protected ?string $socket = null; protected bool $shouldBroadcast = true; @@ -58,7 +70,13 @@ public function broadcastWhen(): bool return $this->shouldBroadcast; } - public function setChannels(Channel|array|string $channels): self + /** + * Set channels + * + * @param \Crustum\Broadcasting\Channel\Channel|array<\Crustum\Broadcasting\Channel\Channel>|string $channels Channels + * @return $this + */ + public function setChannels(Channel|array|string $channels) { if (is_string($channels)) { $this->channels = [new Channel($channels)]; @@ -78,7 +96,13 @@ public function setEventName(?string $name): self return $this; } - public function setData(?array $data): self + /** + * Set data + * + * @param array|null $data Data + * @return $this + */ + public function setData(?array $data) { $this->data = $data; diff --git a/tests/TestApp/Model/Table/RoomsTable.php b/tests/TestApp/Model/Table/RoomsTable.php new file mode 100644 index 0000000..d24d50f --- /dev/null +++ b/tests/TestApp/Model/Table/RoomsTable.php @@ -0,0 +1,28 @@ + $config The configuration for the Table. + * @return void + */ + public function initialize(array $config): void + { + parent::initialize($config); + + $this->setTable('rooms'); + $this->setPrimaryKey('id'); + } +} diff --git a/tests/TestApp/Model/Table/UsersTable.php b/tests/TestApp/Model/Table/UsersTable.php index 7b25c66..3fd6ef7 100644 --- a/tests/TestApp/Model/Table/UsersTable.php +++ b/tests/TestApp/Model/Table/UsersTable.php @@ -4,12 +4,16 @@ namespace TestApp\Model\Table; use Cake\ORM\Table; +use Crustum\Broadcasting\Model\Interface\BroadcastingTraitInterface; +use Crustum\Broadcasting\Model\Trait\BroadcastingTrait; /** * Users Table */ -class UsersTable extends Table +class UsersTable extends Table implements BroadcastingTraitInterface { + use BroadcastingTrait; + /** * Initialize method * diff --git a/tests/TestApp/Trait/TestBroadcastingClass.php b/tests/TestApp/Trait/TestBroadcastingClass.php index 3151ecb..aa5d7c2 100644 --- a/tests/TestApp/Trait/TestBroadcastingClass.php +++ b/tests/TestApp/Trait/TestBroadcastingClass.php @@ -3,21 +3,12 @@ namespace Crustum\Broadcasting\Test\TestApp\Trait; -use Crustum\Broadcasting\Event\BroadcastEvent; -use Crustum\Broadcasting\Trait\BroadcastingTrait; +use Crustum\Broadcasting\TestSuite\BroadcastingTrait as TestSuiteBroadcastingTrait; /** * Test class using BroadcastingTrait. */ -class TestBroadcastingClass extends BroadcastEvent +class TestBroadcastingClass { - use BroadcastingTrait; - - /** - * Create a new broadcast event instance. - */ - public function __construct() - { - parent::__construct('TestEvent', []); - } + use TestSuiteBroadcastingTrait; } diff --git a/tests/TestCase/Broadcaster/LogBroadcasterTest.php b/tests/TestCase/Broadcaster/LogBroadcasterTest.php index eae19b6..6706e39 100644 --- a/tests/TestCase/Broadcaster/LogBroadcasterTest.php +++ b/tests/TestCase/Broadcaster/LogBroadcasterTest.php @@ -111,10 +111,10 @@ public function testConfigurationMethods(): void */ private function createMockRequest(): ServerRequestInterface { - $uri = $this->createMock(UriInterface::class); + $uri = $this->createStub(UriInterface::class); $uri->method('__toString')->willReturn('http://example.com/test'); - $request = $this->createMock(ServerRequestInterface::class); + $request = $this->createStub(ServerRequestInterface::class); $request->method('getUri')->willReturn($uri); $request->method('getMethod')->willReturn('POST'); $request->method('getHeaders')->willReturn([]); diff --git a/tests/TestCase/Broadcaster/PusherBroadcasterTest.php b/tests/TestCase/Broadcaster/PusherBroadcasterTest.php index ddaaf68..fdf4604 100644 --- a/tests/TestCase/Broadcaster/PusherBroadcasterTest.php +++ b/tests/TestCase/Broadcaster/PusherBroadcasterTest.php @@ -5,8 +5,6 @@ use Cake\Datasource\EntityInterface; use Cake\Http\ServerRequest; -use Cake\ORM\Locator\LocatorInterface; -use Cake\ORM\Table; use Cake\TestSuite\TestCase; use Crustum\Broadcasting\Broadcaster\PusherBroadcaster; use Crustum\Broadcasting\Exception\BroadcastingException; @@ -27,68 +25,76 @@ class PusherBroadcasterTest extends TestCase { /** - * PusherBroadcaster instance for testing. + * Fixtures * - * @var \Crustum\Broadcasting\Broadcaster\PusherBroadcaster + * @var array */ - protected PusherBroadcaster $pusherBroadcaster; + protected array $fixtures = [ + 'plugin.Crustum\Broadcasting.Orders', + 'plugin.Crustum\Broadcasting.Rooms', + ]; /** - * Mock Pusher client. + * Create Pusher stub. * - * @var \PHPUnit\Framework\MockObject\MockObject + * @return \Pusher\Pusher */ - protected $mockPusher; + protected function createPusherStub(): Pusher + { + return $this->createStub(Pusher::class); + } /** - * Set up test fixtures. + * Create Pusher mock for method configuration. * - * @return void + * @return \Pusher\Pusher&\PHPUnit\Framework\MockObject\MockObject */ - protected function setUp(): void + protected function createPusherMock(): Pusher { - parent::setUp(); - - $config = [ - 'app_id' => 'test-app-id', - 'key' => 'test-key', - 'secret' => 'test-secret', - 'options' => [ - 'cluster' => 'test-cluster', - 'useTLS' => true, - ], - ]; - - $this->mockPusher = $this->createMock(Pusher::class); - $this->pusherBroadcaster = $this->createMockPusherBroadcaster($config); + return $this->createMock(Pusher::class); } /** - * Tear down test fixtures. + * Create PusherBroadcaster with injected Pusher stub. * - * @return void + * @param array $config Configuration array + * @param \Pusher\Pusher|null $pusher Pusher client stub + * @return \Crustum\Broadcasting\Broadcaster\PusherBroadcaster */ - protected function tearDown(): void + protected function createPusherBroadcasterWithStub(array $config, ?Pusher $pusher = null): PusherBroadcaster { - unset($this->pusherBroadcaster, $this->mockPusher); - parent::tearDown(); + $pusher = $pusher ?? $this->createPusherStub(); + $broadcaster = new TestablePusherBroadcaster($config); + + $reflection = new ReflectionClass($broadcaster); + $pusherClientProperty = $reflection->getProperty('pusherClient'); + $pusherClientProperty->setAccessible(true); + $pusherClientProperty->setValue($broadcaster, $pusher); + + return $broadcaster; } /** - * Create a mock PusherBroadcaster with mocked Pusher client. + * Create PusherBroadcaster with partial mock for method stubbing. * * @param array $config Configuration array - * @return \Crustum\Broadcasting\Broadcaster\PusherBroadcaster + * @param list $methodsToStub Methods to stub + * @param \Pusher\Pusher|null $pusher Pusher client stub + * @return \Crustum\Broadcasting\Broadcaster\PusherBroadcaster&\PHPUnit\Framework\MockObject\MockObject */ - protected function createMockPusherBroadcaster(array $config): PusherBroadcaster + protected function createPusherBroadcasterWithMock(array $config, array $methodsToStub, ?Pusher $pusher = null) { - $broadcaster = $this->getMockBuilder(PusherBroadcaster::class) + $pusher = $pusher ?? $this->createPusherStub(); + /** @var list $methodsToStub */ + $broadcaster = $this->getMockBuilder(TestablePusherBroadcaster::class) ->setConstructorArgs([$config]) - ->onlyMethods(['createPusherClient']) + ->onlyMethods($methodsToStub) ->getMock(); - $broadcaster->method('createPusherClient') - ->willReturn($this->mockPusher); + $reflection = new ReflectionClass($broadcaster); + $pusherClientProperty = $reflection->getProperty('pusherClient'); + $pusherClientProperty->setAccessible(true); + $pusherClientProperty->setValue($broadcaster, $pusher); return $broadcaster; } @@ -100,11 +106,24 @@ protected function createMockPusherBroadcaster(array $config): PusherBroadcaster */ public function testConstructor(): void { - $this->assertInstanceOf(PusherBroadcaster::class, $this->pusherBroadcaster); - $this->assertEquals('test-app-id', $this->pusherBroadcaster->getConfig()['app_id']); - $this->assertEquals('test-key', $this->pusherBroadcaster->getConfig()['key']); - $this->assertEquals('test-secret', $this->pusherBroadcaster->getConfig()['secret']); - $this->assertEquals('test-cluster', $this->pusherBroadcaster->getConfig()['options']['cluster']); + $config = [ + 'app_id' => 'test-app-id', + 'key' => 'test-key', + 'secret' => 'test-secret', + 'options' => [ + 'cluster' => 'test-cluster', + 'useTLS' => true, + ], + ]; + + $pusher = $this->createPusherStub(); + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); + + $this->assertInstanceOf(PusherBroadcaster::class, $broadcaster); + $this->assertEquals('test-app-id', $broadcaster->getConfig()['app_id']); + $this->assertEquals('test-key', $broadcaster->getConfig()['key']); + $this->assertEquals('test-secret', $broadcaster->getConfig()['secret']); + $this->assertEquals('test-cluster', $broadcaster->getConfig()['options']['cluster']); } /** @@ -127,7 +146,16 @@ public function testConstructorWithMissingConfig(): void */ public function testGetName(): void { - $this->assertEquals('pusher', $this->pusherBroadcaster->getName()); + $config = [ + 'app_id' => 'test-app-id', + 'key' => 'test-key', + 'secret' => 'test-secret', + ]; + + $pusher = $this->createPusherStub(); + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); + + $this->assertEquals('pusher', $broadcaster->getName()); } /** @@ -137,11 +165,20 @@ public function testGetName(): void */ public function testSupportsChannelType(): void { - $this->assertTrue($this->pusherBroadcaster->supportsChannelType('public')); - $this->assertTrue($this->pusherBroadcaster->supportsChannelType('private')); - $this->assertTrue($this->pusherBroadcaster->supportsChannelType('presence')); - $this->assertFalse($this->pusherBroadcaster->supportsChannelType('invalid')); - $this->assertFalse($this->pusherBroadcaster->supportsChannelType('')); + $config = [ + 'app_id' => 'test-app-id', + 'key' => 'test-key', + 'secret' => 'test-secret', + ]; + + $pusher = $this->createPusherStub(); + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); + + $this->assertTrue($broadcaster->supportsChannelType('public')); + $this->assertTrue($broadcaster->supportsChannelType('private')); + $this->assertTrue($broadcaster->supportsChannelType('presence')); + $this->assertFalse($broadcaster->supportsChannelType('invalid')); + $this->assertFalse($broadcaster->supportsChannelType('')); } /** @@ -151,13 +188,22 @@ public function testSupportsChannelType(): void */ public function testAuthWithMissingChannelName(): void { + $config = [ + 'app_id' => 'test-app-id', + 'key' => 'test-key', + 'secret' => 'test-secret', + ]; + + $pusher = $this->createPusherStub(); + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); + $request = new ServerRequest(); $request = $request->withParsedBody(['socket_id' => 'test-socket-id']); $this->expectException(BroadcastingException::class); $this->expectExceptionMessage('Missing required parameters: channel_name'); - $this->pusherBroadcaster->auth($request); + $broadcaster->auth($request); } /** @@ -167,13 +213,22 @@ public function testAuthWithMissingChannelName(): void */ public function testAuthWithMissingSocketId(): void { + $config = [ + 'app_id' => 'test-app-id', + 'key' => 'test-key', + 'secret' => 'test-secret', + ]; + + $pusher = $this->createPusherStub(); + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); + $request = new ServerRequest(); $request = $request->withParsedBody(['channel_name' => 'test-channel']); $this->expectException(BroadcastingException::class); $this->expectExceptionMessage('Missing required parameters: socket_id'); - $this->pusherBroadcaster->auth($request); + $broadcaster->auth($request); } /** @@ -183,6 +238,15 @@ public function testAuthWithMissingSocketId(): void */ public function testAuthWithInvalidChannel(): void { + $config = [ + 'app_id' => 'test-app-id', + 'key' => 'test-key', + 'secret' => 'test-secret', + ]; + + $pusher = $this->createPusherStub(); + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); + $request = new ServerRequest(); $request = $request->withParsedBody([ 'channel_name' => 'invalid-channel', @@ -192,7 +256,7 @@ public function testAuthWithInvalidChannel(): void $this->expectException(InvalidChannelException::class); $this->expectExceptionMessage('Unauthorized access to channel [invalid-channel].'); - $this->pusherBroadcaster->auth($request); + $broadcaster->auth($request); } /** @@ -208,15 +272,13 @@ public function testAuthWithPrivateChannel(): void 'secret' => 'test-secret', ]; - $this->mockPusher->method('authorizeChannel') + $pusher = $this->createPusherMock(); + $pusher->expects($this->once()) + ->method('authorizeChannel') ->with('private-test', '123.456') ->willReturn('{"auth":"test-key:test-signature"}'); - $broadcaster = new PusherBroadcaster($config); - - $reflection = new ReflectionClass($broadcaster); - $pusherClientProperty = $reflection->getProperty('pusherClient'); - $pusherClientProperty->setValue($broadcaster, $this->mockPusher); + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); // Register the channel first $broadcaster->registerChannel('private-test', function ($user) { @@ -248,11 +310,13 @@ public function testAuthWithPresenceChannel(): void 'secret' => 'test-secret', ]; - $this->mockPusher->method('authorizePresenceChannel') + $pusher = $this->createPusherMock(); + $pusher->expects($this->once()) + ->method('authorizePresenceChannel') ->with('presence-test', '123.456', '1', $this->anything()) ->willReturn('{"auth":"test-key:test-signature","channel_data":"{\"user_info\":{\"id\":1,\"name\":\"Test User\"}}"}'); - $mockEntity = $this->createMock(EntityInterface::class); + $mockEntity = $this->createStub(EntityInterface::class); $mockEntity->method('get') ->willReturnMap([ ['id', 1], @@ -260,16 +324,9 @@ public function testAuthWithPresenceChannel(): void ['username', 'testuser'], ]); - $broadcaster = $this->getMockBuilder(PusherBroadcaster::class) - ->setConstructorArgs([$config]) - ->onlyMethods(['resolveUserFromRequest']) - ->getMock(); - - $reflection = new ReflectionClass($broadcaster); - $pusherClientProperty = $reflection->getProperty('pusherClient'); - $pusherClientProperty->setValue($broadcaster, $this->mockPusher); - - $broadcaster->method('resolveUserFromRequest') + $broadcaster = $this->createPusherBroadcasterWithMock($config, ['resolveUserFromRequest'], $pusher); + $broadcaster->expects($this->atLeastOnce()) + ->method('resolveUserFromRequest') ->willReturn($mockEntity); $broadcaster->setChannelCallbacks(['presence-test' => function ($user) { @@ -296,7 +353,16 @@ public function testAuthWithPresenceChannel(): void */ public function testAuthWithPresenceChannelWithoutUser(): void { - $this->pusherBroadcaster->registerChannel('presence-test', function ($user) { + $config = [ + 'app_id' => 'test-app-id', + 'key' => 'test-key', + 'secret' => 'test-secret', + ]; + + $pusher = $this->createPusherStub(); + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); + + $broadcaster->registerChannel('presence-test', function ($user) { return true; }); @@ -309,7 +375,7 @@ public function testAuthWithPresenceChannelWithoutUser(): void $this->expectException(BroadcastingException::class); $this->expectExceptionMessage('User not authenticated for presence channel'); - $this->pusherBroadcaster->auth($request); + $broadcaster->auth($request); } /** @@ -325,16 +391,13 @@ public function testValidAuthenticationResponse(): void 'secret' => 'test-secret', ]; - $this->mockPusher->expects($this->once()) + $pusher = $this->createPusherMock(); + $pusher->expects($this->once()) ->method('authorizeChannel') ->with('private-test', '123.456') ->willReturn('{"auth":"test-key:test-signature"}'); - $broadcaster = new PusherBroadcaster($config); - - $reflection = new ReflectionClass($broadcaster); - $pusherClientProperty = $reflection->getProperty('pusherClient'); - $pusherClientProperty->setValue($broadcaster, $this->mockPusher); + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); $request = new ServerRequest(); $request = $request->withParsedBody([ @@ -355,13 +418,24 @@ public function testValidAuthenticationResponse(): void */ public function testBroadcast(): void { + $config = [ + 'app_id' => 'test-app-id', + 'key' => 'test-key', + 'secret' => 'test-secret', + ]; + + $pusher = $this->createPusherMock(); + $pusher->expects($this->once()) + ->method('trigger') + ->willReturn((object)['status' => 200]); + + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); + $channels = ['test-channel']; $event = 'test-event'; $payload = ['data' => 'test-data']; - $this->pusherBroadcaster->broadcast($channels, $event, $payload); - - // Method executed successfully without throwing exception + $broadcaster->broadcast($channels, $event, $payload); } /** @@ -371,14 +445,23 @@ public function testBroadcast(): void */ public function testBroadcastWithEmptyChannels(): void { + $config = [ + 'app_id' => 'test-app-id', + 'key' => 'test-key', + 'secret' => 'test-secret', + ]; + + $pusher = $this->createPusherMock(); + $pusher->expects($this->never()) + ->method('trigger'); + + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); + $channels = []; $event = 'test-event'; $payload = ['data' => 'test-data']; - $this->mockPusher->expects($this->never()) - ->method('trigger'); - - $this->pusherBroadcaster->broadcast($channels, $event, $payload); + $broadcaster->broadcast($channels, $event, $payload); } /** @@ -388,13 +471,24 @@ public function testBroadcastWithEmptyChannels(): void */ public function testBroadcastWithMultipleChannels(): void { + $config = [ + 'app_id' => 'test-app-id', + 'key' => 'test-key', + 'secret' => 'test-secret', + ]; + + $pusher = $this->createPusherMock(); + $pusher->expects($this->once()) + ->method('trigger') + ->willReturn((object)['status' => 200]); + + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); + $channels = ['channel1', 'channel2', 'channel3']; $event = 'multi-channel-event'; $payload = ['message' => 'Hello World', 'timestamp' => time()]; - $this->pusherBroadcaster->broadcast($channels, $event, $payload); - - // Method executed successfully without throwing exception + $broadcaster->broadcast($channels, $event, $payload); } /** @@ -404,6 +498,19 @@ public function testBroadcastWithMultipleChannels(): void */ public function testBroadcastWithComplexPayload(): void { + $config = [ + 'app_id' => 'test-app-id', + 'key' => 'test-key', + 'secret' => 'test-secret', + ]; + + $pusher = $this->createPusherMock(); + $pusher->expects($this->once()) + ->method('trigger') + ->willReturn((object)['status' => 200]); + + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); + $channels = ['complex-channel']; $event = 'complex-event'; $payload = [ @@ -412,9 +519,7 @@ public function testBroadcastWithComplexPayload(): void 'metadata' => ['tags' => ['important', 'urgent']], ]; - $this->pusherBroadcaster->broadcast($channels, $event, $payload); - - // Method executed successfully without throwing exception + $broadcaster->broadcast($channels, $event, $payload); } /** @@ -424,7 +529,16 @@ public function testBroadcastWithComplexPayload(): void */ public function testGetClient(): void { - $client = $this->pusherBroadcaster->getClient(); + $config = [ + 'app_id' => 'test-app-id', + 'key' => 'test-key', + 'secret' => 'test-secret', + ]; + + $pusher = $this->createPusherStub(); + $broadcaster = $this->createPusherBroadcasterWithStub($config, $pusher); + + $client = $broadcaster->getClient(); $this->assertInstanceOf(Pusher::class, $client); } @@ -441,51 +555,32 @@ public function testAuthWithChannelClass(): void 'secret' => 'test-secret', ]; - $this->mockPusher->method('authorizeChannel') + $pusher = $this->createPusherMock(); + $pusher->expects($this->once()) + ->method('authorizeChannel') ->with('orders.123', '123.456') ->willReturn('{"auth":"test-key:test-signature"}'); - $mockUser = $this->createMock(EntityInterface::class); + $mockUser = $this->createStub(EntityInterface::class); $mockUser->method('get') ->willReturnMap([ ['id', 1], ['name', 'Test User'], ]); - $mockOrder = $this->createMock(EntityInterface::class); - $mockOrder->method('get') - ->willReturnMap([ - ['id', 123], - ['user_id', 1], - ]); - - $mockTable = $this->getMockBuilder(Table::class) - ->disableOriginalConstructor() - ->onlyMethods(['get']) - ->getMock(); - $mockTable->method('get') - ->with('123') - ->willReturn($mockOrder); - - $broadcaster = $this->getMockBuilder(PusherBroadcaster::class) - ->setConstructorArgs([$config]) - ->onlyMethods(['retrieveUserFromCakeRequest', 'getTableLocator']) - ->getMock(); - - $reflection = new ReflectionClass($broadcaster); - $pusherClientProperty = $reflection->getProperty('pusherClient'); - $pusherClientProperty->setValue($broadcaster, $this->mockPusher); + $broadcaster = $this->createPusherBroadcasterWithMock( + $config, + ['retrieveUserFromCakeRequest', 'getTableLocator'], + $pusher, + ); - $broadcaster->method('retrieveUserFromCakeRequest') + $broadcaster->expects($this->once()) + ->method('retrieveUserFromCakeRequest') ->willReturn($mockUser); - $mockLocator = $this->createMock(LocatorInterface::class); - $mockLocator->method('get') - ->with('Orders') - ->willReturn($mockTable); - - $broadcaster->method('getTableLocator') - ->willReturn($mockLocator); + $broadcaster->expects($this->once()) + ->method('getTableLocator') + ->willReturn($this->getTableLocator()); $broadcaster->registerChannel('orders.{order}', TestOrderChannel::class); @@ -514,52 +609,44 @@ public function testAuthWithChannelClassPresence(): void 'secret' => 'test-secret', ]; - $this->mockPusher->method('authorizePresenceChannel') + $pusher = $this->createPusherMock(); + $pusher->expects($this->once()) + ->method('authorizePresenceChannel') ->with('presence-rooms.456', '123.456', '1', $this->anything()) ->willReturn('{"auth":"test-key:test-signature","channel_data":"{\"user_info\":{\"id\":1,\"name\":\"Test User\",\"email\":\"test@example.com\"}}"}'); - $mockUser = $this->createMock(EntityInterface::class); + $mockUser = $this->createStub(EntityInterface::class); $mockUser->method('get') - ->willReturnMap([ - ['id', 1], - ['name', 'Test User'], - ['email', 'test@example.com'], - ]); - - $mockRoom = $this->createMock(EntityInterface::class); + ->willReturnCallback(function ($field) { + return match ($field) { + 'id' => 1, + 'name' => 'Test User', + 'email' => 'test@example.com', + default => null, + }; + }); + + $mockRoom = $this->createStub(EntityInterface::class); $mockRoom->method('get') ->willReturnMap([ ['id', 456], ['user_id', 1], ]); - $mockTable = $this->getMockBuilder(Table::class) - ->disableOriginalConstructor() - ->onlyMethods(['get']) - ->getMock(); - $mockTable->method('get') - ->with('456') - ->willReturn($mockRoom); - - $broadcaster = $this->getMockBuilder(PusherBroadcaster::class) - ->setConstructorArgs([$config]) - ->onlyMethods(['retrieveUserFromCakeRequest', 'getTableLocator']) - ->getMock(); + $broadcaster = $this->createPusherBroadcasterWithMock( + $config, + ['retrieveUserFromCakeRequest', 'resolveEntityFromKey'], + $pusher, + ); - $reflection = new ReflectionClass($broadcaster); - $pusherClientProperty = $reflection->getProperty('pusherClient'); - $pusherClientProperty->setValue($broadcaster, $this->mockPusher); - - $broadcaster->method('retrieveUserFromCakeRequest') + $broadcaster->expects($this->any()) + ->method('retrieveUserFromCakeRequest') ->willReturn($mockUser); - $mockLocator = $this->createMock(LocatorInterface::class); - $mockLocator->method('get') - ->with('Rooms') - ->willReturn($mockTable); - - $broadcaster->method('getTableLocator') - ->willReturn($mockLocator); + $broadcaster->expects($this->once()) + ->method('resolveEntityFromKey') + ->with('room', '456') + ->willReturn($mockRoom); $broadcaster->registerChannel('presence-rooms.{room}', TestPresenceChannel::class); @@ -591,18 +678,17 @@ public function testAuthWithInvalidChannelClassThrowsException(): void 'secret' => 'test-secret', ]; - $mockUser = $this->createMock(EntityInterface::class); - - $broadcaster = $this->getMockBuilder(PusherBroadcaster::class) - ->setConstructorArgs([$config]) - ->onlyMethods(['retrieveUserFromCakeRequest']) - ->getMock(); + $pusher = $this->createPusherStub(); + $mockUser = $this->createStub(EntityInterface::class); - $reflection = new ReflectionClass($broadcaster); - $pusherClientProperty = $reflection->getProperty('pusherClient'); - $pusherClientProperty->setValue($broadcaster, $this->mockPusher); + $broadcaster = $this->createPusherBroadcasterWithMock( + $config, + ['retrieveUserFromCakeRequest'], + $pusher, + ); - $broadcaster->method('retrieveUserFromCakeRequest') + $broadcaster->expects($this->atLeastOnce()) + ->method('retrieveUserFromCakeRequest') ->willReturn($mockUser); $broadcaster->registerChannel('invalid.{id}', InvalidChannel::class); @@ -632,18 +718,17 @@ public function testAuthWithNonExistentChannelClassThrowsException(): void 'secret' => 'test-secret', ]; - $mockUser = $this->createMock(EntityInterface::class); - - $broadcaster = $this->getMockBuilder(PusherBroadcaster::class) - ->setConstructorArgs([$config]) - ->onlyMethods(['retrieveUserFromCakeRequest']) - ->getMock(); + $pusher = $this->createPusherStub(); + $mockUser = $this->createStub(EntityInterface::class); - $reflection = new ReflectionClass($broadcaster); - $pusherClientProperty = $reflection->getProperty('pusherClient'); - $pusherClientProperty->setValue($broadcaster, $this->mockPusher); + $broadcaster = $this->createPusherBroadcasterWithMock( + $config, + ['retrieveUserFromCakeRequest'], + $pusher, + ); - $broadcaster->method('retrieveUserFromCakeRequest') + $broadcaster->expects($this->atLeastOnce()) + ->method('retrieveUserFromCakeRequest') ->willReturn($mockUser); $broadcaster->registerChannel('nonexistent.{id}', 'NonExistentChannel'); @@ -673,46 +758,26 @@ public function testAuthWithChannelClassUnauthorizedThrowsException(): void 'secret' => 'test-secret', ]; - $mockUser = $this->createMock(EntityInterface::class); + $pusher = $this->createPusherStub(); + $mockUser = $this->createStub(EntityInterface::class); $mockUser->method('get') ->willReturnMap([ ['id', 1], ]); - $mockOrder = $this->createMock(EntityInterface::class); - $mockOrder->method('get') - ->willReturnMap([ - ['id', 123], - ['user_id', 999], - ]); + $broadcaster = $this->createPusherBroadcasterWithMock( + $config, + ['retrieveUserFromCakeRequest', 'getTableLocator'], + $pusher, + ); - $mockTable = $this->getMockBuilder(Table::class) - ->disableOriginalConstructor() - ->onlyMethods(['get']) - ->getMock(); - $mockTable->method('get') - ->with('123') - ->willReturn($mockOrder); - - $broadcaster = $this->getMockBuilder(PusherBroadcaster::class) - ->setConstructorArgs([$config]) - ->onlyMethods(['retrieveUserFromCakeRequest', 'getTableLocator']) - ->getMock(); - - $reflection = new ReflectionClass($broadcaster); - $pusherClientProperty = $reflection->getProperty('pusherClient'); - $pusherClientProperty->setValue($broadcaster, $this->mockPusher); - - $broadcaster->method('retrieveUserFromCakeRequest') + $broadcaster->expects($this->once()) + ->method('retrieveUserFromCakeRequest') ->willReturn($mockUser); - $mockLocator = $this->createMock(LocatorInterface::class); - $mockLocator->method('get') - ->with('Orders') - ->willReturn($mockTable); - - $broadcaster->method('getTableLocator') - ->willReturn($mockLocator); + $broadcaster->expects($this->once()) + ->method('getTableLocator') + ->willReturn($this->getTableLocator()); $broadcaster->registerChannel('private-orders.{order}', UnauthorizedChannel::class); @@ -733,7 +798,7 @@ public function testAuthWithChannelClassUnauthorizedThrowsException(): void public function testAuthWithChannelClassWrongUserThrowsException(): void { $this->expectException(InvalidChannelException::class); - $this->expectExceptionMessage('Unauthorized access to channel [orders.123].'); + $this->expectExceptionMessage('Unauthorized access to channel [orders.124].'); $config = [ 'app_id' => 'test-app-id', @@ -741,52 +806,33 @@ public function testAuthWithChannelClassWrongUserThrowsException(): void 'secret' => 'test-secret', ]; - $mockUser = $this->createMock(EntityInterface::class); + $pusher = $this->createPusherStub(); + + $mockUser = $this->createStub(EntityInterface::class); $mockUser->method('get') ->willReturnMap([ ['id', 1], ]); - $mockOrder = $this->createMock(EntityInterface::class); - $mockOrder->method('get') - ->willReturnMap([ - ['id', 123], - ['user_id', 999], - ]); - - $mockTable = $this->getMockBuilder(Table::class) - ->disableOriginalConstructor() - ->onlyMethods(['get']) - ->getMock(); - $mockTable->method('get') - ->with('123') - ->willReturn($mockOrder); - - $broadcaster = $this->getMockBuilder(PusherBroadcaster::class) - ->setConstructorArgs([$config]) - ->onlyMethods(['retrieveUserFromCakeRequest', 'getTableLocator']) - ->getMock(); - - $reflection = new ReflectionClass($broadcaster); - $pusherClientProperty = $reflection->getProperty('pusherClient'); - $pusherClientProperty->setValue($broadcaster, $this->mockPusher); + $broadcaster = $this->createPusherBroadcasterWithMock( + $config, + ['retrieveUserFromCakeRequest', 'getTableLocator'], + $pusher, + ); - $broadcaster->method('retrieveUserFromCakeRequest') + $broadcaster->expects($this->once()) + ->method('retrieveUserFromCakeRequest') ->willReturn($mockUser); - $mockLocator = $this->createMock(LocatorInterface::class); - $mockLocator->method('get') - ->with('Orders') - ->willReturn($mockTable); - - $broadcaster->method('getTableLocator') - ->willReturn($mockLocator); + $broadcaster->expects($this->once()) + ->method('getTableLocator') + ->willReturn($this->getTableLocator()); $broadcaster->registerChannel('orders.{order}', TestOrderChannel::class); $request = new ServerRequest(); $request = $request->withParsedBody([ - 'channel_name' => 'orders.123', + 'channel_name' => 'orders.124', 'socket_id' => '123.456', ]); diff --git a/tests/TestCase/Broadcaster/RedisBroadcasterTest.php b/tests/TestCase/Broadcaster/RedisBroadcasterTest.php index cfb4c30..7db15a6 100644 --- a/tests/TestCase/Broadcaster/RedisBroadcasterTest.php +++ b/tests/TestCase/Broadcaster/RedisBroadcasterTest.php @@ -51,8 +51,6 @@ protected function setUp(): void */ protected function tearDown(): void { - unset($this->redisBroadcaster); - parent::tearDown(); } diff --git a/tests/TestCase/Broadcaster/TestablePusherBroadcaster.php b/tests/TestCase/Broadcaster/TestablePusherBroadcaster.php new file mode 100644 index 0000000..209c5d4 --- /dev/null +++ b/tests/TestCase/Broadcaster/TestablePusherBroadcaster.php @@ -0,0 +1,48 @@ +testPusherClient = $client; + } + + /** + * Create Pusher client instance. + * + * @param array{driver?: string, key: string, secret: string, app_id: string, options?: array} $config Pusher configuration + * @return \Pusher\Pusher + */ + protected function createPusherClient(array $config): Pusher + { + if ($this->testPusherClient !== null) { + return $this->testPusherClient; + } + + return parent::createPusherClient($config); + } +} diff --git a/tests/TestCase/Channel/EncryptedPrivateChannelTest.php b/tests/TestCase/Channel/EncryptedPrivateChannelTest.php index 0a04160..f566ece 100644 --- a/tests/TestCase/Channel/EncryptedPrivateChannelTest.php +++ b/tests/TestCase/Channel/EncryptedPrivateChannelTest.php @@ -39,8 +39,6 @@ protected function setUp(): void */ protected function tearDown(): void { - unset($this->encryptedPrivateChannel); - parent::tearDown(); } diff --git a/tests/TestCase/Controller/BroadcastingAuthControllerTest.php b/tests/TestCase/Controller/BroadcastingAuthControllerTest.php index b53b616..2a4da2e 100644 --- a/tests/TestCase/Controller/BroadcastingAuthControllerTest.php +++ b/tests/TestCase/Controller/BroadcastingAuthControllerTest.php @@ -53,7 +53,7 @@ private function getResponseBody(): array protected function clearBroadcastingConfigurations(): void { foreach (Broadcasting::configured() as $configName) { - Broadcasting::drop($configName); + Broadcasting::drop((string)$configName); } Broadcasting::getRegistry()->reset(); diff --git a/tests/TestCase/Event/BroadcastableInterfaceTest.php b/tests/TestCase/Event/BroadcastableInterfaceTest.php index dd6d9b7..3bfc42f 100644 --- a/tests/TestCase/Event/BroadcastableInterfaceTest.php +++ b/tests/TestCase/Event/BroadcastableInterfaceTest.php @@ -108,6 +108,7 @@ public function testSetChannels(): void $this->testClass->setChannels($newChannel); $channels = $this->testClass->broadcastChannel(); + $this->assertIsArray($channels); $this->assertCount(1, $channels); $this->assertEquals('new-channel', $channels[0]->getName()); } diff --git a/tests/TestCase/Job/BroadcastJobTest.php b/tests/TestCase/Job/BroadcastJobTest.php index 4ff79f9..d4dcf0e 100644 --- a/tests/TestCase/Job/BroadcastJobTest.php +++ b/tests/TestCase/Job/BroadcastJobTest.php @@ -27,7 +27,6 @@ protected function setUp(): void protected function tearDown(): void { - unset($this->broadcastJob); Broadcasting::drop('test'); parent::tearDown(); @@ -104,12 +103,18 @@ public function testExecuteWithEmptyPayload(): void $this->assertEquals(InteropProcessor::ACK, $result); } - protected function createMessageMock(array $data) + /** + * Create a mock message + * + * @param array $data Message data + * @return \Cake\Queue\Job\Message + */ + protected function createMessageMock(array $data): Message { - $originalMessage = $this->createMock(QueueMessage::class); + $originalMessage = $this->createStub(QueueMessage::class); $originalMessage->method('getMessageId')->willReturn('test-message-id'); - $message = $this->createMock(Message::class); + $message = $this->createStub(Message::class); $message->method('getArgument')->willReturnCallback(function ($key, $default = null) use ($data) { return $data[$key] ?? $default; }); diff --git a/tests/TestCase/Model/Behavior/BroadcastingBehaviorTest.php b/tests/TestCase/Model/Behavior/BroadcastingBehaviorTest.php index d999a12..6372db4 100644 --- a/tests/TestCase/Model/Behavior/BroadcastingBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/BroadcastingBehaviorTest.php @@ -13,6 +13,8 @@ /** * Broadcasting Behavior Test + * + * Tests behavior-specific functionality: event handling and event mapping */ class BroadcastingBehaviorTest extends TestCase { @@ -47,7 +49,7 @@ class BroadcastingBehaviorTest extends TestCase protected function clearBroadcastingConfigurations(): void { foreach (Broadcasting::configured() as $configName) { - Broadcasting::drop($configName); + Broadcasting::drop((string)$configName); } Broadcasting::getRegistry()->reset(); @@ -76,7 +78,9 @@ public function setUp(): void $this->table = new UsersTable(); $this->table->addBehavior('Crustum/Broadcasting.Broadcasting'); - $this->behavior = $this->table->getBehavior('Broadcasting'); + /** @var \Crustum\Broadcasting\Model\Behavior\BroadcastingBehavior $behavior */ + $behavior = $this->table->getBehavior('Broadcasting'); + $this->behavior = $behavior; } /** @@ -91,11 +95,11 @@ public function tearDown(): void } /** - * Test default configuration + * Test default events configuration * * @return void */ - public function testDefaultConfiguration(): void + public function testDefaultEventsConfiguration(): void { $expectedEvents = [ 'Model.afterSave' => 'saved', @@ -103,10 +107,6 @@ public function testDefaultConfiguration(): void ]; $this->assertEquals($expectedEvents, $this->behavior->getConfig('events')); - $this->assertTrue($this->behavior->getConfig('enabled')); - $this->assertEquals('default', $this->behavior->getConfig('connection')); - $this->assertNull($this->behavior->getConfig('channels')); - $this->assertNull($this->behavior->getConfig('payload')); } /** @@ -124,115 +124,6 @@ public function testImplementedEvents(): void $this->assertEquals('handleEvent', $events['Model.afterDelete']); } - /** - * Test enable/disable broadcasting - * - * @return void - */ - public function testEnableDisableBroadcasting(): void - { - $this->assertTrue($this->table->isBroadcastingEnabled()); - - $this->table->disableBroadcasting(); - $this->assertFalse($this->table->isBroadcastingEnabled()); - - $this->table->enableBroadcasting(); - $this->assertTrue($this->table->isBroadcastingEnabled()); - } - - /** - * Test event handling when broadcasting is disabled - * - * @return void - */ - public function testHandleEventWhenDisabled(): void - { - $this->table->disableBroadcasting(); - - $user = new User([ - 'username' => 'test_user', - 'email' => 'test@example.com', - 'password' => 'password123', - ]); - - /** @var \Cake\Event\EventInterface&\PHPUnit\Framework\MockObject\MockObject $event */ - $event = $this->createMock(EventInterface::class); - $event->method('getName')->willReturn('Model.afterSave'); - - $this->behavior->handleEvent($event, $user); - - $this->assertFalse($this->table->isBroadcastingEnabled()); - } - - /** - * Test setting broadcast channels - * - * @return void - */ - public function testSetBroadcastChannels(): void - { - $channels = ['user.1', 'admin']; - $this->table->setBroadcastChannels($channels); - - $this->assertEquals($channels, $this->behavior->getConfig('channels')); - } - - /** - * Test setting broadcast payload - * - * @return void - */ - public function testSetBroadcastPayload(): void - { - $payload = ['id' => 1, 'status' => 'active']; - $this->table->setBroadcastPayload($payload); - - $this->assertEquals($payload, $this->behavior->getConfig('payload')); - } - - /** - * Test setting broadcast connection - * - * @return void - */ - public function testSetBroadcastConnection(): void - { - $this->table->setBroadcastConnection('pusher'); - $this->assertEquals('pusher', $this->behavior->getConfig('connection')); - } - - /** - * Test setting broadcast queue - * - * @return void - */ - public function testSetBroadcastQueue(): void - { - $this->table->setBroadcastQueue('broadcasts'); - $this->assertEquals('broadcasts', $this->behavior->getConfig('queue')); - - $this->table->setBroadcastQueue(null); - $this->assertNull($this->behavior->getConfig('queue')); - } - - /** - * Test broadcast event method exists - * - * @return void - */ - public function testBroadcastEventMethod(): void - { - $this->table->setBroadcastChannels(['test-channel']); - - $user = new User([ - 'username' => 'test_user', - 'email' => 'test@example.com', - 'password' => 'password123', - ]); - - $this->table->broadcastEvent($user, 'created'); - } - /** * Test custom events configuration * @@ -257,129 +148,74 @@ public function testCustomEventsConfiguration(): void } /** - * Test behavior with all configuration options - * - * @return void - */ - public function testFullConfiguration(): void - { - $fullTable = new UsersTable(); - $fullTable->addBehavior('Crustum/Broadcasting.Broadcasting', [ - 'events' => [ - 'Model.afterSave' => 'created', - ], - 'connection' => 'pusher', - 'queue' => 'broadcasts', - 'channels' => ['admin'], - 'payload' => ['custom' => 'data'], - 'enabled' => false, - ]); - - $behavior = $fullTable->getBehavior('Broadcasting'); - - $this->assertEquals('pusher', $behavior->getConfig('connection')); - $this->assertEquals('broadcasts', $behavior->getConfig('queue')); - $this->assertEquals(['admin'], $behavior->getConfig('channels')); - $this->assertEquals(['custom' => 'data'], $behavior->getConfig('payload')); - $this->assertFalse($behavior->getConfig('enabled')); - } - - /** - * Test broadcasting with proper configuration + * Test handleEvent maps events correctly * * @return void */ - public function testBroadcastingWithConfiguration(): void + public function testHandleEventMapsEvents(): void { - $this->table->setBroadcastChannels(['user.123']); - $this->table->setBroadcastConnection('null'); - $this->table->setBroadcastPayload(['id' => 123, 'name' => 'Test User']); + $this->table->setBroadcastChannels(['test-channel']); $user = new User([ - 'id' => 123, 'username' => 'test_user', 'email' => 'test@example.com', 'password' => 'password123', ]); - $this->table->broadcastEvent($user, 'created'); - - $this->assertEquals(['user.123'], $this->behavior->getConfig('channels')); - $this->assertEquals('null', $this->behavior->getConfig('connection')); - $this->assertEquals(['id' => 123, 'name' => 'Test User'], $this->behavior->getConfig('payload')); - } - - /** - * Test setBroadcastEventName method - * - * @return void - */ - public function testSetBroadcastEventName(): void - { - // Test string event name - $this->table->setBroadcastEventName('CustomEventName'); - $this->assertEquals('CustomEventName', $this->behavior->getConfig('eventName')); - - // Test callback event name - $this->table->setBroadcastEventName(function ($entity, $event) { - return 'Custom' . ucfirst($event); - }); - $this->assertIsCallable($this->behavior->getConfig('eventName')); + /** @var \Cake\Event\EventInterface<\Cake\ORM\Table>&\PHPUnit\Framework\MockObject\MockObject $event */ + $event = $this->createStub(EventInterface::class); + $event->method('getName')->willReturn('Model.afterSave'); + $event->method('getSubject')->willReturn($this->table); - // Test null (use default) - $this->table->setBroadcastEventName(null); - $this->assertNull($this->behavior->getConfig('eventName')); + $this->behavior->handleEvent($event, $user); } /** - * Test selective broadcast events configuration + * Test handleEvent maps afterSave to created for new entities * * @return void */ - public function testSelectiveBroadcastEvents(): void + public function testHandleEventMapsAfterSaveToCreated(): void { - $defaultEvents = $this->behavior->getConfig('broadcastEvents'); - $this->assertTrue($defaultEvents['created']); - $this->assertTrue($defaultEvents['updated']); - $this->assertTrue($defaultEvents['deleted']); + $this->table->setBroadcastChannels(['test-channel']); - $this->table->setBroadcastEvents([ - 'created' => true, - 'updated' => false, - 'deleted' => true, + $user = new User([ + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', ]); - $events = $this->behavior->getConfig('broadcastEvents'); - $this->assertTrue($events['created']); - $this->assertFalse($events['updated']); - $this->assertTrue($events['deleted']); + $user->setNew(true); - // Test enable/disable individual events - $this->table->enableBroadcastEvent('updated'); - $this->assertTrue($this->behavior->getConfig('broadcastEvents')['updated']); + /** @var \Cake\Event\EventInterface<\Cake\ORM\Table>&\PHPUnit\Framework\MockObject\MockObject $event */ + $event = $this->createStub(EventInterface::class); + $event->method('getName')->willReturn('Model.afterSave'); + $event->method('getSubject')->willReturn($this->table); - $this->table->disableBroadcastEvent('created'); - $this->assertFalse($this->behavior->getConfig('broadcastEvents')['created']); + $this->behavior->handleEvent($event, $user); } /** - * Test Event naming through broadcastEvent + * Test handleEvent maps afterSave to updated for existing entities * * @return void */ - public function testEventNaming(): void + public function testHandleEventMapsAfterSaveToUpdated(): void { - $user = $this->table->newEntity([ + $this->table->setBroadcastChannels(['test-channel']); + + $user = new User([ 'id' => 1, - 'name' => 'Test User', + 'username' => 'test_user', 'email' => 'test@example.com', + 'password' => 'password123', ]); + $user->setNew(false); - $this->table->broadcastEvent($user, 'created'); - - $this->assertTrue($this->behavior->isBroadcastingEnabled()); - - $this->assertEquals('default', $this->behavior->getConfig('connection')); + /** @var \Cake\Event\EventInterface<\Cake\ORM\Table>&\PHPUnit\Framework\MockObject\MockObject $event */ + $event = $this->createStub(EventInterface::class); + $event->method('getName')->willReturn('Model.afterSave'); + $event->method('getSubject')->willReturn($this->table); - $this->assertEquals('Users', $user->getSource()); + $this->behavior->handleEvent($event, $user); } } diff --git a/tests/TestCase/Model/Trait/BroadcastingTraitTest.php b/tests/TestCase/Model/Trait/BroadcastingTraitTest.php new file mode 100644 index 0000000..c8e66a2 --- /dev/null +++ b/tests/TestCase/Model/Trait/BroadcastingTraitTest.php @@ -0,0 +1,453 @@ + + */ + protected array $fixtures = [ + 'plugin.Crustum\Broadcasting.Users', + ]; + + /** + * Test table instance + * + * @var \TestApp\Model\Table\UsersTable + */ + protected UsersTable $table; + + /** + * Clear all Broadcasting configurations + * + * @return void + */ + protected function clearBroadcastingConfigurations(): void + { + foreach (Broadcasting::configured() as $configName) { + Broadcasting::drop((string)$configName); + } + Broadcasting::getRegistry()->reset(); + + $reflection = new ReflectionClass(Broadcasting::class); + $property = $reflection->getProperty('_channelsLoaded'); + $property->setValue(null, false); + } + + /** + * Set up test case + * + * @return void + */ + public function setUp(): void + { + parent::setUp(); + + $this->clearBroadcastingConfigurations(); + + Broadcasting::setConfig('default', [ + 'className' => 'Crustum/Broadcasting.Null', + ]); + Broadcasting::setConfig('null', [ + 'className' => 'Crustum/Broadcasting.Null', + ]); + + TestBroadcaster::replaceAllBroadcasters(); + TestQueueAdapter::replaceQueueAdapter(); + + $this->table = new UsersTable(); + $this->table->addBehavior('Crustum/Broadcasting.Broadcasting'); + } + + /** + * Tear down test case + * + * @return void + */ + public function tearDown(): void + { + TestQueueAdapter::clearQueuedJobs(); + $this->clearBroadcastingConfigurations(); + parent::tearDown(); + } + + /** + * Test enable/disable broadcasting + * + * @return void + */ + public function testEnableDisableBroadcasting(): void + { + $this->assertTrue($this->table->isBroadcastingEnabled()); + + $this->table->disableBroadcasting(); + $this->assertFalse($this->table->isBroadcastingEnabled()); + + $this->table->enableBroadcasting(); + $this->assertTrue($this->table->isBroadcastingEnabled()); + } + + /** + * Test setting broadcast channels + * + * @return void + */ + public function testSetBroadcastChannels(): void + { + $channels = ['user.1', 'admin']; + $this->table->setBroadcastChannels($channels); + + $user = new User([ + 'id' => 1, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $this->table->broadcastEvent($user, 'created'); + + $this->assertBroadcastSent('UserCreated'); + $this->assertBroadcastSentToChannels(['user.1', 'admin'], 'UserCreated'); + } + + /** + * Test setting broadcast channels with closure + * + * @return void + */ + public function testSetBroadcastChannelsWithClosure(): void + { + $this->table->setBroadcastChannels(function ($entity, $event) { + return ['user.' . $entity->get('id')]; + }); + + $user = new User([ + 'id' => 123, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $this->table->broadcastEvent($user, 'created'); + + $this->assertBroadcastSent('UserCreated'); + $this->assertBroadcastSentToChannels(['user.123'], 'UserCreated'); + } + + /** + * Test setting broadcast payload + * + * @return void + */ + public function testSetBroadcastPayload(): void + { + $payload = ['id' => 1, 'status' => 'active']; + $this->table->setBroadcastPayload($payload); + + $user = new User([ + 'id' => 1, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $this->table->setBroadcastChannels(['test-channel']); + $this->table->broadcastEvent($user, 'created'); + + $broadcasts = TestBroadcaster::getBroadcasts(); + $this->assertCount(1, $broadcasts); + $this->assertEquals($payload, $broadcasts[0]['payload']); + } + + /** + * Test setting broadcast payload with closure + * + * @return void + */ + public function testSetBroadcastPayloadWithClosure(): void + { + $this->table->setBroadcastPayload(function ($entity, $event) { + return [ + 'id' => $entity->get('id'), + 'event' => $event, + ]; + }); + + $user = new User([ + 'id' => 123, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $this->table->setBroadcastChannels(['test-channel']); + $this->table->broadcastEvent($user, 'created'); + + $broadcasts = TestBroadcaster::getBroadcasts(); + $this->assertCount(1, $broadcasts); + $this->assertEquals(['id' => 123, 'event' => 'created'], $broadcasts[0]['payload']); + } + + /** + * Test setting broadcast connection + * + * @return void + */ + public function testSetBroadcastConnection(): void + { + $this->table->setBroadcastConnection('null'); + + $user = new User([ + 'id' => 1, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $this->table->setBroadcastChannels(['test-channel']); + $this->table->broadcastEvent($user, 'created'); + + $this->assertBroadcastSentViaConnection('null', 'UserCreated'); + } + + /** + * Test setting broadcast queue + * + * @return void + */ + public function testSetBroadcastQueue(): void + { + $this->table->setBroadcastQueue('broadcasts'); + + $user = new User([ + 'id' => 1, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $this->table->setBroadcastChannels(['test-channel']); + $this->table->broadcastEvent($user, 'created'); + + $this->assertBroadcastQueued('UserCreated'); + $this->assertNoBroadcastsSent(); + + TestQueueAdapter::clearQueuedJobs(); + + $this->table->setBroadcastQueue(null); + $this->table->broadcastEvent($user, 'updated'); + + $this->assertBroadcastSent('UserUpdated'); + $this->assertNoBroadcastsQueued(); + } + + /** + * Test setting broadcast event name + * + * @return void + */ + public function testSetBroadcastEventName(): void + { + $this->table->setBroadcastEventName('CustomEventName'); + + $user = new User([ + 'id' => 1, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $this->table->setBroadcastChannels(['test-channel']); + $this->table->broadcastEvent($user, 'created'); + + $this->assertBroadcastSent('CustomEventName'); + } + + /** + * Test setting broadcast event name with closure + * + * @return void + */ + public function testSetBroadcastEventNameWithClosure(): void + { + $this->table->setBroadcastEventName(function ($entity, $event) { + return 'Custom' . ucfirst($event); + }); + + $user = new User([ + 'id' => 1, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $this->table->setBroadcastChannels(['test-channel']); + $this->table->broadcastEvent($user, 'created'); + + $this->assertBroadcastSent('CustomCreated'); + } + + /** + * Test setting broadcast events + * + * @return void + */ + public function testSetBroadcastEvents(): void + { + $this->table->setBroadcastEvents([ + 'created' => true, + 'updated' => false, + 'deleted' => true, + ]); + + $user = new User([ + 'id' => 1, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $this->table->setBroadcastChannels(['test-channel']); + + $this->table->broadcastEvent($user, 'created'); + $this->assertBroadcastSent('UserCreated'); + + TestBroadcaster::clearBroadcasts(); + + $this->table->broadcastEvent($user, 'updated'); + $this->assertNoBroadcastsSent(); + + TestBroadcaster::clearBroadcasts(); + + $this->table->broadcastEvent($user, 'deleted'); + $this->assertBroadcastSent('UserDeleted'); + } + + /** + * Test enable/disable individual broadcast events + * + * @return void + */ + public function testEnableDisableBroadcastEvent(): void + { + $user = new User([ + 'id' => 1, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $this->table->setBroadcastChannels(['test-channel']); + + $this->table->disableBroadcastEvent('created'); + $this->table->broadcastEvent($user, 'created'); + $this->assertNoBroadcastsSent(); + + $this->table->enableBroadcastEvent('created'); + $this->table->broadcastEvent($user, 'created'); + $this->assertBroadcastSent('UserCreated'); + } + + /** + * Test broadcastEvent when broadcasting is disabled + * + * @return void + */ + public function testBroadcastEventWhenDisabled(): void + { + $this->table->disableBroadcasting(); + + $user = new User([ + 'id' => 1, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $this->table->setBroadcastChannels(['test-channel']); + $this->table->broadcastEvent($user, 'created'); + + $this->assertNoBroadcastsSent(); + } + + /** + * Test broadcastEvent with default channel (entity itself) + * + * @return void + */ + public function testBroadcastEventWithDefaultChannel(): void + { + $user = new User([ + 'id' => 123, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $this->table->broadcastEvent($user, 'created'); + + $this->assertBroadcastSent('UserCreated'); + $this->assertBroadcastSentToChannels(['TestApp.Model.Entity.User.123'], 'UserCreated'); + } + + /** + * Test initialization from behavior config + * + * @return void + */ + public function testInitializationFromBehaviorConfig(): void + { + $table = new UsersTable(); + $table->addBehavior('Crustum/Broadcasting.Broadcasting', [ + 'connection' => 'null', + 'queue' => null, + 'channels' => ['admin'], + 'payload' => ['custom' => 'data'], + 'enabled' => false, + 'broadcastEvents' => [ + 'created' => false, + 'updated' => true, + ], + ]); + + $this->assertFalse($table->isBroadcastingEnabled()); + + $table->enableBroadcasting(); + + $user = new User([ + 'id' => 999, + 'username' => 'test_user', + 'email' => 'test@example.com', + 'password' => 'password123', + ]); + + $table->broadcastEvent($user, 'created'); + $this->assertNoBroadcastsSent(); + + TestBroadcaster::clearBroadcasts(); + + $table->broadcastEvent($user, 'updated'); + $this->assertBroadcastSent('UserUpdated'); + $this->assertBroadcastSentToChannels(['admin'], 'UserUpdated'); + } +} diff --git a/tests/TestCase/PendingBroadcastTest.php b/tests/TestCase/PendingBroadcastTest.php index ca40808..6325de5 100644 --- a/tests/TestCase/PendingBroadcastTest.php +++ b/tests/TestCase/PendingBroadcastTest.php @@ -9,6 +9,9 @@ use Cake\TestSuite\TestCase; use Crustum\Broadcasting\Broadcasting; use Crustum\Broadcasting\PendingBroadcast; +use Crustum\Broadcasting\TestSuite\BroadcastingTrait; +use Crustum\Broadcasting\TestSuite\TestBroadcaster; +use Crustum\Broadcasting\TestSuite\TestQueueAdapter; use RuntimeException; /** @@ -18,6 +21,8 @@ */ class PendingBroadcastTest extends TestCase { + use BroadcastingTrait; + /** * Clear all Broadcasting configurations * @@ -26,7 +31,7 @@ class PendingBroadcastTest extends TestCase protected function clearBroadcastingConfigurations(): void { foreach (Broadcasting::configured() as $configName) { - Broadcasting::drop($configName); + Broadcasting::drop((string)$configName); } Broadcasting::getRegistry()->reset(); } @@ -42,11 +47,13 @@ public function setUp(): void $this->clearBroadcastingConfigurations(); - Broadcasting::setConfig('default', [ - 'className' => 'Crustum/Broadcasting.Null', - ]); + TestBroadcaster::replaceAllBroadcasters(); + TestBroadcaster::clearBroadcasts(); + TestQueueAdapter::replaceQueueAdapter(); + TestQueueAdapter::clearQueuedJobs(); + Broadcasting::setConfig('pusher', [ - 'className' => 'Crustum/Broadcasting.Null', + 'className' => TestBroadcaster::class, ]); QueueManager::setConfig('default', [ @@ -175,7 +182,11 @@ public function testSendBroadcastsImmediately(): void ->data(['id' => 1, 'title' => 'Test']) ->send(); - $this->assertTrue(true); + $this->assertBroadcastSent('PostCreated'); + $this->assertBroadcastSentToChannel('posts', 'PostCreated'); + $broadcasts = TestBroadcaster::getBroadcasts(); + $this->assertCount(1, $broadcasts); + $this->assertEquals(['id' => 1, 'title' => 'Test'], $broadcasts[0]['payload']); } /** @@ -190,7 +201,12 @@ public function testQueueBroadcast(): void ->data(['id' => 1, 'title' => 'Test']) ->queue('broadcasting'); - $this->assertTrue(true); + $this->assertBroadcastQueued('PostCreated'); + $this->assertBroadcastQueuedToChannel('posts', 'PostCreated'); + $this->assertNoBroadcastsSent(); + $queued = TestQueueAdapter::getQueuedBroadcastsByEvent('PostCreated'); + $this->assertCount(1, $queued); + $this->assertEquals(['id' => 1, 'title' => 'Test'], $queued[0]['data']['payload']); } /** @@ -206,7 +222,11 @@ public function testCompleteFluentChain(): void ->connection('pusher') ->send(); - $this->assertTrue(true); + $this->assertBroadcastSent('PostCreated'); + $broadcasts = TestBroadcaster::getBroadcasts(); + $this->assertCount(1, $broadcasts); + $this->assertEquals(['posts', 'notifications'], $broadcasts[0]['channels']); + $this->assertEquals(['id' => 1, 'title' => 'Test Post'], $broadcasts[0]['payload']); } /** @@ -229,7 +249,10 @@ public function testFluentChainWithSocketExclusion(): void ->toOthers() ->send(); - $this->assertTrue(true); + $this->assertBroadcastSent('PostUpdated'); + $broadcasts = TestBroadcaster::getBroadcasts(); + $this->assertCount(1, $broadcasts); + $this->assertEquals('user-socket', $broadcasts[0]['socket']); } /** @@ -275,7 +298,9 @@ public function testAutoSendInDestructor(): void unset($pending); - $this->assertTrue(true); + $this->assertBroadcastSent('PostCreated'); + $broadcasts = TestBroadcaster::getBroadcasts(); + $this->assertCount(1, $broadcasts); } /** @@ -290,6 +315,9 @@ public function testMultipleChannels(): void ->data(['id' => 1]) ->send(); - $this->assertTrue(true); + $this->assertBroadcastSent('PostPublished'); + $broadcasts = TestBroadcaster::getBroadcasts(); + $this->assertCount(1, $broadcasts); + $this->assertEquals(['posts', 'feed', 'notifications'], $broadcasts[0]['channels']); } } diff --git a/tests/TestCase/Registry/BroadcasterRegistryTest.php b/tests/TestCase/Registry/BroadcasterRegistryTest.php index 1f8be44..0305de0 100644 --- a/tests/TestCase/Registry/BroadcasterRegistryTest.php +++ b/tests/TestCase/Registry/BroadcasterRegistryTest.php @@ -263,7 +263,9 @@ public function testRegistryMaintainsSeparateInstances(): void $broadcaster2 = $this->registry->load('instance2', $config2); $this->assertNotSame($broadcaster1, $broadcaster2); - $this->assertNotEquals($broadcaster1->getConfig('key'), $broadcaster2->getConfig('key')); + $config1 = $broadcaster1->getConfig(); + $config2 = $broadcaster2->getConfig(); + $this->assertNotEquals($config1['key'], $config2['key']); } /** diff --git a/tests/TestCase/TestSuite/TestBroadcasterTest.php b/tests/TestCase/TestSuite/TestBroadcasterTest.php index da6a84b..9a75207 100644 --- a/tests/TestCase/TestSuite/TestBroadcasterTest.php +++ b/tests/TestCase/TestSuite/TestBroadcasterTest.php @@ -26,7 +26,7 @@ public function setUp(): void parent::setUp(); foreach (Broadcasting::configured() as $config) { - Broadcasting::drop($config); + Broadcasting::drop((string)$config); } Broadcasting::setConfig('default', [ diff --git a/tests/TestCase/Trait/PusherChannelConventionsTraitTest.php b/tests/TestCase/Trait/PusherChannelConventionsTraitTest.php index 47e21a0..84debd8 100644 --- a/tests/TestCase/Trait/PusherChannelConventionsTraitTest.php +++ b/tests/TestCase/Trait/PusherChannelConventionsTraitTest.php @@ -39,8 +39,6 @@ protected function setUp(): void */ protected function tearDown(): void { - unset($this->testClass); - parent::tearDown(); } diff --git a/tests/schema.php b/tests/schema.php index acaa754..bcee8bd 100644 --- a/tests/schema.php +++ b/tests/schema.php @@ -165,4 +165,21 @@ ], ], ], + [ + 'table' => 'rooms', + 'columns' => [ + 'id' => [ + 'type' => 'integer', + 'autoIncrement' => true, + ], + ], + 'constraints' => [ + 'primary' => [ + 'type' => 'primary', + 'columns' => [ + 'id', + ], + ], + ], + ], ];