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',
+ ],
+ ],
+ ],
+ ],
];