seed(RoleSeeder::class); $this->org = Organisation::factory()->create(); $this->otherOrg = Organisation::factory()->create(); $this->admin = User::factory()->create(); $this->org->users()->attach($this->admin, ['role' => 'org_admin']); $this->outsider = User::factory()->create(); $this->otherOrg->users()->attach($this->outsider, ['role' => 'org_admin']); } public function test_unauthenticated_index_returns_401(): void { $this->getJson("/api/v1/organisations/{$this->org->id}/forms/schemas") ->assertStatus(401); } public function test_index_returns_schemas_for_this_org(): void { Sanctum::actingAs($this->admin); FormSchema::factory()->count(3)->create(['organisation_id' => $this->org->id]); FormSchema::factory()->count(2)->create(['organisation_id' => $this->otherOrg->id]); $response = $this->getJson("/api/v1/organisations/{$this->org->id}/forms/schemas"); $response->assertOk(); $response->assertJsonCount(3, 'data'); } public function test_store_creates_schema(): void { Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->org->id}/forms/schemas", [ 'name' => 'Aanmeldformulier', 'purpose' => FormPurpose::EVENT_REGISTRATION->value, ]); $response->assertCreated(); $this->assertSame('Aanmeldformulier', $response->json('data.name')); $this->assertSame($this->org->id, $response->json('data.organisation_id')); } public function test_store_from_outsider_returns_403(): void { Sanctum::actingAs($this->outsider); $this->postJson("/api/v1/organisations/{$this->org->id}/forms/schemas", [ 'name' => 'Blocked', 'purpose' => FormPurpose::INCIDENT_REPORT->value, ])->assertStatus(403); } public function test_update_bumps_version_on_structural_change(): void { Sanctum::actingAs($this->admin); $schema = FormSchema::factory()->create([ 'organisation_id' => $this->org->id, 'version' => 1, ]); $this->putJson("/api/v1/organisations/{$this->org->id}/forms/schemas/{$schema->id}", [ 'freeze_on_submit' => true, ])->assertOk(); $this->assertSame(2, (int) $schema->fresh()->version); } public function test_destroy_without_confirmation_when_submissions_exist_fails(): void { Sanctum::actingAs($this->admin); $schema = FormSchema::factory()->create(['organisation_id' => $this->org->id, 'name' => 'Delete-me']); \App\Models\FormBuilder\FormSubmission::factory()->create(['form_schema_id' => $schema->id]); $this->deleteJson("/api/v1/organisations/{$this->org->id}/forms/schemas/{$schema->id}") ->assertStatus(500); // RuntimeException bubbles as 500 from DestructiveConfirmationRequired } public function test_destroy_with_matching_confirmation_succeeds(): void { Sanctum::actingAs($this->admin); $schema = FormSchema::factory()->create(['organisation_id' => $this->org->id, 'name' => 'Delete-me']); \App\Models\FormBuilder\FormSubmission::factory()->create(['form_schema_id' => $schema->id]); $this->deleteJson("/api/v1/organisations/{$this->org->id}/forms/schemas/{$schema->id}?confirmed_name=Delete-me") ->assertStatus(204); } public function test_publish_sets_is_published_true(): void { Sanctum::actingAs($this->admin); // USER_PROFILE has no required bindings, so publishing an empty // schema is allowed — keeps this test orthogonal to the purpose // binding check (that behaviour is covered separately by // PurposeSchemaLifecycleTest). $schema = FormSchema::factory() ->forPurpose(FormPurpose::USER_PROFILE) ->create(['organisation_id' => $this->org->id, 'is_published' => false]); $this->postJson("/api/v1/organisations/{$this->org->id}/forms/schemas/{$schema->id}/publish") ->assertOk() ->assertJsonPath('data.is_published', true); } public function test_rotate_public_token_moves_current_to_previous(): void { Sanctum::actingAs($this->admin); $schema = FormSchema::factory()->create([ 'organisation_id' => $this->org->id, 'public_token' => (string) \Illuminate\Support\Str::ulid(), ]); $originalToken = $schema->public_token; $response = $this->postJson( "/api/v1/organisations/{$this->org->id}/forms/schemas/{$schema->id}/rotate-public-token", ['grace_days' => 7], ); $response->assertOk(); $fresh = $schema->fresh(); $this->assertNotSame($originalToken, $fresh->public_token); $this->assertSame($originalToken, $fresh->public_token_previous); } public function test_edit_lock_returns_409_when_another_user_holds(): void { Sanctum::actingAs($this->admin); $other = User::factory()->create(); $this->org->users()->attach($other, ['role' => 'org_admin']); $schema = FormSchema::factory()->create([ 'organisation_id' => $this->org->id, 'edit_lock_user_id' => $other->id, 'edit_lock_expires_at' => now()->addMinutes(5), ]); $response = $this->postJson("/api/v1/organisations/{$this->org->id}/forms/schemas/{$schema->id}/edit-lock"); $this->assertSame(500, $response->status()); // EditLockConflictException surfaces as 500 without handler. } }