buildValidEventRegistrationSchema(); $this->service()->publish($schema, $this->actor()); $this->assertTrue($schema->refresh()->is_published); } public function test_missing_required_bindings_throws_existing_exception_first(): void { $schema = FormSchema::factory()->create([ 'purpose' => FormPurpose::EVENT_REGISTRATION->value, ]); // No bindings → required_bindings (person.email/first_name/last_name) unmet. $this->expectException(PurposeRequirementsNotMetException::class); $this->service()->publish($schema, $this->actor()); } public function test_missing_identity_key_flag_throws_publish_guard_violation(): void { $schema = $this->buildValidEventRegistrationSchema(); FormFieldBinding::query()->withoutGlobalScopes() ->whereIn('owner_id', $schema->fields->pluck('id')) ->where('target_attribute', 'email') ->update(['is_identity_key' => false]); $schema->load('fields.bindings'); try { $this->service()->publish($schema, $this->actor()); $this->fail('Expected PublishGuardViolationException'); } catch (PublishGuardViolationException $e) { $codes = array_map(static fn (\App\FormBuilder\Publishing\PublishGuardResult $v): string => $v->guardCode, $e->violations); $this->assertContains('requires_identity_key_binding:person:email', $codes); } $this->assertFalse($schema->refresh()->is_published); } public function test_violations_are_sorted_lexicographically(): void { $schema = $this->buildValidEventRegistrationSchema(); // Trigger TWO violations: drop is_identity_key + create ambiguous trust. FormFieldBinding::query()->withoutGlobalScopes() ->whereIn('owner_id', $schema->fields->pluck('id')) ->where('target_attribute', 'email') ->update(['is_identity_key' => false, 'trust_level' => 60]); FormFieldBinding::query()->withoutGlobalScopes() ->whereIn('owner_id', $schema->fields->pluck('id')) ->where('target_attribute', 'first_name') ->update(['trust_level' => 60]); $schema->load('fields.bindings'); try { $this->service()->publish($schema, $this->actor()); $this->fail('Expected PublishGuardViolationException'); } catch (PublishGuardViolationException $e) { $codes = array_map(static fn (\App\FormBuilder\Publishing\PublishGuardResult $v): string => $v->guardCode, $e->violations); $sorted = $codes; sort($sorted); $this->assertSame($sorted, $codes, 'Violations must be sorted lexicographically by code'); } } public function test_response_renders_as_422_with_violation_payload(): void { $schema = $this->buildValidEventRegistrationSchema(); FormFieldBinding::query()->withoutGlobalScopes() ->whereIn('owner_id', $schema->fields->pluck('id')) ->where('target_attribute', 'email') ->update(['is_identity_key' => false]); $schema->load('fields.bindings'); try { $this->service()->publish($schema, $this->actor()); $this->fail('Expected PublishGuardViolationException'); } catch (PublishGuardViolationException $e) { $response = $e->render(request()); $this->assertSame(422, $response->getStatusCode()); $body = json_decode((string) $response->getContent(), true); $this->assertSame('publish_blocked', $body['error']); $this->assertSame('event_registration', $body['purpose_slug']); $this->assertNotEmpty($body['violations']); } } private function service(): FormSchemaService { return $this->app->make(FormSchemaService::class); } private function actor(): User { return User::factory()->create(); } private function buildValidEventRegistrationSchema(): FormSchema { $schema = FormSchema::factory()->create([ 'purpose' => FormPurpose::EVENT_REGISTRATION->value, 'section_level_submit' => false, 'is_published' => false, ]); $crowdType = \App\Models\CrowdType::factory()->create([ 'organisation_id' => $schema->organisation_id, ]); $schema->default_crowd_type_id = $crowdType->id; $schema->save(); $emailField = FormField::factory()->create([ 'form_schema_id' => $schema->id, 'field_type' => FormFieldType::EMAIL->value, ]); FormFieldBinding::factory()->forField($emailField)->entityOwned('person', 'email') ->create(['is_identity_key' => true, 'trust_level' => 80]); $firstNameField = FormField::factory()->create([ 'form_schema_id' => $schema->id, 'field_type' => FormFieldType::TEXT->value, ]); FormFieldBinding::factory()->forField($firstNameField)->entityOwned('person', 'first_name') ->create(['is_identity_key' => false, 'trust_level' => 70]); $lastNameField = FormField::factory()->create([ 'form_schema_id' => $schema->id, 'field_type' => FormFieldType::TEXT->value, ]); FormFieldBinding::factory()->forField($lastNameField)->entityOwned('person', 'last_name') ->create(['is_identity_key' => false, 'trust_level' => 50]); return $schema->fresh(['fields.bindings', 'fields.configs', 'sections']); } }