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->member = User::factory()->create(); $this->org->users()->attach($this->member, ['role' => 'org_member']); $this->member->assignRole('org_member'); $this->submitter = User::factory()->create(); $this->org->users()->attach($this->submitter, ['role' => 'org_member']); $this->submitter->assignRole('org_member'); $this->schema = FormSchema::factory()->create(['organisation_id' => $this->org->id]); $this->publicField = FormField::factory()->create([ 'form_schema_id' => $this->schema->id, 'field_type' => FormFieldType::TEXT->value, 'slug' => 'motivatie', 'label' => 'Motivatie', 'role_restrictions' => null, 'is_admin_only' => false, ]); $this->adminOnlyField = FormField::factory()->create([ 'form_schema_id' => $this->schema->id, 'field_type' => FormFieldType::TEXTAREA->value, 'slug' => 'admin_notes', 'label' => 'Interne notities', 'role_restrictions' => null, 'is_admin_only' => true, ]); } public function test_field_access_service_hides_admin_only_from_non_admin(): void { $service = app(FieldAccessService::class); $this->assertTrue($service->canRead($this->admin, $this->adminOnlyField)); $this->assertFalse($service->canRead($this->member, $this->adminOnlyField)); $this->assertTrue($service->canRead($this->member, $this->publicField)); } public function test_subject_self_sees_their_own_value_even_when_restricted(): void { $service = app(FieldAccessService::class); $submission = FormSubmission::factory()->create([ 'form_schema_id' => $this->schema->id, 'subject_type' => 'user', 'subject_id' => $this->submitter->id, 'status' => FormSubmissionStatus::DRAFT->value, ]); $this->assertTrue($service->canRead($this->submitter, $this->adminOnlyField, $submission)); } public function test_value_upsert_rejects_write_to_admin_only_field_from_non_admin(): void { Sanctum::actingAs($this->submitter); $submission = FormSubmission::factory()->create([ 'form_schema_id' => $this->schema->id, 'subject_type' => 'user', 'subject_id' => $this->submitter->id, 'status' => FormSubmissionStatus::DRAFT->value, ]); // Submitter IS subject-self, so per §18.3 they can write to their // own values regardless of role_restrictions. Admin-only does not // trump subject-self for writes to the submitter's own submission. $response = $this->putJson( "/api/v1/organisations/{$this->org->id}/forms/submissions/{$submission->id}/field-values", ['values' => ['admin_notes' => 'x']], ); $response->assertOk(); } public function test_admin_only_value_hidden_in_resource_for_non_admin_viewer(): void { // Member views a submission that is not their own. admin_notes // must not leak. $submission = FormSubmission::factory()->create([ 'form_schema_id' => $this->schema->id, 'subject_type' => 'user', 'subject_id' => $this->submitter->id, 'submitted_by_user_id' => $this->submitter->id, 'status' => FormSubmissionStatus::SUBMITTED->value, 'submitted_at' => now(), ]); FormValue::create([ 'form_submission_id' => $submission->id, 'form_field_id' => $this->publicField->id, 'value' => 'public motivation', ]); FormValue::create([ 'form_submission_id' => $submission->id, 'form_field_id' => $this->adminOnlyField->id, 'value' => 'private notes', ]); // Admin SEES both Sanctum::actingAs($this->admin); $adminResp = $this->getJson("/api/v1/organisations/{$this->org->id}/forms/submissions/{$submission->id}"); $adminResp->assertOk(); $this->assertArrayHasKey('admin_notes', (array) $adminResp->json('data.values')); $this->assertArrayHasKey('motivatie', (array) $adminResp->json('data.values')); // Submitter (subject-self) SEES both via subject-self bypass Sanctum::actingAs($this->submitter); $subResp = $this->getJson("/api/v1/organisations/{$this->org->id}/forms/submissions/{$submission->id}"); $subResp->assertOk(); $this->assertArrayHasKey('admin_notes', (array) $subResp->json('data.values')); } }