create(); $workspaceA = Workspace::factory()->create(); $workspaceB = Workspace::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspaceA->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspaceB->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); $this->actingAs($user) ->get(DecisionRegister::getUrl(panel: 'admin')) ->assertRedirect('/admin/choose-workspace'); }); it('returns 404 for users outside the active workspace on the decision register route', function (): void { $user = User::factory()->create(); $workspace = Workspace::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) Workspace::factory()->create()->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get(DecisionRegister::getUrl(panel: 'admin')) ->assertNotFound(); }); it('opens the default unfiltered register for authorized workspace members with no visible decisions', function (): void { $tenant = ManagedEnvironment::factory()->create(['status' => 'active']); [$user, $tenant] = createUserWithTenant($tenant, role: 'readonly', workspaceRole: 'readonly'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(DecisionRegister::getUrl(panel: 'admin')) ->assertOk() ->assertSee('No open decisions match this filter right now.'); }); it('registers the decision register page for authorized workspace members even when the register is empty', function (): void { $tenant = ManagedEnvironment::factory()->create(['status' => 'active']); [$user, $tenant] = createUserWithTenant($tenant, role: 'readonly', workspaceRole: 'readonly'); $response = $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(route('admin.workspace.home', ['workspace' => $tenant->workspace])) ->assertOk(); $response->assertSee(DecisionRegister::getUrl(panel: 'admin'), false); expect(DecisionRegister::canAccess())->toBeTrue(); }); it('returns 404 for explicit tenant filters outside the actor scope', function (): void { $visibleTenant = ManagedEnvironment::factory()->create(['status' => 'active']); [$user, $visibleTenant] = createUserWithTenant($visibleTenant, role: 'readonly', workspaceRole: 'readonly'); $hiddenTenant = ManagedEnvironment::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $visibleTenant->workspace_id, ]); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $visibleTenant->workspace_id]) ->get(DecisionRegister::getUrl(panel: 'admin').'?managed_environment_id='.(string) $hiddenTenant->getKey()) ->assertNotFound(); }); it('allows readonly tenant members to open the decision register when visible decisions exist', function (): void { $tenant = ManagedEnvironment::factory()->create(['status' => 'active']); [$user, $tenant] = createUserWithTenant($tenant, role: 'readonly', workspaceRole: 'readonly'); decisionRegisterAuthException( tenant: $tenant, actor: $user, status: FindingException::STATUS_PENDING, validityState: FindingException::VALIDITY_MISSING_SUPPORT, decisionType: FindingExceptionDecision::TYPE_REQUESTED, decisionReason: 'Visible approval request', ); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(DecisionRegister::getUrl(panel: 'admin')) ->assertOk() ->assertSee('Decision register'); }); it('registers the decision register page once visible open decisions exist', function (): void { $tenant = ManagedEnvironment::factory()->create(['status' => 'active']); [$user, $tenant] = createUserWithTenant($tenant, role: 'readonly', workspaceRole: 'readonly'); decisionRegisterAuthException( tenant: $tenant, actor: $user, status: FindingException::STATUS_PENDING, validityState: FindingException::VALIDITY_MISSING_SUPPORT, decisionType: FindingExceptionDecision::TYPE_REQUESTED, decisionReason: 'Visible approval request', ); $response = $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(route('admin.workspace.home', ['workspace' => $tenant->workspace])) ->assertOk(); $response->assertSee(DecisionRegister::getUrl(panel: 'admin')); expect(DecisionRegister::canAccess())->toBeTrue(); }); it('registers the decision register page and redirects the default route when only recently closed decisions are visible', function (): void { $tenant = ManagedEnvironment::factory()->create(['status' => 'active']); [$user, $tenant] = createUserWithTenant($tenant, role: 'readonly', workspaceRole: 'readonly'); decisionRegisterAuthException( tenant: $tenant, actor: $user, status: FindingException::STATUS_REJECTED, validityState: FindingException::VALIDITY_REJECTED, decisionType: FindingExceptionDecision::TYPE_REJECTED, decisionReason: 'Recently rejected closure reason', ); $response = $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(route('admin.workspace.home', ['workspace' => $tenant->workspace])) ->assertOk(); $response->assertSee(DecisionRegister::getUrl(panel: 'admin')); expect(DecisionRegister::canAccess())->toBeTrue(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(DecisionRegister::getUrl(panel: 'admin')) ->assertRedirect(DecisionRegister::getUrl(panel: 'admin', parameters: ['register_state' => 'recently_closed'])); }); it('does not render direct evidence links when the actor lacks evidence destination access', function (): void { $tenant = ManagedEnvironment::factory()->create(['status' => 'active']); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner'); Gate::define(Capabilities::EVIDENCE_VIEW, fn (): bool => false); $snapshot = EvidenceSnapshot::query()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'status' => EvidenceSnapshotStatus::Active->value, 'completeness_state' => EvidenceCompletenessState::Complete->value, 'summary' => ['finding_count' => 1], 'generated_at' => now(), ]); $exception = decisionRegisterAuthException( tenant: $tenant, actor: $user, status: FindingException::STATUS_PENDING, validityState: FindingException::VALIDITY_MISSING_SUPPORT, decisionType: FindingExceptionDecision::TYPE_REQUESTED, decisionReason: 'Evidence access denied request', ); $exception->forceFill(['evidence_summary' => ['reference_count' => 1]])->save(); $exception->evidenceReferences()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'source_type' => 'evidence_snapshot', 'source_id' => (string) $snapshot->getKey(), 'label' => 'Evidence snapshot', 'summary_payload' => [], ]); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(DecisionRegister::getUrl(panel: 'admin')) ->assertOk() ->assertSee('1 proof item') ->assertSee('View proof') ->assertDontSee('View evidence') ->assertDontSee('/admin/t', false); }); function decisionRegisterAuthException( ManagedEnvironment $tenant, User $actor, string $status, string $validityState, string $decisionType, string $decisionReason, ): FindingException { $finding = Finding::factory()->for($tenant)->create([ 'workspace_id' => (int) $tenant->workspace_id, ]); $exception = FindingException::query()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'finding_id' => (int) $finding->getKey(), 'requested_by_user_id' => (int) $actor->getKey(), 'owner_user_id' => (int) $actor->getKey(), 'status' => $status, 'current_validity_state' => $validityState, 'request_reason' => 'Decision register authorization test', 'requested_at' => now()->subDay(), 'review_due_at' => now()->addDay(), 'evidence_summary' => ['reference_count' => 0], ]); $decision = $exception->decisions()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'actor_user_id' => (int) $actor->getKey(), 'decision_type' => $decisionType, 'reason' => $decisionReason, 'metadata' => [], 'decided_at' => now()->subDay(), ]); $exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save(); return $exception->fresh(['currentDecision']); }