TenantAtlas/apps/platform/tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php
ahmido eca19819d1 feat: add workspace baseline compare matrix (#221)
## Summary
- add a workspace-scoped baseline compare matrix page under baseline profiles
- derive matrix tenant summaries, subject rows, cell states, freshness, and trust from existing snapshots, compare runs, and findings
- add confirmation-gated `Compare assigned tenants` actions on the baseline detail and matrix surfaces without introducing a workspace umbrella run
- preserve matrix navigation context into tenant compare and finding drilldowns and add centralized matrix badge semantics
- include spec, plan, data model, contracts, quickstart, tasks, and focused feature/browser coverage for Spec 190

## Verification
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Badges/BaselineCompareMatrixBadgesTest.php tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php tests/Feature/Baselines/BaselineComparePerformanceGuardTest.php tests/Feature/Filament/BaselineCompareMatrixPageTest.php tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Guards/NoAdHocStatusBadgesTest.php tests/Feature/Guards/NoDiagnosticWarningBadgesTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- completed an integrated-browser smoke flow locally for matrix render, differ filter, finding drilldown round-trip, and `Compare assigned tenants` confirmation/action

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #221
2026-04-11 10:20:25 +00:00

125 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\BaselineCompareLanding;
use App\Filament\Resources\BaselineProfileResource;
use App\Filament\Resources\FindingResource;
use App\Models\User;
use App\Services\Auth\CapabilityResolver;
use App\Services\Auth\WorkspaceCapabilityResolver;
use App\Support\Auth\Capabilities;
use App\Support\Navigation\CanonicalNavigationContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Tests\Feature\Concerns\BuildsBaselineCompareMatrixFixtures;
uses(RefreshDatabase::class, BuildsBaselineCompareMatrixFixtures::class);
it('returns 404 for non-members on the workspace baseline compare matrix', function (): void {
$fixture = $this->makeBaselineCompareMatrixFixture();
$nonMember = User::factory()->create();
$this->actingAs($nonMember)->withSession([
\App\Support\Workspaces\WorkspaceContext::SESSION_KEY => (int) $fixture['workspace']->getKey(),
]);
$this->get(BaselineProfileResource::compareMatrixUrl($fixture['profile']))
->assertNotFound();
});
it('returns 403 for workspace members missing baseline view capability on the matrix route', function (): void {
$fixture = $this->makeBaselineCompareMatrixFixture();
$viewer = User::factory()->create();
\App\Models\WorkspaceMembership::factory()->create([
'workspace_id' => (int) $fixture['workspace']->getKey(),
'user_id' => (int) $viewer->getKey(),
'role' => 'readonly',
]);
$resolver = \Mockery::mock(WorkspaceCapabilityResolver::class);
$resolver->shouldReceive('isMember')->andReturnTrue();
$resolver->shouldReceive('can')->andReturnFalse();
app()->instance(WorkspaceCapabilityResolver::class, $resolver);
$this->actingAs($viewer)->withSession([
\App\Support\Workspaces\WorkspaceContext::SESSION_KEY => (int) $fixture['workspace']->getKey(),
]);
$this->get(BaselineProfileResource::compareMatrixUrl($fixture['profile']))
->assertForbidden();
});
it('returns 404 for matrix tenant drilldowns when the actor is not a tenant member', function (): void {
$fixture = $this->makeBaselineCompareMatrixFixture();
$nonMember = User::factory()->create();
$this->actingAs($nonMember)->withSession([
\App\Support\Workspaces\WorkspaceContext::SESSION_KEY => (int) $fixture['workspace']->getKey(),
]);
$query = CanonicalNavigationContext::forBaselineCompareMatrix(
$fixture['profile'],
subjectKey: 'wifi-corp-profile',
)->toQuery();
$this->get(BaselineCompareLanding::getUrl(parameters: $query, panel: 'tenant', tenant: $fixture['visibleTenant']))
->assertNotFound();
});
it('returns 403 for matrix tenant drilldowns when tenant view capability is missing', function (): void {
$fixture = $this->makeBaselineCompareMatrixFixture();
$resolver = \Mockery::mock(CapabilityResolver::class);
$resolver->shouldReceive('isMember')->andReturnTrue();
$resolver->shouldReceive('can')->andReturnFalse();
app()->instance(CapabilityResolver::class, $resolver);
$this->actingAs($fixture['user']);
$fixture['visibleTenant']->makeCurrent();
$query = CanonicalNavigationContext::forBaselineCompareMatrix(
$fixture['profile'],
subjectKey: 'wifi-corp-profile',
tenant: $fixture['visibleTenant'],
)->toQuery();
$this->get(BaselineCompareLanding::getUrl(parameters: $query, panel: 'tenant', tenant: $fixture['visibleTenant']))
->assertForbidden();
});
it('returns 403 for matrix finding drilldowns when findings view capability is missing', function (): void {
$fixture = $this->makeBaselineCompareMatrixFixture();
$run = $this->makeBaselineCompareMatrixRun(
$fixture['visibleTenant'],
$fixture['profile'],
$fixture['snapshot'],
);
$finding = $this->makeBaselineCompareMatrixFinding(
$fixture['visibleTenant'],
$fixture['profile'],
$run,
'wifi-corp-profile',
);
$this->actingAs($fixture['user']);
$fixture['visibleTenant']->makeCurrent();
Gate::define(Capabilities::TENANT_FINDINGS_VIEW, fn (): bool => false);
$query = CanonicalNavigationContext::forBaselineCompareMatrix(
$fixture['profile'],
subjectKey: 'wifi-corp-profile',
tenant: $fixture['visibleTenant'],
)->toQuery();
$this->get(FindingResource::getUrl('view', [
'record' => $finding,
...$query,
], tenant: $fixture['visibleTenant']))
->assertForbidden();
});