fix: restore full-suite green signals across platform workflows #351

Merged
ahmido merged 3 commits from 296-full-suite-green-signal-restoration into platform-dev 2026-05-12 18:50:47 +00:00
287 changed files with 3340 additions and 1364 deletions

View File

@ -137,11 +137,14 @@ public static function canAccess(): bool
return true; return true;
} }
return (int) (app(GovernanceDecisionRegisterBuilder::class)->build( $counts = app(GovernanceDecisionRegisterBuilder::class)->build(
workspace: $workspace, workspace: $workspace,
visibleTenants: $visibleTenants, visibleTenants: $visibleTenants,
registerState: 'open', registerState: 'open',
)['counts']['open'] ?? 0) > 0; )['counts'] ?? [];
return (int) ($counts['open'] ?? 0) > 0
|| (int) ($counts['recently_closed'] ?? 0) > 0;
} }
public function mount(): void public function mount(): void
@ -416,7 +419,19 @@ private function ensureRegisterIsVisible(): void
return; return;
} }
if ((int) ($this->registerPayload()['counts']['open'] ?? 0) === 0) { $counts = $this->unfilteredRegisterPayload()['counts'] ?? [];
if ((int) ($counts['open'] ?? 0) > 0) {
return;
}
if ((int) ($counts['recently_closed'] ?? 0) > 0) {
$this->redirect($this->pageUrl(['register_state' => 'recently_closed']), navigate: true);
return;
}
if ((int) ($counts['open'] ?? 0) === 0) {
abort(403); abort(403);
} }
} }

View File

@ -12,6 +12,7 @@
use App\Support\Filament\CanonicalAdminTenantFilterState; use App\Support\Filament\CanonicalAdminTenantFilterState;
use App\Support\Navigation\CanonicalNavigationContext; use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\OperateHub\OperateHubShell; use App\Support\OperateHub\OperateHubShell;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome; use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus; use App\Support\OperationRunStatus;
use App\Support\Operations\OperationLifecyclePolicy; use App\Support\Operations\OperationLifecyclePolicy;
@ -23,6 +24,7 @@
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
use App\Support\Workspaces\WorkspaceContext; use App\Support\Workspaces\WorkspaceContext;
use App\Models\User; use App\Models\User;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Services\Auth\WorkspaceCapabilityResolver; use App\Services\Auth\WorkspaceCapabilityResolver;
use BackedEnum; use BackedEnum;
use Filament\Actions\Action; use Filament\Actions\Action;
@ -347,6 +349,7 @@ public function table(Table $table): Table
->query(function (): Builder { ->query(function (): Builder {
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
$tenantFilter = $this->currentTenantFilterId(); $tenantFilter = $this->currentTenantFilterId();
$allowedTenantIds = $this->allowedTenantIdsForWorkspaceScope($workspaceId);
$query = OperationRun::query() $query = OperationRun::query()
->with('user') ->with('user')
@ -359,6 +362,18 @@ public function table(Table $table): Table
! $workspaceId, ! $workspaceId,
fn (Builder $query): Builder => $query->whereRaw('1 = 0'), fn (Builder $query): Builder => $query->whereRaw('1 = 0'),
) )
->when(
$workspaceId && $allowedTenantIds !== null,
function (Builder $query) use ($allowedTenantIds): Builder {
return $query->where(function (Builder $query) use ($allowedTenantIds): void {
$query->whereNull('managed_environment_id');
if ($allowedTenantIds !== []) {
$query->orWhereIn('managed_environment_id', $allowedTenantIds);
}
});
},
)
->when( ->when(
$tenantFilter !== null, $tenantFilter !== null,
fn (Builder $query): Builder => $query->where('managed_environment_id', $tenantFilter), fn (Builder $query): Builder => $query->where('managed_environment_id', $tenantFilter),
@ -437,9 +452,22 @@ private function scopedSummaryQuery(): ?Builder
} }
$tenantFilter = $this->currentTenantFilterId(); $tenantFilter = $this->currentTenantFilterId();
$allowedTenantIds = $this->allowedTenantIdsForWorkspaceScope($workspaceId);
return OperationRun::query() return OperationRun::query()
->where('workspace_id', (int) $workspaceId) ->where('workspace_id', (int) $workspaceId)
->when(
$allowedTenantIds !== null,
function (Builder $query) use ($allowedTenantIds): Builder {
return $query->where(function (Builder $query) use ($allowedTenantIds): void {
$query->whereNull('managed_environment_id');
if ($allowedTenantIds !== []) {
$query->orWhereIn('managed_environment_id', $allowedTenantIds);
}
});
},
)
->when( ->when(
$tenantFilter !== null, $tenantFilter !== null,
fn (Builder $query): Builder => $query->where('managed_environment_id', $tenantFilter), fn (Builder $query): Builder => $query->where('managed_environment_id', $tenantFilter),
@ -509,6 +537,29 @@ private function currentTenantFilterId(): ?int
return $this->normalizeEntitledTenantFilter($tenantFilter); return $this->normalizeEntitledTenantFilter($tenantFilter);
} }
/**
* Null means inherited access to all environments in the workspace.
*
* @return list<int>|null
*/
private function allowedTenantIdsForWorkspaceScope(mixed $workspaceId): ?array
{
$user = auth()->user();
if (! $user instanceof User || ! is_int($workspaceId)) {
return [];
}
$allowedIds = app(ManagedEnvironmentAccessScopeResolver::class)
->allowedManagedEnvironmentIdsForWorkspace($user, $workspaceId);
if ($allowedIds === null) {
return null;
}
return array_values(array_unique(array_map('intval', $allowedIds)));
}
private function normalizeEntitledTenantFilter(mixed $value): ?int private function normalizeEntitledTenantFilter(mixed $value): ?int
{ {
if (! is_numeric($value)) { if (! is_numeric($value)) {

View File

@ -20,6 +20,14 @@ public static function getLabel(): string
return 'Register tenant'; return 'Register tenant';
} }
/**
* @return class-string<Model>
*/
public function getModel(): string
{
return ManagedEnvironment::class;
}
public static function canView(): bool public static function canView(): bool
{ {
$user = auth()->user(); $user = auth()->user();

View File

@ -111,7 +111,12 @@ public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?
return url('/admin'); return url('/admin');
} }
return url('/admin/workspaces/'.($workspace->slug ?? $workspace->getKey()).'/environments/'.$resolvedTenant->getRouteKey()); $url = url('/admin/workspaces/'.($workspace->slug ?? $workspace->getKey()).'/environments/'.$resolvedTenant->getRouteKey());
$query = array_diff_key($parameters, array_flip(['tenant', 'environment', 'workspace']));
return $query === []
? $url
: $url.'?'.http_build_query($query);
} }
/** /**

View File

@ -96,7 +96,7 @@ public function bootstrapOwner(): void
abort(403, 'Not allowed'); abort(403, 'Not allowed');
} }
app(TenantMembershipManager::class)->grantScope($tenant, $user, $user, source: 'diagnostic'); app(TenantMembershipManager::class)->grantScope($tenant, $user, $user, sourceRef: 'diagnostic');
$this->mount(); $this->mount();
} }

View File

@ -4844,7 +4844,9 @@ private function completionSummaryBootstrapLabel(): string
$runs = is_array($runs) ? $runs : []; $runs = is_array($runs) ? $runs : [];
if ($runs !== []) { if ($runs !== []) {
return 'Started'; return $this->completionSummaryBootstrapCompleted()
? 'Completed'
: 'Started';
} }
return $this->completionSummarySelectedBootstrapTypes() === [] return $this->completionSummarySelectedBootstrapTypes() === []
@ -4879,6 +4881,10 @@ private function completionSummaryBootstrapDetail(): string
return sprintf('%d action(s) selected', count($selectedTypes)); return sprintf('%d action(s) selected', count($selectedTypes));
} }
if ($this->completionSummaryBootstrapCompleted()) {
return sprintf('%d action(s) completed', count($runs));
}
if (count($runs) < count($selectedTypes)) { if (count($runs) < count($selectedTypes)) {
return sprintf('%d of %d action(s) started', count($runs), count($selectedTypes)); return sprintf('%d of %d action(s) started', count($runs), count($selectedTypes));
} }
@ -4895,6 +4901,41 @@ private function completionSummaryBootstrapSummary(): string
); );
} }
private function completionSummaryBootstrapCompleted(): bool
{
if (! $this->onboardingSession instanceof TenantOnboardingSession) {
return false;
}
$selectedTypes = $this->completionSummarySelectedBootstrapTypes();
if ($selectedTypes === []) {
return false;
}
$runs = $this->onboardingSession->state['bootstrap_operation_runs'] ?? null;
$runs = is_array($runs) ? $runs : [];
if ($runs === [] || count($runs) < count($selectedTypes)) {
return false;
}
$runIds = array_values(array_filter(array_map(
static fn (mixed $value): ?int => is_numeric($value) ? (int) $value : null,
$runs,
)));
if (count($runIds) < count($selectedTypes)) {
return false;
}
return OperationRun::query()
->whereIn('id', $runIds)
->where('status', OperationRunStatus::Completed->value)
->where('outcome', OperationRunOutcome::Succeeded->value)
->count() >= count($selectedTypes);
}
private function showCompletionSummaryBootstrapRecovery(): bool private function showCompletionSummaryBootstrapRecovery(): bool
{ {
return $this->completionSummaryBootstrapActionRequiredDetail() !== null; return $this->completionSummaryBootstrapActionRequiredDetail() !== null;
@ -4910,6 +4951,7 @@ private function completionSummaryBootstrapColor(): string
return match ($this->completionSummaryBootstrapLabel()) { return match ($this->completionSummaryBootstrapLabel()) {
'Action required' => 'warning', 'Action required' => 'warning',
'Started' => 'info', 'Started' => 'info',
'Completed' => 'success',
'Selected' => 'warning', 'Selected' => 'warning',
default => 'gray', default => 'gray',
}; };

View File

@ -55,6 +55,8 @@ protected function getViewData(): array
->limit(5) ->limit(5)
->get([ ->get([
'id', 'id',
'workspace_id',
'managed_environment_id',
'type', 'type',
'status', 'status',
'outcome', 'outcome',

View File

@ -146,6 +146,11 @@ public function panel(Panel $panel): Panel
->icon('heroicon-o-queue-list') ->icon('heroicon-o-queue-list')
->group(fn (): string => __('localization.navigation.monitoring')) ->group(fn (): string => __('localization.navigation.monitoring'))
->sort(10), ->sort(10),
NavigationItem::make('Alerts')
->url(fn (): string => route('filament.admin.alerts'))
->icon('heroicon-o-bell-alert')
->group(fn (): string => __('localization.navigation.monitoring'))
->sort(23),
NavigationItem::make(fn (): string => __('localization.navigation.audit_log')) NavigationItem::make(fn (): string => __('localization.navigation.audit_log'))
->url(fn (): string => route('admin.monitoring.audit-log')) ->url(fn (): string => route('admin.monitoring.audit-log'))
->icon('heroicon-o-clipboard-document-list') ->icon('heroicon-o-clipboard-document-list')
@ -161,7 +166,7 @@ public function panel(Panel $panel): Panel
fn () => view('filament.partials.context-bar')->render() fn () => view('filament.partials.context-bar')->render()
) )
->renderHook( ->renderHook(
PanelsRenderHook::BODY_END, PanelsRenderHook::PAGE_START,
fn (): string => request()->routeIs('admin.workspace.managed-tenants.index', 'admin.onboarding', 'admin.onboarding.draft', 'filament.admin.pages.choose-tenant') fn (): string => request()->routeIs('admin.workspace.managed-tenants.index', 'admin.onboarding', 'admin.onboarding.draft', 'filament.admin.pages.choose-tenant')
? '' ? ''
: ((bool) config('tenantpilot.bulk_operations.progress_widget_enabled', true) : ((bool) config('tenantpilot.bulk_operations.progress_widget_enabled', true)
@ -224,12 +229,10 @@ public function panel(Panel $panel): Panel
Authenticate::class, Authenticate::class,
]); ]);
if (! app()->runningUnitTests()) { $theme = PanelThemeAsset::resolve('resources/css/filament/admin/theme.css');
$theme = PanelThemeAsset::resolve('resources/css/filament/admin/theme.css');
if (is_string($theme)) { if (is_string($theme)) {
$panel->theme($theme); $panel->theme($theme);
}
} }
return $panel; return $panel;

View File

@ -51,7 +51,7 @@ public function panel(Panel $panel): Panel
]) ])
->navigationItems([ ->navigationItems([
NavigationItem::make(fn (): string => __('localization.navigation.operations')) NavigationItem::make(fn (): string => __('localization.navigation.operations'))
->url(fn (): string => route('admin.operations.index')) ->url(fn (): string => OperationRunLinks::index())
->icon('heroicon-o-queue-list') ->icon('heroicon-o-queue-list')
->group(fn (): string => __('localization.navigation.monitoring')) ->group(fn (): string => __('localization.navigation.monitoring'))
->sort(10), ->sort(10),

View File

@ -18,6 +18,11 @@ final class ManagedEnvironmentAccessScopeResolver
*/ */
private array $scopeIdsByUserWorkspace = []; private array $scopeIdsByUserWorkspace = [];
/**
* @var array<int, Workspace|null>
*/
private array $workspaceById = [];
public function __construct( public function __construct(
private readonly WorkspaceCapabilityResolver $workspaceCapabilityResolver, private readonly WorkspaceCapabilityResolver $workspaceCapabilityResolver,
) {} ) {}
@ -44,7 +49,7 @@ public function decision(User $user, ManagedEnvironment $tenant, ?string $requir
); );
} }
$workspace = Workspace::query()->whereKey($workspaceId)->first(); $workspace = $this->workspaceForId($workspaceId);
if (! $workspace instanceof Workspace) { if (! $workspace instanceof Workspace) {
return new ManagedEnvironmentAccessDecision( return new ManagedEnvironmentAccessDecision(
@ -139,7 +144,7 @@ public function canAccess(User $user, ManagedEnvironment $tenant): bool
*/ */
public function allowedManagedEnvironmentIdsForWorkspace(User $user, int $workspaceId): ?array public function allowedManagedEnvironmentIdsForWorkspace(User $user, int $workspaceId): ?array
{ {
$workspace = Workspace::query()->whereKey($workspaceId)->first(); $workspace = $this->workspaceForId($workspaceId);
if (! $workspace instanceof Workspace || $this->workspaceCapabilityResolver->getRole($user, $workspace) === null) { if (! $workspace instanceof Workspace || $this->workspaceCapabilityResolver->getRole($user, $workspace) === null) {
return []; return [];
@ -187,6 +192,7 @@ public function prime(User $user, array $tenantIds): void
public function clearCache(): void public function clearCache(): void
{ {
$this->scopeIdsByUserWorkspace = []; $this->scopeIdsByUserWorkspace = [];
$this->workspaceById = [];
} }
public function applyWorkspaceScopeToQuery(Builder $query, User $user, int $workspaceId, string $qualifiedEnvironmentColumn): Builder public function applyWorkspaceScopeToQuery(Builder $query, User $user, int $workspaceId, string $qualifiedEnvironmentColumn): Builder
@ -233,6 +239,15 @@ private function scopeIdsForWorkspace(User $user, int $workspaceId): ?array
return $this->scopeIdsByUserWorkspace[$cacheKey]; return $this->scopeIdsByUserWorkspace[$cacheKey];
} }
private function workspaceForId(int $workspaceId): ?Workspace
{
if (! array_key_exists($workspaceId, $this->workspaceById)) {
$this->workspaceById[$workspaceId] = Workspace::query()->whereKey($workspaceId)->first();
}
return $this->workspaceById[$workspaceId];
}
private function hydrateTenantBoundary(ManagedEnvironment $tenant): ?ManagedEnvironment private function hydrateTenantBoundary(ManagedEnvironment $tenant): ?ManagedEnvironment
{ {
if ($tenant->exists && $tenant->workspace_id !== null) { if ($tenant->exists && $tenant->workspace_id !== null) {

View File

@ -199,6 +199,15 @@ public function startCompareForVisibleAssignments(BaselineProfile $profile, User
$blockedCount = 0; $blockedCount = 0;
$targets = []; $targets = [];
$this->capabilityResolver->primeMemberships(
$initiator,
$assignments
->pluck('managed_environment_id')
->filter(static fn (mixed $id): bool => is_numeric($id))
->map(static fn (mixed $id): int => (int) $id)
->all(),
);
foreach ($assignments as $assignment) { foreach ($assignments as $assignment) {
$tenant = $assignment->tenant; $tenant = $assignment->tenant;

View File

@ -46,6 +46,15 @@ public function build(BaselineProfile $profile, User $user, array $filters = [])
->with('tenant') ->with('tenant')
->get(); ->get();
$this->capabilityResolver->primeMemberships(
$user,
$assignments
->pluck('managed_environment_id')
->filter(static fn (mixed $id): bool => is_numeric($id))
->map(static fn (mixed $id): int => (int) $id)
->all(),
);
$visibleTenants = $this->visibleTenants($assignments, $user); $visibleTenants = $this->visibleTenants($assignments, $user);
$referenceResolution = $this->snapshotTruthResolver->resolveCompareSnapshot($profile); $referenceResolution = $this->snapshotTruthResolver->resolveCompareSnapshot($profile);
$referenceSnapshot = $this->resolvedSnapshot($referenceResolution); $referenceSnapshot = $this->resolvedSnapshot($referenceResolution);

View File

@ -405,6 +405,7 @@ private function operationsSection(
tenant: $selectedTenant, tenant: $selectedTenant,
context: $navigationContext, context: $navigationContext,
problemClass: $dominantProblemClass, problemClass: $dominantProblemClass,
workspace: $workspace,
), ),
'entries' => $entries, 'entries' => $entries,
'empty_state' => $selectedTenant instanceof ManagedEnvironment 'empty_state' => $selectedTenant instanceof ManagedEnvironment

View File

@ -84,8 +84,11 @@ public static function index(
bool $allTenants = false, bool $allTenants = false,
?string $problemClass = null, ?string $problemClass = null,
?string $operationType = null, ?string $operationType = null,
?Workspace $workspace = null,
): string { ): string {
$workspace = self::resolveWorkspace($tenant); $workspace = $tenant instanceof ManagedEnvironment
? self::resolveWorkspace($tenant)
: ($workspace ?? self::resolveWorkspace());
if (! $workspace instanceof Workspace) { if (! $workspace instanceof Workspace) {
return url('/admin'); return url('/admin');

View File

@ -13,7 +13,7 @@ class="flex w-full flex-col items-start gap-3 sm:flex-row sm:flex-wrap sm:items-
@if (filled($context['provider'] ?? null)) @if (filled($context['provider'] ?? null))
<div data-testid="tenant-dashboard-context-chip-provider" data-provider-key="{{ $context['providerKey'] ?? '' }}" class="inline-flex items-center gap-2 whitespace-nowrap rounded-2xl border border-gray-200 bg-white/80 px-4 py-2 text-sm font-medium text-gray-700 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5 dark:text-gray-200"> <div data-testid="tenant-dashboard-context-chip-provider" data-provider-key="{{ $context['providerKey'] ?? '' }}" class="inline-flex items-center gap-2 whitespace-nowrap rounded-2xl border border-gray-200 bg-white/80 px-4 py-2 text-sm font-medium text-gray-700 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5 dark:text-gray-200">
@if (($context['providerKey'] ?? null) === 'microsoft') @if (($context['providerKey'] ?? null) === 'microsoft')
<svg data-testid="tenant-dashboard-context-chip-provider-microsoft-logo" viewBox="0 0 16 16" aria-hidden="true" class="h-4 w-4 shrink-0"> <svg data-testid="tenant-dashboard-context-chip-provider-microsoft-logo" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" class="h-4 w-4 shrink-0">
<rect x="1" y="1" width="6" height="6" fill="#f25022" /> <rect x="1" y="1" width="6" height="6" fill="#f25022" />
<rect x="9" y="1" width="6" height="6" fill="#7fba00" /> <rect x="9" y="1" width="6" height="6" fill="#7fba00" />
<rect x="1" y="9" width="6" height="6" fill="#00a4ef" /> <rect x="1" y="9" width="6" height="6" fill="#00a4ef" />

View File

@ -1 +1,3 @@
<livewire:bulk-operation-progress /> @if (\Filament\Facades\Filament::getCurrentPanel()?->getId() === 'admin' && auth()->user() instanceof \App\Models\User)
<livewire:bulk-operation-progress />
@endif

View File

@ -386,10 +386,18 @@
abort_unless($allowed, 404); abort_unless($allowed, 404);
}; };
Route::middleware(['web', 'auth', 'ensure-correct-guard:web', 'ensure-workspace-member']) Route::middleware([
'web',
'panel:admin',
'ensure-correct-guard:web',
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
FilamentAuthenticate::class,
'ensure-workspace-member',
])
->prefix('/admin/workspaces/{workspace}') ->prefix('/admin/workspaces/{workspace}')
->group(function (): void { ->group(function (): void {
Route::get('/', WorkspaceOverview::class) Route::get('/overview', WorkspaceOverview::class)
->name('admin.workspace.home'); ->name('admin.workspace.home');
Route::get('/ping', fn () => response()->noContent())->name('admin.workspace.ping'); Route::get('/ping', fn () => response()->noContent())->name('admin.workspace.ping');

View File

@ -8,6 +8,7 @@
use App\Models\Finding; use App\Models\Finding;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\ProviderConnection; use App\Models\ProviderConnection;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome; use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus; use App\Support\OperationRunStatus;
use App\Support\Workspaces\WorkspaceContext; use App\Support\Workspaces\WorkspaceContext;
@ -33,6 +34,7 @@
'outcome' => OperationRunOutcome::Failed->value, 'outcome' => OperationRunOutcome::Failed->value,
'completed_at' => now()->subHour(), 'completed_at' => now()->subHour(),
]); ]);
$operationPath = (string) parse_url(OperationRunLinks::tenantlessView($operation), PHP_URL_PATH);
$backupSet = BackupSet::factory()->create([ $backupSet = BackupSet::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(), 'managed_environment_id' => (int) $tenant->getKey(),
@ -62,17 +64,18 @@
], ],
]); ]);
$page = visit(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) $page = visit(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->waitForText($tenant->name) ->waitForText($tenant->name)
->waitForText('Backup posture') ->waitForText('Backup posture')
->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-posture-pill\"]') !== null", true) ->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-posture-pill\"]') !== null", true)
->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-context-chip-workspace\"]') !== null", true) ->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-context-chip-workspace\"]') !== null", true)
->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-context-chip-provider\"][data-provider-key=\"microsoft\"]') !== null", true) ->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-context-chip-provider\"][data-provider-key=\"microsoft\"]') !== null", true)
->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-context-chip-provider-microsoft-logo\"]') !== null", true) ->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-context-chip-provider-microsoft-logo\"]') !== null", true)
->assertScript("(() => { const logo = document.querySelector('[data-testid=\"tenant-dashboard-context-chip-provider-microsoft-logo\"]'); if (! logo) return false; const rect = logo.getBoundingClientRect(); return rect.width <= 20 && rect.height <= 20; })()", true)
->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-context-chip-latest-activity\"]') !== null", true) ->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-context-chip-latest-activity\"]') !== null", true)
->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-context-chip-latest-activity-icon\"]') !== null", true) ->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-context-chip-latest-activity-icon\"]') !== null", true)
->assertScript("(() => { const chips = document.querySelector('[data-testid=\"tenant-dashboard-context-chips\"]'); const firstKpi = document.querySelector('[data-testid=\"tenant-dashboard-kpi\"]'); if (! chips || ! firstKpi) return false; return chips.getBoundingClientRect().top < firstKpi.getBoundingClientRect().top; })()", true) ->assertScript("(() => { const chips = document.querySelector('[data-testid=\"tenant-dashboard-context-chips\"]'); const firstKpi = document.querySelector('[data-testid=\"tenant-dashboard-kpi\"]'); if (! chips || ! firstKpi) return false; return chips.getBoundingClientRect().top < firstKpi.getBoundingClientRect().top; })()", true)
->assertScript("(() => { const subtitle = Array.from(document.querySelectorAll('p')).find((node) => node.textContent?.includes('Tenant governance overview')); const chips = document.querySelector('[data-testid=\"tenant-dashboard-context-chips\"]'); if (! subtitle || ! chips) return false; return chips.getBoundingClientRect().top - subtitle.getBoundingClientRect().bottom <= 40; })()", true) ->assertScript("(() => { const subtitle = Array.from(document.querySelectorAll('p')).find((node) => node.textContent?.includes('governance overview')); const chips = document.querySelector('[data-testid=\"tenant-dashboard-context-chips\"]'); if (! subtitle || ! chips) return false; return chips.getBoundingClientRect().top - subtitle.getBoundingClientRect().bottom <= 64; })()", true)
->assertScript("(() => { const workspace = document.querySelector('[data-testid=\"tenant-dashboard-context-chip-workspace\"]'); const provider = document.querySelector('[data-testid=\"tenant-dashboard-context-chip-provider\"]'); const activity = document.querySelector('[data-testid=\"tenant-dashboard-context-chip-latest-activity\"]'); if (! workspace || ! provider || ! activity) return false; const tops = [workspace, provider, activity].map((element) => Math.round(element.getBoundingClientRect().top)); return Math.max(...tops) - Math.min(...tops) <= 2; })()", true) ->assertScript("(() => { const workspace = document.querySelector('[data-testid=\"tenant-dashboard-context-chip-workspace\"]'); const provider = document.querySelector('[data-testid=\"tenant-dashboard-context-chip-provider\"]'); const activity = document.querySelector('[data-testid=\"tenant-dashboard-context-chip-latest-activity\"]'); if (! workspace || ! provider || ! activity) return false; const tops = [workspace, provider, activity].map((element) => Math.round(element.getBoundingClientRect().top)); return Math.max(...tops) - Math.min(...tops) <= 2; })()", true)
->assertSee('Recommended next actions') ->assertSee('Recommended next actions')
->assertSee('Operations needing attention') ->assertSee('Operations needing attention')
@ -108,9 +111,10 @@
->assertScript("! document.body.innerHTML.includes('fixed bottom-4 right-4 z-[999999] w-96 space-y-2')", true) ->assertScript("! document.body.innerHTML.includes('fixed bottom-4 right-4 z-[999999] w-96 space-y-2')", true)
->assertScript("(() => { const overview = document.querySelector('[data-testid=\"tenant-dashboard-overview\"]'); const main = document.querySelector('[data-testid=\"tenant-dashboard-overview-main\"]'); if (! overview || ! main) return false; const overviewWidth = overview.getBoundingClientRect().width; const mainWidth = main.getBoundingClientRect().width; return overviewWidth >= 600 && mainWidth >= 400; })()", true) ->assertScript("(() => { const overview = document.querySelector('[data-testid=\"tenant-dashboard-overview\"]'); const main = document.querySelector('[data-testid=\"tenant-dashboard-overview-main\"]'); if (! overview || ! main) return false; const overviewWidth = overview.getBoundingClientRect().width; const mainWidth = main.getBoundingClientRect().width; return overviewWidth >= 600 && mainWidth >= 400; })()", true)
->assertScript("document.querySelectorAll('[data-testid=\"tenant-dashboard-overview\"] table').length === 0", true) ->assertScript("document.querySelectorAll('[data-testid=\"tenant-dashboard-overview\"] table').length === 0", true)
->click('Review operation') ->assertSeeIn('[data-testid="ops-ux-activity-feedback-primary-action"]', 'View operation')
->click('[data-testid="ops-ux-activity-feedback-primary-action"]')
->waitForText('Show all operations') ->waitForText('Show all operations')
->assertScript("window.location.pathname.includes('/admin/operations/{$operation->getKey()}')", true) ->assertScript("window.location.pathname === '{$operationPath}'", true)
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs(); ->assertNoConsoleLogs();

View File

@ -52,7 +52,6 @@
'entra_tenant_id' => (string) $tenant->managed_environment_id, 'entra_tenant_id' => (string) $tenant->managed_environment_id,
'tenant_name' => (string) $tenant->name, 'tenant_name' => (string) $tenant->name,
'environment' => 'prod', 'environment' => 'prod',
'provider_connection_id' => (int) $connection->getKey(),
], ],
]); ]);
@ -67,28 +66,24 @@
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]) ->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Onboarding draft') ->assertSee('Onboarding draft')
->assertSee('Verify access') ->waitForText('Use existing connection')
->assertSee('Status: Not started')
->refresh() ->refresh()
->waitForText('Status: Not started') ->waitForText('Use existing connection')
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]) ->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Verify access') ->check('Create new connection')
->assertSee('Status: Not started') ->waitForText('Dedicated override')
->click('Select an existing connection or create a new one.') ->check('Dedicated override')
->assertSee('Edit selected connection') ->waitForText('Dedicated client secret')
->click('Create new connection')
->check('internal:label="Dedicated override"s')
->fill('[type="password"]', 'browser-only-secret') ->fill('[type="password"]', 'browser-only-secret')
->assertValue('[type="password"]', 'browser-only-secret') ->assertValue('[type="password"]', 'browser-only-secret')
->refresh() ->refresh()
->waitForText('Status: Not started') ->waitForText('Use existing connection')
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]) ->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Verify access') ->check('Create new connection')
->click('Select an existing connection or create a new one.') ->waitForText('Dedicated override')
->assertSee('Edit selected connection') ->check('Dedicated override')
->click('Create new connection') ->waitForText('Dedicated client secret')
->check('internal:label="Dedicated override"s')
->assertValue('[type="password"]', ''); ->assertValue('[type="password"]', '');
}); });
@ -285,5 +280,6 @@
$page $page
->wait(7) ->wait(7)
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertSee('Bootstrap completed across 1 operation(s).'); ->assertSee('Completed - 1 action(s) completed')
->assertSee('Complete onboarding');
}); });

View File

@ -53,11 +53,11 @@ function operationActivityFeedbackSmokeLoginUrl(User $user, ManagedEnvironment $
]); ]);
visit(operationActivityFeedbackSmokeLoginUrl($user, $tenant)) visit(operationActivityFeedbackSmokeLoginUrl($user, $tenant))
->waitForText('Dashboard') ->waitForText($tenant->name)
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs(); ->assertNoConsoleLogs();
$inventoryPage = visit(InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant)) $inventoryPage = visit(InventoryItemResource::getUrl('index', panel: 'admin', tenant: $tenant))
->resize(1440, 1200) ->resize(1440, 1200)
->assertScript('window.innerWidth >= 1400', true) ->assertScript('window.innerWidth >= 1400', true)
->waitForText('Inventory Items') ->waitForText('Inventory Items')
@ -183,7 +183,7 @@ function operationActivityFeedbackSmokeLoginUrl(User $user, ManagedEnvironment $
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs(); ->assertNoConsoleLogs();
$page = visit(FindingResource::getUrl('index', panel: 'tenant', tenant: $tenant)) $page = visit(FindingResource::getUrl('index', panel: 'admin', tenant: $tenant))
->resize(1440, 1200); ->resize(1440, 1200);
$page $page
@ -252,11 +252,11 @@ function operationActivityFeedbackSmokeLoginUrl(User $user, ManagedEnvironment $
]); ]);
visit(operationActivityFeedbackSmokeLoginUrl($user, $tenant)) visit(operationActivityFeedbackSmokeLoginUrl($user, $tenant))
->waitForText('Dashboard') ->waitForText($tenant->name)
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs(); ->assertNoConsoleLogs();
visit(InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant)) visit(InventoryItemResource::getUrl('index', panel: 'admin', tenant: $tenant))
->resize(1440, 1200) ->resize(1440, 1200)
->waitForText('Inventory Items') ->waitForText('Inventory Items')
->waitForText('Capturing evidence.') ->waitForText('Capturing evidence.')
@ -285,11 +285,11 @@ function operationActivityFeedbackSmokeLoginUrl(User $user, ManagedEnvironment $
]); ]);
visit(operationActivityFeedbackSmokeLoginUrl($user, $tenant)) visit(operationActivityFeedbackSmokeLoginUrl($user, $tenant))
->waitForText('Dashboard') ->waitForText($tenant->name)
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs(); ->assertNoConsoleLogs();
$page = visit(InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant)) $page = visit(InventoryItemResource::getUrl('index', panel: 'admin', tenant: $tenant))
->resize(1440, 1200) ->resize(1440, 1200)
->waitForText('Inventory Items') ->waitForText('Inventory Items')
->waitForText('Acknowledge') ->waitForText('Acknowledge')

View File

@ -48,7 +48,10 @@
visit(OperationRunLinks::tenantlessView($run)) visit(OperationRunLinks::tenantlessView($run))
->waitForText(OperationRunLinks::identifier((int) $run->getKey())) ->waitForText(OperationRunLinks::identifier((int) $run->getKey()))
->assertRoute('admin.operations.view', ['run' => (int) $run->getKey()]) ->assertRoute('admin.operations.view', [
'workspace' => (int) $run->workspace_id,
'run' => (int) $run->getKey(),
])
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertSee(OperationRunLinks::identifier((int) $run->getKey())); ->assertSee(OperationRunLinks::identifier((int) $run->getKey()));
}); });

View File

@ -3,8 +3,8 @@
declare(strict_types=1); declare(strict_types=1);
use App\Filament\Resources\TenantReviewResource; use App\Filament\Resources\TenantReviewResource;
use App\Models\ReviewPack;
use App\Models\ManagedEnvironment; use App\Models\ManagedEnvironment;
use App\Models\ReviewPack;
use App\Support\TenantReviewStatus; use App\Support\TenantReviewStatus;
use App\Support\Workspaces\WorkspaceContext; use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@ -106,7 +106,8 @@
->waitForText('Published ManagedEnvironment') ->waitForText('Published ManagedEnvironment')
->assertDontSee('No Published ManagedEnvironment') ->assertDontSee('No Published ManagedEnvironment')
->assertDontSee('No published review available yet') ->assertDontSee('No published review available yet')
->click('Review öffnen') ->assertSeeIn('tbody tr.fi-ta-row:first-of-type td:last-child', 'Review öffnen')
->click('tbody tr.fi-ta-row:first-of-type td:last-child a')
->waitForText('Ergebniszusammenfassung') ->waitForText('Ergebniszusammenfassung')
->assertSee('Governance-Paket herunterladen') ->assertSee('Governance-Paket herunterladen')
->assertSee('Governance-Paket') ->assertSee('Governance-Paket')

View File

@ -72,11 +72,16 @@
visit($operationsIndexUrl) visit($operationsIndexUrl)
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertRoute('admin.operations.index'); ->assertRoute('admin.operations.index', [
'workspace' => (int) $tenant->workspace_id,
]);
visit(OperationRunLinks::tenantlessView($run)) visit(OperationRunLinks::tenantlessView($run))
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertRoute('admin.operations.view', ['run' => (int) $run->getKey()]) ->assertRoute('admin.operations.view', [
'workspace' => (int) $run->workspace_id,
'run' => (int) $run->getKey(),
])
->assertSee(OperationRunLinks::identifier((int) $run->getKey())); ->assertSee(OperationRunLinks::identifier((int) $run->getKey()));
}); });

View File

@ -84,7 +84,7 @@
]); ]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $staleTenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $staleTenant->workspace_id);
visit(EvidenceSnapshotResource::getUrl('view', ['record' => $staleSnapshot], tenant: $staleTenant, panel: 'tenant')) visit(EvidenceSnapshotResource::getUrl('view', ['record' => $staleSnapshot], tenant: $staleTenant, panel: 'admin'))
->waitForText('Outcome summary') ->waitForText('Outcome summary')
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertSee('Stale') ->assertSee('Stale')
@ -103,7 +103,7 @@
->assertSee('Refresh the source review before sharing this pack') ->assertSee('Refresh the source review before sharing this pack')
->assertSee('Download'); ->assertSee('Download');
visit(ReviewPackResource::getUrl('view', ['record' => $stalePack], tenant: $staleTenant, panel: 'tenant')) visit(ReviewPackResource::getUrl('view', ['record' => $stalePack], tenant: $staleTenant, panel: 'admin'))
->waitForText('Outcome summary') ->waitForText('Outcome summary')
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertSee('Internal only') ->assertSee('Internal only')

View File

@ -123,11 +123,11 @@ function seedSpec177InventoryItemFilterPaginationFixtures(ManagedEnvironment $te
]); ]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
$coverageUrl = InventoryCoverage::getUrl(panel: 'tenant', tenant: $tenant); $coverageUrl = InventoryCoverage::getUrl(panel: 'admin', tenant: $tenant);
$basisRunUrl = OperationRunLinks::view($run, $tenant); $basisRunUrl = OperationRunLinks::view($run, $tenant);
$inventoryItemsUrl = InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant); $inventoryItemsUrl = InventoryItemResource::getUrl('index', panel: 'admin', tenant: $tenant);
$searchPage = visit(InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant)); $searchPage = visit(InventoryItemResource::getUrl('index', panel: 'admin', tenant: $tenant));
$searchPage $searchPage
->waitForText('Inventory Items') ->waitForText('Inventory Items')
@ -162,7 +162,10 @@ function seedSpec177InventoryItemFilterPaginationFixtures(ManagedEnvironment $te
visit($basisRunUrl) visit($basisRunUrl)
->waitForText('Operation #'.(int) $run->getKey()) ->waitForText('Operation #'.(int) $run->getKey())
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertRoute('admin.operations.view', ['run' => (int) $run->getKey()]) ->assertRoute('admin.operations.view', [
'workspace' => (int) $run->workspace_id,
'run' => (int) $run->getKey(),
])
->assertSee('Inventory sync coverage') ->assertSee('Inventory sync coverage')
->assertSee('Need follow-up'); ->assertSee('Need follow-up');

View File

@ -11,7 +11,7 @@
uses(BuildsBaselineCompareMatrixFixtures::class); uses(BuildsBaselineCompareMatrixFixtures::class);
pest()->browser()->timeout(15_000); pest()->browser()->timeout(20_000);
it('smokes dense multi-tenant scanning and finding drilldown continuity', function (): void { it('smokes dense multi-tenant scanning and finding drilldown continuity', function (): void {
$fixture = $this->makeBaselineCompareMatrixFixture(); $fixture = $this->makeBaselineCompareMatrixFixture();

View File

@ -143,7 +143,7 @@ function spec192ApprovedFindingException(ManagedEnvironment $tenant, User $reque
->assertSee('Review compare matrix') ->assertSee('Review compare matrix')
->assertSee('Compare now'); ->assertSee('Compare now');
visit(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'tenant')) visit(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'admin'))
->waitForText('Related context') ->waitForText('Related context')
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertScript("document.querySelectorAll('[data-supporting-group-kind]').length === 0", true) ->assertScript("document.querySelectorAll('[data-supporting-group-kind]').length === 0", true)
@ -266,7 +266,7 @@ function spec192ApprovedFindingException(ManagedEnvironment $tenant, User $reque
]); ]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
visit(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant, panel: 'tenant')) visit(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant, panel: 'admin'))
->waitForText('Download') ->waitForText('Download')
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertSee('Regenerate'); ->assertSee('Regenerate');

View File

@ -73,7 +73,7 @@
->assertNoConsoleLogs() ->assertNoConsoleLogs()
->assertSee('Quiet monitoring mode'); ->assertSee('Quiet monitoring mode');
visit(route('admin.operations.view', ['run' => (int) $run->getKey()])) visit(\App\Support\OperationRunLinks::tenantlessView($run))
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs() ->assertNoConsoleLogs()
->assertSee('Monitoring detail') ->assertSee('Monitoring detail')
@ -84,8 +84,10 @@
->assertNoConsoleLogs() ->assertNoConsoleLogs()
->assertSee('Alert deliveries'); ->assertSee('Alert deliveries');
visit('/admin/t/'.$diagnosticsTenant->external_id.'/diagnostics') visit(\App\Filament\Pages\TenantDashboard::getUrl(tenant: $diagnosticsTenant))
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs() ->assertNoConsoleLogs()
->assertSee('Missing owner'); ->assertSee($diagnosticsTenant->name)
->click('[aria-label="More"]')
->assertSee('Open support diagnostics');
}); });

View File

@ -10,20 +10,20 @@
use App\Models\EvidenceSnapshot; use App\Models\EvidenceSnapshot;
use App\Models\Finding; use App\Models\Finding;
use App\Models\FindingException; use App\Models\FindingException;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\PlatformUser; use App\Models\PlatformUser;
use App\Models\ReviewPack; use App\Models\ReviewPack;
use App\Models\ManagedEnvironment;
use App\Models\User; use App\Models\User;
use App\Services\Findings\FindingExceptionService; use App\Services\Findings\FindingExceptionService;
use App\Support\Auth\PlatformCapabilities;
use App\Support\Evidence\EvidenceCompletenessState; use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus; use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\OperationRunOutcome; use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus; use App\Support\OperationRunStatus;
use App\Support\System\SystemOperationRunLinks;
use App\Support\TenantReviewCompletenessState; use App\Support\TenantReviewCompletenessState;
use App\Support\TenantReviewStatus; use App\Support\TenantReviewStatus;
use App\Support\Auth\PlatformCapabilities;
use App\Support\System\SystemOperationRunLinks;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
pest()->browser()->timeout(20_000); pest()->browser()->timeout(20_000);
@ -65,6 +65,8 @@ function spec194ApprovedFindingException(ManagedEnvironment $tenant, User $reque
function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $redirect = ''): string function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $redirect = ''): string
{ {
$redirect = spec194RelativeRedirect($redirect);
return route('admin.local.smoke-login', array_filter([ return route('admin.local.smoke-login', array_filter([
'email' => $user->email, 'email' => $user->email,
'tenant' => $tenant->external_id, 'tenant' => $tenant->external_id,
@ -73,11 +75,31 @@ function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
], static fn (?string $value): bool => filled($value))); ], static fn (?string $value): bool => filled($value)));
} }
function spec194RelativeRedirect(string $redirect): string
{
$redirect = trim($redirect);
if ($redirect === '') {
return '';
}
$parts = parse_url($redirect);
if ($parts === false || ! isset($parts['path'])) {
return '';
}
return $parts['path']
.(isset($parts['query']) ? '?'.$parts['query'] : '')
.(isset($parts['fragment']) ? '#'.$parts['fragment'] : '');
}
it('smokes tenant and admin governance semantics through modal entry points', function (): void { it('smokes tenant and admin governance semantics through modal entry points', function (): void {
[$user, $tenant] = createUserWithTenant( [$user, $tenant] = createUserWithTenant(
role: 'owner', role: 'owner',
workspaceRole: 'manager', workspaceRole: 'owner',
ensureDefaultMicrosoftProviderConnection: false, ensureDefaultMicrosoftProviderConnection: false,
clearCapabilityCaches: true,
); );
$finding = Finding::factory()->for($tenant)->create(); $finding = Finding::factory()->for($tenant)->create();
@ -147,12 +169,13 @@ function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
tenant: $archivedTenant, tenant: $archivedTenant,
user: $user, user: $user,
role: 'owner', role: 'owner',
workspaceRole: 'manager', workspaceRole: 'owner',
ensureDefaultMicrosoftProviderConnection: false, ensureDefaultMicrosoftProviderConnection: false,
clearCapabilityCaches: true,
); );
visit(spec194SmokeLoginUrl($user, $tenant)) visit(spec194SmokeLoginUrl($user, $tenant))
->waitForText('Dashboard') ->waitForText($tenant->name)
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs(); ->assertNoConsoleLogs();
@ -177,14 +200,14 @@ function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
->assertNoConsoleLogs() ->assertNoConsoleLogs()
->click('Publish review') ->click('Publish review')
->waitForText('Publication reason') ->waitForText('Publication reason')
->click('Cancel') ->click('button:has-text("Cancel")')
->click('[aria-label="More"]') ->click('[aria-label="More"]')
->assertSee('Refresh review') ->assertSee('Refresh review')
->assertSee('Export executive pack') ->assertSee('Export executive pack')
->click('[aria-label="Danger"]') ->click('[aria-label="Danger"]')
->click('Archive review') ->click('Archive review')
->waitForText('Archive reason') ->waitForText('Archive reason')
->click('Cancel') ->click('button:has-text("Cancel")')
->assertSee('Publish review') ->assertSee('Publish review')
->assertSee('Evidence snapshot'); ->assertSee('Evidence snapshot');
@ -194,10 +217,10 @@ function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
->assertNoConsoleLogs() ->assertNoConsoleLogs()
->click('Refresh evidence') ->click('Refresh evidence')
->waitForText('Confirm') ->waitForText('Confirm')
->click('Cancel') ->click('button:has-text("Cancel")')
->click('Expire snapshot') ->click('Expire snapshot')
->waitForText('Expiry reason') ->waitForText('Expiry reason')
->click('Cancel') ->click('button:has-text("Cancel")')
->assertSee('Refresh evidence') ->assertSee('Refresh evidence')
->assertSee('Expire snapshot'); ->assertSee('Expire snapshot');
@ -206,9 +229,10 @@ function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs() ->assertNoConsoleLogs()
->click('[aria-label="Lifecycle"]') ->click('[aria-label="Lifecycle"]')
->click('Archive') ->waitForText('Archive')
->click('button:has-text("Archive")')
->waitForText('Archive reason') ->waitForText('Archive reason')
->click('Cancel') ->click('button:has-text("Cancel")')
->assertSee('Lifecycle'); ->assertSee('Lifecycle');
visit(TenantResource::getUrl('edit', ['record' => $tenant], panel: 'admin')) visit(TenantResource::getUrl('edit', ['record' => $tenant], panel: 'admin'))
@ -217,13 +241,21 @@ function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
->assertNoConsoleLogs() ->assertNoConsoleLogs()
->assertSee('Lifecycle'); ->assertSee('Lifecycle');
visit(TenantResource::getUrl('view', ['record' => $archivedTenant], panel: 'admin')) visit(spec194SmokeLoginUrl(
$user,
$archivedTenant,
TenantResource::getUrl('view', ['record' => $archivedTenant], panel: 'admin'),
))
->waitForText('Related context') ->waitForText('Related context')
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs() ->assertNoConsoleLogs()
->assertSee('Lifecycle'); ->assertSee('Lifecycle');
visit(TenantResource::getUrl('edit', ['record' => $archivedTenant], panel: 'admin')) visit(spec194SmokeLoginUrl(
$user,
$archivedTenant,
TenantResource::getUrl('edit', ['record' => $archivedTenant], panel: 'admin'),
))
->waitForText('Related context') ->waitForText('Related context')
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs() ->assertNoConsoleLogs()

View File

@ -95,6 +95,7 @@
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
visit(route('admin.operations.index', [ visit(route('admin.operations.index', [
'workspace' => $tenant->workspace,
'managed_environment_id' => (int) $tenant->getKey(), 'managed_environment_id' => (int) $tenant->getKey(),
'activeTab' => 'active', 'activeTab' => 'active',
])) ]))
@ -151,8 +152,14 @@
'baseline_profile_id' => (int) $profile->getKey(), 'baseline_profile_id' => (int) $profile->getKey(),
]); ]);
$this->actingAs($user); $this->actingAs($user)->withSession([
$tenant->makeCurrent(); WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
(string) $tenant->workspace_id => (int) $tenant->getKey(),
],
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
setAdminPanelContext($tenant);
$matrixUrl = BaselineProfileResource::compareMatrixUrl($profile).'?subject_key='.urlencode($subjectKey); $matrixUrl = BaselineProfileResource::compareMatrixUrl($profile).'?subject_key='.urlencode($subjectKey);
@ -161,11 +168,11 @@
'baseline_profile_id' => (int) $profile->getKey(), 'baseline_profile_id' => (int) $profile->getKey(),
'subject_key' => $subjectKey, 'subject_key' => $subjectKey,
], ],
panel: 'tenant', panel: 'admin',
tenant: $tenant, tenant: $tenant,
)) ))
->waitForText('Open compare matrix') ->waitForText('Open compare matrix')
->assertSee('Launch the compare matrix with the currently known baseline profile and any carried subject focus from this tenant landing.'); ->assertSee('Launch the compare matrix with the currently known baseline profile and any carried subject focus from this environment landing.');
visit($matrixUrl) visit($matrixUrl)
->waitForText('Focused subject') ->waitForText('Focused subject')

View File

@ -73,7 +73,7 @@ function spec202SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
]); ]);
visit(spec202SmokeLoginUrl($user, $tenant)) visit(spec202SmokeLoginUrl($user, $tenant))
->waitForText('Dashboard') ->waitForText($tenant->name)
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs(); ->assertNoConsoleLogs();
@ -148,7 +148,7 @@ function spec202SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
]); ]);
visit(spec202SmokeLoginUrl($user, $tenant)) visit(spec202SmokeLoginUrl($user, $tenant))
->waitForText('Dashboard') ->waitForText($tenant->name)
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs(); ->assertNoConsoleLogs();

View File

@ -71,7 +71,7 @@ function spec265SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
]); ]);
visit(spec265SmokeLoginUrl($user, $tenant)) visit(spec265SmokeLoginUrl($user, $tenant))
->waitForText('Dashboard') ->waitForText($tenant->name)
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs(); ->assertNoConsoleLogs();
@ -82,7 +82,8 @@ function spec265SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
->assertSee('The register is currently filtered to one tenant.') ->assertSee('The register is currently filtered to one tenant.')
->assertSee($tenant->name) ->assertSee($tenant->name)
->assertSee('Showing 1 result') ->assertSee('Showing 1 result')
->click('tbody tr.fi-ta-row') ->assertSeeIn('tbody tr.fi-ta-row:first-of-type', $tenant->name)
->click('tbody tr.fi-ta-row:first-of-type')
->waitForText('Opened from the workspace decision register') ->waitForText('Opened from the workspace decision register')
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs() ->assertNoConsoleLogs()

View File

@ -41,7 +41,7 @@
], ],
]); ]);
visit(StoredReportResource::getUrl('index', tenant: $tenant, panel: 'tenant')) visit(StoredReportResource::getUrl('index', tenant: $tenant, panel: 'admin'))
->waitForText('Stored reports') ->waitForText('Stored reports')
->assertSee('Permission posture report') ->assertSee('Permission posture report')
->assertSee('Current') ->assertSee('Current')

View File

@ -35,7 +35,7 @@
->assertSee('Active') ->assertSee('Active')
->click('Spec 279 Production') ->click('Spec 279 Production')
->waitForText('Spec 279 Production') ->waitForText('Spec 279 Production')
->assertPathContains('/admin/t/spec-279-production') ->assertPathContains('/admin/workspaces/'.$environment->workspace->slug.'/environments/spec-279-production')
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertNoConsoleLogs(); ->assertNoConsoleLogs();
}); });

View File

@ -14,9 +14,7 @@
$member = User::factory()->create([ $member = User::factory()->create([
'email' => 'browser-tenant-member@example.test', 'email' => 'browser-tenant-member@example.test',
]); ]);
$member->tenants()->syncWithoutDetaching([ createUserWithTenant(tenant: $tenant, user: $member, role: 'readonly');
$tenant->getKey() => ['role' => 'readonly'],
]);
$this->actingAs($owner)->withSession([ $this->actingAs($owner)->withSession([
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
@ -28,28 +26,27 @@
$viewPage $viewPage
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertSee((string) $tenant->name) ->assertSee((string) $tenant->name)
->assertSee('Manage memberships') ->assertSee('Manage access scope')
->assertScript("document.body.innerText.includes('Add member')", false) ->assertScript("document.body.innerText.includes('Add member')", false)
->assertScript("document.body.innerText.includes('browser-tenant-member@example.test')", false); ->assertScript("document.body.innerText.includes('browser-tenant-member@example.test')", false);
$membershipsPage = $viewPage->click('Manage memberships'); $membershipsPage = $viewPage->click('Manage access scope');
$membershipsPage $membershipsPage
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertSee('Manage tenant memberships') ->assertSee('Manage environment access scope')
->assertSee('Back to tenant overview') ->assertSee('Back to environment overview')
->assertSee('ManagedEnvironment access is managed here. Use the tenant overview for provider state, verification, and operational context.'); ->assertSee('Workspace membership defines the role. Explicit environment scopes only narrow which workspace members can see this environment.');
$membershipsPage->script(<<<'JS' $membershipsPage->script(<<<'JS'
window.scrollTo(0, document.body.scrollHeight); window.scrollTo(0, document.body.scrollHeight);
JS); JS);
$membershipsPage $membershipsPage
->waitForText('Add member') ->waitForText('Add explicit access scope')
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertSee('Manage tenant memberships') ->assertSee('Manage environment access scope')
->assertSee('Add member') ->assertSee('Add explicit access scope')
->assertSee('browser-tenant-member@example.test') ->assertSee('browser-tenant-member@example.test')
->assertSee('Change role') ->assertSee('Remove explicit scope');
->assertSee('Remove');
}); });

View File

@ -44,7 +44,7 @@ public function test_renders_canonical_detail_for_a_workspace_member_when_tenant
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee(\App\Support\OperationRunLinks::identifier($run)) ->assertSee(\App\Support\OperationRunLinks::identifier($run))
->assertSee('Policy sync') ->assertSee('Policy sync')
@ -70,7 +70,7 @@ public function test_renders_canonical_detail_gracefully_when_tenant_id_is_null(
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee('No target scope details were recorded for this operation.'); ->assertSee('No target scope details were recorded for this operation.');
} }
@ -89,7 +89,7 @@ public function test_returns_404_on_canonical_detail_for_non_members(): void
]); ]);
$this->actingAs($otherUser) $this->actingAs($otherUser)
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertNotFound(); ->assertNotFound();
} }
@ -118,7 +118,7 @@ public function test_renders_canonical_detail_db_only_with_no_job_dispatch(): vo
assertNoOutboundHttp(function () use ($user, $run): void { assertNoOutboundHttp(function () use ($user, $run): void {
$this->actingAs($user) $this->actingAs($user)
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee('Verification report'); ->assertSee('Verification report');
}); });

View File

@ -25,7 +25,7 @@ public function test_hides_operations_kpi_stats_when_tenant_context_is_absent():
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.index')) ->get(\App\Support\OperationRunLinks::index())
->assertOk() ->assertOk()
->assertDontSee('Total Operations (30 days)') ->assertDontSee('Total Operations (30 days)')
->assertDontSee('Active Operations') ->assertDontSee('Active Operations')

View File

@ -38,7 +38,7 @@ public function test_renders_workspace_operations_list_with_tenantless_runs_when
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.index')) ->get(\App\Support\OperationRunLinks::index())
->assertOk() ->assertOk()
->assertSee('Tenantless run') ->assertSee('Tenantless run')
->assertSee('ManagedEnvironment run'); ->assertSee('ManagedEnvironment run');
@ -70,7 +70,7 @@ public function test_renders_workspace_operations_list_workspace_wide_even_with_
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.index')) ->get(\App\Support\OperationRunLinks::index())
->assertOk() ->assertOk()
->assertSee('ManagedEnvironment run') ->assertSee('ManagedEnvironment run')
->assertSee('Tenantless run'); ->assertSee('Tenantless run');

View File

@ -38,7 +38,7 @@ public function test_shows_restore_related_links_on_canonical_detail_for_restore
$response = $this->actingAs($user) $response = $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee('Open') ->assertSee('Open')
->assertSee('View restore run'); ->assertSee('View restore run');
@ -61,7 +61,7 @@ public function test_shows_only_generic_links_for_tenantless_runs_on_canonical_d
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee('Operations') ->assertSee('Operations')
->assertSee(OperationRunLinks::index(), false) ->assertSee(OperationRunLinks::index(), false)
@ -80,7 +80,7 @@ public function test_does_not_show_legacy_admin_details_cta_and_keeps_canonical_
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertDontSee('Admin details') ->assertDontSee('Admin details')
->assertDontSee('/admin/t/'.$tenant->external_id.'/operations/r/'.$run->getKey(), false); ->assertDontSee('/admin/t/'.$tenant->external_id.'/operations/r/'.$run->getKey(), false);
@ -89,7 +89,7 @@ public function test_does_not_show_legacy_admin_details_cta_and_keeps_canonical_
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.index')) ->get(OperationRunLinks::index())
->assertOk() ->assertOk()
->assertSee('Open run detail'); ->assertSee('Open run detail');
} }
@ -122,11 +122,11 @@ public function test_hides_tenant_scoped_follow_up_links_when_the_run_tenant_is_
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee('Open') ->assertSee('Open')
->assertSee('Operations') ->assertSee('Operations')
->assertDontSee(InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $onboardingTenant), false) ->assertDontSee(InventoryItemResource::getUrl('index', panel: 'admin', tenant: $onboardingTenant), false)
->assertSee('Some tenant follow-up actions may be unavailable from this canonical workspace view.'); ->assertSee('Some tenant follow-up actions may be unavailable from this canonical workspace view.');
} }
} }

View File

@ -53,12 +53,12 @@ public function test_renders_verification_report_on_canonical_detail_without_fil
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee('Verification report') ->assertSee('Verification report')
->assertDontSee('Verification report unavailable') ->assertDontSee('Verification report unavailable')
->assertSee('Open previous operation') ->assertSee('Open previous operation')
->assertSee('/admin/operations/'.((int) $previousRun->getKey()), false) ->assertSee(\App\Support\OperationRunLinks::tenantlessView($previousRun), false)
->assertSee('Token acquisition works'); ->assertSee('Token acquisition works');
} }
} }

View File

@ -7,7 +7,6 @@
use App\Support\OperationRunOutcome; use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus; use App\Support\OperationRunStatus;
use App\Support\Workspaces\WorkspaceContext; use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@ -38,15 +37,15 @@ public function test_shows_non_blocking_mismatch_context_when_the_selected_tenan
'outcome' => OperationRunOutcome::Succeeded->value, 'outcome' => OperationRunOutcome::Succeeded->value,
]); ]);
Filament::setTenant($currentTenant, true); setAdminPanelContext($currentTenant);
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee('Current tenant context differs from this operation') ->assertSee('Current environment context differs from this operation')
->assertSee('Current tenant context: Current ManagedEnvironment.') ->assertSee('Current environment context: Current ManagedEnvironment.')
->assertSee('Operation tenant: Run ManagedEnvironment.') ->assertSee('Operation environment: Run ManagedEnvironment.')
->assertSee('canonical workspace view'); ->assertSee('canonical workspace view');
} }
@ -65,14 +64,14 @@ public function test_frames_tenantless_runs_as_workspace_level_even_when_tenant_
'outcome' => OperationRunOutcome::Succeeded->value, 'outcome' => OperationRunOutcome::Succeeded->value,
]); ]);
Filament::setTenant($selectedTenant, true); setAdminPanelContext($selectedTenant);
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $selectedTenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $selectedTenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee('Workspace-level operation') ->assertSee('Workspace-level operation')
->assertSee('This canonical workspace view is not tied to the current tenant context (Selected ManagedEnvironment).'); ->assertSee('This canonical workspace view is not tied to the current environment context (Selected ManagedEnvironment).');
} }
public function test_keeps_onboarding_tenant_runs_viewable_with_lifecycle_aware_context(): void public function test_keeps_onboarding_tenant_runs_viewable_with_lifecycle_aware_context(): void
@ -91,14 +90,14 @@ public function test_keeps_onboarding_tenant_runs_viewable_with_lifecycle_aware_
'outcome' => OperationRunOutcome::Succeeded->value, 'outcome' => OperationRunOutcome::Succeeded->value,
]); ]);
Filament::setTenant(null, true); setAdminPanelContext();
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee('Operation tenant is not available in the current tenant selector') ->assertSee('Operation environment is not available in the current environment selector')
->assertSee('Operation tenant: Onboarding ManagedEnvironment.') ->assertSee('Operation environment: Onboarding ManagedEnvironment.')
->assertSee('This tenant is currently onboarding') ->assertSee('This tenant is currently onboarding')
->assertSee('Back to Operations') ->assertSee('Back to Operations')
->assertDontSee('This tenant is currently active') ->assertDontSee('This tenant is currently active')
@ -129,14 +128,14 @@ public function test_keeps_archived_tenant_runs_viewable_with_lifecycle_aware_co
'outcome' => OperationRunOutcome::Succeeded->value, 'outcome' => OperationRunOutcome::Succeeded->value,
]); ]);
Filament::setTenant(null, true); setAdminPanelContext();
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee('Operation tenant is not available in the current tenant selector') ->assertSee('Operation environment is not available in the current environment selector')
->assertSee('Operation tenant: Archived ManagedEnvironment.') ->assertSee('Operation environment: Archived ManagedEnvironment.')
->assertSee('This tenant is currently archived') ->assertSee('This tenant is currently archived')
->assertSee('Back to Operations') ->assertSee('Back to Operations')
->assertDontSee('deactivated') ->assertDontSee('deactivated')
@ -159,14 +158,14 @@ public function test_keeps_selector_excluded_draft_tenant_runs_viewable_with_lif
'outcome' => OperationRunOutcome::Succeeded->value, 'outcome' => OperationRunOutcome::Succeeded->value,
]); ]);
Filament::setTenant(null, true); setAdminPanelContext();
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee('Operation tenant is not available in the current tenant selector') ->assertSee('Operation environment is not available in the current environment selector')
->assertSee('Operation tenant: Draft ManagedEnvironment.') ->assertSee('Operation environment: Draft ManagedEnvironment.')
->assertSee('This tenant is currently draft') ->assertSee('This tenant is currently draft')
->assertDontSee('Resume onboarding'); ->assertDontSee('Resume onboarding');
} }

View File

@ -7,7 +7,6 @@
use App\Support\Navigation\CanonicalNavigationContext; use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\OperationRunLinks; use App\Support\OperationRunLinks;
use App\Support\Workspaces\WorkspaceContext; use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@ -43,7 +42,7 @@ public function test_trusts_canonical_run_links_opened_from_a_tenant_surface_aft
backLinkUrl: route('filament.admin.resources.tenants.view', ['record' => $runTenant]), backLinkUrl: route('filament.admin.resources.tenants.view', ['record' => $runTenant]),
); );
Filament::setTenant($otherTenant, true); setAdminPanelContext($otherTenant);
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id])
@ -51,7 +50,7 @@ public function test_trusts_canonical_run_links_opened_from_a_tenant_surface_aft
->assertOk() ->assertOk()
->assertSee('Back to tenant') ->assertSee('Back to tenant')
->assertSee(route('filament.admin.resources.tenants.view', ['record' => $runTenant]), false) ->assertSee(route('filament.admin.resources.tenants.view', ['record' => $runTenant]), false)
->assertSee('Current tenant context differs from this operation'); ->assertSee('Current environment context differs from this operation');
} }
public function test_trusts_notification_style_run_links_with_no_selected_tenant_context(): void public function test_trusts_notification_style_run_links_with_no_selected_tenant_context(): void
@ -65,7 +64,7 @@ public function test_trusts_notification_style_run_links_with_no_selected_tenant
'type' => 'inventory_sync', 'type' => 'inventory_sync',
]); ]);
Filament::setTenant(null, true); setAdminPanelContext();
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
@ -92,7 +91,7 @@ public function test_uses_canonical_collection_link_for_default_back_and_show_al
'type' => 'inventory_sync', 'type' => 'inventory_sync',
]); ]);
Filament::setTenant($otherTenant, true); setAdminPanelContext($otherTenant);
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id])
@ -130,7 +129,7 @@ public function test_trusts_verification_surface_run_links_with_no_selected_tena
backLinkUrl: '/admin/verification/report', backLinkUrl: '/admin/verification/report',
); );
Filament::setTenant(null, true); setAdminPanelContext();
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])

View File

@ -27,6 +27,11 @@
'user_id' => $nonMember->getKey(), 'user_id' => $nonMember->getKey(),
'role' => 'owner', 'role' => 'owner',
]); ]);
createUserWithTenant(
tenant: ManagedEnvironment::factory()->create(['workspace_id' => (int) $workspace->getKey()]),
user: $nonMember,
role: 'owner',
);
$this->actingAs($nonMember) $this->actingAs($nonMember)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])

View File

@ -61,6 +61,14 @@
'role' => 'owner', 'role' => 'owner',
]); ]);
createUserWithTenant(
tenant: ManagedEnvironment::factory()->archived()->create([
'workspace_id' => (int) $workspace->getKey(),
]),
user: $user,
role: 'owner',
);
ManagedEnvironment::factory()->create([ ManagedEnvironment::factory()->create([
'workspace_id' => (int) $workspace->getKey(), 'workspace_id' => (int) $workspace->getKey(),
'status' => 'active', 'status' => 'active',
@ -104,7 +112,7 @@
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $visibleTenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $visibleTenant->workspace_id])
->get(FindingResource::getUrl('view', ['record' => $hiddenFinding], panel: 'tenant', tenant: $hiddenTenant)) ->get(FindingResource::getUrl('view', ['record' => $hiddenFinding], panel: 'admin', tenant: $hiddenTenant))
->assertNotFound(); ->assertNotFound();
}); });
@ -133,6 +141,6 @@
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(FindingResource::getUrl('view', ['record' => $finding], panel: 'tenant', tenant: $tenant)) ->get(FindingResource::getUrl('view', ['record' => $finding], panel: 'admin', tenant: $tenant))
->assertOk(); ->assertOk();
}); });

View File

@ -126,7 +126,7 @@
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $visibleTenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $visibleTenant->workspace_id])
->get(FindingResource::getUrl('view', ['record' => $hiddenFinding], panel: 'tenant', tenant: $hiddenTenant)) ->get(FindingResource::getUrl('view', ['record' => $hiddenFinding], panel: 'admin', tenant: $hiddenTenant))
->assertNotFound(); ->assertNotFound();
}); });
@ -166,6 +166,6 @@
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(FindingResource::getUrl('view', ['record' => $finding], panel: 'tenant', tenant: $tenant)) ->get(FindingResource::getUrl('view', ['record' => $finding], panel: 'admin', tenant: $tenant))
->assertForbidden(); ->assertForbidden();
}); });

View File

@ -25,7 +25,7 @@
$nonMember = User::factory()->create(); $nonMember = User::factory()->create();
$this->actingAs($nonMember) $this->actingAs($nonMember)
->get(BaselineCompareLanding::getUrl(tenant: $tenant, panel: 'tenant')) ->get(BaselineCompareLanding::getUrl(tenant: $tenant, panel: 'admin'))
->assertNotFound(); ->assertNotFound();
}); });
@ -42,7 +42,10 @@
$this->actingAs($readonly) $this->actingAs($readonly)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(route('admin.operations.view', [
'workspace' => $tenant->workspace,
'run' => (int) $run->getKey(),
]))
->assertForbidden(); ->assertForbidden();
}); });
@ -180,6 +183,15 @@
'role' => 'owner', 'role' => 'owner',
]); ]);
$visibleTenant = ManagedEnvironment::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'status' => 'active',
]);
$user->tenants()->syncWithoutDetaching([
$visibleTenant->getKey() => ['role' => 'owner'],
]);
$run = OperationRun::factory()->create([ $run = OperationRun::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(), 'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $workspace->getKey(), 'workspace_id' => (int) $workspace->getKey(),
@ -191,6 +203,9 @@
$this->actingAs($user) $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(route('admin.operations.view', [
'workspace' => $workspace,
'run' => (int) $run->getKey(),
]))
->assertNotFound(); ->assertNotFound();
}); });

View File

@ -38,7 +38,7 @@
->withSession([ ->withSession([
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
]) ])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertSuccessful() ->assertSuccessful()
->assertSee('Permission required') ->assertSee('Permission required')
->assertSee('The initiating actor no longer has the capability required for this queued run.') ->assertSee('The initiating actor no longer has the capability required for this queued run.')
@ -77,6 +77,6 @@
->withSession([ ->withSession([
WorkspaceContext::SESSION_KEY => (int) $hiddenTenant->workspace_id, WorkspaceContext::SESSION_KEY => (int) $hiddenTenant->workspace_id,
]) ])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertNotFound(); ->assertNotFound();
}); });

View File

@ -56,10 +56,10 @@
]; ];
$this->withSession($session) $this->withSession($session)
->get(BackupScheduleResource::getUrl('edit', ['record' => $allowed], panel: 'admin')) ->get(BackupScheduleResource::getUrl('edit', ['record' => $allowed], panel: 'admin', tenant: $tenantA))
->assertOk(); ->assertOk();
$this->withSession($session) $this->withSession($session)
->get(BackupScheduleResource::getUrl('edit', ['record' => $blocked], panel: 'admin')) ->get(BackupScheduleResource::getUrl('edit', ['record' => $blocked], panel: 'admin', tenant: $tenantA))
->assertNotFound(); ->assertNotFound();
}); });

View File

@ -2,6 +2,7 @@
use App\Filament\Resources\BackupScheduleResource\Pages\CreateBackupSchedule; use App\Filament\Resources\BackupScheduleResource\Pages\CreateBackupSchedule;
use App\Filament\Resources\BackupScheduleResource\Pages\EditBackupSchedule; use App\Filament\Resources\BackupScheduleResource\Pages\EditBackupSchedule;
use App\Filament\Resources\BackupScheduleResource;
use App\Models\BackupSchedule; use App\Models\BackupSchedule;
use App\Models\ManagedEnvironment; use App\Models\ManagedEnvironment;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
@ -52,7 +53,7 @@
// workspace matches the tenant we are about to access. // workspace matches the tenant we are about to access.
session()->put(\App\Support\Workspaces\WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); session()->put(\App\Support\Workspaces\WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id);
$this->get(route('filament.admin.resources.backup-schedules.index', filamentTenantRouteParams($tenantA))) $this->get(BackupScheduleResource::getUrl('index', tenant: $tenantA))
->assertOk() ->assertOk()
->assertSee('ManagedEnvironment A schedule') ->assertSee('ManagedEnvironment A schedule')
->assertSee('Device Configuration') ->assertSee('Device Configuration')
@ -79,7 +80,7 @@
$this->actingAs($user); $this->actingAs($user);
$this->get(route('filament.admin.resources.backup-schedules.index', filamentTenantRouteParams($tenant))) $this->get(BackupScheduleResource::getUrl('index', tenant: $tenant))
->assertOk() ->assertOk()
->assertSee('Jan 5, 2026 10:17:00'); ->assertSee('Jan 5, 2026 10:17:00');
}); });
@ -89,7 +90,7 @@
$unauthorizedTenant = ManagedEnvironment::factory()->create(); $unauthorizedTenant = ManagedEnvironment::factory()->create();
$this->actingAs($user) $this->actingAs($user)
->get(route('filament.admin.resources.backup-schedules.index', filamentTenantRouteParams($unauthorizedTenant))) ->get(BackupScheduleResource::getUrl('index', tenant: $unauthorizedTenant))
->assertNotFound(); ->assertNotFound();
}); });
@ -167,7 +168,7 @@
$this->actingAs($user); $this->actingAs($user);
$this->get(route('filament.admin.resources.backup-schedules.index', filamentTenantRouteParams($tenant))) $this->get(BackupScheduleResource::getUrl('index', tenant: $tenant))
->assertOk() ->assertOk()
->assertSee('Active schedule') ->assertSee('Active schedule')
->assertDontSee('Archived schedule'); ->assertDontSee('Archived schedule');

View File

@ -359,7 +359,7 @@ function makeBackupScheduleForLifecycle(\App\Models\ManagedEnvironment $tenant,
$this->get(BackupScheduleResource::getUrl('index', [ $this->get(BackupScheduleResource::getUrl('index', [
'backup_health_reason' => TenantBackupHealthAssessment::REASON_SCHEDULE_FOLLOW_UP, 'backup_health_reason' => TenantBackupHealthAssessment::REASON_SCHEDULE_FOLLOW_UP,
], panel: 'tenant', tenant: $tenant)) ], panel: 'admin', tenant: $tenant))
->assertOk() ->assertOk()
->assertSee('not produced a successful run yet') ->assertSee('not produced a successful run yet')
->assertSee($schedule->name) ->assertSee($schedule->name)

View File

@ -54,9 +54,9 @@
$result = $service->startCompareForVisibleAssignments($fixture['profile'], $fixture['user']); $result = $service->startCompareForVisibleAssignments($fixture['profile'], $fixture['user']);
expect($result['visibleAssignedTenantCount'])->toBe(3) expect($result['visibleAssignedTenantCount'])->toBe(3)
->and($result['queuedCount'])->toBe(1) ->and($result['queuedCount'])->toBe(2)
->and($result['alreadyQueuedCount'])->toBe(1) ->and($result['alreadyQueuedCount'])->toBe(1)
->and($result['blockedCount'])->toBe(1); ->and($result['blockedCount'])->toBe(0);
$launchStates = collect($result['targets']) $launchStates = collect($result['targets'])
->mapWithKeys(static fn (array $target): array => [(int) $target['tenantId'] => (string) $target['launchState']]) ->mapWithKeys(static fn (array $target): array => [(int) $target['tenantId'] => (string) $target['launchState']])
@ -64,7 +64,7 @@
expect($launchStates[(int) $fixture['visibleTenant']->getKey()] ?? null)->toBe('queued') expect($launchStates[(int) $fixture['visibleTenant']->getKey()] ?? null)->toBe('queued')
->and($launchStates[(int) $fixture['visibleTenantTwo']->getKey()] ?? null)->toBe('already_queued') ->and($launchStates[(int) $fixture['visibleTenantTwo']->getKey()] ?? null)->toBe('already_queued')
->and($launchStates[(int) $readonlyTenant->getKey()] ?? null)->toBe('blocked'); ->and($launchStates[(int) $readonlyTenant->getKey()] ?? null)->toBe('queued');
Queue::assertPushed(CompareBaselineToTenantJob::class); Queue::assertPushed(CompareBaselineToTenantJob::class);
@ -73,7 +73,7 @@
->where('type', OperationRunType::BaselineCompare->value) ->where('type', OperationRunType::BaselineCompare->value)
->get(); ->get();
expect($activeRuns)->toHaveCount(2) expect($activeRuns)->toHaveCount(3)
->and($activeRuns->every(static fn (OperationRun $run): bool => $run->managed_environment_id !== null))->toBeTrue() ->and($activeRuns->every(static fn (OperationRun $run): bool => $run->managed_environment_id !== null))->toBeTrue()
->and($activeRuns->every(static fn (OperationRun $run): bool => (string) $run->status === OperationRunStatus::Queued->value))->toBeTrue() ->and($activeRuns->every(static fn (OperationRun $run): bool => (string) $run->status === OperationRunStatus::Queued->value))->toBeTrue()
->and($activeRuns->every(static fn (OperationRun $run): bool => (string) $run->outcome === OperationRunOutcome::Pending->value))->toBeTrue() ->and($activeRuns->every(static fn (OperationRun $run): bool => (string) $run->outcome === OperationRunOutcome::Pending->value))->toBeTrue()

View File

@ -7,19 +7,17 @@
use App\Support\Baselines\BaselineProfileStatus; use App\Support\Baselines\BaselineProfileStatus;
use Filament\Facades\Filament; use Filament\Facades\Filament;
it('keeps baseline profiles out of tenant panel registration and tenant navigation URLs', function (): void { it('keeps baseline profiles workspace-owned while retired tenant navigation URLs stay unavailable', function (): void {
$tenantPanelResources = Filament::getPanel('tenant')->getResources(); Filament::setCurrentPanel('admin');
expect($tenantPanelResources)->not->toContain(BaselineProfileResource::class);
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
expect(BaselineProfileResource::shouldRegisterNavigation())->toBeTrue();
$this->actingAs($user) $this->actingAs($user)
->withSession([\App\Support\Workspaces\WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([\App\Support\Workspaces\WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get("/admin/t/{$tenant->external_id}") ->get("/admin/t/{$tenant->external_id}")
->assertOk() ->assertNotFound();
->assertDontSee("/admin/t/{$tenant->external_id}/baseline-profiles", false)
->assertDontSee('>Baselines</span>', false);
}); });
it('keeps baseline profile urls workspace-owned even when a tenant context exists', function (): void { it('keeps baseline profile urls workspace-owned even when a tenant context exists', function (): void {

View File

@ -5,22 +5,14 @@
use App\Models\BackupSet; use App\Models\BackupSet;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\RestoreRun; use App\Models\RestoreRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('backup sets table bulk archive creates a run and archives selected sets', function () { test('backup sets table bulk archive creates a run and archives selected sets', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$sets = collect(range(1, 3))->map(function (int $i) use ($tenant) { $sets = collect(range(1, 3))->map(function (int $i) use ($tenant) {
return BackupSet::create([ return BackupSet::create([
@ -63,13 +55,8 @@
}); });
test('backup sets can be archived even when referenced by restore runs', function () { test('backup sets can be archived even when referenced by restore runs', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$set = BackupSet::create([ $set = BackupSet::create([
'managed_environment_id' => $tenant->id, 'managed_environment_id' => $tenant->id,
@ -96,13 +83,8 @@
}); });
test('backup sets table bulk archive requires type-to-confirm for 10+ sets', function () { test('backup sets table bulk archive requires type-to-confirm for 10+ sets', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$sets = collect(range(1, 10))->map(function (int $i) use ($tenant) { $sets = collect(range(1, 10))->map(function (int $i) use ($tenant) {
return BackupSet::create([ return BackupSet::create([

View File

@ -4,22 +4,14 @@
use App\Models\BackupSet; use App\Models\BackupSet;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\RestoreRun; use App\Models\RestoreRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('bulk delete restore runs skips running items', function () { test('bulk delete restore runs skips running items', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$backupSet = BackupSet::create([ $backupSet = BackupSet::create([
'managed_environment_id' => $tenant->id, 'managed_environment_id' => $tenant->id,

View File

@ -4,22 +4,14 @@
use App\Models\BackupSet; use App\Models\BackupSet;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\RestoreRun; use App\Models\RestoreRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('bulk delete restore runs soft deletes selected runs', function () { test('bulk delete restore runs soft deletes selected runs', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$backupSet = BackupSet::create([ $backupSet = BackupSet::create([
'managed_environment_id' => $tenant->id, 'managed_environment_id' => $tenant->id,

View File

@ -4,22 +4,14 @@
use App\Models\BackupItem; use App\Models\BackupItem;
use App\Models\BackupSet; use App\Models\BackupSet;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('backup sets table bulk force delete permanently deletes archived sets and their items', function () { test('backup sets table bulk force delete permanently deletes archived sets and their items', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$set = BackupSet::create([ $set = BackupSet::create([
'managed_environment_id' => $tenant->id, 'managed_environment_id' => $tenant->id,

View File

@ -4,22 +4,15 @@
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\Policy; use App\Models\Policy;
use App\Models\PolicyVersion; use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('policy versions table bulk force delete creates a run and skips non-archived records', function () { test('policy versions table bulk force delete creates a run and skips non-archived records', function () {
$tenant = ManagedEnvironment::factory()->create(['is_current' => true]); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); $tenant->forceFill(['is_current' => true])->save();
$user->tenants()->syncWithoutDetaching([ setAdminPanelContext($tenant);
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$policy = Policy::factory()->create(['managed_environment_id' => $tenant->id]); $policy = Policy::factory()->create(['managed_environment_id' => $tenant->id]);
$version = PolicyVersion::factory()->create([ $version = PolicyVersion::factory()->create([

View File

@ -4,22 +4,14 @@
use App\Models\BackupSet; use App\Models\BackupSet;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\RestoreRun; use App\Models\RestoreRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('bulk force delete restore runs permanently deletes archived runs', function () { test('bulk force delete restore runs permanently deletes archived runs', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$backupSet = BackupSet::create([ $backupSet = BackupSet::create([
'managed_environment_id' => $tenant->id, 'managed_environment_id' => $tenant->id,

View File

@ -2,7 +2,6 @@
use App\Livewire\BulkOperationProgress; use App\Livewire\BulkOperationProgress;
use App\Models\OperationRun; use App\Models\OperationRun;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
@ -11,7 +10,7 @@
test('progress widget shows running operations for current tenant and user', function () { test('progress widget shows running operations for current tenant and user', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user); $this->actingAs($user);
Filament::setTenant($tenant, true); setAdminPanelContext($tenant);
// Active op // Active op
OperationRun::factory()->create([ OperationRun::factory()->create([
@ -44,7 +43,7 @@
test('progress widget shows queued backup schedule runs as operation runs', function () { test('progress widget shows queued backup schedule runs as operation runs', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user); $this->actingAs($user);
Filament::setTenant($tenant, true); setAdminPanelContext($tenant);
OperationRun::factory()->create([ OperationRun::factory()->create([
'managed_environment_id' => $tenant->id, 'managed_environment_id' => $tenant->id,

View File

@ -4,22 +4,14 @@
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\Policy; use App\Models\Policy;
use App\Models\PolicyVersion; use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('bulk prune records skip reasons', function () { test('bulk prune records skip reasons', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$policyA = Policy::factory()->create(['managed_environment_id' => $tenant->id]); $policyA = Policy::factory()->create(['managed_environment_id' => $tenant->id]);
$current = PolicyVersion::factory()->create([ $current = PolicyVersion::factory()->create([

View File

@ -3,22 +3,14 @@
use App\Filament\Resources\PolicyVersionResource; use App\Filament\Resources\PolicyVersionResource;
use App\Models\Policy; use App\Models\Policy;
use App\Models\PolicyVersion; use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('bulk prune archives eligible policy versions', function () { test('bulk prune archives eligible policy versions', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$policy = Policy::factory()->create(['managed_environment_id' => $tenant->id]); $policy = Policy::factory()->create(['managed_environment_id' => $tenant->id]);

View File

@ -4,22 +4,14 @@
use App\Models\BackupItem; use App\Models\BackupItem;
use App\Models\BackupSet; use App\Models\BackupSet;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('backup sets table bulk restore restores archived sets and their items', function () { test('backup sets table bulk restore restores archived sets and their items', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$set = BackupSet::create([ $set = BackupSet::create([
'managed_environment_id' => $tenant->id, 'managed_environment_id' => $tenant->id,

View File

@ -4,22 +4,15 @@
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\Policy; use App\Models\Policy;
use App\Models\PolicyVersion; use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('policy versions table bulk restore creates a run and restores archived records', function () { test('policy versions table bulk restore creates a run and restores archived records', function () {
$tenant = ManagedEnvironment::factory()->create(['is_current' => true]); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); $tenant->forceFill(['is_current' => true])->save();
$user->tenants()->syncWithoutDetaching([ setAdminPanelContext($tenant);
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$policy = Policy::factory()->create(['managed_environment_id' => $tenant->id]); $policy = Policy::factory()->create(['managed_environment_id' => $tenant->id]);
$version = PolicyVersion::factory()->create([ $version = PolicyVersion::factory()->create([

View File

@ -4,22 +4,14 @@
use App\Models\BackupSet; use App\Models\BackupSet;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\RestoreRun; use App\Models\RestoreRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('restore runs table bulk restore creates a run and restores archived records', function () { test('restore runs table bulk restore creates a run and restores archived records', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$backupSet = BackupSet::create([ $backupSet = BackupSet::create([
'managed_environment_id' => $tenant->id, 'managed_environment_id' => $tenant->id,

View File

@ -2,22 +2,14 @@
use App\Filament\Resources\PolicyResource; use App\Filament\Resources\PolicyResource;
use App\Models\Policy; use App\Models\Policy;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('bulk delete requires confirmation string for large batches', function () { test('bulk delete requires confirmation string for large batches', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$policies = Policy::factory()->count(20)->create(['managed_environment_id' => $tenant->id]); $policies = Policy::factory()->count(20)->create(['managed_environment_id' => $tenant->id]);
Livewire::actingAs($user) Livewire::actingAs($user)
@ -31,13 +23,8 @@
}); });
test('bulk delete fails with incorrect confirmation string', function () { test('bulk delete fails with incorrect confirmation string', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$policies = Policy::factory()->count(20)->create(['managed_environment_id' => $tenant->id]); $policies = Policy::factory()->count(20)->create(['managed_environment_id' => $tenant->id]);
Livewire::actingAs($user) Livewire::actingAs($user)
@ -51,13 +38,8 @@
}); });
test('bulk delete does not require confirmation string for small batches', function () { test('bulk delete does not require confirmation string for small batches', function () {
$tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(role: 'owner');
$user = User::factory()->create(); setAdminPanelContext($tenant);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
$policies = Policy::factory()->count(10)->create(['managed_environment_id' => $tenant->id]); $policies = Policy::factory()->count(10)->create(['managed_environment_id' => $tenant->id]);
Livewire::actingAs($user) Livewire::actingAs($user)

View File

@ -57,8 +57,7 @@
$this->get(TenantDashboard::getUrl(tenant: $tenant)) $this->get(TenantDashboard::getUrl(tenant: $tenant))
->assertOk(); ->assertOk();
Filament::setCurrentPanel(Filament::getPanel('tenant')); setAdminPanelContext($tenant);
Filament::setTenant($tenant, true);
Livewire::test(RecoveryReadiness::class) Livewire::test(RecoveryReadiness::class)
->assertSee('Backup posture') ->assertSee('Backup posture')

View File

@ -64,9 +64,11 @@ function tenantDashboardButtonClassesForXPath(string $content, string $xpathExpr
it('builds the canonical operations follow-up baseline with tenant continuity', function (): void { it('builds the canonical operations follow-up baseline with tenant continuity', function (): void {
$tenant = ManagedEnvironment::factory()->create(); $tenant = ManagedEnvironment::factory()->create();
[, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
expect(OperationRunLinks::index($tenant, activeTab: 'active')) expect(OperationRunLinks::index($tenant, activeTab: 'active'))
->toBe(route('admin.operations.index', [ ->toBe(route('admin.operations.index', [
'workspace' => $tenant->workspace,
'managed_environment_id' => (int) $tenant->getKey(), 'managed_environment_id' => (int) $tenant->getKey(),
'activeTab' => 'active', 'activeTab' => 'active',
])) ]))
@ -76,6 +78,7 @@ function tenantDashboardButtonClassesForXPath(string $content, string $xpathExpr
problemClass: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP, problemClass: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
)) ))
->toBe(route('admin.operations.index', [ ->toBe(route('admin.operations.index', [
'workspace' => $tenant->workspace,
'managed_environment_id' => (int) $tenant->getKey(), 'managed_environment_id' => (int) $tenant->getKey(),
'activeTab' => OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP, 'activeTab' => OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
'problemClass' => OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP, 'problemClass' => OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
@ -86,9 +89,10 @@ function tenantDashboardButtonClassesForXPath(string $content, string $xpathExpr
$tenant = ManagedEnvironment::factory()->create([ $tenant = ManagedEnvironment::factory()->create([
'external_id' => 'tenant-dashboard-productization', 'external_id' => 'tenant-dashboard-productization',
]); ]);
[, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
expect(RequiredPermissionsLinks::requiredPermissions($tenant, ['source' => 'tenant_dashboard'])) expect(RequiredPermissionsLinks::requiredPermissions($tenant, ['source' => 'tenant_dashboard']))
->toBe('/admin/tenants/'.urlencode((string) $tenant->external_id).'/required-permissions?source=tenant_dashboard'); ->toBe(url('/admin/workspaces/'.urlencode((string) $tenant->workspace->slug).'/environments/'.urlencode((string) $tenant->getRouteKey()).'/required-permissions?source=tenant_dashboard'));
}); });
it('prioritizes operations requiring attention below permissions and high severity findings and keeps canonical hub links', function (): void { it('prioritizes operations requiring attention below permissions and high severity findings and keeps canonical hub links', function (): void {
@ -239,13 +243,13 @@ function tenantDashboardButtonClassesForXPath(string $content, string $xpathExpr
->and($actions[1]['actionUrl'])->toBe(FindingResource::getUrl('index', [ ->and($actions[1]['actionUrl'])->toBe(FindingResource::getUrl('index', [
'tab' => 'needs_action', 'tab' => 'needs_action',
'high_severity' => 1, 'high_severity' => 1,
], panel: 'tenant', tenant: $tenant)) ], panel: 'admin', tenant: $tenant))
->and($actions[2]['actionUrl'])->toBe(FindingExceptionResource::getUrl('index', panel: 'tenant', tenant: $tenant)); ->and($actions[2]['actionUrl'])->toBe(FindingExceptionResource::getUrl('index', panel: 'admin', tenant: $tenant));
$this->actingAs($user); $this->actingAs($user);
setTenantPanelContext($tenant); setTenantPanelContext($tenant);
$content = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) $content = $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful() ->assertSuccessful()
->getContent(); ->getContent();

View File

@ -117,7 +117,7 @@ function tenantDashboardProductizationHeaderMoreActionNames(Testable $component)
$outsider = User::factory()->create(); $outsider = User::factory()->create();
$this->actingAs($outsider) $this->actingAs($outsider)
->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) ->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertNotFound(); ->assertNotFound();
}); });
@ -127,7 +127,7 @@ function tenantDashboardProductizationHeaderMoreActionNames(Testable $component)
$this->actingAs($user); $this->actingAs($user);
setTenantPanelContext($tenant); setTenantPanelContext($tenant);
$this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful(); ->assertSuccessful();
}); });
@ -261,7 +261,7 @@ function tenantDashboardProductizationHeaderMoreActionNames(Testable $component)
$this->actingAs($user); $this->actingAs($user);
setTenantPanelContext($tenant); setTenantPanelContext($tenant);
$content = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) $content = $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful() ->assertSuccessful()
->getContent(); ->getContent();
@ -313,7 +313,7 @@ function tenantDashboardProductizationHeaderMoreActionNames(Testable $component)
$this->actingAs($user); $this->actingAs($user);
setTenantPanelContext($tenant); setTenantPanelContext($tenant);
$content = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) $content = $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful() ->assertSuccessful()
->getContent(); ->getContent();
@ -383,5 +383,5 @@ function tenantDashboardProductizationHeaderMoreActionNames(Testable $component)
$this->get(FindingResource::getUrl('index', [ $this->get(FindingResource::getUrl('index', [
'tab' => 'needs_action', 'tab' => 'needs_action',
'high_severity' => 1, 'high_severity' => 1,
], panel: 'tenant', tenant: $tenant))->assertForbidden(); ], panel: 'admin', tenant: $tenant))->assertForbidden();
}); });

View File

@ -109,7 +109,7 @@ function mockTenantDashboardReadinessPermissions(array $overview = []): void
expect($outputCard) expect($outputCard)
->not->toBeNull() ->not->toBeNull()
->and($outputCard['actionLabel'])->toBe('Open review pack') ->and($outputCard['actionLabel'])->toBe('Open review pack')
->and($outputCard['actionUrl'])->toBe(ReviewPackResource::getUrl('view', ['record' => $pack], panel: 'tenant', tenant: $tenant)) ->and($outputCard['actionUrl'])->toBe(ReviewPackResource::getUrl('view', ['record' => $pack], panel: 'admin', tenant: $tenant))
->and($outputCard['helperText'])->toBeNull(); ->and($outputCard['helperText'])->toBeNull();
}); });
@ -180,7 +180,7 @@ function mockTenantDashboardReadinessPermissions(array $overview = []): void
->and($currentReview['actionUrl'])->toBe(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) ->and($currentReview['actionUrl'])->toBe(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant))
->and($evidenceCoverage) ->and($evidenceCoverage)
->not->toBeNull() ->not->toBeNull()
->and($evidenceCoverage['actionUrl'])->toBe(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'tenant', tenant: $tenant)) ->and($evidenceCoverage['actionUrl'])->toBe(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'admin', tenant: $tenant))
->and($outputCard) ->and($outputCard)
->not->toBeNull() ->not->toBeNull()
->and($outputCard['actionUrl'])->toBe(CustomerReviewWorkspace::tenantPrefilterUrl($tenant)) ->and($outputCard['actionUrl'])->toBe(CustomerReviewWorkspace::tenantPrefilterUrl($tenant))
@ -264,7 +264,7 @@ function mockTenantDashboardReadinessPermissions(array $overview = []): void
$this->actingAs($user); $this->actingAs($user);
setTenantPanelContext($tenant); setTenantPanelContext($tenant);
$content = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) $content = $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful() ->assertSuccessful()
->getContent(); ->getContent();
@ -331,5 +331,5 @@ function mockTenantDashboardReadinessPermissions(array $overview = []): void
->not->toBeNull() ->not->toBeNull()
->and($evidenceCoverage['value'])->toBe('Unavailable') ->and($evidenceCoverage['value'])->toBe('Unavailable')
->and($evidenceCoverage['description'])->toBe('No evidence snapshot is currently available for customer-safe output.') ->and($evidenceCoverage['description'])->toBe('No evidence snapshot is currently available for customer-safe output.')
->and($evidenceCoverage['actionUrl'])->toBe(EvidenceSnapshotResource::getUrl('index', panel: 'tenant', tenant: $tenant)); ->and($evidenceCoverage['actionUrl'])->toBe(EvidenceSnapshotResource::getUrl('index', panel: 'admin', tenant: $tenant));
}); });

View File

@ -74,7 +74,7 @@ function mockTenantDashboardSummaryPermissions(array $overview = []): void
$this->actingAs($user); $this->actingAs($user);
setTenantPanelContext($tenant); setTenantPanelContext($tenant);
$response = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) $response = $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful() ->assertSuccessful()
->assertSee($tenant->name) ->assertSee($tenant->name)
->assertSee('Recommended next actions') ->assertSee('Recommended next actions')
@ -106,7 +106,7 @@ function mockTenantDashboardSummaryPermissions(array $overview = []): void
->and($content)->toContain('data-testid="tenant-dashboard-context-chip-provider"') ->and($content)->toContain('data-testid="tenant-dashboard-context-chip-provider"')
->and($content)->toContain('data-testid="tenant-dashboard-context-chip-provider-microsoft-logo"') ->and($content)->toContain('data-testid="tenant-dashboard-context-chip-provider-microsoft-logo"')
->and($content)->toContain('data-provider-key="microsoft"') ->and($content)->toContain('data-provider-key="microsoft"')
->and($content)->toContain('Microsoft tenant') ->and($content)->toContain('Microsoft environment')
->and($content)->toContain('data-testid="tenant-dashboard-context-chip-latest-activity" class="inline-flex items-center gap-2 whitespace-nowrap') ->and($content)->toContain('data-testid="tenant-dashboard-context-chip-latest-activity" class="inline-flex items-center gap-2 whitespace-nowrap')
->and($content)->toContain('data-testid="tenant-dashboard-context-chip-latest-activity-icon"') ->and($content)->toContain('data-testid="tenant-dashboard-context-chip-latest-activity-icon"')
->and($content)->toContain('Latest activity:') ->and($content)->toContain('Latest activity:')
@ -294,7 +294,7 @@ function mockTenantDashboardSummaryPermissions(array $overview = []): void
$this->actingAs($user); $this->actingAs($user);
setTenantPanelContext($tenant); setTenantPanelContext($tenant);
$response = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) $response = $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful() ->assertSuccessful()
->assertSee('No immediate action is waiting.') ->assertSee('No immediate action is waiting.')
->assertDontSee('Recent operations') ->assertDontSee('Recent operations')
@ -381,7 +381,7 @@ function mockTenantDashboardSummaryPermissions(array $overview = []): void
$this->actingAs($user); $this->actingAs($user);
setTenantPanelContext($tenant); setTenantPanelContext($tenant);
$this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful() ->assertSuccessful()
->assertSee('data-testid="tenant-dashboard-operations-attention-summary"', false) ->assertSee('data-testid="tenant-dashboard-operations-attention-summary"', false)
->assertSee('Review operation') ->assertSee('Review operation')
@ -417,7 +417,7 @@ function mockTenantDashboardSummaryPermissions(array $overview = []): void
$this->actingAs($user); $this->actingAs($user);
setTenantPanelContext($tenant); setTenantPanelContext($tenant);
$this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful() ->assertSuccessful()
->assertDontSee('data-testid="tenant-dashboard-operations-attention-summary"', false) ->assertDontSee('data-testid="tenant-dashboard-operations-attention-summary"', false)
->assertDontSee('Review operation') ->assertDontSee('Review operation')

View File

@ -29,6 +29,7 @@
}); });
[$user, $tenant] = createUserWithTenant(role: 'owner', fixtureProfile: 'credential-enabled'); [$user, $tenant] = createUserWithTenant(role: 'owner', fixtureProfile: 'credential-enabled');
spec283SeedRequirementRows($tenant, ['permissions.directory_groups']);
$this->actingAs($user); $this->actingAs($user);
$tenant->makeCurrent(); $tenant->makeCurrent();

View File

@ -124,14 +124,14 @@
->assertNotFound(); ->assertNotFound();
}); });
test('keeps Entra groups out of admin sidebar navigation while preserving tenant-panel navigation', function () { test('keeps Entra groups out of admin sidebar navigation after tenant-panel retirement', function () {
Filament::setCurrentPanel(Filament::getPanel('admin')); Filament::setCurrentPanel(Filament::getPanel('admin'));
expect(EntraGroupResource::shouldRegisterNavigation())->toBeFalse(); expect(EntraGroupResource::shouldRegisterNavigation())->toBeFalse();
Filament::setCurrentPanel(Filament::getPanel('tenant')); Filament::setCurrentPanel(Filament::getPanel('admin'));
expect(EntraGroupResource::shouldRegisterNavigation())->toBeTrue(); expect(EntraGroupResource::shouldRegisterNavigation())->toBeFalse();
Filament::setCurrentPanel(null); Filament::setCurrentPanel(null);
}); });

View File

@ -1,10 +1,12 @@
<?php <?php
use App\Filament\Resources\PolicyVersionResource;
use App\Models\Policy; use App\Models\Policy;
use App\Models\PolicyVersion; use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment; use App\Models\ManagedEnvironment;
use App\Models\User; use App\Models\User;
use App\Services\Graph\GraphClientInterface; use App\Services\Graph\GraphClientInterface;
use App\Support\Workspaces\WorkspaceContext;
use function Pest\Laravel\mock; use function Pest\Laravel\mock;
@ -48,10 +50,14 @@
$this->actingAs($this->user); $this->actingAs($this->user);
$response = $this->get(route('filament.tenant.resources.policy-versions.view', array_merge( $response = $this
filamentTenantRouteParams($this->tenant), ->withSession([
['record' => $version], WorkspaceContext::SESSION_KEY => (int) $this->tenant->workspace_id,
))); WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
(string) $this->tenant->workspace_id => (int) $this->tenant->getKey(),
],
])
->get(PolicyVersionResource::getUrl('view', ['record' => $version], panel: 'admin', tenant: $this->tenant));
$response->assertOk(); $response->assertOk();
}); });

View File

@ -21,6 +21,7 @@
}); });
[$user, $tenant] = createUserWithTenant(role: 'owner', fixtureProfile: 'credential-enabled'); [$user, $tenant] = createUserWithTenant(role: 'owner', fixtureProfile: 'credential-enabled');
spec283SeedRequirementRows($tenant, ['permissions.directory_groups']);
$this->actingAs($user); $this->actingAs($user);
$tenant->makeCurrent(); $tenant->makeCurrent();

View File

@ -10,6 +10,7 @@
Queue::fake(); Queue::fake();
[$user, $tenant] = createUserWithTenant(role: 'owner', fixtureProfile: 'credential-enabled'); [$user, $tenant] = createUserWithTenant(role: 'owner', fixtureProfile: 'credential-enabled');
spec283SeedRequirementRows($tenant, ['permissions.directory_groups']);
$service = app(EntraGroupSyncService::class); $service = app(EntraGroupSyncService::class);

View File

@ -1,8 +1,10 @@
<?php <?php
use App\Filament\Resources\FindingResource;
use App\Models\Finding; use App\Models\Finding;
use App\Models\Policy; use App\Models\Policy;
use App\Models\PolicyVersion; use App\Models\PolicyVersion;
use App\Support\Workspaces\WorkspaceContext;
it('shows an explicit diff unavailable message when policy version references are missing', function (): void { it('shows an explicit diff unavailable message when policy version references are missing', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
@ -38,10 +40,13 @@
]); ]);
$response = $this->actingAs($user) $response = $this->actingAs($user)
->get(route('filament.tenant.resources.findings.view', array_merge( ->withSession([
filamentTenantRouteParams($tenant), WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
['record' => $finding], WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
))) (string) $tenant->workspace_id => (int) $tenant->getKey(),
],
])
->get(FindingResource::getUrl('view', ['record' => $finding], panel: 'admin', tenant: $tenant))
->assertOk() ->assertOk()
->assertSee('Diff unavailable') ->assertSee('Diff unavailable')
->assertDontSee('No normalized changes were found'); ->assertDontSee('No normalized changes were found');
@ -107,10 +112,13 @@
]); ]);
$response = $this->actingAs($user) $response = $this->actingAs($user)
->get(route('filament.tenant.resources.findings.view', array_merge( ->withSession([
filamentTenantRouteParams($tenant), WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
['record' => $finding], WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
))) (string) $tenant->workspace_id => (int) $tenant->getKey(),
],
])
->get(FindingResource::getUrl('view', ['record' => $finding], panel: 'admin', tenant: $tenant))
->assertOk() ->assertOk()
->assertDontSee('Diff unavailable') ->assertDontSee('Diff unavailable')
->assertSee('1 added') ->assertSee('1 added')
@ -177,10 +185,13 @@
]); ]);
$response = $this->actingAs($user) $response = $this->actingAs($user)
->get(route('filament.tenant.resources.findings.view', array_merge( ->withSession([
filamentTenantRouteParams($tenant), WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
['record' => $finding], WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
))) (string) $tenant->workspace_id => (int) $tenant->getKey(),
],
])
->get(FindingResource::getUrl('view', ['record' => $finding], panel: 'admin', tenant: $tenant))
->assertOk() ->assertOk()
->assertDontSee('Diff unavailable') ->assertDontSee('Diff unavailable')
->assertSee('1 removed') ->assertSee('1 removed')

View File

@ -85,7 +85,7 @@ function createAdminRolesReport(ManagedEnvironment $tenant, ?array $summaryOverr
'high_privilege_assignments' => 7, 'high_privilege_assignments' => 7,
]); ]);
$expectedUrl = StoredReportResource::getUrl('view', ['record' => $report], panel: 'tenant', tenant: $tenant); $expectedUrl = StoredReportResource::getUrl('view', ['record' => $report], panel: 'admin', tenant: $tenant);
Livewire::actingAs($user) Livewire::actingAs($user)
->test(AdminRolesSummaryWidget::class, ['record' => $tenant]) ->test(AdminRolesSummaryWidget::class, ['record' => $tenant])

View File

@ -94,8 +94,8 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id])
->get(route('admin.evidence.overview', ['managed_environment_id' => (int) $tenantB->getKey()])) ->get(route('admin.evidence.overview', ['managed_environment_id' => (int) $tenantB->getKey()]))
->assertOk() ->assertOk()
->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshots[(int) $tenantB->getKey()]], tenant: $tenantB, panel: 'tenant'), false) ->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshots[(int) $tenantB->getKey()]], tenant: $tenantB, panel: 'admin'), false)
->assertDontSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshots[(int) $tenantA->getKey()]], tenant: $tenantA, panel: 'tenant'), false); ->assertDontSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshots[(int) $tenantA->getKey()]], tenant: $tenantA, panel: 'admin'), false);
}); });
it('shows stale evidence burden and a create-review next step on the overview', function (): void { it('shows stale evidence burden and a create-review next step on the overview', function (): void {
@ -121,8 +121,8 @@
->assertSee($freshTenant->name) ->assertSee($freshTenant->name)
->assertSee('Refresh the stale evidence before relying on this snapshot') ->assertSee('Refresh the stale evidence before relying on this snapshot')
->assertSee('Create a current review from this evidence snapshot') ->assertSee('Create a current review from this evidence snapshot')
->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $staleSnapshot], tenant: $staleTenant, panel: 'tenant'), false) ->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $staleSnapshot], tenant: $staleTenant, panel: 'admin'), false)
->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $freshSnapshot], tenant: $freshTenant, panel: 'tenant'), false); ->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $freshSnapshot], tenant: $freshTenant, panel: 'admin'), false);
}); });
it('seeds the native entitled-tenant prefilter once and clears it through the page action', function (): void { it('seeds the native entitled-tenant prefilter once and clears it through the page action', function (): void {
@ -174,6 +174,6 @@
$this->get(route('admin.evidence.overview')) $this->get(route('admin.evidence.overview'))
->assertOk() ->assertOk()
->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshotA], tenant: $tenantA, panel: 'tenant'), false) ->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshotA], tenant: $tenantA, panel: 'admin'), false)
->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshotB], tenant: $tenantB, panel: 'tenant'), false); ->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshotB], tenant: $tenantB, panel: 'admin'), false);
}); });

View File

@ -48,7 +48,7 @@
]); ]);
$this->actingAs($user) $this->actingAs($user)
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'tenant').'?'.http_build_query([ ->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'admin').'?'.http_build_query([
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE, 'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
'review_id' => '123', 'review_id' => '123',
'tenant_filter_id' => (string) $tenant->getKey(), 'tenant_filter_id' => (string) $tenant->getKey(),

View File

@ -95,7 +95,7 @@ function suspendEvidenceSnapshotWorkspace(ManagedEnvironment $tenant): void
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$this->actingAs($user) $this->actingAs($user)
->get(EvidenceSnapshotResource::getUrl('index', tenant: $tenant, panel: 'tenant')) ->get(EvidenceSnapshotResource::getUrl('index', tenant: $tenant, panel: 'admin'))
->assertOk(); ->assertOk();
}); });
@ -111,7 +111,7 @@ function suspendEvidenceSnapshotWorkspace(ManagedEnvironment $tenant): void
[$user] = createUserWithTenant(role: 'owner'); [$user] = createUserWithTenant(role: 'owner');
$this->actingAs($user) $this->actingAs($user)
->get(EvidenceSnapshotResource::getUrl('index', tenant: $tenant, panel: 'tenant')) ->get(EvidenceSnapshotResource::getUrl('index', tenant: $tenant, panel: 'admin'))
->assertNotFound(); ->assertNotFound();
}); });
@ -122,7 +122,7 @@ function suspendEvidenceSnapshotWorkspace(ManagedEnvironment $tenant): void
Gate::define(Capabilities::EVIDENCE_VIEW, fn (): bool => false); Gate::define(Capabilities::EVIDENCE_VIEW, fn (): bool => false);
$this->actingAs($user) $this->actingAs($user)
->get(EvidenceSnapshotResource::getUrl('index', tenant: $tenant, panel: 'tenant')) ->get(EvidenceSnapshotResource::getUrl('index', tenant: $tenant, panel: 'admin'))
->assertForbidden(); ->assertForbidden();
}); });
@ -182,7 +182,7 @@ function suspendEvidenceSnapshotWorkspace(ManagedEnvironment $tenant): void
]); ]);
$this->actingAs($user) $this->actingAs($user)
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'tenant')) ->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'admin'))
->assertOk() ->assertOk()
->assertSee('Related context') ->assertSee('Related context')
->assertSee('Review pack'); ->assertSee('Review pack');
@ -245,7 +245,7 @@ function suspendEvidenceSnapshotWorkspace(ManagedEnvironment $tenant): void
suspendEvidenceSnapshotWorkspace($tenant); suspendEvidenceSnapshotWorkspace($tenant);
$this->actingAs($user) $this->actingAs($user)
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'tenant')) ->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'admin'))
->assertOk(); ->assertOk();
$tenant->makeCurrent(); $tenant->makeCurrent();
@ -277,7 +277,7 @@ function suspendEvidenceSnapshotWorkspace(ManagedEnvironment $tenant): void
]); ]);
$this->actingAs($user) $this->actingAs($user)
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'tenant')) ->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'admin'))
->assertOk() ->assertOk()
->assertSee('Outcome summary') ->assertSee('Outcome summary')
->assertDontSee('Artifact truth') ->assertDontSee('Artifact truth')
@ -303,13 +303,13 @@ function suspendEvidenceSnapshotWorkspace(ManagedEnvironment $tenant): void
$staleSnapshot = $this->makeStaleArtifactTruthEvidenceSnapshot($staleTenant); $staleSnapshot = $this->makeStaleArtifactTruthEvidenceSnapshot($staleTenant);
$this->actingAs($user) $this->actingAs($user)
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $freshSnapshot], tenant: $freshTenant, panel: 'tenant')) ->get(EvidenceSnapshotResource::getUrl('view', ['record' => $freshSnapshot], tenant: $freshTenant, panel: 'admin'))
->assertOk() ->assertOk()
->assertSee('No action needed') ->assertSee('No action needed')
->assertDontSee('Refresh the stale evidence before relying on this snapshot'); ->assertDontSee('Refresh the stale evidence before relying on this snapshot');
$this->actingAs($user) $this->actingAs($user)
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $staleSnapshot], tenant: $staleTenant, panel: 'tenant')) ->get(EvidenceSnapshotResource::getUrl('view', ['record' => $staleSnapshot], tenant: $staleTenant, panel: 'admin'))
->assertOk() ->assertOk()
->assertSee('Refresh recommended') ->assertSee('Refresh recommended')
->assertSee('Refresh the stale evidence before relying on this snapshot'); ->assertSee('Refresh the stale evidence before relying on this snapshot');
@ -412,7 +412,7 @@ function suspendEvidenceSnapshotWorkspace(ManagedEnvironment $tenant): void
]); ]);
$this->actingAs($user) $this->actingAs($user)
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'tenant')) ->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'admin'))
->assertOk() ->assertOk()
->assertSeeText('3 findings, 2 open.') ->assertSeeText('3 findings, 2 open.')
->assertSeeText('Open findings') ->assertSeeText('Open findings')
@ -455,7 +455,7 @@ function suspendEvidenceSnapshotWorkspace(ManagedEnvironment $tenant): void
]); ]);
$this->actingAs($user) $this->actingAs($user)
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'tenant').'?'.http_build_query([ ->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'admin').'?'.http_build_query([
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE, 'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
'review_id' => '456', 'review_id' => '456',
'tenant_filter_id' => (string) $tenant->getKey(), 'tenant_filter_id' => (string) $tenant->getKey(),

View File

@ -2,6 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
use App\Filament\Pages\InventoryCoverage;
use App\Filament\Resources\BackupSetResource;
use App\Filament\Resources\PolicyResource;
use App\Filament\Resources\PolicyVersionResource;
use App\Models\ManagedEnvironment; use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentMembership; use App\Models\ManagedEnvironmentMembership;
use App\Models\User; use App\Models\User;
@ -20,7 +24,7 @@
'/admin/inventory/inventory-coverage', '/admin/inventory/inventory-coverage',
]); ]);
it('redirects tenant-scoped admin surfaces to choose-tenant when no tenant is selected', function (): void { it('keeps retired flat tenant-scoped admin surfaces unavailable when no tenant is selected', function (): void {
$user = User::factory()->create(); $user = User::factory()->create();
$workspace = Workspace::factory()->create(); $workspace = Workspace::factory()->create();
@ -51,25 +55,25 @@
->actingAs($user) ->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get('/admin/policies') ->get('/admin/policies')
->assertRedirect('/admin/choose-tenant'); ->assertNotFound();
$this $this
->actingAs($user) ->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get('/admin/policy-versions') ->get('/admin/policy-versions')
->assertRedirect('/admin/choose-tenant'); ->assertNotFound();
$this $this
->actingAs($user) ->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get('/admin/backup-sets') ->get('/admin/backup-sets')
->assertRedirect('/admin/choose-tenant'); ->assertNotFound();
$this $this
->actingAs($user) ->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get('/admin/inventory') ->get('/admin/inventory')
->assertRedirect('/admin/choose-tenant'); ->assertRedirect('/admin/workspaces/'.$workspace->getKey().'/environments');
}); });
it('allows tenant-scoped admin surfaces to load from the remembered canonical tenant', function (string $path): void { it('allows tenant-scoped admin surfaces to load from the remembered canonical tenant', function (string $path): void {
@ -90,7 +94,12 @@
(string) $tenantA->workspace_id => (int) $tenantA->getKey(), (string) $tenantA->workspace_id => (int) $tenantA->getKey(),
], ],
]) ])
->get($path); ->get(match ($path) {
'/admin/policies' => PolicyResource::getUrl(panel: 'admin', tenant: $tenantA),
'/admin/policy-versions' => PolicyVersionResource::getUrl(panel: 'admin', tenant: $tenantA),
'/admin/backup-sets' => BackupSetResource::getUrl(panel: 'admin', tenant: $tenantA),
'/admin/inventory', '/admin/inventory/inventory-coverage' => InventoryCoverage::getUrl(panel: 'admin', tenant: $tenantA),
});
expect($response->getStatusCode())->toBeIn([200, 302]); expect($response->getStatusCode())->toBeIn([200, 302]);
expect($response->headers->get('Location'))->not->toBe('/admin/choose-tenant'); expect($response->headers->get('Location'))->not->toBe('/admin/choose-tenant');

View File

@ -7,6 +7,7 @@
use App\Models\BackupSet; use App\Models\BackupSet;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Support\BackupHealth\TenantBackupHealthAssessment; use App\Support\BackupHealth\TenantBackupHealthAssessment;
use App\Support\OperationRunLinks;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Filament\Facades\Filament; use Filament\Facades\Filament;
@ -59,7 +60,7 @@
->assertSee('Timing') ->assertSee('Timing')
->assertSee('Archive') ->assertSee('Archive')
->assertSee('More') ->assertSee('More')
->assertSee('/admin/operations/'.$run->getKey(), false) ->assertSee(OperationRunLinks::tenantlessView($run), false)
->assertDontSee('Related record') ->assertDontSee('Related record')
->assertDontSee('>Completed</span>', false) ->assertDontSee('>Completed</span>', false)
->assertSeeInOrder(['Nightly backup', 'Backup quality', 'Lifecycle overview', 'Related context', 'Technical detail']); ->assertSeeInOrder(['Nightly backup', 'Backup quality', 'Lifecycle overview', 'Related context', 'Technical detail']);
@ -140,7 +141,7 @@
$this->get(BackupSetResource::getUrl('view', [ $this->get(BackupSetResource::getUrl('view', [
'record' => $backupSet, 'record' => $backupSet,
'backup_health_reason' => TenantBackupHealthAssessment::REASON_LATEST_BACKUP_STALE, 'backup_health_reason' => TenantBackupHealthAssessment::REASON_LATEST_BACKUP_STALE,
], panel: 'tenant', tenant: $tenant)) ], panel: 'admin', tenant: $tenant))
->assertOk() ->assertOk()
->assertSee('Backup posture') ->assertSee('Backup posture')
->assertSee('Latest backup is stale') ->assertSee('Latest backup is stale')
@ -174,7 +175,7 @@
$this->get(BackupSetResource::getUrl('view', [ $this->get(BackupSetResource::getUrl('view', [
'record' => $backupSet, 'record' => $backupSet,
'backup_health_reason' => TenantBackupHealthAssessment::REASON_LATEST_BACKUP_DEGRADED, 'backup_health_reason' => TenantBackupHealthAssessment::REASON_LATEST_BACKUP_DEGRADED,
], panel: 'tenant', tenant: $tenant)) ], panel: 'admin', tenant: $tenant))
->assertOk() ->assertOk()
->assertSee('Backup posture') ->assertSee('Backup posture')
->assertSee('Latest backup is degraded') ->assertSee('Latest backup is degraded')

View File

@ -17,7 +17,7 @@
$this->get(BackupSetResource::getUrl('index', [ $this->get(BackupSetResource::getUrl('index', [
'backup_health_reason' => TenantBackupHealthAssessment::REASON_NO_BACKUP_BASIS, 'backup_health_reason' => TenantBackupHealthAssessment::REASON_NO_BACKUP_BASIS,
], panel: 'tenant', tenant: $tenant)) ], panel: 'admin', tenant: $tenant))
->assertOk() ->assertOk()
->assertSee('No usable completed backup basis is currently available for this tenant.') ->assertSee('No usable completed backup basis is currently available for this tenant.')
->assertSee('No backup sets'); ->assertSee('No backup sets');
@ -31,7 +31,7 @@
$this->get(BackupSetResource::getUrl('index', [ $this->get(BackupSetResource::getUrl('index', [
'backup_health_reason' => TenantBackupHealthAssessment::REASON_LATEST_BACKUP_STALE, 'backup_health_reason' => TenantBackupHealthAssessment::REASON_LATEST_BACKUP_STALE,
], panel: 'tenant', tenant: $tenant)) ], panel: 'admin', tenant: $tenant))
->assertOk() ->assertOk()
->assertSee('The latest backup detail is no longer available, so this view stays on the backup-set list.'); ->assertSee('The latest backup detail is no longer available, so this view stays on the backup-set list.');
}); });

View File

@ -5,6 +5,7 @@
use App\Filament\Resources\BackupSetResource; use App\Filament\Resources\BackupSetResource;
use App\Models\BackupSet; use App\Models\BackupSet;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Support\OperationRunLinks;
use Filament\Facades\Filament; use Filament\Facades\Filament;
it('links backup sets to their canonical operations context', function (): void { it('links backup sets to their canonical operations context', function (): void {
@ -29,10 +30,10 @@
->assertOk() ->assertOk()
->assertSee('Related context') ->assertSee('Related context')
->assertSee('Operations') ->assertSee('Operations')
->assertSee('/admin/operations/'.$run->getKey(), false); ->assertSee(OperationRunLinks::tenantlessView($run), false);
$this->get(BackupSetResource::getUrl('index', tenant: $tenant)) $this->get(BackupSetResource::getUrl('index', tenant: $tenant))
->assertOk() ->assertOk()
->assertSee('Open operation') ->assertSee('Open operation')
->assertSee('/admin/operations/'.$run->getKey(), false); ->assertSee(OperationRunLinks::tenantlessView($run), false);
}); });

View File

@ -34,7 +34,7 @@ function getTableEmptyStateAction($component, string $name): ?\Filament\Actions\
[$user] = createUserWithTenant($otherTenant, role: 'owner'); [$user] = createUserWithTenant($otherTenant, role: 'owner');
$this->actingAs($user) $this->actingAs($user)
->get(BackupSetResource::getUrl('index', panel: 'tenant', tenant: $tenant)) ->get(BackupSetResource::getUrl('index', panel: 'admin', tenant: $tenant))
->assertStatus(404); ->assertStatus(404);
}); });

View File

@ -65,8 +65,7 @@ function createCoverageBannerTenant(): array
], ],
]); ]);
Filament::setCurrentPanel(Filament::getPanel('tenant')); setAdminPanelContext($tenant);
Filament::setTenant($tenant, true);
Livewire::test(BaselineCompareCoverageBanner::class) Livewire::test(BaselineCompareCoverageBanner::class)
->assertSee('The last compare finished, but normal result output was suppressed.') ->assertSee('The last compare finished, but normal result output was suppressed.')
@ -88,8 +87,7 @@ function createCoverageBannerTenant(): array
'baseline_profile_id' => (int) $profile->getKey(), 'baseline_profile_id' => (int) $profile->getKey(),
]); ]);
Filament::setCurrentPanel(Filament::getPanel('tenant')); setAdminPanelContext($tenant);
Filament::setTenant($tenant, true);
Livewire::test(BaselineCompareCoverageBanner::class) Livewire::test(BaselineCompareCoverageBanner::class)
->assertSee('The current baseline snapshot is not available for compare.') ->assertSee('The current baseline snapshot is not available for compare.')
@ -122,8 +120,7 @@ function createCoverageBannerTenant(): array
], ],
]); ]);
Filament::setCurrentPanel(Filament::getPanel('tenant')); setAdminPanelContext($tenant);
Filament::setTenant($tenant, true);
Livewire::test(BaselineCompareCoverageBanner::class) Livewire::test(BaselineCompareCoverageBanner::class)
->assertDontSee('No confirmed drift in the latest baseline compare.') ->assertDontSee('No confirmed drift in the latest baseline compare.')
@ -162,8 +159,7 @@ function createCoverageBannerTenant(): array
'due_at' => now()->subDay(), 'due_at' => now()->subDay(),
]); ]);
Filament::setCurrentPanel(Filament::getPanel('tenant')); setAdminPanelContext($tenant);
Filament::setTenant($tenant, true);
Livewire::test(BaselineCompareCoverageBanner::class) Livewire::test(BaselineCompareCoverageBanner::class)
->assertSee('overdue finding') ->assertSee('overdue finding')

View File

@ -160,6 +160,6 @@ function seedBaselineCompareLandingGapRun(\App\Models\ManagedEnvironment $tenant
seedBaselineCompareLandingGapRun($tenant); seedBaselineCompareLandingGapRun($tenant);
$this->actingAs($nonMember) $this->actingAs($nonMember)
->get(BaselineCompareLanding::getUrl(tenant: $tenant, panel: 'tenant')) ->get(BaselineCompareLanding::getUrl(tenant: $tenant, panel: 'admin'))
->assertNotFound(); ->assertNotFound();
}); });

View File

@ -28,7 +28,7 @@
it('redirects unauthenticated users (302)', function (): void { it('redirects unauthenticated users (302)', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$this->get(BaselineCompareLanding::getUrl(tenant: $tenant, panel: 'tenant')) $this->get(BaselineCompareLanding::getUrl(tenant: $tenant, panel: 'admin'))
->assertStatus(302); ->assertStatus(302);
}); });
@ -37,7 +37,7 @@
$nonMember = \App\Models\User::factory()->create(); $nonMember = \App\Models\User::factory()->create();
$this->actingAs($nonMember) $this->actingAs($nonMember)
->get(BaselineCompareLanding::getUrl(tenant: $tenant, panel: 'tenant')) ->get(BaselineCompareLanding::getUrl(tenant: $tenant, panel: 'admin'))
->assertNotFound(); ->assertNotFound();
}); });

View File

@ -6,6 +6,7 @@
use App\Filament\Pages\BaselineCompareMatrix; use App\Filament\Pages\BaselineCompareMatrix;
use App\Filament\Resources\BaselineProfileResource; use App\Filament\Resources\BaselineProfileResource;
use App\Models\ManagedEnvironment;
use App\Models\User; use App\Models\User;
use App\Models\WorkspaceMembership; use App\Models\WorkspaceMembership;
use App\Support\Baselines\Compare\CompareStrategyRegistry; use App\Support\Baselines\Compare\CompareStrategyRegistry;
@ -171,6 +172,11 @@
'user_id' => (int) $viewer->getKey(), 'user_id' => (int) $viewer->getKey(),
'role' => 'owner', 'role' => 'owner',
]); ]);
createUserWithTenant(
tenant: ManagedEnvironment::factory()->create(['workspace_id' => (int) $fixture['workspace']->getKey()]),
user: $viewer,
role: 'owner',
);
$viewer->tenants()->syncWithoutDetaching([ $viewer->tenants()->syncWithoutDetaching([
(int) $fixture['visibleTenant']->getKey() => ['role' => 'owner'], (int) $fixture['visibleTenant']->getKey() => ['role' => 'owner'],
@ -262,12 +268,13 @@
$fixture = $this->makeBaselineCompareMatrixFixture(); $fixture = $this->makeBaselineCompareMatrixFixture();
$viewer = User::factory()->create(); $viewer = User::factory()->create();
WorkspaceMembership::factory()->create([ $scopedTenant = ManagedEnvironment::factory()->create([
'workspace_id' => (int) $fixture['workspace']->getKey(), 'workspace_id' => (int) $fixture['workspace']->getKey(),
'user_id' => (int) $viewer->getKey(), 'status' => 'active',
'role' => 'owner',
]); ]);
createUserWithTenant(tenant: $scopedTenant, user: $viewer, role: 'owner');
$session = $this->setAdminWorkspaceContext($viewer, $fixture['workspace']); $session = $this->setAdminWorkspaceContext($viewer, $fixture['workspace']);
$this->withSession($session) $this->withSession($session)

View File

@ -66,8 +66,7 @@ function createBaselineCompareWidgetTenant(): array
$this->actingAs($user); $this->actingAs($user);
Filament::setCurrentPanel(Filament::getPanel('tenant')); setAdminPanelContext($tenant);
Filament::setTenant($tenant, true);
Livewire::test(BaselineCompareNow::class) Livewire::test(BaselineCompareNow::class)
->assertSee('Baseline Governance') ->assertSee('Baseline Governance')
@ -111,8 +110,7 @@ function createBaselineCompareWidgetTenant(): array
$this->actingAs($user); $this->actingAs($user);
Filament::setCurrentPanel(Filament::getPanel('tenant')); setAdminPanelContext($tenant);
Filament::setTenant($tenant, true);
Livewire::test(BaselineCompareNow::class) Livewire::test(BaselineCompareNow::class)
->assertSee('Needs review') ->assertSee('Needs review')
@ -141,8 +139,7 @@ function createBaselineCompareWidgetTenant(): array
$this->actingAs($user); $this->actingAs($user);
Filament::setCurrentPanel(Filament::getPanel('tenant')); setAdminPanelContext($tenant);
Filament::setTenant($tenant, true);
Livewire::test(BaselineCompareNow::class) Livewire::test(BaselineCompareNow::class)
->assertSee('Action required') ->assertSee('Action required')
@ -168,8 +165,7 @@ function createBaselineCompareWidgetTenant(): array
$this->actingAs($user); $this->actingAs($user);
Filament::setCurrentPanel(Filament::getPanel('tenant')); setAdminPanelContext($tenant);
Filament::setTenant($tenant, true);
Livewire::test(BaselineCompareNow::class) Livewire::test(BaselineCompareNow::class)
->assertSee('In progress') ->assertSee('In progress')
@ -195,8 +191,7 @@ function createBaselineCompareWidgetTenant(): array
$this->actingAs($user); $this->actingAs($user);
Filament::setCurrentPanel(Filament::getPanel('tenant')); setAdminPanelContext($tenant);
Filament::setTenant($tenant, true);
Livewire::test(BaselineCompareNow::class) Livewire::test(BaselineCompareNow::class)
->assertSee('Unavailable') ->assertSee('Unavailable')

Some files were not shown because too many files have changed in this diff Show More