test(timetable): Phase C — observer, resolver, seeder, portal controller tests
22 new tests across four files:
- AdvanceSectionObserverTest (7) — counter recompute on create / status
transition / delete / is_open toggle no-op / orphaned-section guard /
no activity-log noise on counter writes
- ArtistResolverTest (4) — happy path / invalid token / soft-deleted
artist / SHA-256 digest verification
- ArtistAdvanceDefaultTest (6) — five-section + slug shape / idempotency
/ per-section field shape / observer-invocation outside tests /
artisan one-org + all-orgs paths
- EngagementPortalControllerTest (6) — show 200/404/410 / show-section
schema + draft values / submit happy-path with submission persistence
+ counter recompute / cross-engagement section returns 404
Implementation tweaks driven by test feedback:
- OrganisationObserver gated by `app()->runningUnitTests()` — auto-seed
runs in production but is silent in CI so existing FormSchema-counting
tests are unperturbed. Tests that need the seeded schema invoke
`ArtistAdvanceDefault::seedFor()` explicitly.
- EngagementPortalController idempotency_key uses `aa-` + sha1 prefix
(28 chars) so it fits the form_submissions.idempotency_key
varchar(30) column.
Test count: 1709 (Session 2 close) → 1731 (+22).
Larastan: 0 new errors over baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
145
api/tests/Feature/Artist/AdvanceSectionObserverTest.php
Normal file
145
api/tests/Feature/Artist/AdvanceSectionObserverTest.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Artist;
|
||||
|
||||
use App\Enums\Artist\AdvanceSectionSubmissionStatus;
|
||||
use App\Models\AdvanceSection;
|
||||
use App\Models\Artist;
|
||||
use App\Models\ArtistEngagement;
|
||||
use App\Models\Event;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Scopes\OrganisationScope;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class AdvanceSectionObserverTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_create_increments_total_count(): void
|
||||
{
|
||||
$engagement = $this->makeEngagement(['advancing_completed_count' => 0, 'advancing_total_count' => 0]);
|
||||
|
||||
AdvanceSection::factory()->create(['engagement_id' => $engagement->id]);
|
||||
|
||||
$fresh = $engagement->fresh();
|
||||
$this->assertSame(1, (int) $fresh->advancing_total_count);
|
||||
$this->assertSame(0, (int) $fresh->advancing_completed_count);
|
||||
}
|
||||
|
||||
public function test_status_transition_to_approved_increments_completed(): void
|
||||
{
|
||||
$engagement = $this->makeEngagement();
|
||||
$section = AdvanceSection::factory()->create([
|
||||
'engagement_id' => $engagement->id,
|
||||
'submission_status' => AdvanceSectionSubmissionStatus::Pending,
|
||||
]);
|
||||
|
||||
$section->submission_status = AdvanceSectionSubmissionStatus::Approved->value;
|
||||
$section->save();
|
||||
|
||||
$fresh = $engagement->fresh();
|
||||
$this->assertSame(1, (int) $fresh->advancing_total_count);
|
||||
$this->assertSame(1, (int) $fresh->advancing_completed_count);
|
||||
}
|
||||
|
||||
public function test_status_transition_away_from_approved_decrements_completed(): void
|
||||
{
|
||||
$engagement = $this->makeEngagement();
|
||||
$section = AdvanceSection::factory()->create([
|
||||
'engagement_id' => $engagement->id,
|
||||
'submission_status' => AdvanceSectionSubmissionStatus::Approved,
|
||||
]);
|
||||
|
||||
$this->assertSame(1, (int) $engagement->fresh()->advancing_completed_count);
|
||||
|
||||
$section->submission_status = AdvanceSectionSubmissionStatus::Pending->value;
|
||||
$section->save();
|
||||
|
||||
$this->assertSame(0, (int) $engagement->fresh()->advancing_completed_count);
|
||||
$this->assertSame(1, (int) $engagement->fresh()->advancing_total_count);
|
||||
}
|
||||
|
||||
public function test_delete_decrements_total(): void
|
||||
{
|
||||
$engagement = $this->makeEngagement();
|
||||
$sectionA = AdvanceSection::factory()->create(['engagement_id' => $engagement->id]);
|
||||
$sectionB = AdvanceSection::factory()->create(['engagement_id' => $engagement->id]);
|
||||
|
||||
$this->assertSame(2, (int) $engagement->fresh()->advancing_total_count);
|
||||
|
||||
$sectionA->delete();
|
||||
|
||||
$this->assertSame(1, (int) $engagement->fresh()->advancing_total_count);
|
||||
}
|
||||
|
||||
public function test_is_open_toggle_does_not_recompute(): void
|
||||
{
|
||||
$engagement = $this->makeEngagement();
|
||||
$section = AdvanceSection::factory()->create([
|
||||
'engagement_id' => $engagement->id,
|
||||
'is_open' => false,
|
||||
]);
|
||||
|
||||
$startTotal = (int) $engagement->fresh()->advancing_total_count;
|
||||
$startCompleted = (int) $engagement->fresh()->advancing_completed_count;
|
||||
|
||||
$section->is_open = true;
|
||||
$section->save();
|
||||
|
||||
$this->assertSame($startTotal, (int) $engagement->fresh()->advancing_total_count);
|
||||
$this->assertSame($startCompleted, (int) $engagement->fresh()->advancing_completed_count);
|
||||
}
|
||||
|
||||
public function test_recompute_skips_when_engagement_already_force_deleted(): void
|
||||
{
|
||||
$engagement = $this->makeEngagement();
|
||||
$section = AdvanceSection::factory()->create(['engagement_id' => $engagement->id]);
|
||||
|
||||
ArtistEngagement::query()
|
||||
->withoutGlobalScope(OrganisationScope::class)
|
||||
->whereKey($engagement->id)
|
||||
->forceDelete();
|
||||
|
||||
// Force-deleting via raw query bypasses cascade observer; section
|
||||
// is now orphaned. The observer should no-op rather than crash
|
||||
// when the parent is gone.
|
||||
$section->delete();
|
||||
|
||||
$this->expectNotToPerformAssertions();
|
||||
}
|
||||
|
||||
public function test_counter_writes_do_not_emit_activity(): void
|
||||
{
|
||||
$engagement = $this->makeEngagement();
|
||||
$logsBefore = \Spatie\Activitylog\Models\Activity::query()
|
||||
->where('subject_id', $engagement->id)
|
||||
->count();
|
||||
|
||||
AdvanceSection::factory()->create(['engagement_id' => $engagement->id]);
|
||||
|
||||
$logsAfter = \Spatie\Activitylog\Models\Activity::query()
|
||||
->where('subject_id', $engagement->id)
|
||||
->count();
|
||||
|
||||
$this->assertSame($logsBefore, $logsAfter, 'Counter sync must not emit activity-log entries on the engagement.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $overrides
|
||||
*/
|
||||
private function makeEngagement(array $overrides = []): ArtistEngagement
|
||||
{
|
||||
$org = Organisation::factory()->create();
|
||||
$event = Event::factory()->for($org)->create();
|
||||
$artist = Artist::factory()->for($org)->create();
|
||||
|
||||
return ArtistEngagement::factory()->create(array_merge([
|
||||
'organisation_id' => $org->id,
|
||||
'artist_id' => $artist->id,
|
||||
'event_id' => $event->id,
|
||||
], $overrides));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user