create(); createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner'); $finding = Finding::factory()->for($tenant)->create(['status' => Finding::STATUS_NEW]); $this->actingAs($requester); Filament::setTenant($tenant, true); Livewire::test(ViewFinding::class, ['record' => $finding->getKey()]) ->callAction('request_exception', [ 'owner_user_id' => (int) $requester->getKey(), 'request_reason' => 'Awaiting remediation window', 'review_due_at' => now()->addDays(7)->toDateTimeString(), 'expires_at' => now()->addDays(30)->toDateTimeString(), ]) ->assertHasNoActionErrors(); $exception = FindingException::query()->where('finding_id', (int) $finding->getKey())->first(); expect($exception)->toBeInstanceOf(FindingException::class) ->and($exception?->status)->toBe(FindingException::STATUS_PENDING); $this->actingAs($approver); Filament::setCurrentPanel('admin'); Filament::setTenant(null, true); Filament::bootCurrentPanel(); session()->put(\App\Support\Workspaces\WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); Livewire::withQueryParams([ 'exception' => (int) $exception?->getKey(), ]) ->test(FindingExceptionsQueue::class) ->assertActionVisible('approve_selected_exception') ->assertSee('Awaiting remediation window'); $pendingException = FindingException::query()->findOrFail((int) $exception?->getKey()); app(FindingExceptionService::class)->approve($pendingException, $approver, [ 'effective_from' => now()->addHour()->toDateTimeString(), 'expires_at' => now()->addDays(30)->toDateTimeString(), 'approval_reason' => 'Approved with compensating controls', ]); expect($exception?->fresh()?->status)->toBe(FindingException::STATUS_ACTIVE) ->and($finding->fresh()?->status)->toBe(Finding::STATUS_RISK_ACCEPTED) ->and(AuditLog::query() ->where('action', AuditActionId::FindingExceptionRequested->value) ->where('resource_type', 'finding_exception') ->where('resource_id', (string) $exception?->getKey()) ->exists())->toBeTrue() ->and(AuditLog::query() ->where('action', AuditActionId::FindingExceptionApproved->value) ->where('resource_type', 'finding_exception') ->where('resource_id', (string) $exception?->getKey()) ->exists())->toBeTrue(); }); it('requests and rejects a finding exception while keeping the finding out of accepted risk', function (): void { [$requester, $tenant] = createUserWithTenant(role: 'owner'); $approver = \App\Models\User::factory()->create(); createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner'); $finding = Finding::factory()->for($tenant)->create(['status' => Finding::STATUS_NEW]); $this->actingAs($requester); Filament::setTenant($tenant, true); Livewire::test(ViewFinding::class, ['record' => $finding->getKey()]) ->callAction('request_exception', [ 'owner_user_id' => (int) $requester->getKey(), 'request_reason' => 'Awaiting vendor remediation timeline', 'review_due_at' => now()->addDays(7)->toDateTimeString(), 'expires_at' => now()->addDays(21)->toDateTimeString(), ]) ->assertHasNoActionErrors(); $exception = FindingException::query()->where('finding_id', (int) $finding->getKey())->firstOrFail(); $this->actingAs($approver); Filament::setCurrentPanel('admin'); Filament::setTenant(null, true); Filament::bootCurrentPanel(); session()->put(\App\Support\Workspaces\WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); Livewire::withQueryParams([ 'exception' => (int) $exception->getKey(), ]) ->test(FindingExceptionsQueue::class) ->assertActionVisible('reject_selected_exception') ->assertSee('Awaiting vendor remediation timeline'); app(FindingExceptionService::class)->reject($exception->fresh(), $approver, [ 'rejection_reason' => 'Remediation must be completed before acceptance is granted.', ]); expect($exception->fresh()?->status)->toBe(FindingException::STATUS_REJECTED) ->and($finding->fresh()?->status)->toBe(Finding::STATUS_NEW) ->and(AuditLog::query() ->where('action', AuditActionId::FindingExceptionRequested->value) ->where('resource_type', 'finding_exception') ->where('resource_id', (string) $exception->getKey()) ->exists())->toBeTrue() ->and(AuditLog::query() ->where('action', AuditActionId::FindingExceptionRejected->value) ->where('resource_type', 'finding_exception') ->where('resource_id', (string) $exception->getKey()) ->exists())->toBeTrue(); });