Files
crewli/api/tests/Feature/FormBuilder/Bindings/Pipeline/PostEventEvaluationPurposePipelineTest.php
bert.hausmans e1551b24bc test(form-builder): per-purpose pipeline smoke for the 6 non-event_registration purposes (WS-6)
RFC §3 Q9 contract — applicator is purpose-agnostic; per-purpose
differences live in PurposeSubjectResolver. Sessie 2's smoke matrix
covered only event_registration; this commit fills the remaining six.

Coverage per file (18 tests total, all passing):

  - SignatureContractPurposePipelineTest (3 tests)
    happy path + conflict resolution + no_auth missing-context
    target: User via auth (User::email/first_name/last_name)

  - UserProfilePurposePipelineTest (3 tests)
    happy path + conflict resolution + no_auth missing-context
    target: User via auth

  - SupplierIntakePurposePipelineTest (3 tests)
    happy path + conflict resolution + no_production_request missing-context
    target: Company subject pre-set by production_request flow

  - PostEventEvaluationPurposePipelineTest (4 tests)
    happy path + conflict resolution + no_person_for_user + no_auth
    target: Person via auth user → Person.event_id link

  - IncidentReportPurposePipelineTest (4 tests)
    happy path (auth + Person link)
    + conflict resolution
    + anonymous-allowed (null subject → COMPLETED, empty applications)
    + auth-without-Person (null subject branch)
    Unique purpose: only one allowed to legitimately resolve to no subject.

  - ArtistAdvancePurposePipelineTest (1 test)
    no_portal_token missing-context only.
    Happy path + subject_not_found branches require the Artist model
    (BACKLOG: ARCH-09); morphTo can't materialise a non-existent class.
    Documented inline; full coverage follows once ARCH-09 lands.

Each test wires the schema_snapshot directly with the applicator-shape
binding entries (matches sessie 2's FormBindingApplicatorIntegrationTest
pattern). All bindings use registered binding-target attributes from
config/form_builder/binding_targets.php to satisfy BindingTypeRegistry's
strict resolve() at apply time.

Refs: RFC-WS-6.md §3 Q9, ARCH-BINDINGS.md § 6.5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:11 +02:00

207 lines
7.8 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\FormBuilder\Bindings\Pipeline;
use App\Enums\FormBuilder\ApplyStatus;
use App\Enums\FormBuilder\FormFieldBindingMergeStrategy;
use App\Enums\FormBuilder\FormPurpose;
use App\Exceptions\FormBuilder\PurposeSubjectResolutionException;
use App\FormBuilder\Bindings\BindingPassResult;
use App\FormBuilder\Bindings\FormBindingApplicator;
use App\Models\Event;
use App\Models\FormBuilder\FormField;
use App\Models\FormBuilder\FormFieldBinding;
use App\Models\FormBuilder\FormSchema;
use App\Models\FormBuilder\FormSubmission;
use App\Models\FormBuilder\FormValue;
use App\Models\Person;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
/**
* End-to-end pipeline smoke for the post_event_evaluation purpose.
*
* RFC §3 Q9 case matrix:
* - Happy path
* - Conflict resolution (trust precedence)
* - Missing required context (auth user has no Person link for the event)
*
* Subject resolution: PostEventEvaluationSubjectResolver returns the
* Person linked to the auth User for the submission's event_id. Throws
* `no_person_for_user` if no link exists, `no_auth` if no auth user.
*
* Ref: ARCH-BINDINGS.md § 6.5
*/
final class PostEventEvaluationPurposePipelineTest extends TestCase
{
use RefreshDatabase;
public function test_happy_path_applies_bindings_and_marks_completed(): void
{
$event = Event::factory()->create();
$user = User::factory()->create();
$person = Person::factory()->create([
'event_id' => $event->id,
'user_id' => $user->id,
'first_name' => 'Old',
'last_name' => 'Name',
]);
$submission = $this->makeSubmission($event, $user, $person, [
'first_name' => ['value' => 'Jan', 'trust' => 70],
'last_name' => ['value' => 'Jansen', 'trust' => 60],
]);
$result = DB::transaction(fn (): BindingPassResult => resolve(FormBindingApplicator::class)->apply($submission));
$this->assertSame(ApplyStatus::COMPLETED, $result->applyStatus());
$this->assertSame('person', $result->provisionedSubjectType);
$person->refresh();
$this->assertSame('Jan', $person->first_name);
$this->assertSame('Jansen', $person->last_name);
}
public function test_conflict_resolution_picks_highest_trust(): void
{
$event = Event::factory()->create();
$user = User::factory()->create();
$person = Person::factory()->create([
'event_id' => $event->id,
'user_id' => $user->id,
'first_name' => 'Initial',
]);
$submission = $this->makeSubmission($event, $user, $person, [
'first_name__low' => ['value' => 'LowTrust', 'trust' => 30, 'attribute' => 'first_name'],
'first_name__high' => ['value' => 'HighTrust', 'trust' => 90, 'attribute' => 'first_name'],
]);
$result = DB::transaction(fn (): BindingPassResult => resolve(FormBindingApplicator::class)->apply($submission));
$this->assertSame(ApplyStatus::COMPLETED, $result->applyStatus());
$person->refresh();
$this->assertSame('HighTrust', $person->first_name);
}
public function test_missing_person_link_throws_resolution_exception(): void
{
// Auth user is set, but no Person row links it for this event.
$event = Event::factory()->create();
$user = User::factory()->create();
$schema = FormSchema::factory()->create([
'organisation_id' => $event->organisation_id,
'purpose' => FormPurpose::POST_EVENT_EVALUATION->value,
]);
$submission = FormSubmission::factory()->forEvent($event)->create([
'form_schema_id' => $schema->id,
'submitted_by_user_id' => $user->id,
'subject_type' => null,
'subject_id' => null,
]);
$submission->schema_snapshot = ['fields' => []];
$submission->save();
try {
DB::transaction(fn () => resolve(FormBindingApplicator::class)->apply($submission->fresh()));
$this->fail('Expected PurposeSubjectResolutionException');
} catch (PurposeSubjectResolutionException $e) {
$this->assertSame('post_event_evaluation', $e->purposeSlug);
$this->assertSame('no_person_for_user', $e->reasonCode);
}
}
public function test_missing_auth_user_throws_resolution_exception(): void
{
$event = Event::factory()->create();
$schema = FormSchema::factory()->create([
'organisation_id' => $event->organisation_id,
'purpose' => FormPurpose::POST_EVENT_EVALUATION->value,
]);
$submission = FormSubmission::factory()->forEvent($event)->create([
'form_schema_id' => $schema->id,
'submitted_by_user_id' => null,
'subject_type' => null,
'subject_id' => null,
]);
$submission->schema_snapshot = ['fields' => []];
$submission->save();
try {
DB::transaction(fn () => resolve(FormBindingApplicator::class)->apply($submission->fresh()));
$this->fail('Expected PurposeSubjectResolutionException');
} catch (PurposeSubjectResolutionException $e) {
$this->assertSame('post_event_evaluation', $e->purposeSlug);
$this->assertSame('no_auth', $e->reasonCode);
}
}
/**
* @param array<string, array{value:string, trust:int, attribute?:string}> $bindingSpecs
*/
private function makeSubmission(Event $event, User $user, Person $person, array $bindingSpecs): FormSubmission
{
$schema = FormSchema::factory()->create([
'organisation_id' => $event->organisation_id,
'purpose' => FormPurpose::POST_EVENT_EVALUATION->value,
]);
$submission = FormSubmission::factory()->forEvent($event)->create([
'form_schema_id' => $schema->id,
'submitted_by_user_id' => $user->id,
'subject_type' => 'person',
'subject_id' => $person->id,
]);
$snapshotFields = [];
foreach ($bindingSpecs as $slug => $spec) {
$attribute = $spec['attribute'] ?? $slug;
$field = FormField::factory()->create([
'form_schema_id' => $schema->id,
'slug' => $slug,
]);
$binding = FormFieldBinding::factory()->forField($field)
->entityOwned('person', $attribute)
->create([
'merge_strategy' => FormFieldBindingMergeStrategy::Overwrite->value,
'trust_level' => $spec['trust'],
]);
$snapshotFields[] = [
'id' => (string) $field->id,
'slug' => (string) $field->slug,
'sort_order' => (int) $field->sort_order,
'bindings' => [[
'id' => (string) $binding->id,
'mode' => 'entity_owned',
'entity' => 'person',
'column' => $attribute,
'merge_strategy' => 'overwrite',
'trust_level' => $spec['trust'],
'is_identity_key' => false,
]],
];
$this->writeValue($submission->id, $field->id, $spec['value']);
}
$submission->schema_snapshot = ['fields' => $snapshotFields];
$submission->save();
return $submission->fresh();
}
private function writeValue(string $submissionId, string $fieldId, mixed $value): void
{
$row = new FormValue;
$row->form_submission_id = $submissionId;
$row->form_field_id = $fieldId;
$row->setAttribute('value', $value);
$row->value_anonymised = false;
$row->save();
}
}