Automatisch erstellter PR: Synchronisiere `platform-dev` nach `dev`. Enthält alle Änderungen, die aktuell in `platform-dev` vorhanden sind. Bitte Review und Merge gegen `dev`. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #306
289 lines
11 KiB
PHP
289 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\AlertDelivery;
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantTriageReview;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\BackupHealth\TenantBackupHealthResolver;
|
|
use App\Support\GovernanceInbox\GovernanceInboxSectionBuilder;
|
|
use App\Support\Navigation\CanonicalNavigationContext;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\PortfolioTriage\PortfolioArrivalContextToken;
|
|
use App\Support\PortfolioTriage\TenantTriageReviewFingerprint;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('builds visible governance inbox sections in canonical order with source links', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$user = User::factory()->create();
|
|
|
|
$alphaTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'name' => 'Alpha Tenant',
|
|
'external_id' => 'alpha-tenant',
|
|
]);
|
|
$bravoTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'name' => 'Bravo Tenant',
|
|
'external_id' => 'bravo-tenant',
|
|
]);
|
|
|
|
Finding::factory()
|
|
->for($alphaTenant)
|
|
->assignedTo((int) $user->getKey())
|
|
->ownedBy((int) $user->getKey())
|
|
->overdueByHours()
|
|
->create([
|
|
'status' => Finding::STATUS_IN_PROGRESS,
|
|
'subject_external_id' => 'assigned-finding',
|
|
]);
|
|
|
|
Finding::factory()
|
|
->for($bravoTenant)
|
|
->reopened()
|
|
->create([
|
|
'subject_external_id' => 'intake-finding',
|
|
]);
|
|
|
|
$exceptionFinding = Finding::factory()
|
|
->for($alphaTenant)
|
|
->riskAccepted()
|
|
->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'subject_external_id' => 'exception-finding',
|
|
]);
|
|
|
|
FindingException::query()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $alphaTenant->getKey(),
|
|
'finding_id' => (int) $exceptionFinding->getKey(),
|
|
'requested_by_user_id' => (int) $user->getKey(),
|
|
'owner_user_id' => (int) $user->getKey(),
|
|
'status' => FindingException::STATUS_PENDING,
|
|
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
|
'request_reason' => 'Needs approval',
|
|
'requested_at' => now()->subDay(),
|
|
'review_due_at' => now()->addDay(),
|
|
'evidence_summary' => ['reference_count' => 0],
|
|
]);
|
|
|
|
OperationRun::factory()
|
|
->forTenant($alphaTenant)
|
|
->create([
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'completed_at' => now()->subMinute(),
|
|
]);
|
|
|
|
OperationRun::factory()
|
|
->forTenant($bravoTenant)
|
|
->create([
|
|
'type' => OperationRunType::InventorySync->value,
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subMinutes(6),
|
|
]);
|
|
|
|
AlertDelivery::factory()->create([
|
|
'tenant_id' => null,
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'status' => AlertDelivery::STATUS_FAILED,
|
|
'event_type' => 'alerts.failed_delivery',
|
|
'payload' => [
|
|
'title' => 'Delivery failed',
|
|
'body' => 'Alert delivery could not be completed.',
|
|
],
|
|
]);
|
|
|
|
$backupHealthResolver = app(TenantBackupHealthResolver::class);
|
|
$fingerprints = app(TenantTriageReviewFingerprint::class);
|
|
|
|
$alphaBackupFingerprint = $fingerprints->forBackupHealth($backupHealthResolver->assess($alphaTenant));
|
|
$bravoBackupFingerprint = $fingerprints->forBackupHealth($backupHealthResolver->assess($bravoTenant));
|
|
|
|
expect($alphaBackupFingerprint)->not->toBeNull()
|
|
->and($bravoBackupFingerprint)->not->toBeNull();
|
|
|
|
TenantTriageReview::factory()
|
|
->for($alphaTenant)
|
|
->followUpNeeded()
|
|
->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'reviewed_by_user_id' => (int) $user->getKey(),
|
|
'concern_family' => PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH,
|
|
'review_fingerprint' => $alphaBackupFingerprint['fingerprint'],
|
|
'review_snapshot' => $alphaBackupFingerprint['snapshot'],
|
|
'reviewed_at' => now()->subDay(),
|
|
]);
|
|
|
|
TenantTriageReview::factory()
|
|
->for($bravoTenant)
|
|
->reviewed()
|
|
->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'reviewed_by_user_id' => (int) $user->getKey(),
|
|
'concern_family' => PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH,
|
|
'review_fingerprint' => hash('sha256', 'stale-review-fingerprint'),
|
|
'review_snapshot' => $bravoBackupFingerprint['snapshot'],
|
|
'reviewed_at' => now()->subDays(2),
|
|
]);
|
|
|
|
$context = new CanonicalNavigationContext(
|
|
sourceSurface: 'governance.inbox',
|
|
canonicalRouteName: 'filament.admin.pages.governance.inbox',
|
|
backLinkLabel: 'Back to governance inbox',
|
|
backLinkUrl: '/admin/governance/inbox',
|
|
);
|
|
|
|
$payload = app(GovernanceInboxSectionBuilder::class)->build(
|
|
user: $user,
|
|
workspace: $workspace,
|
|
authorizedTenants: [$alphaTenant, $bravoTenant],
|
|
visibleFindingTenants: [$alphaTenant, $bravoTenant],
|
|
reviewTenants: [$alphaTenant, $bravoTenant],
|
|
canViewAlerts: true,
|
|
canViewFindingExceptions: true,
|
|
navigationContext: $context,
|
|
);
|
|
|
|
expect(collect($payload['sections'])->pluck('key')->all())
|
|
->toBe([
|
|
'assigned_findings',
|
|
'intake_findings',
|
|
'finding_exceptions',
|
|
'stale_operations',
|
|
'alert_delivery_failures',
|
|
'review_follow_up',
|
|
])
|
|
->and($payload['family_counts'])->toMatchArray([
|
|
'assigned_findings' => 1,
|
|
'intake_findings' => 1,
|
|
'finding_exceptions' => 1,
|
|
'stale_operations' => 2,
|
|
'alert_delivery_failures' => 1,
|
|
'review_follow_up' => 2,
|
|
]);
|
|
|
|
$sections = collect($payload['sections'])->keyBy('key');
|
|
|
|
expect($sections['assigned_findings']['dominant_action_url'])
|
|
->toContain('/admin/findings/my-work')
|
|
->toContain('nav%5Bback_label%5D=Back+to+governance+inbox')
|
|
->and($sections['finding_exceptions']['dominant_action_label'])->toBe('Open finding exceptions')
|
|
->and($sections['finding_exceptions']['dominant_action_url'])->toContain('/admin/finding-exceptions/queue')
|
|
->and($sections['finding_exceptions']['entries'][0]['destination_url'])->toContain('exception='.(string) $sections['finding_exceptions']['entries'][0]['source_key'])
|
|
->and($sections['stale_operations']['dominant_action_label'])->toBe('Open terminal follow-up')
|
|
->and($sections['stale_operations']['dominant_action_url'])->toContain('problemClass=terminal_follow_up')
|
|
->and($sections['alert_delivery_failures']['dominant_action_url'])->toContain('tableFilters%5Bstatus%5D%5Bvalue%5D=failed')
|
|
->and(collect($sections['review_follow_up']['entries'])->pluck('status_label')->all())
|
|
->toBe(['Follow-up needed', 'Changed since review']);
|
|
});
|
|
|
|
it('keeps an explicitly selected visible family with an honest empty state when tenant filtering removes every row', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$user = User::factory()->create();
|
|
|
|
$alphaTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'name' => 'Alpha Tenant',
|
|
'external_id' => 'alpha-tenant',
|
|
]);
|
|
$bravoTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'name' => 'Bravo Tenant',
|
|
'external_id' => 'bravo-tenant',
|
|
]);
|
|
|
|
AlertDelivery::factory()->create([
|
|
'tenant_id' => null,
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'status' => AlertDelivery::STATUS_FAILED,
|
|
]);
|
|
|
|
$payload = app(GovernanceInboxSectionBuilder::class)->build(
|
|
user: $user,
|
|
workspace: $workspace,
|
|
authorizedTenants: [$alphaTenant, $bravoTenant],
|
|
visibleFindingTenants: [],
|
|
reviewTenants: [],
|
|
canViewAlerts: true,
|
|
selectedTenant: $alphaTenant,
|
|
selectedFamily: 'alert_delivery_failures',
|
|
);
|
|
|
|
expect($payload['sections'])->toHaveCount(1)
|
|
->and($payload['sections'][0]['key'])->toBe('alert_delivery_failures')
|
|
->and($payload['sections'][0]['count'])->toBe(0)
|
|
->and($payload['sections'][0]['empty_state'])->toContain('tenant filter');
|
|
});
|
|
|
|
it('omits finding exceptions when the exception family is hidden or tenant scope is inaccessible', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$user = User::factory()->create();
|
|
|
|
$visibleTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'name' => 'Visible Tenant',
|
|
]);
|
|
$hiddenTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'name' => 'Hidden Tenant',
|
|
]);
|
|
|
|
$finding = Finding::factory()
|
|
->for($hiddenTenant)
|
|
->riskAccepted()
|
|
->create(['workspace_id' => (int) $workspace->getKey()]);
|
|
|
|
FindingException::query()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $hiddenTenant->getKey(),
|
|
'finding_id' => (int) $finding->getKey(),
|
|
'requested_by_user_id' => (int) $user->getKey(),
|
|
'owner_user_id' => (int) $user->getKey(),
|
|
'status' => FindingException::STATUS_PENDING,
|
|
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
|
'request_reason' => 'Hidden request',
|
|
'requested_at' => now()->subDay(),
|
|
'review_due_at' => now()->addDay(),
|
|
'evidence_summary' => ['reference_count' => 0],
|
|
]);
|
|
|
|
$builder = app(GovernanceInboxSectionBuilder::class);
|
|
|
|
$payloadWithoutCapability = $builder->build(
|
|
user: $user,
|
|
workspace: $workspace,
|
|
authorizedTenants: [$visibleTenant, $hiddenTenant],
|
|
visibleFindingTenants: [],
|
|
reviewTenants: [],
|
|
canViewAlerts: false,
|
|
canViewFindingExceptions: false,
|
|
);
|
|
|
|
$payloadWithHiddenTenantOnly = $builder->build(
|
|
user: $user,
|
|
workspace: $workspace,
|
|
authorizedTenants: [$visibleTenant],
|
|
visibleFindingTenants: [],
|
|
reviewTenants: [],
|
|
canViewAlerts: false,
|
|
canViewFindingExceptions: true,
|
|
);
|
|
|
|
expect(collect($payloadWithoutCapability['available_families'])->pluck('key')->all())
|
|
->not->toContain('finding_exceptions')
|
|
->and($payloadWithHiddenTenantOnly['family_counts']['finding_exceptions'] ?? null)->toBe(0)
|
|
->and($payloadWithHiddenTenantOnly['sections'])->toBe([]);
|
|
});
|