TenantAtlas/apps/platform/tests/Feature/Governance/DecisionRegisterAuthorizationTest.php
ahmido d85ef4cc1c Spec 314: enforce workspace hub navigation context contract (#369)
## Summary
- add a shared workspace hub registry for canonical workspace-scoped navigation entry
- keep sidebar and global workspace hub URLs free of inherited environment query and filter state
- add focused feature and browser coverage for workspace hub shell and data-scope contracts

## Validation
- 54 focused feature tests passed (205 assertions)
- 1 browser smoke test passed (361 assertions)
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `git diff --check`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #369
2026-05-16 09:54:29 +00:00

262 lines
10 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\Governance\DecisionRegister;
use App\Models\EvidenceSnapshot;
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\FindingExceptionDecision;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Support\Auth\Capabilities;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
uses(RefreshDatabase::class);
afterEach(function (): void {
Filament::setCurrentPanel(null);
});
it('redirects decision register visits without workspace context into the existing workspace chooser flow', function (): void {
$user = User::factory()->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']);
}