seed(RoleSeeder::class); $this->org = Organisation::factory()->create(); $this->admin = User::factory()->create(); $this->org->users()->attach($this->admin, ['role' => 'org_admin']); $this->admin->assignRole('org_admin'); $this->schema = FormSchema::factory()->create(['organisation_id' => $this->org->id]); } public function test_store_creates_webhook(): void { Sanctum::actingAs($this->admin); $response = $this->postJson( "/api/v1/organisations/{$this->org->id}/forms/schemas/{$this->schema->id}/webhooks", [ 'name' => 'Zapier test', 'trigger_event' => 'submission_submitted', 'url' => 'https://hooks.example.com/zap', 'secret' => 'super-secret', 'is_active' => true, ], ); $response->assertCreated(); // Resource must NOT leak url or secret. $this->assertArrayNotHasKey('url', (array) $response->json('data')); $this->assertArrayNotHasKey('secret', (array) $response->json('data')); $this->assertSame('hooks.example.com', $response->json('data.url_host')); $this->assertTrue($response->json('data.has_secret')); } public function test_delivery_job_rejects_private_ip_url(): void { $webhook = FormSchemaWebhook::factory()->create([ 'form_schema_id' => $this->schema->id, 'url' => 'http://10.0.0.5/evil', ]); /** @var FormWebhookDelivery $delivery */ $delivery = FormWebhookDelivery::create([ 'form_schema_webhook_id' => $webhook->id, 'form_submission_id' => \App\Models\FormBuilder\FormSubmission::factory()->create(['form_schema_id' => $this->schema->id])->id, 'trigger_event' => 'submission_submitted', 'status' => 'pending', 'attempts' => 0, 'payload_snapshot' => ['event' => 'test'], ]); (new DeliverFormWebhookJob($delivery->id))->handle(); $fresh = $delivery->fresh(); $this->assertSame('failed', $fresh->status instanceof \BackedEnum ? $fresh->status->value : $fresh->status); $this->assertStringContainsString('SSRF', (string) $fresh->response_body_excerpt); } }