Nine test files under tests/Feature/Artist/ exercising:
ArtistEngagementStateMachineTest 8 tests — terminal blocks, conditional
gates (Option/Contracted), full happy
path, cancel cascade
LaneCascadeServiceTest 5 tests — simple move, cascade-bump,
version mismatch, park, unpark
BumaVatCalculationTest 6 tests — D26 formula coverage:
Organisation/BookingAgency/NotApplicable,
VAT off, breakdown sum, zero fee
DemoteExpiredOptionsTest 4 tests — expired demote, future
untouched, non-Option untouched, run
twice → single option_expired entry
IdempotencyKey60sRedisTest 4 tests — missing header 400, first
cache, replay header, failed not cached
ArtistControllerTest 8 tests — index/create/destroy + cross-
tenant + duplicate detection + restore
StageControllerTest 7 tests — create + uniqueness, destroy
cascade-park, reorder permutation,
replaceDays orphan 409 + force_orphan
ArtistEngagementControllerTest 5 tests — index/create/update/destroy +
422 on invalid status transition
TimetableMoveControllerTest 3 tests — happy path with idempotency
header, missing header → 400, version
mismatch → 409
ArtistPolicyTest 6 tests — role checks, cross-tenant
denial, super_admin bypass, D27 active-
engagement gate
ActivityLogShapeTest 4 tests — performance.moved cascade
props, status_changed vs cancelled,
stage.day_added subject + props,
stage.reordered on Event subject
Bug fixes surfaced by Phase C:
Schema reality: events table uses `start_date`/`end_date` (date), not
`start_at`/`end_at`. Updated WithinEventBounds rule and the two stage_day
resolvers (LaneCascadeService + MoveTimetablePerformanceRequest) to
query the actual columns. ArtistResource.engagements_summary upcoming
filter likewise.
performances table has no organisation_id column (FK-chain via
engagement_id). Removed the org-id filter from the Rule::exists in
MoveTimetablePerformanceRequest; cross-tenant is caught by the policy
in TimetableMoveController.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
146 lines
4.8 KiB
PHP
146 lines
4.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Artist;
|
|
|
|
use App\Enums\Artist\ArtistEngagementStatus;
|
|
use App\Models\Artist;
|
|
use App\Models\ArtistEngagement;
|
|
use App\Models\Event;
|
|
use App\Models\Organisation;
|
|
use App\Models\User;
|
|
use Database\Seeders\RoleSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Laravel\Sanctum\Sanctum;
|
|
use Tests\TestCase;
|
|
|
|
final class ArtistControllerTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private Organisation $org;
|
|
|
|
private Organisation $otherOrg;
|
|
|
|
private User $orgAdmin;
|
|
|
|
private User $programManager;
|
|
|
|
private User $outsider;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->seed(RoleSeeder::class);
|
|
|
|
$this->org = Organisation::factory()->create();
|
|
$this->otherOrg = Organisation::factory()->create();
|
|
|
|
$this->orgAdmin = User::factory()->create();
|
|
$this->org->users()->attach($this->orgAdmin, ['role' => 'org_admin']);
|
|
|
|
$this->programManager = User::factory()->create();
|
|
$this->org->users()->attach($this->programManager, ['role' => 'program_manager']);
|
|
|
|
$this->outsider = User::factory()->create();
|
|
$this->otherOrg->users()->attach($this->outsider, ['role' => 'org_admin']);
|
|
}
|
|
|
|
public function test_index_lists_artists_for_member(): void
|
|
{
|
|
Artist::factory()->count(3)->create(['organisation_id' => $this->org->id]);
|
|
Sanctum::actingAs($this->orgAdmin);
|
|
|
|
$response = $this->getJson("/api/v1/organisations/{$this->org->id}/artists");
|
|
|
|
$response->assertOk();
|
|
$this->assertCount(3, $response->json('data'));
|
|
}
|
|
|
|
public function test_index_unauthenticated_returns_401(): void
|
|
{
|
|
$this->getJson("/api/v1/organisations/{$this->org->id}/artists")->assertUnauthorized();
|
|
}
|
|
|
|
public function test_outsider_cannot_view_other_org_artists(): void
|
|
{
|
|
Artist::factory()->create(['organisation_id' => $this->org->id]);
|
|
Sanctum::actingAs($this->outsider);
|
|
|
|
$this->getJson("/api/v1/organisations/{$this->org->id}/artists")->assertForbidden();
|
|
}
|
|
|
|
public function test_program_manager_can_create(): void
|
|
{
|
|
Sanctum::actingAs($this->programManager);
|
|
|
|
$response = $this->postJson("/api/v1/organisations/{$this->org->id}/artists", [
|
|
'name' => 'Headhunterz',
|
|
]);
|
|
|
|
$response->assertCreated();
|
|
$this->assertSame('Headhunterz', $response->json('data.name'));
|
|
}
|
|
|
|
public function test_duplicate_name_returns_409_with_existing_id(): void
|
|
{
|
|
$existing = Artist::factory()->create([
|
|
'organisation_id' => $this->org->id,
|
|
'name' => 'Devin Wild',
|
|
]);
|
|
|
|
Sanctum::actingAs($this->programManager);
|
|
$response = $this->postJson("/api/v1/organisations/{$this->org->id}/artists", [
|
|
'name' => 'devin wild',
|
|
]);
|
|
|
|
$response->assertStatus(409);
|
|
$this->assertSame((string) $existing->id, (string) $response->json('errors.duplicate_artist_id'));
|
|
}
|
|
|
|
public function test_destroy_blocked_with_active_engagement(): void
|
|
{
|
|
$artist = Artist::factory()->create(['organisation_id' => $this->org->id]);
|
|
$event = Event::factory()->create(['organisation_id' => $this->org->id]);
|
|
ArtistEngagement::factory()->create([
|
|
'artist_id' => $artist->id,
|
|
'event_id' => $event->id,
|
|
'booking_status' => ArtistEngagementStatus::Confirmed,
|
|
]);
|
|
|
|
Sanctum::actingAs($this->orgAdmin);
|
|
$this->deleteJson("/api/v1/organisations/{$this->org->id}/artists/{$artist->id}")
|
|
->assertForbidden();
|
|
|
|
$this->assertDatabaseHas('artists', ['id' => $artist->id, 'deleted_at' => null]);
|
|
}
|
|
|
|
public function test_destroy_with_only_terminal_engagements_succeeds(): void
|
|
{
|
|
$artist = Artist::factory()->create(['organisation_id' => $this->org->id]);
|
|
$event = Event::factory()->create(['organisation_id' => $this->org->id]);
|
|
ArtistEngagement::factory()->create([
|
|
'artist_id' => $artist->id,
|
|
'event_id' => $event->id,
|
|
'booking_status' => ArtistEngagementStatus::Cancelled,
|
|
]);
|
|
|
|
Sanctum::actingAs($this->orgAdmin);
|
|
$this->deleteJson("/api/v1/organisations/{$this->org->id}/artists/{$artist->id}")
|
|
->assertNoContent();
|
|
}
|
|
|
|
public function test_restore_brings_back_soft_deleted_artist(): void
|
|
{
|
|
$artist = Artist::factory()->create(['organisation_id' => $this->org->id]);
|
|
$artist->delete();
|
|
|
|
Sanctum::actingAs($this->orgAdmin);
|
|
$response = $this->postJson("/api/v1/organisations/{$this->org->id}/artists/{$artist->id}/restore");
|
|
|
|
$response->assertOk();
|
|
$this->assertDatabaseHas('artists', ['id' => $artist->id, 'deleted_at' => null]);
|
|
}
|
|
}
|