## Summary - add the Spec 322 artifact set for the browser no-drift regression guard - add Feature navigation guards for admin surface scope, environment CTA URLs, and legacy alias rejection - add Browser smoke coverage for workspace hubs, environment-owned surfaces, workspace-owned analysis surfaces, and alerts/audit flows - add the Spec 322 browser support harness used by the new smoke coverage ## Validation - `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php tests/Feature/Navigation/Spec322LegacyQueryAliasGuardTest.php tests/Feature/Navigation/Spec322EnvironmentCtaUrlContractTest.php --compact` - `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec322WorkspaceHubNoDriftSmokeTest.php tests/Browser/Spec322EnvironmentOwnedSurfaceSmokeTest.php tests/Browser/Spec322WorkspaceOwnedAnalysisSmokeTest.php tests/Browser/Spec322AlertsAuditNoDriftSmokeTest.php --compact` - `cd apps/platform && ./vendor/bin/sail pint --dirty` - `git diff --check` ## Notes - a broader filtered regression run still reports existing Baseline Compare feature-test failures outside this diff Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #379
308 lines
12 KiB
PHP
308 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Browser\Support;
|
|
|
|
use App\Models\AlertDelivery;
|
|
use App\Models\AlertDestination;
|
|
use App\Models\AlertRule;
|
|
use App\Models\AuditLog;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\FindingExceptionDecision;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Support\EnvironmentReviewStatus;
|
|
use App\Support\Evidence\EvidenceSnapshotStatus;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
|
|
final class Spec322WorkspaceEnvironmentBrowserHarness
|
|
{
|
|
/**
|
|
* @return array{
|
|
* user: User,
|
|
* workspace: Workspace,
|
|
* environmentA: ManagedEnvironment,
|
|
* environmentB: ManagedEnvironment,
|
|
* runA: OperationRun,
|
|
* runB: OperationRun,
|
|
* connectionA: ProviderConnection,
|
|
* connectionB: ProviderConnection,
|
|
* exceptionA: FindingException,
|
|
* exceptionB: FindingException,
|
|
* snapshotA: EvidenceSnapshot,
|
|
* snapshotB: EvidenceSnapshot,
|
|
* reviewA: EnvironmentReview,
|
|
* reviewB: EnvironmentReview,
|
|
* deliveryA: AlertDelivery,
|
|
* deliveryB: AlertDelivery,
|
|
* auditA: AuditLog,
|
|
* auditB: AuditLog,
|
|
* auditWorkspace: AuditLog
|
|
* }
|
|
*/
|
|
public static function fixture(): array
|
|
{
|
|
$environmentA = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec322 Browser Environment A',
|
|
'external_id' => 'spec322-browser-environment-a',
|
|
]);
|
|
[$user, $environmentA] = \createUserWithTenant(tenant: $environmentA, role: 'owner', workspaceRole: 'owner');
|
|
|
|
$environmentB = ManagedEnvironment::factory()->active()->create([
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'name' => 'Spec322 Browser Environment B',
|
|
'external_id' => 'spec322-browser-environment-b',
|
|
]);
|
|
\createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'owner');
|
|
|
|
$workspace = $environmentA->workspace()->firstOrFail();
|
|
|
|
$runA = OperationRun::factory()->forTenant($environmentA)->create(['type' => 'policy.sync']);
|
|
$runB = OperationRun::factory()->forTenant($environmentB)->create(['type' => 'inventory_sync']);
|
|
|
|
$exceptionA = self::findingException($environmentA, $user, 'Spec322 Browser Governance A', 'Spec322 Browser Decision A');
|
|
$exceptionB = self::findingException($environmentB, $user, 'Spec322 Browser Governance B', 'Spec322 Browser Decision B');
|
|
|
|
$connectionA = ProviderConnection::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'managed_environment_id' => (int) $environmentA->getKey(),
|
|
'display_name' => 'Spec322 Browser Provider A',
|
|
]);
|
|
$connectionB = ProviderConnection::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'managed_environment_id' => (int) $environmentB->getKey(),
|
|
'display_name' => 'Spec322 Browser Provider B',
|
|
]);
|
|
|
|
$snapshotA = self::evidenceSnapshot($environmentA);
|
|
$snapshotB = self::evidenceSnapshot($environmentB);
|
|
|
|
$reviewA = self::publishedReview($environmentA, $user, $snapshotA);
|
|
$reviewB = self::publishedReview($environmentB, $user, $snapshotB);
|
|
|
|
$rule = AlertRule::factory()->create(['workspace_id' => (int) $workspace->getKey()]);
|
|
$destination = AlertDestination::factory()->create(['workspace_id' => (int) $workspace->getKey()]);
|
|
|
|
$deliveryA = AlertDelivery::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'managed_environment_id' => (int) $environmentA->getKey(),
|
|
'alert_rule_id' => (int) $rule->getKey(),
|
|
'alert_destination_id' => (int) $destination->getKey(),
|
|
'status' => AlertDelivery::STATUS_FAILED,
|
|
'created_at' => now()->subHour(),
|
|
]);
|
|
$deliveryB = AlertDelivery::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'managed_environment_id' => (int) $environmentB->getKey(),
|
|
'alert_rule_id' => (int) $rule->getKey(),
|
|
'alert_destination_id' => (int) $destination->getKey(),
|
|
'status' => AlertDelivery::STATUS_SENT,
|
|
'created_at' => now()->subHour(),
|
|
]);
|
|
|
|
$auditA = self::auditRecord($environmentA, 'Spec322 browser audit A');
|
|
$auditB = self::auditRecord($environmentB, 'Spec322 browser audit B');
|
|
$auditWorkspace = self::auditRecord(null, 'Spec322 browser workspace audit', [
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
]);
|
|
|
|
return compact(
|
|
'user',
|
|
'workspace',
|
|
'environmentA',
|
|
'environmentB',
|
|
'runA',
|
|
'runB',
|
|
'connectionA',
|
|
'connectionB',
|
|
'exceptionA',
|
|
'exceptionB',
|
|
'snapshotA',
|
|
'snapshotB',
|
|
'reviewA',
|
|
'reviewB',
|
|
'deliveryA',
|
|
'deliveryB',
|
|
'auditA',
|
|
'auditB',
|
|
'auditWorkspace',
|
|
);
|
|
}
|
|
|
|
public static function authenticate(object $testCase, User $user, Workspace $workspace, ?ManagedEnvironment $rememberedEnvironment = null): void
|
|
{
|
|
$session = [
|
|
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
|
];
|
|
|
|
if ($rememberedEnvironment instanceof ManagedEnvironment) {
|
|
$session[WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY] = [
|
|
(string) $workspace->getKey() => (int) $rememberedEnvironment->getKey(),
|
|
];
|
|
}
|
|
|
|
$testCase->actingAs($user)->withSession($session);
|
|
|
|
foreach ($session as $key => $value) {
|
|
session()->put($key, $value);
|
|
}
|
|
|
|
\setAdminPanelContext($rememberedEnvironment);
|
|
}
|
|
|
|
public static function assertWorkspaceOnly(mixed $page, ?string $wideText = null, ?string $environmentName = null): mixed
|
|
{
|
|
$page
|
|
->waitForText(__('localization.shell.no_environment_selected'))
|
|
->assertDontSee('Environment filter:')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
|
|
if ($wideText !== null) {
|
|
$page->assertSee($wideText);
|
|
}
|
|
|
|
if ($environmentName !== null) {
|
|
$page->assertDontSee(__('localization.shell.environment_scope').': '.$environmentName);
|
|
}
|
|
|
|
return self::assertNoLegacyQuery($page);
|
|
}
|
|
|
|
public static function assertFilteredWorkspaceHub(mixed $page, ManagedEnvironment $environment, ?string $hiddenText = null): mixed
|
|
{
|
|
$page
|
|
->waitForText('Environment filter:')
|
|
->assertSee($environment->name)
|
|
->assertDontSee(__('localization.shell.environment_scope').': '.$environment->name)
|
|
->assertSee('Clear filter')
|
|
->assertScript('window.location.search.includes("environment_id=")', true)
|
|
->assertScript('! window.location.search.includes("tenant=")', true)
|
|
->assertScript('! window.location.search.includes("tenant_id=")', true)
|
|
->assertScript('! window.location.search.includes("managed_environment_id=")', true)
|
|
->assertScript('! window.location.search.includes("tenant_scope=")', true)
|
|
->assertScript('! window.location.search.includes("tableFilters")', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
|
|
if ($hiddenText !== null) {
|
|
$page->assertDontSee($hiddenText);
|
|
}
|
|
|
|
return $page;
|
|
}
|
|
|
|
public static function assertNoLegacyQuery(mixed $page): mixed
|
|
{
|
|
return $page
|
|
->assertScript('! window.location.search.includes("environment_id=")', true)
|
|
->assertScript('! window.location.search.includes("tenant=")', true)
|
|
->assertScript('! window.location.search.includes("tenant_id=")', true)
|
|
->assertScript('! window.location.search.includes("managed_environment_id=")', true)
|
|
->assertScript('! window.location.search.includes("tenant_scope=")', true)
|
|
->assertScript('! window.location.search.includes("tableFilters")', true);
|
|
}
|
|
|
|
public static function clearWorkspaceHubEnvironmentFilter(mixed $page): mixed
|
|
{
|
|
$page->assertScript('document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\') instanceof HTMLAnchorElement', true);
|
|
$page->script('window.location.assign(document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\').href);');
|
|
|
|
return $page->waitForText(__('localization.shell.no_environment_selected'));
|
|
}
|
|
|
|
private static function findingException(
|
|
ManagedEnvironment $environment,
|
|
User $actor,
|
|
string $requestReason,
|
|
string $decisionReason,
|
|
): FindingException {
|
|
$finding = Finding::factory()->for($environment)->riskAccepted()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'subject_external_id' => str()->slug($requestReason),
|
|
]);
|
|
|
|
$exception = FindingException::query()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'finding_id' => (int) $finding->getKey(),
|
|
'requested_by_user_id' => (int) $actor->getKey(),
|
|
'owner_user_id' => (int) $actor->getKey(),
|
|
'status' => FindingException::STATUS_PENDING,
|
|
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
|
'request_reason' => $requestReason,
|
|
'requested_at' => now()->subDay(),
|
|
'review_due_at' => now()->addDay(),
|
|
'evidence_summary' => ['reference_count' => 0],
|
|
]);
|
|
|
|
$decision = $exception->decisions()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'actor_user_id' => (int) $actor->getKey(),
|
|
'decision_type' => FindingExceptionDecision::TYPE_REQUESTED,
|
|
'reason' => $decisionReason,
|
|
'metadata' => [],
|
|
'decided_at' => now()->subDay(),
|
|
]);
|
|
|
|
$exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();
|
|
|
|
return $exception->fresh(['currentDecision']);
|
|
}
|
|
|
|
private static function evidenceSnapshot(ManagedEnvironment $environment): EvidenceSnapshot
|
|
{
|
|
return EvidenceSnapshot::query()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'status' => EvidenceSnapshotStatus::Active->value,
|
|
'summary' => ['missing_dimensions' => 0, 'stale_dimensions' => 0],
|
|
'generated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
private static function publishedReview(ManagedEnvironment $environment, User $user, EvidenceSnapshot $snapshot): EnvironmentReview
|
|
{
|
|
$review = \composeEnvironmentReviewForTest($environment, $user, $snapshot);
|
|
$review->forceFill([
|
|
'status' => EnvironmentReviewStatus::Published->value,
|
|
'published_at' => now(),
|
|
'published_by_user_id' => (int) $user->getKey(),
|
|
])->save();
|
|
|
|
return $review->fresh();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $attributes
|
|
*/
|
|
private static function auditRecord(?ManagedEnvironment $environment, string $summary, array $attributes = []): AuditLog
|
|
{
|
|
$workspaceId = array_key_exists('workspace_id', $attributes)
|
|
? (int) $attributes['workspace_id']
|
|
: (int) ($environment?->workspace_id);
|
|
|
|
return AuditLog::query()->create(array_merge([
|
|
'workspace_id' => $workspaceId,
|
|
'managed_environment_id' => $environment?->getKey(),
|
|
'actor_email' => 'spec322-browser@example.test',
|
|
'actor_name' => 'Spec322 Browser Operator',
|
|
'action' => 'operation.completed',
|
|
'status' => 'success',
|
|
'resource_type' => 'operation_run',
|
|
'resource_id' => '322',
|
|
'summary' => $summary,
|
|
'metadata' => [],
|
|
'recorded_at' => now(),
|
|
], $attributes));
|
|
}
|
|
}
|