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 (int) (app(GovernanceDecisionRegisterBuilder::class)->build(
$counts = app(GovernanceDecisionRegisterBuilder::class)->build(
workspace: $workspace,
visibleTenants: $visibleTenants,
registerState: 'open',
)['counts']['open'] ?? 0) > 0;
)['counts'] ?? [];
return (int) ($counts['open'] ?? 0) > 0
|| (int) ($counts['recently_closed'] ?? 0) > 0;
}
public function mount(): void
@ -416,7 +419,19 @@ private function ensureRegisterIsVisible(): void
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);
}
}

View File

@ -12,6 +12,7 @@
use App\Support\Filament\CanonicalAdminTenantFilterState;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\OperateHub\OperateHubShell;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Operations\OperationLifecyclePolicy;
@ -23,6 +24,7 @@
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
use App\Support\Workspaces\WorkspaceContext;
use App\Models\User;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Services\Auth\WorkspaceCapabilityResolver;
use BackedEnum;
use Filament\Actions\Action;
@ -347,6 +349,7 @@ public function table(Table $table): Table
->query(function (): Builder {
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
$tenantFilter = $this->currentTenantFilterId();
$allowedTenantIds = $this->allowedTenantIdsForWorkspaceScope($workspaceId);
$query = OperationRun::query()
->with('user')
@ -359,6 +362,18 @@ public function table(Table $table): Table
! $workspaceId,
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(
$tenantFilter !== null,
fn (Builder $query): Builder => $query->where('managed_environment_id', $tenantFilter),
@ -437,9 +452,22 @@ private function scopedSummaryQuery(): ?Builder
}
$tenantFilter = $this->currentTenantFilterId();
$allowedTenantIds = $this->allowedTenantIdsForWorkspaceScope($workspaceId);
return OperationRun::query()
->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(
$tenantFilter !== null,
fn (Builder $query): Builder => $query->where('managed_environment_id', $tenantFilter),
@ -509,6 +537,29 @@ private function currentTenantFilterId(): ?int
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
{
if (! is_numeric($value)) {

View File

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

View File

@ -111,7 +111,12 @@ public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?
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');
}
app(TenantMembershipManager::class)->grantScope($tenant, $user, $user, source: 'diagnostic');
app(TenantMembershipManager::class)->grantScope($tenant, $user, $user, sourceRef: 'diagnostic');
$this->mount();
}

View File

@ -4844,7 +4844,9 @@ private function completionSummaryBootstrapLabel(): string
$runs = is_array($runs) ? $runs : [];
if ($runs !== []) {
return 'Started';
return $this->completionSummaryBootstrapCompleted()
? 'Completed'
: 'Started';
}
return $this->completionSummarySelectedBootstrapTypes() === []
@ -4879,6 +4881,10 @@ private function completionSummaryBootstrapDetail(): string
return sprintf('%d action(s) selected', count($selectedTypes));
}
if ($this->completionSummaryBootstrapCompleted()) {
return sprintf('%d action(s) completed', count($runs));
}
if (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
{
return $this->completionSummaryBootstrapActionRequiredDetail() !== null;
@ -4910,6 +4951,7 @@ private function completionSummaryBootstrapColor(): string
return match ($this->completionSummaryBootstrapLabel()) {
'Action required' => 'warning',
'Started' => 'info',
'Completed' => 'success',
'Selected' => 'warning',
default => 'gray',
};

View File

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

View File

@ -146,6 +146,11 @@ public function panel(Panel $panel): Panel
->icon('heroicon-o-queue-list')
->group(fn (): string => __('localization.navigation.monitoring'))
->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'))
->url(fn (): string => route('admin.monitoring.audit-log'))
->icon('heroicon-o-clipboard-document-list')
@ -161,7 +166,7 @@ public function panel(Panel $panel): Panel
fn () => view('filament.partials.context-bar')->render()
)
->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')
? ''
: ((bool) config('tenantpilot.bulk_operations.progress_widget_enabled', true)
@ -224,12 +229,10 @@ public function panel(Panel $panel): Panel
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)) {
$panel->theme($theme);
}
if (is_string($theme)) {
$panel->theme($theme);
}
return $panel;

View File

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

View File

@ -18,6 +18,11 @@ final class ManagedEnvironmentAccessScopeResolver
*/
private array $scopeIdsByUserWorkspace = [];
/**
* @var array<int, Workspace|null>
*/
private array $workspaceById = [];
public function __construct(
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) {
return new ManagedEnvironmentAccessDecision(
@ -139,7 +144,7 @@ public function canAccess(User $user, ManagedEnvironment $tenant): bool
*/
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) {
return [];
@ -187,6 +192,7 @@ public function prime(User $user, array $tenantIds): void
public function clearCache(): void
{
$this->scopeIdsByUserWorkspace = [];
$this->workspaceById = [];
}
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];
}
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
{
if ($tenant->exists && $tenant->workspace_id !== null) {

View File

@ -199,6 +199,15 @@ public function startCompareForVisibleAssignments(BaselineProfile $profile, User
$blockedCount = 0;
$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) {
$tenant = $assignment->tenant;

View File

@ -46,6 +46,15 @@ public function build(BaselineProfile $profile, User $user, array $filters = [])
->with('tenant')
->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);
$referenceResolution = $this->snapshotTruthResolver->resolveCompareSnapshot($profile);
$referenceSnapshot = $this->resolvedSnapshot($referenceResolution);

View File

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

View File

@ -84,8 +84,11 @@ public static function index(
bool $allTenants = false,
?string $problemClass = null,
?string $operationType = null,
?Workspace $workspace = null,
): string {
$workspace = self::resolveWorkspace($tenant);
$workspace = $tenant instanceof ManagedEnvironment
? self::resolveWorkspace($tenant)
: ($workspace ?? self::resolveWorkspace());
if (! $workspace instanceof Workspace) {
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))
<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')
<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="9" y="1" width="6" height="6" fill="#7fba00" />
<rect x="1" y="9" width="6" height="6" fill="#00a4ef" />
@ -33,4 +33,4 @@ class="flex w-full flex-col items-start gap-3 sm:flex-row sm:flex-wrap sm:items-
<span>{{ __('localization.dashboard.overview.context_latest_activity_chip', ['time' => $context['latestActivity']]) }}</span>
</div>
@endif
</div>
</div>

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);
};
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}')
->group(function (): void {
Route::get('/', WorkspaceOverview::class)
Route::get('/overview', WorkspaceOverview::class)
->name('admin.workspace.home');
Route::get('/ping', fn () => response()->noContent())->name('admin.workspace.ping');

View File

@ -8,6 +8,7 @@
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Workspaces\WorkspaceContext;
@ -33,6 +34,7 @@
'outcome' => OperationRunOutcome::Failed->value,
'completed_at' => now()->subHour(),
]);
$operationPath = (string) parse_url(OperationRunLinks::tenantlessView($operation), PHP_URL_PATH);
$backupSet = BackupSet::factory()->create([
'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('Backup posture')
->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-provider\"][data-provider-key=\"microsoft\"]') !== 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-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 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)
->assertSee('Recommended next actions')
->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("(() => { 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)
->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')
->assertScript("window.location.pathname.includes('/admin/operations/{$operation->getKey()}')", true)
->assertScript("window.location.pathname === '{$operationPath}'", true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();

View File

@ -52,7 +52,6 @@
'entra_tenant_id' => (string) $tenant->managed_environment_id,
'tenant_name' => (string) $tenant->name,
'environment' => 'prod',
'provider_connection_id' => (int) $connection->getKey(),
],
]);
@ -67,28 +66,24 @@
->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Onboarding draft')
->assertSee('Verify access')
->assertSee('Status: Not started')
->waitForText('Use existing connection')
->refresh()
->waitForText('Status: Not started')
->waitForText('Use existing connection')
->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Verify access')
->assertSee('Status: Not started')
->click('Select an existing connection or create a new one.')
->assertSee('Edit selected connection')
->click('Create new connection')
->check('internal:label="Dedicated override"s')
->check('Create new connection')
->waitForText('Dedicated override')
->check('Dedicated override')
->waitForText('Dedicated client secret')
->fill('[type="password"]', 'browser-only-secret')
->assertValue('[type="password"]', 'browser-only-secret')
->refresh()
->waitForText('Status: Not started')
->waitForText('Use existing connection')
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Verify access')
->click('Select an existing connection or create a new one.')
->assertSee('Edit selected connection')
->click('Create new connection')
->check('internal:label="Dedicated override"s')
->check('Create new connection')
->waitForText('Dedicated override')
->check('Dedicated override')
->waitForText('Dedicated client secret')
->assertValue('[type="password"]', '');
});
@ -285,5 +280,6 @@
$page
->wait(7)
->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))
->waitForText('Dashboard')
->waitForText($tenant->name)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$inventoryPage = visit(InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant))
$inventoryPage = visit(InventoryItemResource::getUrl('index', panel: 'admin', tenant: $tenant))
->resize(1440, 1200)
->assertScript('window.innerWidth >= 1400', true)
->waitForText('Inventory Items')
@ -183,7 +183,7 @@ function operationActivityFeedbackSmokeLoginUrl(User $user, ManagedEnvironment $
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$page = visit(FindingResource::getUrl('index', panel: 'tenant', tenant: $tenant))
$page = visit(FindingResource::getUrl('index', panel: 'admin', tenant: $tenant))
->resize(1440, 1200);
$page
@ -252,11 +252,11 @@ function operationActivityFeedbackSmokeLoginUrl(User $user, ManagedEnvironment $
]);
visit(operationActivityFeedbackSmokeLoginUrl($user, $tenant))
->waitForText('Dashboard')
->waitForText($tenant->name)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
visit(InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant))
visit(InventoryItemResource::getUrl('index', panel: 'admin', tenant: $tenant))
->resize(1440, 1200)
->waitForText('Inventory Items')
->waitForText('Capturing evidence.')
@ -285,11 +285,11 @@ function operationActivityFeedbackSmokeLoginUrl(User $user, ManagedEnvironment $
]);
visit(operationActivityFeedbackSmokeLoginUrl($user, $tenant))
->waitForText('Dashboard')
->waitForText($tenant->name)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$page = visit(InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant))
$page = visit(InventoryItemResource::getUrl('index', panel: 'admin', tenant: $tenant))
->resize(1440, 1200)
->waitForText('Inventory Items')
->waitForText('Acknowledge')

View File

@ -48,7 +48,10 @@
visit(OperationRunLinks::tenantlessView($run))
->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()
->assertSee(OperationRunLinks::identifier((int) $run->getKey()));
});
});

View File

@ -3,8 +3,8 @@
declare(strict_types=1);
use App\Filament\Resources\TenantReviewResource;
use App\Models\ReviewPack;
use App\Models\ManagedEnvironment;
use App\Models\ReviewPack;
use App\Support\TenantReviewStatus;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
@ -106,7 +106,8 @@
->waitForText('Published ManagedEnvironment')
->assertDontSee('No Published ManagedEnvironment')
->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')
->assertSee('Governance-Paket herunterladen')
->assertSee('Governance-Paket')

View File

@ -72,11 +72,16 @@
visit($operationsIndexUrl)
->assertNoJavaScriptErrors()
->assertRoute('admin.operations.index');
->assertRoute('admin.operations.index', [
'workspace' => (int) $tenant->workspace_id,
]);
visit(OperationRunLinks::tenantlessView($run))
->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()));
});

View File

@ -84,7 +84,7 @@
]);
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')
->assertNoJavaScriptErrors()
->assertSee('Stale')
@ -103,7 +103,7 @@
->assertSee('Refresh the source review before sharing this pack')
->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')
->assertNoJavaScriptErrors()
->assertSee('Internal only')

View File

@ -123,11 +123,11 @@ function seedSpec177InventoryItemFilterPaginationFixtures(ManagedEnvironment $te
]);
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);
$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
->waitForText('Inventory Items')
@ -162,7 +162,10 @@ function seedSpec177InventoryItemFilterPaginationFixtures(ManagedEnvironment $te
visit($basisRunUrl)
->waitForText('Operation #'.(int) $run->getKey())
->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('Need follow-up');

View File

@ -11,7 +11,7 @@
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 {
$fixture = $this->makeBaselineCompareMatrixFixture();

View File

@ -143,7 +143,7 @@ function spec192ApprovedFindingException(ManagedEnvironment $tenant, User $reque
->assertSee('Review compare matrix')
->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')
->assertNoJavaScriptErrors()
->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);
visit(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant, panel: 'tenant'))
visit(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant, panel: 'admin'))
->waitForText('Download')
->assertNoJavaScriptErrors()
->assertSee('Regenerate');

View File

@ -73,7 +73,7 @@
->assertNoConsoleLogs()
->assertSee('Quiet monitoring mode');
visit(route('admin.operations.view', ['run' => (int) $run->getKey()]))
visit(\App\Support\OperationRunLinks::tenantlessView($run))
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->assertSee('Monitoring detail')
@ -84,8 +84,10 @@
->assertNoConsoleLogs()
->assertSee('Alert deliveries');
visit('/admin/t/'.$diagnosticsTenant->external_id.'/diagnostics')
visit(\App\Filament\Pages\TenantDashboard::getUrl(tenant: $diagnosticsTenant))
->assertNoJavaScriptErrors()
->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\Finding;
use App\Models\FindingException;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\PlatformUser;
use App\Models\ReviewPack;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\Findings\FindingExceptionService;
use App\Support\Auth\PlatformCapabilities;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\System\SystemOperationRunLinks;
use App\Support\TenantReviewCompletenessState;
use App\Support\TenantReviewStatus;
use App\Support\Auth\PlatformCapabilities;
use App\Support\System\SystemOperationRunLinks;
use Illuminate\Foundation\Testing\RefreshDatabase;
pest()->browser()->timeout(20_000);
@ -65,6 +65,8 @@ function spec194ApprovedFindingException(ManagedEnvironment $tenant, User $reque
function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $redirect = ''): string
{
$redirect = spec194RelativeRedirect($redirect);
return route('admin.local.smoke-login', array_filter([
'email' => $user->email,
'tenant' => $tenant->external_id,
@ -73,11 +75,31 @@ function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
], 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 {
[$user, $tenant] = createUserWithTenant(
role: 'owner',
workspaceRole: 'manager',
workspaceRole: 'owner',
ensureDefaultMicrosoftProviderConnection: false,
clearCapabilityCaches: true,
);
$finding = Finding::factory()->for($tenant)->create();
@ -147,12 +169,13 @@ function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
tenant: $archivedTenant,
user: $user,
role: 'owner',
workspaceRole: 'manager',
workspaceRole: 'owner',
ensureDefaultMicrosoftProviderConnection: false,
clearCapabilityCaches: true,
);
visit(spec194SmokeLoginUrl($user, $tenant))
->waitForText('Dashboard')
->waitForText($tenant->name)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
@ -177,14 +200,14 @@ function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
->assertNoConsoleLogs()
->click('Publish review')
->waitForText('Publication reason')
->click('Cancel')
->click('button:has-text("Cancel")')
->click('[aria-label="More"]')
->assertSee('Refresh review')
->assertSee('Export executive pack')
->click('[aria-label="Danger"]')
->click('Archive review')
->waitForText('Archive reason')
->click('Cancel')
->click('button:has-text("Cancel")')
->assertSee('Publish review')
->assertSee('Evidence snapshot');
@ -194,10 +217,10 @@ function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
->assertNoConsoleLogs()
->click('Refresh evidence')
->waitForText('Confirm')
->click('Cancel')
->click('button:has-text("Cancel")')
->click('Expire snapshot')
->waitForText('Expiry reason')
->click('Cancel')
->click('button:has-text("Cancel")')
->assertSee('Refresh evidence')
->assertSee('Expire snapshot');
@ -206,9 +229,10 @@ function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->click('[aria-label="Lifecycle"]')
->click('Archive')
->waitForText('Archive')
->click('button:has-text("Archive")')
->waitForText('Archive reason')
->click('Cancel')
->click('button:has-text("Cancel")')
->assertSee('Lifecycle');
visit(TenantResource::getUrl('edit', ['record' => $tenant], panel: 'admin'))
@ -217,13 +241,21 @@ function spec194SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
->assertNoConsoleLogs()
->assertSee('Lifecycle');
visit(TenantResource::getUrl('view', ['record' => $archivedTenant], panel: 'admin'))
visit(spec194SmokeLoginUrl(
$user,
$archivedTenant,
TenantResource::getUrl('view', ['record' => $archivedTenant], panel: 'admin'),
))
->waitForText('Related context')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->assertSee('Lifecycle');
visit(TenantResource::getUrl('edit', ['record' => $archivedTenant], panel: 'admin'))
visit(spec194SmokeLoginUrl(
$user,
$archivedTenant,
TenantResource::getUrl('edit', ['record' => $archivedTenant], panel: 'admin'),
))
->waitForText('Related context')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()

View File

@ -95,6 +95,7 @@
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
visit(route('admin.operations.index', [
'workspace' => $tenant->workspace,
'managed_environment_id' => (int) $tenant->getKey(),
'activeTab' => 'active',
]))
@ -151,8 +152,14 @@
'baseline_profile_id' => (int) $profile->getKey(),
]);
$this->actingAs($user);
$tenant->makeCurrent();
$this->actingAs($user)->withSession([
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);
@ -161,11 +168,11 @@
'baseline_profile_id' => (int) $profile->getKey(),
'subject_key' => $subjectKey,
],
panel: 'tenant',
panel: 'admin',
tenant: $tenant,
))
->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)
->waitForText('Focused subject')

View File

@ -73,7 +73,7 @@ function spec202SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
]);
visit(spec202SmokeLoginUrl($user, $tenant))
->waitForText('Dashboard')
->waitForText($tenant->name)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
@ -148,7 +148,7 @@ function spec202SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
]);
visit(spec202SmokeLoginUrl($user, $tenant))
->waitForText('Dashboard')
->waitForText($tenant->name)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
@ -160,4 +160,4 @@ function spec202SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
->assertSee('Support readiness')
->assertSee('Capture: blocked. Compare: blocked.')
->assertSee('Stored scope is invalid and must be repaired before capture or compare can continue.');
});
});

View File

@ -71,7 +71,7 @@ function spec265SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
]);
visit(spec265SmokeLoginUrl($user, $tenant))
->waitForText('Dashboard')
->waitForText($tenant->name)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
@ -82,7 +82,8 @@ function spec265SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
->assertSee('The register is currently filtered to one tenant.')
->assertSee($tenant->name)
->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')
->assertNoJavaScriptErrors()
->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')
->assertSee('Permission posture report')
->assertSee('Current')

View File

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

View File

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

View File

@ -44,7 +44,7 @@ public function test_renders_canonical_detail_for_a_workspace_member_when_tenant
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk()
->assertSee(\App\Support\OperationRunLinks::identifier($run))
->assertSee('Policy sync')
@ -70,7 +70,7 @@ public function test_renders_canonical_detail_gracefully_when_tenant_id_is_null(
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk()
->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)
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertNotFound();
}
@ -118,7 +118,7 @@ public function test_renders_canonical_detail_db_only_with_no_job_dispatch(): vo
assertNoOutboundHttp(function () use ($user, $run): void {
$this->actingAs($user)
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk()
->assertSee('Verification report');
});

View File

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

View File

@ -38,7 +38,7 @@ public function test_renders_workspace_operations_list_with_tenantless_runs_when
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.index'))
->get(\App\Support\OperationRunLinks::index())
->assertOk()
->assertSee('Tenantless run')
->assertSee('ManagedEnvironment run');
@ -70,7 +70,7 @@ public function test_renders_workspace_operations_list_workspace_wide_even_with_
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.index'))
->get(\App\Support\OperationRunLinks::index())
->assertOk()
->assertSee('ManagedEnvironment 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)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk()
->assertSee('Open')
->assertSee('View restore run');
@ -61,7 +61,7 @@ public function test_shows_only_generic_links_for_tenantless_runs_on_canonical_d
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk()
->assertSee('Operations')
->assertSee(OperationRunLinks::index(), false)
@ -80,7 +80,7 @@ public function test_does_not_show_legacy_admin_details_cta_and_keeps_canonical_
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk()
->assertDontSee('Admin details')
->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)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.index'))
->get(OperationRunLinks::index())
->assertOk()
->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)
->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->get(\App\Support\OperationRunLinks::tenantlessView($run))
->assertOk()
->assertSee('Open')
->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.');
}
}

View File

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

View File

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

View File

@ -7,7 +7,6 @@
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\OperationRunLinks;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
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]),
);
Filament::setTenant($otherTenant, true);
setAdminPanelContext($otherTenant);
$this->actingAs($user)
->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()
->assertSee('Back to tenant')
->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
@ -65,7 +64,7 @@ public function test_trusts_notification_style_run_links_with_no_selected_tenant
'type' => 'inventory_sync',
]);
Filament::setTenant(null, true);
setAdminPanelContext();
$this->actingAs($user)
->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',
]);
Filament::setTenant($otherTenant, true);
setAdminPanelContext($otherTenant);
$this->actingAs($user)
->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',
);
Filament::setTenant(null, true);
setAdminPanelContext();
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])

View File

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

View File

@ -61,6 +61,14 @@
'role' => 'owner',
]);
createUserWithTenant(
tenant: ManagedEnvironment::factory()->archived()->create([
'workspace_id' => (int) $workspace->getKey(),
]),
user: $user,
role: 'owner',
);
ManagedEnvironment::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'status' => 'active',
@ -104,7 +112,7 @@
$this->actingAs($user)
->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();
});
@ -133,6 +141,6 @@
$this->actingAs($user)
->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();
});

View File

@ -126,7 +126,7 @@
$this->actingAs($user)
->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();
});
@ -166,6 +166,6 @@
$this->actingAs($user)
->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();
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -54,9 +54,9 @@
$result = $service->startCompareForVisibleAssignments($fixture['profile'], $fixture['user']);
expect($result['visibleAssignedTenantCount'])->toBe(3)
->and($result['queuedCount'])->toBe(1)
->and($result['queuedCount'])->toBe(2)
->and($result['alreadyQueuedCount'])->toBe(1)
->and($result['blockedCount'])->toBe(1);
->and($result['blockedCount'])->toBe(0);
$launchStates = collect($result['targets'])
->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')
->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);
@ -73,7 +73,7 @@
->where('type', OperationRunType::BaselineCompare->value)
->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 => (string) $run->status === OperationRunStatus::Queued->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 Filament\Facades\Filament;
it('keeps baseline profiles out of tenant panel registration and tenant navigation URLs', function (): void {
$tenantPanelResources = Filament::getPanel('tenant')->getResources();
expect($tenantPanelResources)->not->toContain(BaselineProfileResource::class);
it('keeps baseline profiles workspace-owned while retired tenant navigation URLs stay unavailable', function (): void {
Filament::setCurrentPanel('admin');
[$user, $tenant] = createUserWithTenant(role: 'owner');
expect(BaselineProfileResource::shouldRegisterNavigation())->toBeTrue();
$this->actingAs($user)
->withSession([\App\Support\Workspaces\WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get("/admin/t/{$tenant->external_id}")
->assertOk()
->assertDontSee("/admin/t/{$tenant->external_id}/baseline-profiles", false)
->assertDontSee('>Baselines</span>', false);
->assertNotFound();
});
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\OperationRun;
use App\Models\RestoreRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
test('backup sets table bulk archive creates a run and archives selected sets', function () {
$tenant = ManagedEnvironment::factory()->create();
$user = User::factory()->create();
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
[$user, $tenant] = createUserWithTenant(role: 'owner');
setAdminPanelContext($tenant);
$sets = collect(range(1, 3))->map(function (int $i) use ($tenant) {
return BackupSet::create([
@ -63,13 +55,8 @@
});
test('backup sets can be archived even when referenced by restore runs', function () {
$tenant = ManagedEnvironment::factory()->create();
$user = User::factory()->create();
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
[$user, $tenant] = createUserWithTenant(role: 'owner');
setAdminPanelContext($tenant);
$set = BackupSet::create([
'managed_environment_id' => $tenant->id,
@ -96,13 +83,8 @@
});
test('backup sets table bulk archive requires type-to-confirm for 10+ sets', function () {
$tenant = ManagedEnvironment::factory()->create();
$user = User::factory()->create();
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
Filament::setTenant($tenant, true);
[$user, $tenant] = createUserWithTenant(role: 'owner');
setAdminPanelContext($tenant);
$sets = collect(range(1, 10))->map(function (int $i) use ($tenant) {
return BackupSet::create([

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,8 +57,7 @@
$this->get(TenantDashboard::getUrl(tenant: $tenant))
->assertOk();
Filament::setCurrentPanel(Filament::getPanel('tenant'));
Filament::setTenant($tenant, true);
setAdminPanelContext($tenant);
Livewire::test(RecoveryReadiness::class)
->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 {
$tenant = ManagedEnvironment::factory()->create();
[, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
expect(OperationRunLinks::index($tenant, activeTab: 'active'))
->toBe(route('admin.operations.index', [
'workspace' => $tenant->workspace,
'managed_environment_id' => (int) $tenant->getKey(),
'activeTab' => 'active',
]))
@ -76,6 +78,7 @@ function tenantDashboardButtonClassesForXPath(string $content, string $xpathExpr
problemClass: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
))
->toBe(route('admin.operations.index', [
'workspace' => $tenant->workspace,
'managed_environment_id' => (int) $tenant->getKey(),
'activeTab' => 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([
'external_id' => 'tenant-dashboard-productization',
]);
[, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
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 {
@ -239,13 +243,13 @@ function tenantDashboardButtonClassesForXPath(string $content, string $xpathExpr
->and($actions[1]['actionUrl'])->toBe(FindingResource::getUrl('index', [
'tab' => 'needs_action',
'high_severity' => 1,
], panel: 'tenant', tenant: $tenant))
->and($actions[2]['actionUrl'])->toBe(FindingExceptionResource::getUrl('index', panel: 'tenant', tenant: $tenant));
], panel: 'admin', tenant: $tenant))
->and($actions[2]['actionUrl'])->toBe(FindingExceptionResource::getUrl('index', panel: 'admin', tenant: $tenant));
$this->actingAs($user);
setTenantPanelContext($tenant);
$content = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
$content = $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful()
->getContent();

View File

@ -117,7 +117,7 @@ function tenantDashboardProductizationHeaderMoreActionNames(Testable $component)
$outsider = User::factory()->create();
$this->actingAs($outsider)
->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertNotFound();
});
@ -127,7 +127,7 @@ function tenantDashboardProductizationHeaderMoreActionNames(Testable $component)
$this->actingAs($user);
setTenantPanelContext($tenant);
$this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
$this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful();
});
@ -261,7 +261,7 @@ function tenantDashboardProductizationHeaderMoreActionNames(Testable $component)
$this->actingAs($user);
setTenantPanelContext($tenant);
$content = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
$content = $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful()
->getContent();
@ -313,7 +313,7 @@ function tenantDashboardProductizationHeaderMoreActionNames(Testable $component)
$this->actingAs($user);
setTenantPanelContext($tenant);
$content = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
$content = $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful()
->getContent();
@ -383,5 +383,5 @@ function tenantDashboardProductizationHeaderMoreActionNames(Testable $component)
$this->get(FindingResource::getUrl('index', [
'tab' => 'needs_action',
'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)
->not->toBeNull()
->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();
});
@ -180,7 +180,7 @@ function mockTenantDashboardReadinessPermissions(array $overview = []): void
->and($currentReview['actionUrl'])->toBe(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant))
->and($evidenceCoverage)
->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)
->not->toBeNull()
->and($outputCard['actionUrl'])->toBe(CustomerReviewWorkspace::tenantPrefilterUrl($tenant))
@ -264,7 +264,7 @@ function mockTenantDashboardReadinessPermissions(array $overview = []): void
$this->actingAs($user);
setTenantPanelContext($tenant);
$content = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
$content = $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful()
->getContent();
@ -331,5 +331,5 @@ function mockTenantDashboardReadinessPermissions(array $overview = []): void
->not->toBeNull()
->and($evidenceCoverage['value'])->toBe('Unavailable')
->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);
setTenantPanelContext($tenant);
$response = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
$response = $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful()
->assertSee($tenant->name)
->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-microsoft-logo"')
->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-icon"')
->and($content)->toContain('Latest activity:')
@ -294,7 +294,7 @@ function mockTenantDashboardSummaryPermissions(array $overview = []): void
$this->actingAs($user);
setTenantPanelContext($tenant);
$response = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
$response = $this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful()
->assertSee('No immediate action is waiting.')
->assertDontSee('Recent operations')
@ -381,7 +381,7 @@ function mockTenantDashboardSummaryPermissions(array $overview = []): void
$this->actingAs($user);
setTenantPanelContext($tenant);
$this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
$this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful()
->assertSee('data-testid="tenant-dashboard-operations-attention-summary"', false)
->assertSee('Review operation')
@ -417,7 +417,7 @@ function mockTenantDashboardSummaryPermissions(array $overview = []): void
$this->actingAs($user);
setTenantPanelContext($tenant);
$this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
$this->get(TenantDashboard::getUrl(panel: 'admin', tenant: $tenant))
->assertSuccessful()
->assertDontSee('data-testid="tenant-dashboard-operations-attention-summary"', false)
->assertDontSee('Review operation')

View File

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

View File

@ -124,14 +124,14 @@
->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'));
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);
});

View File

@ -1,10 +1,12 @@
<?php
use App\Filament\Resources\PolicyVersionResource;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\Graph\GraphClientInterface;
use App\Support\Workspaces\WorkspaceContext;
use function Pest\Laravel\mock;
@ -48,10 +50,14 @@
$this->actingAs($this->user);
$response = $this->get(route('filament.tenant.resources.policy-versions.view', array_merge(
filamentTenantRouteParams($this->tenant),
['record' => $version],
)));
$response = $this
->withSession([
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();
});

View File

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

View File

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

View File

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

View File

@ -85,7 +85,7 @@ function createAdminRolesReport(ManagedEnvironment $tenant, ?array $summaryOverr
'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)
->test(AdminRolesSummaryWidget::class, ['record' => $tenant])

View File

@ -94,8 +94,8 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id])
->get(route('admin.evidence.overview', ['managed_environment_id' => (int) $tenantB->getKey()]))
->assertOk()
->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshots[(int) $tenantB->getKey()]], tenant: $tenantB, panel: 'tenant'), false)
->assertDontSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshots[(int) $tenantA->getKey()]], tenant: $tenantA, 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: 'admin'), false);
});
it('shows stale evidence burden and a create-review next step on the overview', function (): void {
@ -121,8 +121,8 @@
->assertSee($freshTenant->name)
->assertSee('Refresh the stale evidence before relying on this 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' => $freshSnapshot], tenant: $freshTenant, panel: 'tenant'), false);
->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $staleSnapshot], tenant: $staleTenant, panel: 'admin'), 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 {
@ -174,6 +174,6 @@
$this->get(route('admin.evidence.overview'))
->assertOk()
->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshotA], tenant: $tenantA, panel: 'tenant'), false)
->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshotB], tenant: $tenantB, panel: 'tenant'), false);
->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshotA], tenant: $tenantA, panel: 'admin'), false)
->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshotB], tenant: $tenantB, panel: 'admin'), false);
});

View File

@ -48,7 +48,7 @@
]);
$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,
'review_id' => '123',
'tenant_filter_id' => (string) $tenant->getKey(),

View File

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

View File

@ -2,6 +2,10 @@
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\ManagedEnvironmentMembership;
use App\Models\User;
@ -20,7 +24,7 @@
'/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();
$workspace = Workspace::factory()->create();
@ -51,25 +55,25 @@
->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get('/admin/policies')
->assertRedirect('/admin/choose-tenant');
->assertNotFound();
$this
->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get('/admin/policy-versions')
->assertRedirect('/admin/choose-tenant');
->assertNotFound();
$this
->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get('/admin/backup-sets')
->assertRedirect('/admin/choose-tenant');
->assertNotFound();
$this
->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->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 {
@ -90,7 +94,12 @@
(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->headers->get('Location'))->not->toBe('/admin/choose-tenant');

View File

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

View File

@ -17,7 +17,7 @@
$this->get(BackupSetResource::getUrl('index', [
'backup_health_reason' => TenantBackupHealthAssessment::REASON_NO_BACKUP_BASIS,
], panel: 'tenant', tenant: $tenant))
], panel: 'admin', tenant: $tenant))
->assertOk()
->assertSee('No usable completed backup basis is currently available for this tenant.')
->assertSee('No backup sets');
@ -31,7 +31,7 @@
$this->get(BackupSetResource::getUrl('index', [
'backup_health_reason' => TenantBackupHealthAssessment::REASON_LATEST_BACKUP_STALE,
], panel: 'tenant', tenant: $tenant))
], panel: 'admin', tenant: $tenant))
->assertOk()
->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\Models\BackupSet;
use App\Models\OperationRun;
use App\Support\OperationRunLinks;
use Filament\Facades\Filament;
it('links backup sets to their canonical operations context', function (): void {
@ -29,10 +30,10 @@
->assertOk()
->assertSee('Related context')
->assertSee('Operations')
->assertSee('/admin/operations/'.$run->getKey(), false);
->assertSee(OperationRunLinks::tenantlessView($run), false);
$this->get(BackupSetResource::getUrl('index', tenant: $tenant))
->assertOk()
->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');
$this->actingAs($user)
->get(BackupSetResource::getUrl('index', panel: 'tenant', tenant: $tenant))
->get(BackupSetResource::getUrl('index', panel: 'admin', tenant: $tenant))
->assertStatus(404);
});

View File

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

View File

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

View File

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

View File

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

View File

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

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