Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/Events/SponsorServices/DeletedEventDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
];
}
}
}
2 changes: 1 addition & 1 deletion app/Services/Model/Imp/SummitMediaFileTypeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public function delete(int $id): void
});

PublishSponsorServiceDomainEventsJob::dispatch(
DeletedEventDTO::fromEntity($type)->serialize(),
DeletedEventDTO::fromId($id)->serialize(),
SummitMediaFileTypeDomainEvents::SummitMediaFileTypeDeleted);
}
}
2 changes: 1 addition & 1 deletion app/Services/Model/Imp/SummitService.php
Original file line number Diff line number Diff line change
Expand Up @@ -1719,7 +1719,7 @@ public function deleteSummit($summit_id)
});

PublishSponsorServiceDomainEventsJob::dispatch(
DeletedEventDTO::fromEntity($summit)->serialize(), SummitDomainEvents::SummitDeleted);
DeletedEventDTO::fromId($summit_id)->serialize(), SummitDomainEvents::SummitDeleted);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion app/Services/Model/Imp/SummitSponsorService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions app/Services/Model/Imp/SummitSponsorshipService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php namespace Tests\Unit\Services;
/**
* Copyright 2026 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

use App\Events\SponsorServices\SummitMediaFileTypeDomainEvents;
use App\Jobs\SponsorServices\PublishSponsorServiceDomainEventsJob;
use App\Services\Model\ISummitMediaFileTypeService;
use Illuminate\Support\Facades\Queue;
use models\summit\SummitMediaFileType;
use Tests\InsertSummitTestData;
use Tests\TestCase;

/**
* Class SummitMediaFileTypeServiceEventDispatchTest
*
* Regression coverage for the same 'id' => 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');
}
}
69 changes: 69 additions & 0 deletions tests/Unit/Services/SummitServiceEventDispatchTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php namespace Tests\Unit\Services;
/**
* Copyright 2026 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

use App\Events\SponsorServices\SummitDomainEvents;
use App\Jobs\SponsorServices\PublishSponsorServiceDomainEventsJob;
use Illuminate\Support\Facades\Queue;
use services\model\ISummitService;
use Tests\InsertSummitTestData;
use Tests\TestCase;

/**
* Class SummitServiceEventDispatchTest
*
* Companion to SummitSponsorshipServiceEventDispatchTest: verifies the
* dispatched 'id' for SummitDeleted matches the original summit id. Unlike
* the sponsorship/add-on/media-file-type cases, deleteSummit() only flips a
* soft-delete flag (no Doctrine remove()), so this is a characterization test
* confirming the id was never actually zeroed here - see test body.
*
* @package Tests\Unit\Services
*/
class SummitServiceEventDispatchTest extends TestCase
{
use InsertSummitTestData;

protected function setUp(): void
{
parent::setUp();
self::insertSummitTestData();
}

public function tearDown(): void
{
self::clearSummitTestData();
parent::tearDown();
}

private function getService(): ISummitService
{
return app(ISummitService::class);
}

public function testDeleteSummitDispatchesSummitDeletedWithOriginalId(): void
{
$summit_id = self::$summit->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');
}
}
27 changes: 27 additions & 0 deletions tests/Unit/Services/SummitSponsorServiceEventDispatchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
98 changes: 98 additions & 0 deletions tests/Unit/Services/SummitSponsorshipServiceEventDispatchTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php namespace Tests\Unit\Services;
/**
* Copyright 2026 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

use App\Events\SponsorServices\SponsorDomainEvents;
use App\Jobs\SponsorServices\PublishSponsorServiceDomainEventsJob;
use App\Services\Model\ISummitSponsorshipService;
use Illuminate\Support\Facades\Queue;
use Tests\InsertSummitTestData;
use Tests\TestCase;

/**
* Class SummitSponsorshipServiceEventDispatchTest
*
* Regression coverage for a bug where SponsorshipRemoved/SponsorshipAddOnRemoved
* jobs were dispatched with 'id' => 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');
}
}
Loading