diff --git a/app/Events/SponsorServices/DeletedEventDTO.php b/app/Events/SponsorServices/DeletedEventDTO.php index 8d17ff7e2..abc8f8b04 100644 --- a/app/Events/SponsorServices/DeletedEventDTO.php +++ b/app/Events/SponsorServices/DeletedEventDTO.php @@ -27,10 +27,15 @@ public static function fromEntity($entity): self return new self($entity->getId()); } + public static function fromId(int $id): self + { + return new self($id); + } + public function serialize(): array { return [ 'id' => $this->id ]; } -} \ No newline at end of file +} diff --git a/app/Services/Model/Imp/SummitMediaFileTypeService.php b/app/Services/Model/Imp/SummitMediaFileTypeService.php index acc1bd91c..ed7d310ff 100644 --- a/app/Services/Model/Imp/SummitMediaFileTypeService.php +++ b/app/Services/Model/Imp/SummitMediaFileTypeService.php @@ -117,7 +117,7 @@ public function delete(int $id): void }); PublishSponsorServiceDomainEventsJob::dispatch( - DeletedEventDTO::fromEntity($type)->serialize(), + DeletedEventDTO::fromId($id)->serialize(), SummitMediaFileTypeDomainEvents::SummitMediaFileTypeDeleted); } } diff --git a/app/Services/Model/Imp/SummitService.php b/app/Services/Model/Imp/SummitService.php index b19b6ec08..155427fcd 100644 --- a/app/Services/Model/Imp/SummitService.php +++ b/app/Services/Model/Imp/SummitService.php @@ -1719,7 +1719,7 @@ public function deleteSummit($summit_id) }); PublishSponsorServiceDomainEventsJob::dispatch( - DeletedEventDTO::fromEntity($summit)->serialize(), SummitDomainEvents::SummitDeleted); + DeletedEventDTO::fromId($summit_id)->serialize(), SummitDomainEvents::SummitDeleted); } /** diff --git a/app/Services/Model/Imp/SummitSponsorService.php b/app/Services/Model/Imp/SummitSponsorService.php index 1bcb0d156..80a9c9faa 100644 --- a/app/Services/Model/Imp/SummitSponsorService.php +++ b/app/Services/Model/Imp/SummitSponsorService.php @@ -444,7 +444,7 @@ public function deleteSponsor(Summit $summit, int $sponsor_id): void }); PublishSponsorServiceDomainEventsJob::dispatch( - DeletedEventDTO::fromEntity($sponsor)->serialize(), + DeletedEventDTO::fromId($sponsor_id)->serialize(), SponsorDomainEvents::SponsorDeleted); } diff --git a/app/Services/Model/Imp/SummitSponsorshipService.php b/app/Services/Model/Imp/SummitSponsorshipService.php index d815238b8..df4c61962 100644 --- a/app/Services/Model/Imp/SummitSponsorshipService.php +++ b/app/Services/Model/Imp/SummitSponsorshipService.php @@ -113,7 +113,7 @@ public function removeSponsorship(Summit $summit, int $sponsor_id, int $sponsors SponsorDomainEvents::SponsorUpdated); PublishSponsorServiceDomainEventsJob::dispatch( - DeletedEventDTO::fromEntity($sponsorship)->serialize(), + DeletedEventDTO::fromId($sponsorship_id)->serialize(), SponsorDomainEvents::SponsorshipRemoved); } @@ -253,7 +253,7 @@ public function removeAddOn(Summit $summit, int $sponsor_id, int $sponsorship_id SponsorDomainEvents::SponsorshipUpdated); PublishSponsorServiceDomainEventsJob::dispatch( - DeletedEventDTO::fromEntity($add_on)->serialize(), + DeletedEventDTO::fromId($add_on_id)->serialize(), SponsorDomainEvents::SponsorshipAddOnRemoved); } diff --git a/tests/Unit/Services/SummitMediaFileTypeServiceEventDispatchTest.php b/tests/Unit/Services/SummitMediaFileTypeServiceEventDispatchTest.php new file mode 100644 index 000000000..5b6b3ce2f --- /dev/null +++ b/tests/Unit/Services/SummitMediaFileTypeServiceEventDispatchTest.php @@ -0,0 +1,78 @@ + 0 dispatch bug fixed in + * SummitSponsorshipServiceEventDispatchTest: SummitMediaFileTypeService::delete() + * removes the entity inside the transaction (real Doctrine remove(), not a + * soft-delete), which flushes before the domain event dispatch. Doctrine nulls + * the identifier on the deleted entity, so DeletedEventDTO::fromEntity($type) + * read a stale getId(). The fix builds the DTO from the id captured before + * removal (DeletedEventDTO::fromId($id)). + * + * @package Tests\Unit\Services + */ +class SummitMediaFileTypeServiceEventDispatchTest extends TestCase +{ + use InsertSummitTestData; + + protected function setUp(): void + { + parent::setUp(); + self::insertSummitTestData(); + } + + public function tearDown(): void + { + self::clearSummitTestData(); + parent::tearDown(); + } + + private function getService(): ISummitMediaFileTypeService + { + return app(ISummitMediaFileTypeService::class); + } + + public function testDeleteDispatchesSummitMediaFileTypeDeletedWithOriginalId(): void + { + $type = new SummitMediaFileType(); + $type->setName('Test Type ' . str_random(6)); + $type->setDescription('desc'); + $type->setAllowedExtensions('.PDF'); + self::$em->persist($type); + self::$em->flush(); + $id = $type->getId(); + + Queue::fake(); + + $this->getService()->delete($id); + + $jobs = Queue::pushed(PublishSponsorServiceDomainEventsJob::class, function ($job) { + return $job->getEventType() === SummitMediaFileTypeDomainEvents::SummitMediaFileTypeDeleted; + })->all(); + + $this->assertCount(1, $jobs, 'Expected 1 SummitMediaFileTypeDeleted'); + $this->assertSame($id, $jobs[0]->getPayload()['id'], 'Dispatched id must be the removed type id, not 0'); + } +} diff --git a/tests/Unit/Services/SummitServiceEventDispatchTest.php b/tests/Unit/Services/SummitServiceEventDispatchTest.php new file mode 100644 index 000000000..522caab10 --- /dev/null +++ b/tests/Unit/Services/SummitServiceEventDispatchTest.php @@ -0,0 +1,69 @@ +getId(); + + Queue::fake(); + + $this->getService()->deleteSummit($summit_id); + + $jobs = Queue::pushed(PublishSponsorServiceDomainEventsJob::class, function ($job) { + return $job->getEventType() === SummitDomainEvents::SummitDeleted; + })->all(); + + $this->assertCount(1, $jobs, 'Expected 1 SummitDeleted'); + $this->assertSame($summit_id, $jobs[0]->getPayload()['id'], 'Dispatched id must be the deleted summit id, not 0'); + } +} diff --git a/tests/Unit/Services/SummitSponsorServiceEventDispatchTest.php b/tests/Unit/Services/SummitSponsorServiceEventDispatchTest.php index df286f98d..b02b91c0b 100644 --- a/tests/Unit/Services/SummitSponsorServiceEventDispatchTest.php +++ b/tests/Unit/Services/SummitSponsorServiceEventDispatchTest.php @@ -205,4 +205,31 @@ public function testUpdateSponsorReplacesSponsorshipDispatchesRemovedBeforeCreat $this->assertLessThan($created_idx, $removed_idx, 'SponsorshipRemoved must be dispatched before SponsorshipCreated'); } + + // ------------------------------------------------------------------------- + // deleteSponsor - regression: dispatched id must be the removed sponsor id, + // not 0 (Doctrine nulls the identifier after the orphan-removal flush, so + // building the DTO from the entity after the transaction closed read a + // stale getId(); the fix builds it from the id captured before removal). + // ------------------------------------------------------------------------- + + public function testDeleteSponsorDispatchesSponsorDeletedWithOriginalId(): void + { + // Fixture sponsors may have a SponsorSummitRegistrationDiscountCode FK + // attached (InsertSummitTestData assigns them randomly), which blocks a + // hard delete. Use a fresh, unrelated sponsor instead. + $sponsor = $this->getService()->addSponsor(self::$summit, [ + 'company_id' => self::$companies_without_sponsor[0]->getId(), + 'sponsorship_id' => self::$default_summit_sponsor_type->getId(), + ]); + $sponsor_id = $sponsor->getId(); + + Queue::fake(); + + $this->getService()->deleteSponsor(self::$summit, $sponsor_id); + + $jobs = $this->jobsFor(SponsorDomainEvents::SponsorDeleted); + $this->assertCount(1, $jobs, 'Expected 1 SponsorDeleted'); + $this->assertSame($sponsor_id, $jobs[0]->getPayload()['id'], 'Dispatched id must be the removed sponsor id, not 0'); + } } diff --git a/tests/Unit/Services/SummitSponsorshipServiceEventDispatchTest.php b/tests/Unit/Services/SummitSponsorshipServiceEventDispatchTest.php new file mode 100644 index 000000000..1df490728 --- /dev/null +++ b/tests/Unit/Services/SummitSponsorshipServiceEventDispatchTest.php @@ -0,0 +1,98 @@ + 0: the removed entity had already been + * flushed (Doctrine nulls the identifier on a deleted entity), so building the + * DeletedEventDTO from the entity (DeletedEventDTO::fromEntity($entity)) after + * the transaction closed read a stale getId(). The fix builds the DTO from the + * id captured before removal (DeletedEventDTO::fromId($id)). + * + * @package Tests\Unit\Services + */ +class SummitSponsorshipServiceEventDispatchTest extends TestCase +{ + use InsertSummitTestData; + + protected function setUp(): void + { + parent::setUp(); + self::insertSummitTestData(); + } + + public function tearDown(): void + { + self::clearSummitTestData(); + parent::tearDown(); + } + + private function getService(): ISummitSponsorshipService + { + return app(ISummitSponsorshipService::class); + } + + /** + * @return PublishSponsorServiceDomainEventsJob[] + */ + private function jobsFor(string $event_type): array + { + return Queue::pushed(PublishSponsorServiceDomainEventsJob::class, function ($job) use ($event_type) { + return $job->getEventType() === $event_type; + })->all(); + } + + public function testRemoveSponsorshipDispatchesSponsorshipRemovedWithOriginalId(): void + { + $sponsor = self::$sponsors[0]; + $sponsorship = $sponsor->getSponsorships()->first(); + $this->assertNotFalse($sponsorship, 'Pre-condition: sponsor must have a sponsorship'); + $sponsorship_id = $sponsorship->getId(); + + Queue::fake(); + + $this->getService()->removeSponsorship(self::$summit, $sponsor->getId(), $sponsorship_id); + + $jobs = $this->jobsFor(SponsorDomainEvents::SponsorshipRemoved); + $this->assertCount(1, $jobs, 'Expected 1 SponsorshipRemoved'); + $this->assertSame($sponsorship_id, $jobs[0]->getPayload()['id'], 'Dispatched id must be the removed sponsorship id, not 0'); + } + + public function testRemoveAddOnDispatchesSponsorshipAddOnRemovedWithOriginalId(): void + { + $sponsor = self::$sponsors[0]; + $sponsorship = $sponsor->getSponsorships()->first(); + $this->assertNotFalse($sponsorship, 'Pre-condition: sponsor must have a sponsorship'); + $add_on = $sponsorship->getAddOns()->first(); + $this->assertNotFalse($add_on, 'Pre-condition: sponsorship must have an add-on'); + $add_on_id = $add_on->getId(); + + Queue::fake(); + + $this->getService()->removeAddOn(self::$summit, $sponsor->getId(), $sponsorship->getId(), $add_on_id); + + $jobs = $this->jobsFor(SponsorDomainEvents::SponsorshipAddOnRemoved); + $this->assertCount(1, $jobs, 'Expected 1 SponsorshipAddOnRemoved'); + $this->assertSame($add_on_id, $jobs[0]->getPayload()['id'], 'Dispatched id must be the removed add-on id, not 0'); + } +}