diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitEventsApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitEventsApiController.php index 3bad7e46e..40e042ee0 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitEventsApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitEventsApiController.php @@ -2902,4 +2902,64 @@ public function getScheduledEventStreamingInfo($summit_id, $event_id) }); } + + + #[OA\Get( + path: '/api/v1/summits/{id}/events/all/published/occupancy/overflow', + operationId: 'getOverflowPublishedEvents', + summary: 'Get all published overflow events for a summit', + description: 'Retrieves a paginated list of all events (published and with occupancy == OVERFLOW) for a specific summit.', + security: [['summit_events_api_oauth2' => [SummitScopes::ReadOverflowEvents]]], + tags: ['Summit Events', 'Overflow'], + parameters: [ + new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Summit ID or slug', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'page', in: 'query', required: false, description: 'Page number', schema: new OA\Schema(type: 'integer', default: 1)), + new OA\Parameter(name: 'per_page', in: 'query', required: false, description: 'Items per page', schema: new OA\Schema(type: 'integer', default: 10, maximum: 100)), + new OA\Parameter(name: 'filter[]', in: 'query', required: false, description: 'Filter expressions', style: 'form', explode: true, schema: new OA\Schema(type: 'array', items: new OA\Items(type: 'string'))), + new OA\Parameter(name: 'order', in: 'query', required: false, description: 'Order by field(s)', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'expand', in: 'query', required: false, description: 'Expand relationships', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response(response: Response::HTTP_OK, description: 'Events retrieved successfully', content: new OA\JsonContent(ref: '#/components/schemas/PaginatedSummitEventsResponse')), + new OA\Response(response: Response::HTTP_BAD_REQUEST, description: 'Bad Request'), + new OA\Response(response: Response::HTTP_UNAUTHORIZED, description: 'Unauthorized'), + new OA\Response(response: Response::HTTP_FORBIDDEN, description: 'Forbidden'), + new OA\Response(response: Response::HTTP_NOT_FOUND, description: 'Not Found'), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: 'Server Error'), + ] + )] + + /** + * @param $summit_id + * @return mixed + */ + public function getOverflowPublishedEvents($summit_id) + { + $req = request(); + $req->attributes->set('timing.controller_start', microtime(true)); + return $this->processRequest(function () use ($summit_id, $req) { + $current_user = $this->resource_server_context->getCurrentUser(true); + return $this->withReplica(function() use ($summit_id, $current_user, $req) { + $strategy = new RetrieveAllOverflowPublishedSummitEventsStrategy($this->repository, $this->event_repository, $this->resource_server_context); + $response = $strategy->getEvents(['summit_id' => $summit_id]); + $req->attributes->set('timing.serializer_start', microtime(true)); + $data = $response->toArray + ( + SerializerUtils::getExpand(), + SerializerUtils::getFields(), + SerializerUtils::getRelations(), + [ + 'current_user' => $current_user + ], + IPresentationSerializerTypes::OverflowStream + ); + $req->attributes->set('timing.serializer_end', microtime(true)); + $result = $this->ok($data); + $req->attributes->set('timing.controller_end', microtime(true)); + return $result; + }); + + }); + } + } diff --git a/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrieveAllOverflowPublishedSummitEventsStrategy.php b/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrieveAllOverflowPublishedSummitEventsStrategy.php new file mode 100644 index 000000000..f46a3f9c3 --- /dev/null +++ b/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrieveAllOverflowPublishedSummitEventsStrategy.php @@ -0,0 +1,25 @@ +addFilterCondition(FilterParser::buildFilter('occupancy','==',SummitEvent::OccupancyOverflow)); + return $filter; + } +} diff --git a/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrieveAllPublishedSummitEventsStrategy.php b/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrieveAllPublishedSummitEventsStrategy.php index e5157c12e..7c752b283 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrieveAllPublishedSummitEventsStrategy.php +++ b/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrieveAllPublishedSummitEventsStrategy.php @@ -42,4 +42,4 @@ protected function buildFilter() $filter->addFilterCondition(FilterParser::buildFilter('type_allows_location','==','1')); return $filter; } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrievePublishedSummitEventsBySummitStrategy.php b/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrievePublishedSummitEventsBySummitStrategy.php index 3cd64bef2..0acd990d5 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrievePublishedSummitEventsBySummitStrategy.php +++ b/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrievePublishedSummitEventsBySummitStrategy.php @@ -19,7 +19,7 @@ * Class RetrievePublishedSummitEventsBySummitStrategy * @package App\Http\Controllers */ -final class RetrievePublishedSummitEventsBySummitStrategy extends RetrieveAllSummitEventsBySummitStrategy +class RetrievePublishedSummitEventsBySummitStrategy extends RetrieveAllSummitEventsBySummitStrategy { /** * @return array @@ -41,4 +41,4 @@ protected function buildFilter() $filter->addFilterCondition(FilterParser::buildFilter('type_allows_location','==','1')); return $filter; } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrieveSummitEventsStrategy.php b/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrieveSummitEventsStrategy.php index 5d6716fc3..e78de2784 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrieveSummitEventsStrategy.php +++ b/app/Http/Controllers/Apis/Protected/Summit/Strategies/events/RetrieveSummitEventsStrategy.php @@ -213,6 +213,7 @@ protected function getValidFilters() 'progress' => ['=='], 'status' => ['=='], 'rsvp_type'=> ['==', '<>','=@', '@@'], + 'occupancy' => ['=='], ]; } @@ -273,6 +274,7 @@ protected function getFilterValidatorRules(): array 'progress' => 'sometimes|integer', 'status' =>'sometimes|string', 'rsvp_type' => 'sometimes|string|in:' . implode(',', SummitEvent::AllowedRSVPTypes), + 'occupancy' => 'sometimes|string|in:' . implode(',', SummitEvent::ValidOccupanciesValues), ]; } } diff --git a/app/ModelSerializers/Summit/SummitEventOverflowStreamingSerializer.php b/app/ModelSerializers/Summit/SummitEventOverflowStreamingSerializer.php index a2761267b..267640dad 100644 --- a/app/ModelSerializers/Summit/SummitEventOverflowStreamingSerializer.php +++ b/app/ModelSerializers/Summit/SummitEventOverflowStreamingSerializer.php @@ -12,9 +12,7 @@ * limitations under the License. **/ -use App\ModelSerializers\Traits\RequestCache; use Libs\ModelSerializers\AbstractSerializer; -use libs\utils\CacheRegions; use models\summit\SummitEvent; /** @@ -23,7 +21,6 @@ */ class SummitEventOverflowStreamingSerializer extends AbstractSerializer { - use RequestCache; /** * @param $expand * @param array $fields @@ -33,25 +30,19 @@ class SummitEventOverflowStreamingSerializer extends AbstractSerializer */ public function serialize($expand = null, array $fields = [], array $relations = [], array $params = []) { - return $this->cache( - CacheRegions::getCacheRegionForSummitEvent($this->object->getIdentifier()), - sprintf("SummitEventOverflowStreamingSerializer_%s", $this->object->getIdentifier()), - function () use ($expand, $fields, $relations, $params) { + $event = $this->object; + if (!$event instanceof SummitEvent) return []; - $event = $this->object; - if (!$event instanceof SummitEvent) return []; - - $values['id'] = $event->getId(); - $values['title'] = $event->getTitle(); - $values['start_date'] = $event->getStartDate()->getTimestamp(); - $values['end_date'] = $event->getEndDate()->getTimestamp(); - $values['overflow_streaming_url'] = $event->getOverflowStreamingUrl(); - $values['overflow_stream_is_secure'] = $event->getOverflowStreamIsSecure(); - $values['overflow_tokens'] = []; - if($event->getOverflowStreamIsSecure()){ - $values['overflow_tokens'] = $event->getOverflowStreamingTokens(); - } - return $values; - }); + $values['id'] = $event->getId(); + $values['title'] = $event->getTitle(); + $values['start_date'] = $event->getStartDate()->getTimestamp(); + $values['end_date'] = $event->getEndDate()->getTimestamp(); + $values['overflow_streaming_url'] = $event->getOverflowStreamingUrl(); + $values['overflow_stream_is_secure'] = $event->getOverflowStreamIsSecure(); + $values['overflow_tokens'] = []; + if($event->getOverflowStreamIsSecure()){ + $values['overflow_tokens'] = $event->getOverflowStreamingTokens(); + } + return $values; } } \ No newline at end of file diff --git a/app/Repositories/Summit/DoctrineSummitEventRepository.php b/app/Repositories/Summit/DoctrineSummitEventRepository.php index 93498f5b8..f9c58fc6e 100644 --- a/app/Repositories/Summit/DoctrineSummitEventRepository.php +++ b/app/Repositories/Summit/DoctrineSummitEventRepository.php @@ -135,6 +135,7 @@ protected function getFilterMappings() 'review_status' => 'REVIEW_STATUS(e.id)', 'submission_source' => 'e.submission_source:json_string', 'rsvp_type' => 'e.rsvp_type:json_string', + 'occupancy' => 'e.occupancy:json_string', 'summit_id' => "s.id", 'tags' => "t.tag", 'is_chair_visible' => Filter::buildBooleanField("c.chair_visible"), diff --git a/app/Security/SummitScopes.php b/app/Security/SummitScopes.php index 04b4d21be..5911424aa 100644 --- a/app/Security/SummitScopes.php +++ b/app/Security/SummitScopes.php @@ -21,6 +21,7 @@ final class SummitScopes { const ReadSummitData = SCOPE_BASE_REALM.'/summits/read'; const ReadAllSummitData = SCOPE_BASE_REALM.'/summits/read/all'; + const ReadOverflowEvents = SCOPE_BASE_REALM.'/summits/events/overflow/read'; // me const MeRead = SCOPE_BASE_REALM.'/me/read'; diff --git a/database/migrations/config/APIEndpointsMigrationHelper.php b/database/migrations/config/APIEndpointsMigrationHelper.php index 3ded89412..32ad97dd6 100644 --- a/database/migrations/config/APIEndpointsMigrationHelper.php +++ b/database/migrations/config/APIEndpointsMigrationHelper.php @@ -231,4 +231,96 @@ protected function deleteApiScopes(string $apiName, array $scopes): string AND s.name IN ({$scopeList}); SQL; } + + /** + * Register one endpoint plus its scope and authz-group associations, issuing every + * addSql() call needed. Unlike the insert/delete builders above (which return a SQL + * string for the caller to pass to addSql()), this method has the side effect itself. + * + * @param string $apiName API identifier (e.g., 'summits') + * @param string $endpointName Endpoint identifier (e.g., 'get-overflow-published-events') + * @param string $route Route pattern, matching the Laravel route exactly (same {param} names) + * @param string $httpMethod 'GET', 'POST', 'PUT', 'DELETE', etc. + * @param array $scopes Scope URIs to associate (e.g. SummitScopes::ReadSummitData) + * @param array $authzGroups Group slugs to associate (e.g. IGroup::SuperAdmins) + * @param bool $active + * @param bool $allowCors + * @param bool $allowCredentials + * @return void + */ + protected function registerEndpoint( + string $apiName, + string $endpointName, + string $route, + string $httpMethod, + array $scopes = [], + array $authzGroups = [], + bool $active = true, + bool $allowCors = true, + bool $allowCredentials = true + ): void { + $this->addSql($this->insertEndpoint($apiName, $endpointName, $route, $httpMethod, $active, $allowCors, $allowCredentials)); + + foreach ($scopes as $scope) { + $this->addSql($this->insertEndpointScope($apiName, $endpointName, $scope)); + } + + foreach ($authzGroups as $group) { + $this->addSql($this->insertEndpointAuthzGroup($apiName, $endpointName, $group)); + } + } + + /** + * Register a batch of endpoints via registerEndpoint(). Each entry is an assoc array + * shaped like ApiEndpointsSeeder's endpoint definitions: + * ['name' => ..., 'route' => ..., 'http_method' => ..., 'scopes' => [...], 'authz_groups' => [...]]. + * + * @param string $apiName + * @param array $endpoints + * @return void + */ + protected function registerEndpoints(string $apiName, array $endpoints): void + { + foreach ($endpoints as $endpoint) { + $this->registerEndpoint( + $apiName, + $endpoint['name'], + $endpoint['route'], + $endpoint['http_method'], + $endpoint['scopes'] ?? [], + $endpoint['authz_groups'] ?? [], + $endpoint['active'] ?? true, + $endpoint['allow_cors'] ?? true, + $endpoint['allow_credentials'] ?? true + ); + } + } + + /** + * Remove one endpoint. api_endpoints has ON DELETE CASCADE to endpoint_api_scopes and + * endpoint_api_authz_groups (verified against the config DB schema), so deleting the + * endpoint row alone is sufficient to remove its associations too. + * + * @param string $apiName + * @param string $endpointName + * @return void + */ + protected function unregisterEndpoint(string $apiName, string $endpointName): void + { + $this->addSql($this->deleteEndpoint($apiName, $endpointName)); + } + + /** + * Remove a batch of endpoints via unregisterEndpoint(). + * + * @param string $apiName + * @param array $endpointNames + * @return void + */ + protected function unregisterEndpoints(string $apiName, array $endpointNames): void + { + foreach ($endpointNames as $endpointName) { + $this->unregisterEndpoint($apiName, $endpointName); + } + } } diff --git a/database/migrations/config/Version20260715120000.php b/database/migrations/config/Version20260715120000.php new file mode 100644 index 000000000..61bea222a --- /dev/null +++ b/database/migrations/config/Version20260715120000.php @@ -0,0 +1,69 @@ +addSql($this->insertApiScope( + self::API_NAME, + SummitScopes::ReadOverflowEvents, + 'Read Summit Overflow Events Data', + 'Grants read only access to published summit events currently in OVERFLOW occupancy, including overflow streaming URLs and tokens' + )); + + $this->registerEndpoint( + self::API_NAME, + self::ENDPOINT_NAME, + self::ENDPOINT_ROUTE, + 'GET', + [ + SummitScopes::ReadOverflowEvents, + ] + ); + } + + public function down(Schema $schema): void + { + $this->unregisterEndpoint(self::API_NAME, self::ENDPOINT_NAME); + $this->addSql($this->deleteApiScopes(self::API_NAME, [SummitScopes::ReadOverflowEvents])); + } +} diff --git a/database/seeders/ApiEndpointsSeeder.php b/database/seeders/ApiEndpointsSeeder.php index 87a497306..25774a918 100644 --- a/database/seeders/ApiEndpointsSeeder.php +++ b/database/seeders/ApiEndpointsSeeder.php @@ -4254,6 +4254,14 @@ private function seedSummitEndpoints() SummitScopes::ReadAllSummitData ], ], + [ + 'name' => 'get-overflow-published-events', + 'route' => '/api/v1/summits/{id}/events/all/published/occupancy/overflow', + 'http_method' => 'GET', + 'scopes' => [ + SummitScopes::ReadOverflowEvents, + ], + ], [ 'name' => 'get-schedule-empty-spots', 'route' => '/api/v1/summits/{id}/events/published/empty-spots', diff --git a/database/seeders/ApiScopesSeeder.php b/database/seeders/ApiScopesSeeder.php index 6941e8956..6eab76a31 100644 --- a/database/seeders/ApiScopesSeeder.php +++ b/database/seeders/ApiScopesSeeder.php @@ -63,6 +63,11 @@ private function seedSummitScopes() 'short_description' => 'Get All Summits Data', 'description' => 'Grants read only access for All Summits Data', ], + [ + 'name' => SummitScopes::ReadOverflowEvents, + 'short_description' => 'Read Summit Overflow Events Data', + 'description' => 'Grants read only access to published summit events currently in OVERFLOW occupancy, including overflow streaming URLs and tokens', + ], [ 'name' => SummitScopes::MeRead, 'short_description' => 'Get own summit member data', diff --git a/routes/api_v1.php b/routes/api_v1.php index ef07a0e8c..05bffbdc3 100644 --- a/routes/api_v1.php +++ b/routes/api_v1.php @@ -668,6 +668,14 @@ Route::get('/empty-spots', 'OAuth2SummitEventsApiController@getScheduleEmptySpots'); }); + Route::group(['prefix' => 'all'], function () { + Route::group(['prefix' => 'published'], function () { + Route::group(['prefix' => 'occupancy'], function () { + Route::get('overflow', 'OAuth2SummitEventsApiController@getOverflowPublishedEvents'); + }); + }); + }); + Route::post('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitEventsApiController@addEvent']); Route::group(['prefix' => '{event_id}'], function () { diff --git a/tests/ProtectedApiTestCase.php b/tests/ProtectedApiTestCase.php index 85f1483ae..817c15460 100644 --- a/tests/ProtectedApiTestCase.php +++ b/tests/ProtectedApiTestCase.php @@ -114,6 +114,7 @@ public function get($token_value) $url . '/consultants/read', SummitScopes::ReadSummitData, SummitScopes::ReadAllSummitData, + SummitScopes::ReadOverflowEvents, SummitScopes::WriteSummitData, SummitScopes::WriteEventData, SummitScopes::PublishEventData, @@ -219,6 +220,7 @@ public function get($token_value) $url . '/consultants/read', SummitScopes::ReadSummitData, SummitScopes::ReadAllSummitData, + SummitScopes::ReadOverflowEvents, SummitScopes::WriteSummitData, SummitScopes::WriteEventData, SummitScopes::PublishEventData, diff --git a/tests/SerializerTests.php b/tests/SerializerTests.php index 63a589359..e5439c39a 100644 --- a/tests/SerializerTests.php +++ b/tests/SerializerTests.php @@ -12,9 +12,11 @@ * limitations under the License. **/ +use App\ModelSerializers\Summit\SummitEventOverflowStreamingSerializer; use models\oauth2\IResourceServerContext; use models\summit\Summit; use models\summit\SummitAttendee; +use models\summit\SummitEvent; use models\summit\SummitRegistrationPromoCode; use ModelSerializers\SerializerDecorator; use ModelSerializers\SummitAttendeeAdminSerializer; @@ -88,4 +90,30 @@ public function testAdminSummitAttendeeSerilizer(){ $this->assertTrue(is_array($values)); $this->assertTrue(isset($values['notes'])); } + + public function testOverflowStreamingSerializerReflectsCurrentTokensOnEachCall() + { + $event = Mockery::mock(SummitEvent::class)->makePartial(); + $event->shouldReceive('getId')->andReturn(1001); + $event->shouldReceive('getTitle')->andReturn('Overflow Event'); + $event->shouldReceive('getStartDate')->andReturn(new \DateTime('2026-07-15 10:00:00')); + $event->shouldReceive('getEndDate')->andReturn(new \DateTime('2026-07-15 11:00:00')); + $event->shouldReceive('getOverflowStreamingUrl')->andReturn('https://stream.example.org'); + $event->shouldReceive('getOverflowStreamIsSecure')->andReturn(true); + $event->shouldReceive('getOverflowStreamingTokens')->andReturn( + ['playback_token' => 'token-A'], + ['playback_token' => 'token-B'] + ); + + $resource_server_context = Mockery::mock(IResourceServerContext::class); + $serializer = new SerializerDecorator(new SummitEventOverflowStreamingSerializer($event, $resource_server_context)); + + $first = $serializer->serialize(); + $second = $serializer->serialize(); + + // must not freeze the first computed token set forever - each call reflects + // whatever getOverflowStreamingTokens() currently returns. + $this->assertEquals('token-A', $first['overflow_tokens']['playback_token']); + $this->assertEquals('token-B', $second['overflow_tokens']['playback_token']); + } } \ No newline at end of file diff --git a/tests/oauth2/OAuth2SummitEventsApiTest.php b/tests/oauth2/OAuth2SummitEventsApiTest.php index 870a63905..f00452cc2 100644 --- a/tests/oauth2/OAuth2SummitEventsApiTest.php +++ b/tests/oauth2/OAuth2SummitEventsApiTest.php @@ -2229,4 +2229,81 @@ public function testImportEventData(){ $this->assertResponseStatus(200); } + + public function testGetOverflowPublishedEvents() + { + $start_date = clone(self::$summit->getBeginDate()); + $end_date = clone($start_date); + $end_date = $end_date->add(new \DateInterval("PT1H")); + + $overflow_event = new SummitEvent(); + $overflow_event->setTitle(sprintf("Overflow Event %s", str_random(16))); + $overflow_event->setAbstract(sprintf("Overflow Event Abstract %s", str_random(16))); + $overflow_event->setCategory(self::$defaultTrack); + $overflow_event->setType(self::$defaultEventType); + self::$summit->addEvent($overflow_event); + $overflow_event->setStartDate($start_date); + $overflow_event->setEndDate($end_date); + $overflow_event->publish(); + $overflow_event->setOverflow("https://testoverflow.org", true); + + // same summit, overflow occupancy, but NOT published -> must be excluded by the published filter + $unpublished_overflow_event = new SummitEvent(); + $unpublished_overflow_event->setTitle(sprintf("Unpublished Overflow Event %s", str_random(16))); + $unpublished_overflow_event->setAbstract(sprintf("Unpublished Overflow Event Abstract %s", str_random(16))); + $unpublished_overflow_event->setCategory(self::$defaultTrack); + $unpublished_overflow_event->setType(self::$defaultEventType); + self::$summit->addEvent($unpublished_overflow_event); + $unpublished_overflow_event->setStartDate($start_date); + $unpublished_overflow_event->setEndDate($end_date); + $unpublished_overflow_event->setOverflow("https://testoverflow-unpublished.org", true); + + // different summit, published, overflow occupancy -> must be excluded by the summit_id filter + $other_summit_start_date = clone(self::$summit2->getBeginDate()); + $other_summit_end_date = clone($other_summit_start_date); + $other_summit_end_date = $other_summit_end_date->add(new \DateInterval("PT1H")); + + $other_summit_overflow_event = new SummitEvent(); + $other_summit_overflow_event->setTitle(sprintf("Other Summit Overflow Event %s", str_random(16))); + $other_summit_overflow_event->setAbstract(sprintf("Other Summit Overflow Event Abstract %s", str_random(16))); + $other_summit_overflow_event->setCategory(self::$defaultTrack); + $other_summit_overflow_event->setType(self::$defaultEventType); + self::$summit2->addEvent($other_summit_overflow_event); + $other_summit_overflow_event->setStartDate($other_summit_start_date); + $other_summit_overflow_event->setEndDate($other_summit_end_date); + $other_summit_overflow_event->publish(); + $other_summit_overflow_event->setOverflow("https://testoverflow-othersummit.org", true); + + self::$em->persist($overflow_event); + self::$em->persist($unpublished_overflow_event); + self::$em->persist($other_summit_overflow_event); + self::$em->flush(); + + $params = [ + 'id' => self::$summit->getId(), + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action + ( + "GET", + "OAuth2SummitEventsApiController@getOverflowPublishedEvents", + $params, + array(), + array(), + array(), + $headers + ); + + $this->assertResponseStatus(200); + + $result = json_decode($response->getContent(), true); + $this->assertNotNull($result); + $this->assertEquals(1, $result['total']); + $this->assertEquals($overflow_event->getId(), $result['data'][0]['id']); + } }