From 630bd0311c8ec75edc476fa0aee67bf0f496fd99 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sat, 16 May 2026 22:39:45 +0200 Subject: [PATCH] feat: enforce environment-owned baseline compare routing --- .../Filament/Pages/BaselineCompareLanding.php | 299 ++++++++++++++++-- .../Filament/Pages/BaselineCompareMatrix.php | 6 +- .../Widgets/Dashboard/BaselineCompareNow.php | 6 +- .../Widgets/Dashboard/NeedsAttention.php | 6 +- .../BaselineCompareCoverageBanner.php | 4 +- .../EnvironmentDashboardSummaryBuilder.php | 4 +- .../app/Support/ManagedEnvironmentLinks.php | 9 + .../EnsureEnvironmentContextSelected.php | 2 +- .../app/Support/OperationRunLinks.php | 3 +- .../Workspaces/WorkspaceOverviewBuilder.php | 14 +- ...ineCompareEnvironmentRouteContractTest.php | 124 ++++++++ ...ineCompareLandingAdminTenantParityTest.php | 12 +- ...CompareLandingDuplicateNamesBannerTest.php | 6 +- .../BaselineCompareLandingRbacLabelsTest.php | 4 +- ...BaselineCompareLandingStartSurfaceTest.php | 25 +- ...aselineCompareLandingWhyNoFindingsTest.php | 10 +- .../PanelNavigationSegregationTest.php | 2 + ...dEnvironmentCanonicalRouteContractTest.php | 2 + .../Navigation/WorkspaceHubRegistryTest.php | 3 + ...BaselineCompareMatrixAuthorizationTest.php | 14 +- .../Rbac/DriftLandingUiEnforcementTest.php | 6 +- apps/platform/tests/Pest.php | 29 +- .../Unit/Tenants/AdminSurfaceScopeTest.php | 1 + 23 files changed, 493 insertions(+), 98 deletions(-) create mode 100644 apps/platform/tests/Feature/Filament/BaselineCompareEnvironmentRouteContractTest.php diff --git a/apps/platform/app/Filament/Pages/BaselineCompareLanding.php b/apps/platform/app/Filament/Pages/BaselineCompareLanding.php index 5b02db98..179bbbc4 100644 --- a/apps/platform/app/Filament/Pages/BaselineCompareLanding.php +++ b/apps/platform/app/Filament/Pages/BaselineCompareLanding.php @@ -4,14 +4,13 @@ namespace App\Filament\Pages; -use App\Filament\Concerns\ResolvesPanelTenantContext; -use App\Filament\Concerns\UsesAdminEnvironmentFilterQueryParameter; use App\Filament\Resources\BaselineProfileResource; use App\Filament\Resources\FindingResource; use App\Models\BaselineProfile; use App\Models\ManagedEnvironment; use App\Models\OperationRun; use App\Models\User; +use App\Models\Workspace; use App\Services\Auth\CapabilityResolver; use App\Services\Baselines\BaselineCompareService; use App\Support\Auth\Capabilities; @@ -31,17 +30,18 @@ use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot; +use App\Support\Workspaces\WorkspaceContext; use BackedEnum; use Filament\Actions\Action; +use Filament\Facades\Filament; use Filament\Notifications\Notification; use Filament\Pages\Page; +use Illuminate\Database\Eloquent\Model; +use Livewire\Attributes\Locked; use UnitEnum; class BaselineCompareLanding extends Page { - use ResolvesPanelTenantContext; - use UsesAdminEnvironmentFilterQueryParameter; - protected const MONITORING_PAGE_STATE_CONTRACT = [ 'surfaceKey' => 'baseline_compare_landing', 'surfaceType' => 'launch_context_support', @@ -96,6 +96,16 @@ class BaselineCompareLanding extends Page 'localOnlyStateKeys' => [], ]; + private const LEGACY_SCOPE_QUERY_KEYS = [ + 'environment_id', + 'tenant', + 'tenant_id', + 'managed_environment_id', + 'environment', + 'tenant_scope', + 'tableFilters', + ]; + protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-scale'; protected static string|UnitEnum|null $navigationGroup = 'Governance'; @@ -106,6 +116,8 @@ class BaselineCompareLanding extends Page protected static ?string $title = 'Baseline Compare'; + protected static ?string $slug = 'workspaces/{workspace}/environments/{environment}/baseline-compare'; + protected string $view = 'filament.pages.baseline-compare-landing'; public static function shouldRegisterNavigation(): bool @@ -184,23 +196,44 @@ public static function shouldRegisterNavigation(): bool public ?string $matrixSubjectKey = null; + #[Locked] + public ?int $scopedEnvironmentId = null; + + /** + * @param array $parameters + */ + public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string + { + $panelId = $panel ?? Filament::getCurrentOrDefaultPanel()?->getId() ?? 'admin'; + + if ($panelId !== 'admin') { + return parent::getUrl($parameters, $isAbsolute, $panelId, $tenant); + } + + $environment = static::resolveAdminUrlEnvironment($parameters, $tenant); + + if (! $environment instanceof ManagedEnvironment) { + return url('/admin'); + } + + $workspace = static::resolveAdminUrlWorkspace($environment, $parameters); + + if (! $workspace instanceof Workspace && ! is_string($workspace) && ! is_int($workspace)) { + return url('/admin'); + } + + $parameters = static::withoutLegacyScopeQuery($parameters); + $parameters['environment'] = $environment; + $parameters['workspace'] = $workspace instanceof Workspace + ? static::workspaceRouteKey($workspace) + : $workspace; + + return parent::getUrl($parameters, $isAbsolute, $panelId, null); + } + public static function canAccess(): bool { - $user = auth()->user(); - - if (! $user instanceof User) { - return false; - } - - $tenant = static::resolveTenantContextForCurrentPanel(); - - if (! $tenant instanceof ManagedEnvironment) { - return false; - } - - $resolver = app(CapabilityResolver::class); - - return $resolver->can($user, $tenant, Capabilities::TENANT_VIEW); + return static::hasEnvironmentAccess(static::resolveRouteOwnedEnvironment()); } /** @@ -211,20 +244,31 @@ public static function monitoringPageStateContract(): array return self::MONITORING_PAGE_STATE_CONTRACT; } - public function mount(): void + public function mount(ManagedEnvironment|string|null $environment = null): void { + $tenant = static::resolveRouteOwnedEnvironment($environment); + + if (! $tenant instanceof ManagedEnvironment || ! static::hasEnvironmentAccess($tenant)) { + abort(404); + } + + $this->scopedEnvironmentId = (int) $tenant->getKey(); $this->navigationContextPayload = is_array(request()->query('nav')) ? request()->query('nav') : null; $baselineProfileId = request()->query('baseline_profile_id'); $subjectKey = request()->query('subject_key'); $this->matrixBaselineProfileId = is_numeric($baselineProfileId) ? (int) $baselineProfileId : null; $this->matrixSubjectKey = is_string($subjectKey) && trim($subjectKey) !== '' ? trim($subjectKey) : null; - $this->refreshStats(); + $this->refreshStatsForEnvironment($tenant); } public function refreshStats(): void { - $tenant = static::resolveTenantContextForCurrentPanel(); + $this->refreshStatsForEnvironment($this->currentEnvironment()); + } + + private function refreshStatsForEnvironment(?ManagedEnvironment $tenant): void + { $stats = BaselineCompareStats::forTenant($tenant); $aggregate = $tenant instanceof ManagedEnvironment ? $this->governanceAggregate($tenant, $stats) @@ -449,10 +493,10 @@ private function compareNowAction(): Action return; } - $tenant = static::resolveTenantContextForCurrentPanel(); + $tenant = $this->currentEnvironment(); if (! $tenant instanceof ManagedEnvironment) { - Notification::make()->title('Select a tenant to compare baselines')->danger()->send(); + Notification::make()->title('Open an environment to compare baselines')->danger()->send(); return; } @@ -516,7 +560,7 @@ private function compareNowAction(): Action public function getFindingsUrl(): ?string { - $tenant = static::resolveTenantContextForCurrentPanel(); + $tenant = $this->currentEnvironment(); if (! $tenant instanceof ManagedEnvironment) { return null; @@ -531,7 +575,7 @@ public function getRunUrl(): ?string return null; } - $tenant = static::resolveTenantContextForCurrentPanel(); + $tenant = $this->currentEnvironment(); if (! $tenant instanceof ManagedEnvironment) { return null; @@ -582,7 +626,7 @@ private function navigationContext(): ?CanonicalNavigationContext private function resolveCompareMatrixProfile(): ?BaselineProfile { - $tenant = static::resolveTenantContextForCurrentPanel(); + $tenant = $this->currentEnvironment(); if (! $tenant instanceof ManagedEnvironment) { return null; @@ -606,4 +650,203 @@ private function resolveCompareMatrixProfile(): ?BaselineProfile return null; } + + private function currentEnvironment(): ?ManagedEnvironment + { + if ($this->scopedEnvironmentId !== null) { + $tenant = ManagedEnvironment::query()->whereKey($this->scopedEnvironmentId)->first(); + + return $tenant instanceof ManagedEnvironment && static::hasEnvironmentAccess($tenant) + ? $tenant + : null; + } + + $tenant = static::resolveRouteOwnedEnvironment(); + + return $tenant instanceof ManagedEnvironment && static::hasEnvironmentAccess($tenant) + ? $tenant + : null; + } + + protected static function resolveRouteOwnedEnvironment(ManagedEnvironment|string|null $environment = null): ?ManagedEnvironment + { + if ($environment instanceof ManagedEnvironment) { + return $environment; + } + + if (is_string($environment) && $environment !== '') { + return ManagedEnvironment::query() + ->where('slug', $environment) + ->first(); + } + + $routeEnvironment = request()->route('environment'); + + if ($routeEnvironment instanceof ManagedEnvironment) { + return $routeEnvironment; + } + + if (is_string($routeEnvironment) && $routeEnvironment !== '') { + return ManagedEnvironment::query() + ->where('slug', $routeEnvironment) + ->first(); + } + + return static::resolveRefererOwnedEnvironment(); + } + + private static function hasEnvironmentAccess(?ManagedEnvironment $environment): bool + { + $user = auth()->user(); + + if (! $environment instanceof ManagedEnvironment || ! $user instanceof User) { + return false; + } + + $routeWorkspace = request()->route('workspace'); + + if ($routeWorkspace instanceof Workspace && (int) $routeWorkspace->getKey() !== (int) $environment->workspace_id) { + return false; + } + + if (is_string($routeWorkspace) && $routeWorkspace !== '') { + $workspace = $environment->workspace instanceof Workspace + ? $environment->workspace + : $environment->workspace()->first(); + + if (! $workspace instanceof Workspace) { + return false; + } + + if ($routeWorkspace !== static::workspaceRouteKey($workspace) && $routeWorkspace !== (string) $workspace->getKey()) { + return false; + } + } + + $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); + + if ($workspaceId !== null && (int) $workspaceId !== (int) $environment->workspace_id) { + return false; + } + + /** @var CapabilityResolver $resolver */ + $resolver = app(CapabilityResolver::class); + + return $resolver->isMember($user, $environment) + && $resolver->can($user, $environment, Capabilities::TENANT_VIEW); + } + + /** + * @param array $parameters + */ + private static function resolveAdminUrlEnvironment(array $parameters, ?Model $tenant = null): ?ManagedEnvironment + { + $parameterEnvironment = $parameters['tenant'] ?? $parameters['environment'] ?? null; + + if ($parameterEnvironment instanceof ManagedEnvironment) { + return $parameterEnvironment; + } + + if ($tenant instanceof ManagedEnvironment) { + return $tenant; + } + + $routeEnvironment = static::resolveRouteOwnedEnvironment(); + + if ($routeEnvironment instanceof ManagedEnvironment) { + return $routeEnvironment; + } + + $filamentTenant = Filament::getTenant(); + + return $filamentTenant instanceof ManagedEnvironment ? $filamentTenant : null; + } + + /** + * @param array $parameters + */ + private static function resolveAdminUrlWorkspace(ManagedEnvironment $environment, array $parameters): Workspace|string|int|null + { + $workspace = $parameters['workspace'] ?? null; + + if ($workspace instanceof Workspace || is_string($workspace) || is_int($workspace)) { + return $workspace; + } + + $environmentWorkspace = $environment->workspace; + + if ($environmentWorkspace instanceof Workspace) { + return $environmentWorkspace; + } + + return $environment->workspace()->first(); + } + + /** + * @param array $parameters + * @return array + */ + private static function withoutLegacyScopeQuery(array $parameters): array + { + foreach (self::LEGACY_SCOPE_QUERY_KEYS as $key) { + unset($parameters[$key]); + } + + return $parameters; + } + + private static function workspaceRouteKey(Workspace $workspace): string + { + $slug = $workspace->getAttribute('slug'); + + return is_string($slug) && $slug !== '' + ? $slug + : (string) $workspace->getKey(); + } + + private static function resolveRefererOwnedEnvironment(): ?ManagedEnvironment + { + $referer = request()->headers->get('referer'); + + if (! is_string($referer) || $referer === '') { + return null; + } + + $path = parse_url($referer, PHP_URL_PATH); + + if (! is_string($path)) { + return null; + } + + if (preg_match('#^/admin/workspaces/([^/]+)/environments/([^/]+)/baseline-compare$#', $path, $matches) !== 1) { + return null; + } + + $workspaceRouteKey = rawurldecode($matches[1]); + $environmentRouteKey = rawurldecode($matches[2]); + + $environment = ManagedEnvironment::query() + ->where('slug', $environmentRouteKey) + ->first(); + + if (! $environment instanceof ManagedEnvironment) { + return null; + } + + $workspace = $environment->workspace; + + if (! $workspace instanceof Workspace) { + $workspace = $environment->workspace()->first(); + } + + if (! $workspace instanceof Workspace) { + return null; + } + + if ($workspaceRouteKey !== static::workspaceRouteKey($workspace) && $workspaceRouteKey !== (string) $workspace->getKey()) { + return null; + } + + return $environment; + } } diff --git a/apps/platform/app/Filament/Pages/BaselineCompareMatrix.php b/apps/platform/app/Filament/Pages/BaselineCompareMatrix.php index c1576197..7e15ee4a 100644 --- a/apps/platform/app/Filament/Pages/BaselineCompareMatrix.php +++ b/apps/platform/app/Filament/Pages/BaselineCompareMatrix.php @@ -16,6 +16,7 @@ use App\Support\Baselines\BaselineCompareMatrixBuilder; use App\Support\Baselines\BaselineReasonCodes; use App\Support\Baselines\Compare\CompareStrategyRegistry; +use App\Support\ManagedEnvironmentLinks; use App\Support\Navigation\CanonicalNavigationContext; use App\Support\OperationRunLinks; use App\Support\OperationRunType; @@ -445,10 +446,7 @@ public function tenantCompareUrl(int $tenantId, ?string $subjectKey = null): ?st return null; } - return BaselineCompareLanding::getUrl( - parameters: $this->navigationContext($tenant, $subjectKey)->toQuery(), - tenant: $tenant, - ); + return ManagedEnvironmentLinks::baselineCompareUrl($tenant, $this->navigationContext($tenant, $subjectKey)->toQuery()); } public function findingUrl(int $tenantId, int $findingId, ?string $subjectKey = null): ?string diff --git a/apps/platform/app/Filament/Widgets/Dashboard/BaselineCompareNow.php b/apps/platform/app/Filament/Widgets/Dashboard/BaselineCompareNow.php index a92da0e5..c16e6e7d 100644 --- a/apps/platform/app/Filament/Widgets/Dashboard/BaselineCompareNow.php +++ b/apps/platform/app/Filament/Widgets/Dashboard/BaselineCompareNow.php @@ -4,16 +4,16 @@ namespace App\Filament\Widgets\Dashboard; -use App\Filament\Pages\BaselineCompareLanding; use App\Filament\Resources\FindingResource; use App\Models\FindingException; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Models\User; use App\Support\Auth\Capabilities; use App\Support\Baselines\BaselineCompareSummaryAssessment; use App\Support\Baselines\TenantGovernanceAggregate; use App\Support\Baselines\TenantGovernanceAggregateResolver; +use App\Support\ManagedEnvironmentLinks; use App\Support\OperationRunLinks; use App\Support\Rbac\UiTooltips; use Filament\Facades\Filament; @@ -53,7 +53,7 @@ protected function getViewData(): array return $empty; } - $tenantLandingUrl = BaselineCompareLanding::getUrl(tenant: $tenant); + $tenantLandingUrl = ManagedEnvironmentLinks::baselineCompareUrl($tenant); $operationsFollowUpCount = (int) OperationRun::query() ->where('managed_environment_id', (int) $tenant->getKey()) ->dashboardNeedsFollowUp() diff --git a/apps/platform/app/Filament/Widgets/Dashboard/NeedsAttention.php b/apps/platform/app/Filament/Widgets/Dashboard/NeedsAttention.php index 0feef38a..c762862f 100644 --- a/apps/platform/app/Filament/Widgets/Dashboard/NeedsAttention.php +++ b/apps/platform/app/Filament/Widgets/Dashboard/NeedsAttention.php @@ -4,15 +4,14 @@ namespace App\Filament\Widgets\Dashboard; -use App\Filament\Pages\BaselineCompareLanding; use App\Filament\Resources\BackupScheduleResource; use App\Filament\Resources\BackupSetResource; use App\Filament\Resources\FindingResource; use App\Filament\Resources\RestoreRunResource; use App\Models\FindingException; +use App\Models\ManagedEnvironment; use App\Models\OperationRun; use App\Models\RestoreRun; -use App\Models\ManagedEnvironment; use App\Models\User; use App\Support\Auth\Capabilities; use App\Support\BackupHealth\BackupHealthActionTarget; @@ -21,6 +20,7 @@ use App\Support\BackupHealth\TenantBackupHealthResolver; use App\Support\Baselines\TenantGovernanceAggregate; use App\Support\Baselines\TenantGovernanceAggregateResolver; +use App\Support\ManagedEnvironmentLinks; use App\Support\OperationRunLinks; use App\Support\OpsUx\ActiveRuns; use App\Support\Rbac\UiTooltips; @@ -164,7 +164,7 @@ protected function getViewData(): array 'badge' => 'Baseline', 'badgeColor' => $compareAssessment->tone, 'actionLabel' => 'Open Baseline Compare', - 'actionUrl' => BaselineCompareLanding::getUrl(tenant: $tenant), + 'actionUrl' => ManagedEnvironmentLinks::baselineCompareUrl($tenant), ]; } diff --git a/apps/platform/app/Filament/Widgets/ManagedEnvironment/BaselineCompareCoverageBanner.php b/apps/platform/app/Filament/Widgets/ManagedEnvironment/BaselineCompareCoverageBanner.php index 1c777a82..bd93bdf2 100644 --- a/apps/platform/app/Filament/Widgets/ManagedEnvironment/BaselineCompareCoverageBanner.php +++ b/apps/platform/app/Filament/Widgets/ManagedEnvironment/BaselineCompareCoverageBanner.php @@ -4,10 +4,10 @@ namespace App\Filament\Widgets\ManagedEnvironment; -use App\Filament\Pages\BaselineCompareLanding; use App\Models\ManagedEnvironment; use App\Support\Baselines\TenantGovernanceAggregate; use App\Support\Baselines\TenantGovernanceAggregateResolver; +use App\Support\ManagedEnvironmentLinks; use App\Support\OperationRunLinks; use Filament\Facades\Filament; use Filament\Widgets\Widget; @@ -36,7 +36,7 @@ protected function getViewData(): array ? OperationRunLinks::view($aggregate->stats->operationRunId, $tenant) : null; - $landingUrl = BaselineCompareLanding::getUrl(tenant: $tenant); + $landingUrl = ManagedEnvironmentLinks::baselineCompareUrl($tenant); $nextActionUrl = match ($aggregate->nextActionTarget) { 'run' => $runUrl, 'findings' => \App\Filament\Resources\FindingResource::getUrl('index', tenant: $tenant), diff --git a/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php b/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php index 343e538e..092707ec 100644 --- a/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php +++ b/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php @@ -4,7 +4,6 @@ namespace App\Support\EnvironmentDashboard; -use App\Filament\Pages\BaselineCompareLanding; use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Resources\BackupScheduleResource; use App\Filament\Resources\BackupSetResource; @@ -32,6 +31,7 @@ use App\Support\Baselines\TenantGovernanceAggregate; use App\Support\Baselines\TenantGovernanceAggregateResolver; use App\Support\Links\RequiredPermissionsLinks; +use App\Support\ManagedEnvironmentLinks; use App\Support\OperationCatalog; use App\Support\OperationRunLinks; use App\Support\OperationRunOutcome; @@ -1443,7 +1443,7 @@ private function baselineCompareAction(ManagedEnvironment $tenant, ?User $user, return $this->actionPayload( label: $label, - url: $canOpen ? BaselineCompareLanding::getUrl(tenant: $tenant) : null, + url: $canOpen ? ManagedEnvironmentLinks::baselineCompareUrl($tenant) : null, helperText: $canOpen ? null : $this->overviewText('helper_baseline_compare_requires_permissions'), ); } diff --git a/apps/platform/app/Support/ManagedEnvironmentLinks.php b/apps/platform/app/Support/ManagedEnvironmentLinks.php index 5fb82f6f..506b6f2b 100644 --- a/apps/platform/app/Support/ManagedEnvironmentLinks.php +++ b/apps/platform/app/Support/ManagedEnvironmentLinks.php @@ -2,6 +2,7 @@ namespace App\Support; +use App\Filament\Pages\BaselineCompareLanding; use App\Filament\Resources\ProviderConnectionResource; use App\Models\ManagedEnvironment; use App\Models\ProviderConnection; @@ -68,6 +69,14 @@ public static function accessScopesUrl(ManagedEnvironment $environment, array $q return self::environmentChildUrl('admin.workspace.environments.access-scopes', $environment, $query); } + /** + * @param array $query + */ + public static function baselineCompareUrl(ManagedEnvironment $environment, array $query = []): string + { + return BaselineCompareLanding::getUrl($query, panel: 'admin', tenant: $environment); + } + /** * @param array $query */ diff --git a/apps/platform/app/Support/Middleware/EnsureEnvironmentContextSelected.php b/apps/platform/app/Support/Middleware/EnsureEnvironmentContextSelected.php index cfb2f05b..8e7591b2 100644 --- a/apps/platform/app/Support/Middleware/EnsureEnvironmentContextSelected.php +++ b/apps/platform/app/Support/Middleware/EnsureEnvironmentContextSelected.php @@ -165,7 +165,7 @@ private function configureNavigationForRequest(\Filament\Panel $panel, Request $ private function isWorkspaceScopedPageWithTenant(string $path): bool { - return preg_match('#^/admin/workspaces/[^/]+/environments/[^/]+/(required-permissions|diagnostics|access-scopes)$#', $path) === 1; + return preg_match('#^/admin/workspaces/[^/]+/environments/[^/]+/(required-permissions|diagnostics|access-scopes|baseline-compare)$#', $path) === 1; } private function isLivewireUpdatePath(string $path): bool diff --git a/apps/platform/app/Support/OperationRunLinks.php b/apps/platform/app/Support/OperationRunLinks.php index 642dc84d..fce3e5db 100644 --- a/apps/platform/app/Support/OperationRunLinks.php +++ b/apps/platform/app/Support/OperationRunLinks.php @@ -2,7 +2,6 @@ namespace App\Support; -use App\Filament\Pages\BaselineCompareLanding; use App\Filament\Resources\BackupScheduleResource; use App\Filament\Resources\BackupSetResource; use App\Filament\Resources\BaselineSnapshotResource; @@ -186,7 +185,7 @@ public static function related(OperationRun $run, ?ManagedEnvironment $tenant): } if ($canonicalType === 'baseline.compare') { - $links['Drift'] = BaselineCompareLanding::getUrl(tenant: $tenant); + $links['Drift'] = ManagedEnvironmentLinks::baselineCompareUrl($tenant); } if ($canonicalType === 'baseline.capture') { diff --git a/apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php b/apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php index 58c2770f..99f2f1e1 100644 --- a/apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php +++ b/apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php @@ -4,7 +4,6 @@ namespace App\Support\Workspaces; -use App\Filament\Pages\BaselineCompareLanding; use App\Filament\Pages\ChooseEnvironment; use App\Filament\Pages\Findings\FindingsHygieneReport; use App\Filament\Pages\Findings\MyFindingsInbox; @@ -12,16 +11,16 @@ use App\Models\AlertDelivery; use App\Models\Finding; use App\Models\FindingException; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; use App\Models\ManagedEnvironmentTriageReview; +use App\Models\OperationRun; use App\Models\User; use App\Models\Workspace; use App\Services\Auth\CapabilityResolver; use App\Services\Auth\ManagedEnvironmentAccessScopeResolver; use App\Services\Auth\WorkspaceCapabilityResolver; -use App\Services\Findings\FindingAssignmentHygieneService; use App\Services\Auth\WorkspaceRoleCapabilityMap; +use App\Services\Findings\FindingAssignmentHygieneService; use App\Support\Auth\Capabilities; use App\Support\BackupHealth\TenantBackupHealthAssessment; use App\Support\BackupHealth\TenantBackupHealthResolver; @@ -30,13 +29,13 @@ use App\Support\Baselines\BaselineCompareSummaryAssessment; use App\Support\Baselines\TenantGovernanceAggregate; use App\Support\Baselines\TenantGovernanceAggregateResolver; -use App\Support\Navigation\CanonicalNavigationContext; use App\Support\ManagedEnvironmentLinks; +use App\Support\Navigation\CanonicalNavigationContext; use App\Support\OperationCatalog; use App\Support\OperationRunLinks; use App\Support\OpsUx\OperationUxPresenter; -use App\Support\PortfolioTriage\PortfolioArrivalContextToken; use App\Support\PortfolioTriage\ManagedEnvironmentTriageReviewStateResolver; +use App\Support\PortfolioTriage\PortfolioArrivalContextToken; use App\Support\Rbac\UiTooltips; use App\Support\RestoreSafety\RestoreResultAttention; use App\Support\RestoreSafety\RestoreSafetyCopy; @@ -1571,8 +1570,7 @@ private function tenantDashboardTarget( User $user, string $label = 'Open environment', ?array $arrivalState = null, - ): array - { + ): array { if (! $this->canAccessEnvironmentDashboard($user, $tenant)) { return $this->disabledDestination( kind: 'tenant_dashboard', @@ -1683,7 +1681,7 @@ private function baselineCompareTarget(ManagedEnvironment $tenant, User $user, s return $this->destination( kind: 'baseline_compare_landing', - url: BaselineCompareLanding::getUrl(tenant: $tenant), + url: ManagedEnvironmentLinks::baselineCompareUrl($tenant), label: $label, tenant: $tenant, ); diff --git a/apps/platform/tests/Feature/Filament/BaselineCompareEnvironmentRouteContractTest.php b/apps/platform/tests/Feature/Filament/BaselineCompareEnvironmentRouteContractTest.php new file mode 100644 index 00000000..166b4a6f --- /dev/null +++ b/apps/platform/tests/Feature/Filament/BaselineCompareEnvironmentRouteContractTest.php @@ -0,0 +1,124 @@ +actingAs($user); + + $url = ManagedEnvironmentLinks::baselineCompareUrl($tenant, [ + 'environment_id' => (int) $tenant->getKey(), + 'tenant' => (string) $tenant->external_id, + 'tenant_id' => (int) $tenant->getKey(), + 'managed_environment_id' => (int) $tenant->getKey(), + 'environment' => 'legacy-query-value', + 'tenant_scope' => 'selected', + 'tableFilters' => ['managed_environment_id' => ['value' => (int) $tenant->getKey()]], + 'baseline_profile_id' => 42, + 'subject_key' => 'wifi-corp-profile', + ]); + + parse_str((string) parse_url($url, PHP_URL_QUERY), $query); + + expect((string) parse_url($url, PHP_URL_PATH)) + ->toBe(sprintf( + '/admin/workspaces/%s/environments/%s/baseline-compare', + $tenant->workspace()->firstOrFail()->slug, + $tenant->getRouteKey(), + )) + ->and(array_keys($query))->not->toContain(...baselineCompareRouteContractForbiddenQueryKeys()) + ->and($query)->toMatchArray([ + 'baseline_profile_id' => '42', + 'subject_key' => 'wifi-corp-profile', + ]); +}); + +it('renders baseline compare from the route-owned environment context', function (): void { + [$user, $tenant] = createUserWithTenant(role: 'owner'); + + $this->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) + ->get(ManagedEnvironmentLinks::baselineCompareUrl($tenant)) + ->assertOk() + ->assertSeeText('Baseline Compare') + ->assertSeeText($tenant->workspace()->firstOrFail()->name) + ->assertSeeText($tenant->name) + ->assertSeeText('This environment has no baseline assignment'); +}); + +it('rejects old workspace-style baseline compare URLs and remembered environment fallback', function (): void { + [$user, $tenant] = createUserWithTenant(role: 'owner'); + + Filament::setTenant(null, true); + + $this->actingAs($user) + ->withSession([ + WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $tenant->workspace_id => (int) $tenant->getKey(), + ], + ]) + ->get('/admin/baseline-compare-landing?environment_id='.(int) $tenant->getKey()) + ->assertNotFound(); + + expect(BaselineCompareLanding::canAccess())->toBeFalse(); +}); + +it('rejects baseline compare when the workspace route and environment route disagree', function (): void { + [$user, $tenant] = createUserWithTenant(role: 'owner'); + $foreignTenant = ManagedEnvironment::factory()->active()->create(['name' => 'Foreign Environment']); + createUserWithTenant(tenant: $foreignTenant, user: $user, role: 'owner'); + + $url = sprintf( + '/admin/workspaces/%s/environments/%s/baseline-compare', + $tenant->workspace()->firstOrFail()->slug, + $foreignTenant->getRouteKey(), + ); + + $this->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) + ->get($url) + ->assertNotFound(); +}); + +it('emits the environment-owned route from the environment dashboard baseline compare action', function (): void { + [$user, $tenant] = createUserWithTenant(role: 'owner'); + + $summary = app(EnvironmentDashboardSummaryBuilder::class) + ->build($tenant, $user) + ->toArray(); + + $baselineCompareStatus = Arr::first( + $summary['governanceStatus'], + static fn (array $status): bool => ($status['key'] ?? null) === 'baseline_compare', + ); + + $url = is_array($baselineCompareStatus) ? ($baselineCompareStatus['actionUrl'] ?? null) : null; + parse_str((string) parse_url((string) $url, PHP_URL_QUERY), $query); + + expect($url)->toBe(ManagedEnvironmentLinks::baselineCompareUrl($tenant)) + ->and((string) parse_url((string) $url, PHP_URL_PATH))->toEndWith('/environments/'.$tenant->getRouteKey().'/baseline-compare') + ->and(array_keys($query))->not->toContain(...baselineCompareRouteContractForbiddenQueryKeys()); +}); diff --git a/apps/platform/tests/Feature/Filament/BaselineCompareLandingAdminTenantParityTest.php b/apps/platform/tests/Feature/Filament/BaselineCompareLandingAdminTenantParityTest.php index 030d88d8..04bb4c14 100644 --- a/apps/platform/tests/Feature/Filament/BaselineCompareLandingAdminTenantParityTest.php +++ b/apps/platform/tests/Feature/Filament/BaselineCompareLandingAdminTenantParityTest.php @@ -13,7 +13,6 @@ use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; -use Livewire\Livewire; use Tests\Feature\Baselines\Support\BaselineSubjectResolutionFixtures; uses(RefreshDatabase::class); @@ -97,7 +96,7 @@ function seedBaselineCompareLandingGapRun(\App\Models\ManagedEnvironment $tenant ]); } -it('can access baseline compare when only the remembered admin tenant is available', function (): void { +it('does not authorize baseline compare from only the remembered admin environment', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); @@ -110,7 +109,10 @@ function seedBaselineCompareLandingGapRun(\App\Models\ManagedEnvironment $tenant (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); - expect(BaselineCompareLanding::canAccess())->toBeTrue(); + expect(BaselineCompareLanding::canAccess())->toBeFalse(); + + $this->get('/admin/baseline-compare-landing') + ->assertNotFound(); }); it('renders tenant landing evidence-gap details with the same search affordances as the canonical run detail', function (): void { @@ -122,7 +124,7 @@ function seedBaselineCompareLandingGapRun(\App\Models\ManagedEnvironment $tenant seedBaselineCompareLandingGapRun($tenant); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertSee('Evidence gap details') ->assertSee('Search gap details') ->assertSee('Search by reason, type, class, outcome, action, or subject key') @@ -147,7 +149,7 @@ function seedBaselineCompareLandingGapRun(\App\Models\ManagedEnvironment $tenant seedBaselineCompareLandingGapRun($tenant); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertSee('Evidence gap details') ->assertSee('WiFi-Corp-Profile') ->assertSee('Baseline compare evidence'); diff --git a/apps/platform/tests/Feature/Filament/BaselineCompareLandingDuplicateNamesBannerTest.php b/apps/platform/tests/Feature/Filament/BaselineCompareLandingDuplicateNamesBannerTest.php index aaca7835..fb4c5bc9 100644 --- a/apps/platform/tests/Feature/Filament/BaselineCompareLandingDuplicateNamesBannerTest.php +++ b/apps/platform/tests/Feature/Filament/BaselineCompareLandingDuplicateNamesBannerTest.php @@ -1,12 +1,10 @@ now(), ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertSee(__('baseline-compare.duplicate_warning_title')) ->assertSee('share generic display names') ->assertSee('resulting in 1 ambiguous subject') @@ -139,7 +137,7 @@ 'last_seen_at' => now(), ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertDontSee(__('baseline-compare.duplicate_warning_title')) ->assertDontSee('share generic display names') ->assertDontSee('cannot match them safely to the baseline'); diff --git a/apps/platform/tests/Feature/Filament/BaselineCompareLandingRbacLabelsTest.php b/apps/platform/tests/Feature/Filament/BaselineCompareLandingRbacLabelsTest.php index 9452d324..3b779fa0 100644 --- a/apps/platform/tests/Feature/Filament/BaselineCompareLandingRbacLabelsTest.php +++ b/apps/platform/tests/Feature/Filament/BaselineCompareLandingRbacLabelsTest.php @@ -1,6 +1,5 @@ assertSee('RBAC role definitions') ->assertSee('Compared') ->assertSee('Modified') diff --git a/apps/platform/tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php b/apps/platform/tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php index 62e158b4..c583cce4 100644 --- a/apps/platform/tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php +++ b/apps/platform/tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php @@ -21,7 +21,6 @@ use Filament\Actions\Action; use Filament\Facades\Filament; use Illuminate\Support\Facades\Queue; -use Livewire\Livewire; use Tests\Feature\Baselines\Support\FakeCompareStrategy; use Tests\Feature\Baselines\Support\FakeGovernanceSubjectTaxonomyRegistry; @@ -67,7 +66,7 @@ 'baseline_profile_id' => (int) $profile->getKey(), ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertActionVisible('compareNow') ->assertActionDisabled('compareNow') ->assertDontSee('Monitoring landing') @@ -106,7 +105,7 @@ 'baseline_profile_id' => (int) $profile->getKey(), ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertActionHasLabel('compareNow', 'Compare now (full content)') ->callAction('compareNow') ->assertDispatchedTo(BulkOperationProgress::class, OpsUxBrowserEvents::RunEnqueued, tenantId: (int) $tenant->getKey()); @@ -148,7 +147,7 @@ 'baseline_profile_id' => (int) $profile->getKey(), ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertActionExists('compareNow', function (Action $action): bool { return $action->getLabel() === 'Compare now (full content)' && $action->isConfirmationRequired() @@ -184,7 +183,7 @@ 'baseline_profile_id' => (int) $profile->getKey(), ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertActionHasLabel('compareNow', 'Compare now (full content)') ->assertActionEnabled('compareNow') ->callAction('compareNow') @@ -244,7 +243,7 @@ 'baseline_profile_id' => (int) $profile->getKey(), ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->callAction('compareNow') ->assertNotified('Cannot start comparison') ->assertStatus(200); @@ -260,7 +259,7 @@ $tenant->makeCurrent(); Filament::setTenant($tenant, true); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->call('refreshStats') ->assertStatus(200); }); @@ -289,10 +288,10 @@ 'baseline_profile_id' => (int) $profile->getKey(), ]); - $component = Livewire::withQueryParams([ + $component = baselineCompareLandingLivewire($tenant, [ 'baseline_profile_id' => (int) $profile->getKey(), 'subject_key' => 'wifi-corp-profile', - ])->test(BaselineCompareLanding::class); + ]); expect($component->instance()->openCompareMatrixUrl()) ->toBe(BaselineProfileResource::compareMatrixUrl($profile).'?subject_key=wifi-corp-profile'); @@ -346,7 +345,7 @@ ], ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->call('refreshStats') ->assertSet('operationRunId', (int) $compareRun->getKey()) ->assertSet('coverageStatus', 'ok') @@ -400,7 +399,7 @@ ], ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->call('refreshStats') ->assertSet('operationRunId', (int) $compareRun->getKey()) ->assertSet('coverageStatus', 'warning') @@ -444,7 +443,7 @@ ], ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertSet('state', 'no_snapshot') ->assertSet('snapshotId', null) ->assertSet('message', 'The latest inventory sync failed, so this capture could not use a credible upstream basis.'); @@ -492,7 +491,7 @@ ], ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertSet('state', 'idle') ->assertSet('snapshotId', (int) $snapshot->getKey()) ->assertActionEnabled('compareNow'); diff --git a/apps/platform/tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php b/apps/platform/tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php index aa567a3a..1c0f34e9 100644 --- a/apps/platform/tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php +++ b/apps/platform/tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php @@ -1,6 +1,5 @@ summaryAssessment(); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertSee($summary->headline) ->assertSee('Aligned'); }); @@ -120,7 +118,7 @@ ], ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertSee('Evidence gap details') ->assertSee('Detailed rows were not recorded for this run') ->assertSee('Baseline compare evidence'); @@ -178,7 +176,7 @@ ], ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertDontSee('Evidence gap details') ->assertSee('Baseline compare evidence'); }); @@ -248,7 +246,7 @@ ], ]); - Livewire::test(BaselineCompareLanding::class) + baselineCompareLandingLivewire($tenant) ->assertSee('Baseline compare evidence') ->assertSee('intune_policy') ->assertSee('strategy_failed') diff --git a/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php b/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php index e2268528..fcf74070 100644 --- a/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php +++ b/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php @@ -363,6 +363,8 @@ function seedWorkspaceSidebarVisibleDecision(ManagedEnvironment $tenant, User $a $response->assertSeeText('Baselines'); $response->assertSeeText('Baseline Snapshots'); $response->assertSeeText('Baseline Compare'); + $response->assertSee((string) parse_url(ManagedEnvironmentLinks::baselineCompareUrl($tenant), PHP_URL_PATH), false); + $response->assertDontSee('/admin/baseline-compare-landing', false); $response->assertSeeText('Evidence'); $response->assertSeeText('Risk exceptions'); diff --git a/apps/platform/tests/Feature/Guards/ManagedEnvironmentCanonicalRouteContractTest.php b/apps/platform/tests/Feature/Guards/ManagedEnvironmentCanonicalRouteContractTest.php index c3b57f77..831f1195 100644 --- a/apps/platform/tests/Feature/Guards/ManagedEnvironmentCanonicalRouteContractTest.php +++ b/apps/platform/tests/Feature/Guards/ManagedEnvironmentCanonicalRouteContractTest.php @@ -20,6 +20,7 @@ ManagedEnvironmentLinks::requiredPermissionsUrl($tenant), ManagedEnvironmentLinks::diagnosticsUrl($tenant), ManagedEnvironmentLinks::accessScopesUrl($tenant), + ManagedEnvironmentLinks::baselineCompareUrl($tenant), ManagedEnvironmentLinks::operationsUrl($tenant), ManagedEnvironmentLinks::providerConnectionsUrl($tenant), ManagedEnvironmentResource::getUrl('index'), @@ -47,6 +48,7 @@ ->and(ManagedEnvironmentLinks::requiredPermissionsUrl($tenant))->toEndWith('/required-permissions') ->and(ManagedEnvironmentLinks::diagnosticsUrl($tenant))->toEndWith('/diagnostics') ->and(ManagedEnvironmentLinks::accessScopesUrl($tenant))->toEndWith('/access-scopes') + ->and(ManagedEnvironmentLinks::baselineCompareUrl($tenant))->toEndWith('/baseline-compare') ->and(ManagedEnvironmentLinks::providerConnectionsUrl($tenant))->toContain('/admin/provider-connections?environment_id='.(int) $tenant->getKey()) ->and(OperationRunLinks::index($tenant))->toContain('/admin/workspaces/') ->and(OperationRunLinks::tenantlessView($run))->toContain('/admin/workspaces/'); diff --git a/apps/platform/tests/Feature/Navigation/WorkspaceHubRegistryTest.php b/apps/platform/tests/Feature/Navigation/WorkspaceHubRegistryTest.php index c76a5cda..c335d1cb 100644 --- a/apps/platform/tests/Feature/Navigation/WorkspaceHubRegistryTest.php +++ b/apps/platform/tests/Feature/Navigation/WorkspaceHubRegistryTest.php @@ -41,6 +41,9 @@ ->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/workspaces/1/environments'))->toBeTrue() ->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/workspaces/1/environments/2'))->toBeFalse() ->and(WorkspaceHubRegistry::isExplicitlyExcludedPath('/admin/workspaces/1/environments/2'))->toBeTrue() + ->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/workspaces/1/environments/2/baseline-compare'))->toBeFalse() + ->and(WorkspaceHubRegistry::isExplicitlyExcludedPath('/admin/workspaces/1/environments/2/baseline-compare'))->toBeTrue() + ->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/baseline-compare-landing'))->toBeFalse() ->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/workspaces/1/environments/2/stored-reports'))->toBeFalse() ->and(WorkspaceHubRegistry::isExplicitlyExcludedPath('/admin/workspaces/1/environments/2/stored-reports'))->toBeTrue(); }); diff --git a/apps/platform/tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php b/apps/platform/tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php index a52abde1..6291c5bc 100644 --- a/apps/platform/tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php +++ b/apps/platform/tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php @@ -10,9 +10,9 @@ use App\Services\Auth\WorkspaceCapabilityResolver; use App\Support\Auth\Capabilities; use App\Support\Navigation\CanonicalNavigationContext; +use App\Support\Workspaces\WorkspaceContext; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Gate; -use Livewire\Livewire; use Tests\Feature\Concerns\BuildsBaselineCompareMatrixFixtures; uses(RefreshDatabase::class, BuildsBaselineCompareMatrixFixtures::class); @@ -69,7 +69,7 @@ ->assertNotFound(); }); -it('returns 403 for matrix tenant drilldowns when tenant view capability is missing', function (): void { +it('returns 404 for matrix tenant drilldowns when tenant view capability is missing', function (): void { $fixture = $this->makeBaselineCompareMatrixFixture(); $resolver = \Mockery::mock(CapabilityResolver::class); @@ -77,7 +77,9 @@ $resolver->shouldReceive('can')->andReturnFalse(); app()->instance(CapabilityResolver::class, $resolver); - $this->actingAs($fixture['user']); + $this->actingAs($fixture['user'])->withSession([ + WorkspaceContext::SESSION_KEY => (int) $fixture['workspace']->getKey(), + ]); $fixture['visibleTenant']->makeCurrent(); $query = CanonicalNavigationContext::forBaselineCompareMatrix( @@ -87,7 +89,7 @@ )->toQuery(); $this->get(BaselineCompareLanding::getUrl(parameters: $query, panel: 'admin', tenant: $fixture['visibleTenant'])) - ->assertForbidden(); + ->assertNotFound(); }); it('returns 403 for matrix finding drilldowns when findings view capability is missing', function (): void { @@ -132,10 +134,10 @@ $this->actingAs($fixture['user']); $fixture['visibleTenant']->makeCurrent(); - $component = Livewire::withQueryParams([ + $component = baselineCompareLandingLivewire($fixture['visibleTenant'], [ 'baseline_profile_id' => (int) $foreignProfile->getKey(), 'subject_key' => 'wifi-corp-profile', - ])->test(BaselineCompareLanding::class); + ]); expect($component->instance()->openCompareMatrixUrl()) ->toStartWith(BaselineProfileResource::compareMatrixUrl($fixture['profile'])) diff --git a/apps/platform/tests/Feature/Rbac/DriftLandingUiEnforcementTest.php b/apps/platform/tests/Feature/Rbac/DriftLandingUiEnforcementTest.php index 3845d1a2..f196f128 100644 --- a/apps/platform/tests/Feature/Rbac/DriftLandingUiEnforcementTest.php +++ b/apps/platform/tests/Feature/Rbac/DriftLandingUiEnforcementTest.php @@ -8,9 +8,13 @@ $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) - ->get(BaselineCompareLanding::getUrl(panel: 'admin').'?tenant='.urlencode((string) $tenant->external_id)) + ->get(BaselineCompareLanding::getUrl(tenant: $tenant, panel: 'admin')) ->assertOk(); + $this->actingAs($user) + ->get('/admin/baseline-compare-landing?tenant='.urlencode((string) $tenant->external_id)) + ->assertNotFound(); + $this->actingAs($user) ->get('/admin/t/'.$tenant->external_id.'/drift-landing') ->assertNotFound(); diff --git a/apps/platform/tests/Pest.php b/apps/platform/tests/Pest.php index 40fc2681..13b24f5c 100644 --- a/apps/platform/tests/Pest.php +++ b/apps/platform/tests/Pest.php @@ -1,28 +1,28 @@ \App\Support\ManagedEnvironmentLinks::baselineCompareUrl($tenant), + ]); + + if ($queryParams !== []) { + $manager = $manager->withQueryParams($queryParams); + } + + return $manager->test(\App\Filament\Pages\BaselineCompareLanding::class, [ + 'environment' => $tenant, + ]); +} + /** * @return array{0: BaselineProfile, 1: BaselineSnapshot} */ diff --git a/apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php b/apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php index 888efe00..4d6bb610 100644 --- a/apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php +++ b/apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php @@ -16,6 +16,7 @@ 'retired tenant resource detail' => ['/admin/tenants/tenant-123', AdminSurfaceScope::WorkspaceScoped], 'retired tenant panel route' => ['/admin/t/tenant-123', AdminSurfaceScope::WorkspaceScoped], 'workspace environment detail' => ['/admin/workspaces/acme/environments/tenant-123', AdminSurfaceScope::EnvironmentBound], + 'baseline compare environment route' => ['/admin/workspaces/acme/environments/tenant-123/baseline-compare', AdminSurfaceScope::EnvironmentBound], 'tenant scoped evidence detail' => ['/admin/evidence/123', AdminSurfaceScope::EnvironmentScopedEvidence], 'evidence overview' => ['/admin/evidence/overview', AdminSurfaceScope::WorkspaceWideSurface], 'customer review workspace' => ['/admin/reviews/workspace', AdminSurfaceScope::WorkspaceWideSurface],