From 0c9a15c046acb385df096c257ac1249fbda222f7 Mon Sep 17 00:00:00 2001 From: romanetar Date: Wed, 14 Oct 2026 14:03:44 +0200 Subject: [PATCH 1/2] fix(overflow): keep the existing overflow_stream_key when overflow is re-saved on an event already in OVERFLOW updateOverflowInfo generated a new key on every PUT, invalidating the overflow urls already delivered (sign, event-site player, native apps). A key is now generated only when the event enters overflow or has no key; clearOverflow still nulls it so the next activation gets a fresh one. --- app/Services/Model/Imp/SummitService.php | 12 +- tests/oauth2/OAuth2SummitEventsApiTest.php | 128 +++++++++++++++++++++ 2 files changed, 136 insertions(+), 4 deletions(-) diff --git a/app/Services/Model/Imp/SummitService.php b/app/Services/Model/Imp/SummitService.php index 7b8d94075..7688a316d 100644 --- a/app/Services/Model/Imp/SummitService.php +++ b/app/Services/Model/Imp/SummitService.php @@ -4250,11 +4250,15 @@ public function updateOverflowInfo(Summit $summit, int $event_id, array $payload if (is_null($event)) throw new ValidationException(sprintf("Event id %s does not exists,", $event_id)); - do { - $overflow_key = $event->generateOverflowKey(); - } while (!is_null($this->event_repository->getBySummitAndOverflowStreamKey($summit, $overflow_key))); + // keep the key of an event already on overflow, so the urls already delivered stay valid + if (!$event->isOnOverflow() || is_null($event->getOverflowStreamKey())) { + do { + $overflow_key = $event->generateOverflowKey(); + } while (!is_null($this->event_repository->getBySummitAndOverflowStreamKey($summit, $overflow_key))); + + $event->setOverflowStreamKey($overflow_key); + } - $event->setOverflowStreamKey($overflow_key); $event->setOverflow( $payload['overflow_streaming_url'], boolval($payload['overflow_stream_is_secure'])); diff --git a/tests/oauth2/OAuth2SummitEventsApiTest.php b/tests/oauth2/OAuth2SummitEventsApiTest.php index 79674c39d..2505bdf59 100644 --- a/tests/oauth2/OAuth2SummitEventsApiTest.php +++ b/tests/oauth2/OAuth2SummitEventsApiTest.php @@ -12,6 +12,7 @@ * limitations under the License. **/ use App\Jobs\Emails\IMailTemplatesConstants; +use App\Jobs\ProcessScheduleEntityLifeCycleEvent; use App\Jobs\Emails\Schedule\PresentationActivitySpeakerChangeEmail; use App\Models\Foundation\Main\IGroup; use App\Services\Model\ISummitService; @@ -2641,4 +2642,131 @@ public function testGetOverflowPublishedEvents() $this->assertEquals(1, $result['total']); $this->assertEquals($overflow_event->getId(), $result['data'][0]['id']); } + + private function createPublishedEventForOverflow(): SummitEvent + { + $start_date = clone(self::$summit->getBeginDate()); + $end_date = (clone $start_date)->add(new \DateInterval("PT1H")); + + $event = new SummitEvent(); + $event->setTitle(sprintf("Overflow Key Event %s", str_random(16))); + $event->setAbstract(sprintf("Overflow Key Event Abstract %s", str_random(16))); + $event->setCategory(self::$defaultTrack); + $event->setType(self::$defaultEventType); + self::$summit->addEvent($event); + $event->setStartDate($start_date); + $event->setEndDate($end_date); + $event->publish(); + self::$em->persist($event); + self::$em->flush(); + return $event; + } + + private function putOverflow(SummitEvent $event, string $streaming_url, bool $is_secure = false) + { + return $this->action + ( + "PUT", + "OAuth2SummitEventsApiController@setOverflow", + ['id' => self::$summit->getId(), 'event_id' => $event->getId()], + [], [], [], + ["HTTP_Authorization" => " Bearer " . $this->access_token, "CONTENT_TYPE" => "application/json"], + json_encode([ + 'overflow_streaming_url' => $streaming_url, + 'overflow_stream_is_secure' => $is_secure, + ]) + ); + } + + private function deleteOverflow(SummitEvent $event) + { + return $this->action + ( + "DELETE", + "OAuth2SummitEventsApiController@clearOverflow", + ['id' => self::$summit->getId(), 'event_id' => $event->getId()], + [], [], [], + ["HTTP_Authorization" => " Bearer " . $this->access_token, "CONTENT_TYPE" => "application/json"], + json_encode([]) + ); + } + + private function assertOverflowLifeCycleDispatched(string $expected_key): void + { + Queue::assertPushed(ProcessScheduleEntityLifeCycleEvent::class, function ($job) use ($expected_key) { + return $job->entity_type == Presentation::PresentationOverflowEntityType + && str_contains($job->params['overflow_url'], 'k=' . $expected_key); + }); + } + + public function testSetOverflowTwiceKeepsStreamKey() + { + Queue::fake(); + $event = $this->createPublishedEventForOverflow(); + + $response = $this->putOverflow($event, "https://testoverflow-first.org", true); + $this->assertResponseStatus(201); + $first = json_decode($response->getContent(), true); + $first_key = $first['overflow_stream_key']; + $this->assertNotEmpty($first_key); + $this->assertOverflowLifeCycleDispatched($first_key); + + $response = $this->putOverflow($event, "https://testoverflow-second.org", false); + $this->assertResponseStatus(201); + $second = json_decode($response->getContent(), true); + + $this->assertEquals($first_key, $second['overflow_stream_key']); + $this->assertEquals("https://testoverflow-second.org", $second['overflow_streaming_url']); + $this->assertOverflowLifeCycleDispatched($second['overflow_stream_key']); + // one PresentationOverflow lifecycle job per PUT (other entity types may be pushed by event setup) + $this->assertCount(2, Queue::pushed(ProcessScheduleEntityLifeCycleEvent::class, function ($job) { + return $job->entity_type == Presentation::PresentationOverflowEntityType; + })); + + self::$em->clear(); + $found = self::$em->getRepository(SummitEvent::class) + ->getBySummitAndOverflowStreamKey(self::$summit, $first_key); + $this->assertNotNull($found); + $this->assertEquals($event->getId(), $found->getId()); + } + + public function testSetClearSetOverflowGeneratesNewStreamKey() + { + Queue::fake(); + $event = $this->createPublishedEventForOverflow(); + + $response = $this->putOverflow($event, "https://testoverflow-first.org"); + $this->assertResponseStatus(201); + $first_key = json_decode($response->getContent(), true)['overflow_stream_key']; + $this->assertNotEmpty($first_key); + + $this->deleteOverflow($event); + $this->assertResponseStatus(201); + + $response = $this->putOverflow($event, "https://testoverflow-first.org"); + $this->assertResponseStatus(201); + $new_key = json_decode($response->getContent(), true)['overflow_stream_key']; + + $this->assertNotEmpty($new_key); + $this->assertNotEquals($first_key, $new_key); + $this->assertOverflowLifeCycleDispatched($new_key); + } + + public function testSetOverflowGeneratesKeyWhenOnOverflowWithoutKey() + { + Queue::fake(); + $event = $this->createPublishedEventForOverflow(); + // reachable state: occupancy OVERFLOW but no key (key is only nulled by clearOverflow) + $event->setOverflow("https://testoverflow-nokey.org", false); + $this->assertNull($event->getOverflowStreamKey()); + self::$em->persist($event); + self::$em->flush(); + + $response = $this->putOverflow($event, "https://testoverflow-nokey-updated.org"); + $this->assertResponseStatus(201); + $key = json_decode($response->getContent(), true)['overflow_stream_key']; + + $this->assertNotEmpty($key); + $this->assertOverflowLifeCycleDispatched($key); + } } From f7db04fd8b30068bd1795e20df22f611bb0c9df9 Mon Sep 17 00:00:00 2001 From: smarcet Date: Wed, 14 Oct 2026 17:53:50 -0300 Subject: [PATCH 2/2] test(overflow): cover key rotation after occupancy is stepped down via event update --- tests/oauth2/OAuth2SummitEventsApiTest.php | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/oauth2/OAuth2SummitEventsApiTest.php b/tests/oauth2/OAuth2SummitEventsApiTest.php index 2505bdf59..7c72067c0 100644 --- a/tests/oauth2/OAuth2SummitEventsApiTest.php +++ b/tests/oauth2/OAuth2SummitEventsApiTest.php @@ -2769,4 +2769,46 @@ public function testSetOverflowGeneratesKeyWhenOnOverflowWithoutKey() $this->assertNotEmpty($key); $this->assertOverflowLifeCycleDispatched($key); } + + public function testSetOverflowAfterOccupancyStepDownGeneratesNewStreamKey() + { + Queue::fake(); + $event = $this->createPublishedEventForOverflow(); + + $response = $this->putOverflow($event, "https://testoverflow-first.org"); + $this->assertResponseStatus(201); + $first_key = json_decode($response->getContent(), true)['overflow_stream_key']; + $this->assertNotEmpty($first_key); + + // step occupancy down through the generic event update (room occupancy "-" button), + // which does not clear the overflow key + $response = $this->action + ( + "PUT", + "OAuth2SummitEventsApiController@updateEvent", + ['id' => self::$summit->getId(), 'event_id' => $event->getId()], + [], [], [], + $this->getAuthHeaders(), + json_encode(['occupancy' => SummitEvent::OccupancyFull]) + ); + $this->assertResponseStatus(200); + $updated = json_decode($response->getContent(), true); + $this->assertEquals(SummitEvent::OccupancyFull, $updated['occupancy']); + $this->assertEquals($first_key, $updated['overflow_stream_key']); + + $response = $this->putOverflow($event, "https://testoverflow-second.org"); + $this->assertResponseStatus(201); + $new_key = json_decode($response->getContent(), true)['overflow_stream_key']; + + $this->assertNotEmpty($new_key); + $this->assertNotEquals($first_key, $new_key); + $this->assertOverflowLifeCycleDispatched($new_key); + + self::$em->clear(); + $repository = self::$em->getRepository(SummitEvent::class); + $this->assertNull($repository->getBySummitAndOverflowStreamKey(self::$summit, $first_key)); + $found = $repository->getBySummitAndOverflowStreamKey(self::$summit, $new_key); + $this->assertNotNull($found); + $this->assertEquals($event->getId(), $found->getId()); + } }