create(['name' => 'Existing Owner']); createUserWithTenant(tenant: $tenant, user: $existingOwner, role: 'manager'); $existingAssignee = User::factory()->create(['name' => 'Existing Assignee']); createUserWithTenant(tenant: $tenant, user: $existingAssignee, role: 'operator'); $replacementOwner = User::factory()->create(['name' => 'Replacement Owner']); createUserWithTenant(tenant: $tenant, user: $replacementOwner, role: 'manager'); $replacementAssignee = User::factory()->create(['name' => 'Replacement Assignee']); createUserWithTenant(tenant: $tenant, user: $replacementAssignee, role: 'operator'); $finding = $this->makeFindingForWorkflow($tenant, Finding::STATUS_TRIAGED, [ 'owner_user_id' => (int) $existingOwner->getKey(), 'assignee_user_id' => (int) $existingAssignee->getKey(), ]); $ownerUserId = match ($ownerTarget) { 'replacement' => (int) $replacementOwner->getKey(), 'clear' => null, default => (int) $existingOwner->getKey(), }; $assigneeUserId = match ($assigneeTarget) { 'replacement' => (int) $replacementAssignee->getKey(), 'clear' => null, default => (int) $existingAssignee->getKey(), }; $updated = app(FindingWorkflowService::class)->assign( finding: $finding, tenant: $tenant, actor: $actor, assigneeUserId: $assigneeUserId, ownerUserId: $ownerUserId, ); $audit = $this->latestFindingAudit($updated, 'finding.assigned'); expect($audit)->not->toBeNull(); expect(data_get($audit?->metadata, 'responsibility_change_classification'))->toBe($expectedClassification) ->and(data_get($audit?->metadata, 'responsibility_change_summary'))->toBe($expectedSummary); expect($updated->owner_user_id)->toBe($ownerUserId) ->and($updated->assignee_user_id)->toBe($assigneeUserId); })->with([ 'owner only' => ['replacement', 'existing', 'owner_only', 'Updated the accountable owner and kept the active assignee unchanged.'], 'assignee only' => ['existing', 'replacement', 'assignee_only', 'Updated the active assignee and kept the accountable owner unchanged.'], 'owner and assignee' => ['replacement', 'replacement', 'owner_and_assignee', 'Updated the accountable owner and the active assignee.'], 'clear owner' => ['clear', 'existing', 'clear_owner', 'Cleared the accountable owner and kept the active assignee unchanged.'], 'clear assignee' => ['existing', 'clear', 'clear_assignee', 'Cleared the active assignee and kept the accountable owner unchanged.'], ]); it('preserves 403 semantics for in-scope members without assignment capability', function (): void { [$owner, $tenant] = createUserWithTenant(role: 'owner'); [$readonly] = createUserWithTenant(tenant: $tenant, role: 'readonly'); $finding = $this->makeFindingForWorkflow($tenant, Finding::STATUS_NEW); expect(fn () => app(FindingWorkflowService::class)->assign( finding: $finding, tenant: $tenant, actor: $readonly, assigneeUserId: null, ownerUserId: (int) $owner->getKey(), ))->toThrow(AuthorizationException::class); });