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>
158 lines
5.7 KiB
PHP
158 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Portal;
|
|
|
|
use App\Enums\Artist\AdvanceSectionSubmissionStatus;
|
|
use App\Enums\Artist\AdvanceSectionType;
|
|
use App\FormBuilder\Defaults\ArtistAdvanceDefault;
|
|
use App\Models\AdvanceSection;
|
|
use App\Models\Artist;
|
|
use App\Models\ArtistEngagement;
|
|
use App\Models\Event;
|
|
use App\Models\FormBuilder\FormSchema;
|
|
use App\Models\FormBuilder\FormSchemaSection;
|
|
use App\Models\FormBuilder\FormSubmission;
|
|
use App\Models\Organisation;
|
|
use App\Models\Scopes\OrganisationScope;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Str;
|
|
use Tests\TestCase;
|
|
|
|
final class EngagementPortalControllerTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_show_returns_payload_for_valid_token(): void
|
|
{
|
|
[$plain, $engagement] = $this->makeEngagementWithSection();
|
|
|
|
$response = $this->getJson("/api/v1/p/artist/{$plain}");
|
|
|
|
$response->assertOk();
|
|
$response->assertJsonPath('data.engagement_id', $engagement->id);
|
|
$response->assertJsonStructure([
|
|
'data' => ['engagement_id', 'artist', 'event', 'sections'],
|
|
]);
|
|
}
|
|
|
|
public function test_show_returns_404_for_invalid_token(): void
|
|
{
|
|
$this->getJson('/api/v1/p/artist/not-a-real-token')->assertNotFound();
|
|
}
|
|
|
|
public function test_show_returns_410_when_master_artist_soft_deleted(): void
|
|
{
|
|
[$plain, $engagement] = $this->makeEngagementWithSection();
|
|
|
|
Artist::query()
|
|
->withoutGlobalScope(OrganisationScope::class)
|
|
->whereKey($engagement->artist_id)
|
|
->delete();
|
|
|
|
$this->getJson("/api/v1/p/artist/{$plain}")->assertStatus(410);
|
|
}
|
|
|
|
public function test_show_section_returns_schema_and_existing_values(): void
|
|
{
|
|
[$plain, $engagement, $section] = $this->makeEngagementWithSection();
|
|
|
|
$response = $this->getJson("/api/v1/p/artist/{$plain}/sections/{$section->id}");
|
|
|
|
$response->assertOk();
|
|
$response->assertJsonPath('data.section.id', $section->id);
|
|
$response->assertJsonStructure([
|
|
'data' => ['section', 'fields', 'values'],
|
|
]);
|
|
}
|
|
|
|
public function test_submit_section_creates_submission_and_updates_status(): void
|
|
{
|
|
[$plain, $engagement, $section] = $this->makeEngagementWithSection();
|
|
|
|
$response = $this->postJson("/api/v1/p/artist/{$plain}/sections/{$section->id}", [
|
|
'values' => [
|
|
'general-notes' => 'Hello, world',
|
|
],
|
|
]);
|
|
|
|
$response->assertOk();
|
|
|
|
$section->refresh();
|
|
$this->assertSame(AdvanceSectionSubmissionStatus::Submitted, $section->submission_status);
|
|
$this->assertNotNull($section->last_submitted_at);
|
|
|
|
// FormSubmission persisted with master artist as subject + event_id from engagement
|
|
$submission = FormSubmission::query()
|
|
->withoutGlobalScope(OrganisationScope::class)
|
|
->where('subject_id', $engagement->artist_id)
|
|
->first();
|
|
$this->assertNotNull($submission);
|
|
$this->assertSame('artist', $submission->subject_type);
|
|
$this->assertSame((string) $engagement->event_id, (string) $submission->event_id);
|
|
$this->assertSame($engagement->organisation_id, $submission->organisation_id);
|
|
|
|
// Counter recompute fires (Submitted is not Approved, so completed stays 0)
|
|
$this->assertSame(1, (int) $engagement->fresh()->advancing_total_count);
|
|
}
|
|
|
|
public function test_submit_with_section_from_different_engagement_returns_404(): void
|
|
{
|
|
[$plain, $engagement] = $this->makeEngagementWithSection();
|
|
|
|
$otherOrg = Organisation::factory()->create();
|
|
$otherEvent = Event::factory()->for($otherOrg)->create();
|
|
$otherArtist = Artist::factory()->for($otherOrg)->create();
|
|
$other = ArtistEngagement::factory()->create([
|
|
'organisation_id' => $otherOrg->id,
|
|
'artist_id' => $otherArtist->id,
|
|
'event_id' => $otherEvent->id,
|
|
]);
|
|
$stranger = AdvanceSection::factory()->create(['engagement_id' => $other->id, 'name' => 'Algemeen']);
|
|
|
|
$this->postJson("/api/v1/p/artist/{$plain}/sections/{$stranger->id}", [
|
|
'values' => ['general-notes' => 'x'],
|
|
])->assertNotFound();
|
|
}
|
|
|
|
/**
|
|
* @return array{0: string, 1: ArtistEngagement, 2: AdvanceSection}
|
|
*/
|
|
private function makeEngagementWithSection(): array
|
|
{
|
|
$org = Organisation::factory()->create();
|
|
$event = Event::factory()->for($org)->create();
|
|
$artist = Artist::factory()->for($org)->create();
|
|
|
|
// OrganisationObserver skips auto-seed in tests; seed explicitly.
|
|
ArtistAdvanceDefault::seedFor($org);
|
|
$schema = FormSchema::query()
|
|
->withoutGlobalScope(OrganisationScope::class)
|
|
->where('organisation_id', $org->id)
|
|
->where('purpose', \App\Enums\FormBuilder\FormPurpose::ARTIST_ADVANCE->value)
|
|
->firstOrFail();
|
|
$schemaSection = FormSchemaSection::query()
|
|
->withoutGlobalScope(OrganisationScope::class)
|
|
->where('form_schema_id', $schema->id)
|
|
->where('slug', 'general-info')
|
|
->firstOrFail();
|
|
|
|
$plain = (string) Str::ulid();
|
|
$engagement = ArtistEngagement::factory()->create([
|
|
'organisation_id' => $org->id,
|
|
'artist_id' => $artist->id,
|
|
'event_id' => $event->id,
|
|
'portal_token' => hash('sha256', $plain),
|
|
]);
|
|
$section = AdvanceSection::factory()->create([
|
|
'engagement_id' => $engagement->id,
|
|
'name' => $schemaSection->name,
|
|
'type' => AdvanceSectionType::Custom,
|
|
'is_open' => true,
|
|
]);
|
|
|
|
return [$plain, $engagement, $section];
|
|
}
|
|
}
|