seed(RoleSeeder::class); $this->org = Organisation::factory()->create(); $this->schema = FormSchema::factory()->create([ 'organisation_id' => $this->org->id, 'purpose' => FormPurpose::EVENT_REGISTRATION, 'is_published' => true, 'public_token' => (string) Str::ulid(), ]); FormField::factory()->create([ 'form_schema_id' => $this->schema->id, 'field_type' => FormFieldType::TEXT->value, 'slug' => 'name', 'label' => 'Naam', 'is_portal_visible' => true, 'is_admin_only' => false, ]); FormField::factory()->create([ 'form_schema_id' => $this->schema->id, 'field_type' => FormFieldType::TEXTAREA->value, 'slug' => 'secret_admin_notes', 'label' => 'Admin notes', 'is_portal_visible' => false, 'is_admin_only' => true, ]); } public function test_show_returns_schema_without_hidden_fields(): void { $response = $this->getJson("/api/v1/public/forms/{$this->schema->public_token}"); $response->assertOk(); $slugs = collect($response->json('data.fields'))->pluck('slug')->all(); $this->assertContains('name', $slugs); $this->assertNotContains('secret_admin_notes', $slugs); } public function test_show_unknown_token_returns_404(): void { $this->getJson('/api/v1/public/forms/'.Str::ulid())->assertStatus(404); } public function test_submit_creates_submission(): void { // PUBLIC_RSVP does not require captcha by default Config::set('form_builder.captcha.required_for_purposes', []); // S2c D4 three-step flow: create draft, save values, submit. $create = $this->postJson( "/api/v1/public/forms/{$this->schema->public_token}/submissions", [ 'idempotency_key' => 'public-rsvp-test-001', 'public_submitter_name' => 'Bart', 'public_submitter_email' => 'bart@example.nl', ], ); $create->assertCreated(); $this->assertSame('draft', $create->json('data.status')); $submissionId = $create->json('data.id'); $this->putJson( "/api/v1/public/forms/{$this->schema->public_token}/submissions/{$submissionId}", ['values' => ['name' => 'Bart']], )->assertOk(); $this->postJson( "/api/v1/public/forms/{$this->schema->public_token}/submissions/{$submissionId}/submit", [], ) ->assertCreated() ->assertJsonPath('data.status', 'submitted'); } public function test_submit_with_expired_previous_token_returns_410(): void { $previousToken = (string) Str::ulid(); $this->schema->update([ 'public_token_previous' => $previousToken, 'public_token_rotated_at' => now()->subDays(8), ]); $this->getJson("/api/v1/public/forms/{$previousToken}")->assertStatus(410); } public function test_submit_within_grace_window_still_works(): void { Config::set('form_builder.captcha.required_for_purposes', []); $previousToken = (string) Str::ulid(); $this->schema->update([ 'public_token_previous' => $previousToken, 'public_token_rotated_at' => now()->subDays(2), ]); $this->getJson("/api/v1/public/forms/{$previousToken}")->assertOk(); } }