diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index cf5525f..e3cc518 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: test: - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest strategy: fail-fast: true matrix: diff --git a/src/Pub/Broadcasting/Broadcasters/EventBridgeBroadcaster.php b/src/Pub/Broadcasting/Broadcasters/EventBridgeBroadcaster.php index 2f70441..ca62663 100644 --- a/src/Pub/Broadcasting/Broadcasters/EventBridgeBroadcaster.php +++ b/src/Pub/Broadcasting/Broadcasters/EventBridgeBroadcaster.php @@ -4,6 +4,8 @@ use Aws\EventBridge\EventBridgeClient; use Illuminate\Broadcasting\Broadcasters\Broadcaster; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Log; class EventBridgeBroadcaster extends Broadcaster { @@ -52,9 +54,17 @@ public function broadcast(array $channels, $event, array $payload = []) { $events = $this->mapToEventBridgeEntries($channels, $event, $payload); - $this->eventBridgeClient->putEvents([ + $result = $this->eventBridgeClient->putEvents([ 'Entries' => $events, ]); + + if ($this->failedToBroadcast($result)) { + Log::error('Failed to send events to EventBridge', [ + 'errors' => collect($result->get('Entries'))->filter(function (array $entry) { + return Arr::hasAny($entry, ['ErrorCode', 'ErrorMessage']); + })->toArray(), + ]); + } } /** @@ -76,4 +86,11 @@ protected function mapToEventBridgeEntries(array $channels, string $event, array }) ->all(); } + + protected function failedToBroadcast(?\Aws\Result $result): bool + { + return $result + && $result->hasKey('FailedEntryCount') + && $result->get('FailedEntryCount') > 0; + } } diff --git a/tests/Pub/BasicEvents/EventBridgeTest.php b/tests/Pub/BasicEvents/EventBridgeTest.php index fc99bb5..b14be89 100644 --- a/tests/Pub/BasicEvents/EventBridgeTest.php +++ b/tests/Pub/BasicEvents/EventBridgeTest.php @@ -2,6 +2,8 @@ namespace PodPoint\AwsPubSub\Tests\Pub\BasicEvents; +use Aws\Result; +use Illuminate\Support\Facades\Log; use Mockery as m; use Mockery\MockInterface; use PodPoint\AwsPubSub\Tests\Pub\Concerns\InteractsWithEventBridge; @@ -19,52 +21,52 @@ class EventBridgeTest extends TestCase /** @test */ public function it_broadcasts_basic_event_with_the_event_name_as_the_detail_type_and_serialised_event_as_the_detail() { - $janeRetrieved = new UserRetrieved($this->createJane()); + $event = new UserRetrieved($this->createJane()); - $this->mockEventBridge(function (MockInterface $eventBridge) use ($janeRetrieved) { + $this->mockEventBridge(function (MockInterface $eventBridge) use ($event) { $eventBridge ->shouldReceive('putEvents') ->once() - ->with(m::on(function ($arg) use ($janeRetrieved) { - return $arg['Entries'][0]['Detail'] === json_encode($janeRetrieved) + ->with(m::on(function ($arg) use ($event) { + return $arg['Entries'][0]['Detail'] === json_encode($event) && $arg['Entries'][0]['DetailType'] === UserRetrieved::class && $arg['Entries'][0]['EventBusName'] === 'users' && $arg['Entries'][0]['Source'] === 'my-app'; })); }); - event($janeRetrieved); + event($event); } /** @test */ public function it_broadcasts_basic_event_with_action() { - $janeRetrieved = new UserRetrievedWithCustomName($this->createJane()); + $event = new UserRetrievedWithCustomName($this->createJane()); - $this->mockEventBridge(function (MockInterface $eventBridge) use ($janeRetrieved) { + $this->mockEventBridge(function (MockInterface $eventBridge) use ($event) { $eventBridge ->shouldReceive('putEvents') ->once() - ->with(m::on(function ($arg) use ($janeRetrieved) { - return $arg['Entries'][0]['Detail'] === json_encode($janeRetrieved) + ->with(m::on(function ($arg) use ($event) { + return $arg['Entries'][0]['Detail'] === json_encode($event) && $arg['Entries'][0]['DetailType'] === 'user.retrieved'; })); }); - event($janeRetrieved); + event($event); } /** @test */ public function it_broadcasts_basic_event_with_action_and_custom_payload() { - $janeRetrieved = new UserRetrievedWithCustomPayload($this->createJane()); + $event = new UserRetrievedWithCustomPayload($this->createJane()); - $this->mockEventBridge(function (MockInterface $eventBridge) use ($janeRetrieved) { + $this->mockEventBridge(function (MockInterface $eventBridge) use ($event) { $eventBridge ->shouldReceive('putEvents') ->once() - ->with(m::on(function ($arg) use ($janeRetrieved) { - $customPayload = array_merge($janeRetrieved->broadcastWith(), ['socket' => null]); + ->with(m::on(function ($arg) use ($event) { + $customPayload = array_merge($event->broadcastWith(), ['socket' => null]); return $arg['Entries'][0]['Detail'] === json_encode($customPayload) && $arg['Entries'][0]['DetailType'] === UserRetrievedWithCustomPayload::class @@ -72,22 +74,22 @@ public function it_broadcasts_basic_event_with_action_and_custom_payload() })); }); - event($janeRetrieved); + event($event); } /** @test */ public function it_broadcasts_basic_event_to_multiple_channels_as_buses() { - $janeRetrieved = new UserRetrievedWithMultipleChannels($this->createJane()); + $event = new UserRetrievedWithMultipleChannels($this->createJane()); - $this->mockEventBridge(function (MockInterface $eventBridge) use ($janeRetrieved) { + $this->mockEventBridge(function (MockInterface $eventBridge) use ($event) { $eventBridge ->shouldReceive('putEvents') ->once() - ->with(m::on(function ($arg) use ($janeRetrieved) { - return collect($janeRetrieved->broadcastOn()) - ->map(function ($channel, $key) use ($arg, $janeRetrieved) { - return $arg['Entries'][$key]['Detail'] === json_encode($janeRetrieved) + ->with(m::on(function ($arg) use ($event) { + return collect($event->broadcastOn()) + ->map(function ($channel, $key) use ($arg, $event) { + return $arg['Entries'][$key]['Detail'] === json_encode($event) && $arg['Entries'][$key]['DetailType'] === UserRetrievedWithMultipleChannels::class && $arg['Entries'][$key]['EventBusName'] === $channel; }) @@ -96,7 +98,7 @@ public function it_broadcasts_basic_event_to_multiple_channels_as_buses() })); }); - event($janeRetrieved); + event($event); } /** @test */ @@ -104,14 +106,14 @@ public function it_can_use_a_source() { config(['broadcasting.connections.eventbridge.source' => 'some-other-source']); - $janeRetrieved = new UserRetrievedWithMultipleChannels($this->createJane()); + $event = new UserRetrievedWithMultipleChannels($this->createJane()); - $this->mockEventBridge(function (MockInterface $eventBridge) use ($janeRetrieved) { + $this->mockEventBridge(function (MockInterface $eventBridge) use ($event) { $eventBridge ->shouldReceive('putEvents') ->once() - ->with(m::on(function ($arg) use ($janeRetrieved) { - return collect($janeRetrieved->broadcastOn()) + ->with(m::on(function ($arg) use ($event) { + return collect($event->broadcastOn()) ->map(function ($channel, $key) use ($arg) { return $arg['Entries'][$key]['Source'] === 'some-other-source'; }) @@ -120,7 +122,38 @@ public function it_can_use_a_source() })); }); - event($janeRetrieved); + event($event); + } + + /** @test */ + public function it_logs_errors_when_events_fail_to_send() + { + $event = new UserRetrieved($this->createJane()); + + $failedEntry = [ + 'ErrorCode' => 'InternalFailure', + 'ErrorMessage' => 'Something went wrong', + 'EventId' => $this->faker->uuid, + ]; + + $this->mockEventBridge(function (MockInterface $eventBridge) use ($failedEntry) { + $eventBridge + ->shouldReceive('putEvents') + ->once() + ->andReturn(new Result([ + 'FailedEntryCount' => 1, + 'Entries' => [ + $failedEntry, + ['EventId' => $this->faker->uuid], + ], + ])); + }); + + Log::shouldReceive('error')->once()->with('Failed to send events to EventBridge', [ + 'errors' => [$failedEntry], + ]); + + event($event); } protected function createJane(): User