TenantAtlas/apps/platform/tests/Feature/Filament/CoverageV2ReadinessPageTest.php
ahmido 4aaec3521a feat: add coverage v2 operator surface (#485)
Automated PR provided by Codex via Gitea API.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #485
2026-06-26 12:50:36 +00:00

469 lines
21 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\TenantConfiguration\CoverageV2Readiness;
use App\Filament\Widgets\TenantConfiguration\CoverageV2ResourceInstancesTable;
use App\Filament\Widgets\TenantConfiguration\CoverageV2ResourceTypesTable;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\TenantConfigurationResource;
use App\Models\TenantConfigurationResourceEvidence;
use App\Models\TenantConfigurationResourceType;
use App\Models\TenantConfigurationSupportedScope;
use App\Models\User;
use App\Models\WorkspaceMembership;
use App\Services\Auth\ManagedEnvironmentAccessDecision;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Services\TenantConfiguration\CoverageV2ReadinessReadModel;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\TenantConfiguration\CaptureOutcome;
use App\Support\TenantConfiguration\ClaimState;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\EvidenceState;
use App\Support\TenantConfiguration\IdentityState;
use App\Support\TenantConfiguration\SourceClass;
use App\Support\TenantConfiguration\SupportState;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Livewire\Livewire;
uses(RefreshDatabase::class);
function coverageV2ActingAs(User $user, ManagedEnvironment $environment): void
{
test()->actingAs($user);
$environment->makeCurrent();
Filament::setTenant($environment, true);
}
/**
* @return array{
* contentType: TenantConfigurationResourceType,
* blockedType: TenantConfigurationResourceType,
* betaType: TenantConfigurationResourceType,
* connection: ProviderConnection,
* contentResource: TenantConfigurationResource,
* blockedResource: TenantConfigurationResource,
* betaResource: TenantConfigurationResource
* }
*/
function seedCoverageV2ReadinessScenario(ManagedEnvironment $environment): array
{
$connection = ProviderConnection::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'display_name' => 'Spec 418 Microsoft provider',
]);
$contentType = TenantConfigurationResourceType::factory()->create([
'canonical_type' => 'spec418ContentType',
'display_name' => 'Spec 418 content type',
'source_class' => SourceClass::Tcm->value,
'support_state' => SupportState::Supported->value,
'default_coverage_level' => CoverageLevel::ContentBacked->value,
'default_claim_state' => ClaimState::ClaimAllowed->value,
]);
$blockedType = TenantConfigurationResourceType::factory()->create([
'canonical_type' => 'spec418BlockedType',
'display_name' => 'Spec 418 blocked type',
'source_class' => SourceClass::GraphV1Fallback->value,
'support_state' => SupportState::FallbackSupported->value,
'default_coverage_level' => CoverageLevel::Detected->value,
'default_claim_state' => ClaimState::ClaimLimited->value,
]);
$betaType = TenantConfigurationResourceType::factory()->create([
'canonical_type' => 'spec418BetaType',
'display_name' => 'Spec 418 beta type',
'source_class' => SourceClass::GraphBetaExperimental->value,
'support_state' => SupportState::Experimental->value,
'default_coverage_level' => CoverageLevel::Detected->value,
'default_claim_state' => ClaimState::ClaimBlocked->value,
]);
TenantConfigurationSupportedScope::factory()->create([
'scope_key' => 'spec418_scope',
'display_name' => 'Spec 418 scope',
'minimum_coverage_level' => CoverageLevel::ContentBacked->value,
'included_resource_types' => [$contentType->canonical_type],
'allow_graph_fallback' => false,
'allow_beta' => false,
'customer_claims_allowed' => false,
]);
$contentResource = TenantConfigurationResource::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
'resource_type_id' => (int) $contentType->getKey(),
'canonical_type' => $contentType->canonical_type,
'source_display_name' => 'Spec 418 captured assignment filter',
'source_class' => SourceClass::Tcm->value,
'latest_evidence_state' => EvidenceState::ContentBacked->value,
'latest_identity_state' => IdentityState::Stable->value,
'latest_claim_state' => ClaimState::ClaimAllowed->value,
'latest_captured_at' => now(),
]);
$blockedResource = TenantConfigurationResource::factory()
->identityConflict()
->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
'resource_type_id' => (int) $blockedType->getKey(),
'canonical_type' => $blockedType->canonical_type,
'source_display_name' => 'Spec 418 conflicting assignment filter',
'source_class' => SourceClass::GraphV1Fallback->value,
'latest_evidence_state' => EvidenceState::PermissionBlocked->value,
'latest_captured_at' => now(),
]);
$betaResource = TenantConfigurationResource::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
'resource_type_id' => (int) $betaType->getKey(),
'canonical_type' => $betaType->canonical_type,
'source_display_name' => 'Spec 418 beta resource',
'source_class' => SourceClass::GraphBetaExperimental->value,
'latest_evidence_state' => EvidenceState::NotCaptured->value,
'latest_identity_state' => IdentityState::Stable->value,
'latest_claim_state' => ClaimState::ClaimBlocked->value,
]);
foreach ([$contentResource, $blockedResource] as $resource) {
$run = OperationRun::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'type' => OperationRunType::TenantConfigurationCapture->value,
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
]);
$evidence = TenantConfigurationResourceEvidence::factory()->create([
'resource_id' => (int) $resource->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
'resource_type_id' => (int) $resource->resource_type_id,
'operation_run_id' => (int) $run->getKey(),
'payload_hash' => $resource->is($blockedResource)
? str_repeat('b', 64)
: str_repeat('a', 64),
'raw_payload' => ['secret' => 'raw-response-secret'],
'normalized_payload' => ['secret' => 'normalized-secret'],
'permission_context' => ['token' => 'permission-secret'],
'evidence_state' => $resource->latest_evidence_state->value,
'coverage_level' => $resource->is($blockedResource)
? CoverageLevel::Detected->value
: CoverageLevel::ContentBacked->value,
'capture_outcome' => $resource->is($blockedResource)
? CaptureOutcome::BlockedPermission->value
: CaptureOutcome::Captured->value,
'source_contract_key' => 'spec418.contract',
'source_version' => 'v1.0',
'source_schema_hash' => 'spec418-schema-hash',
'captured_at' => now(),
]);
$resource->forceFill([
'latest_evidence_id' => (int) $evidence->getKey(),
'latest_payload_hash' => (string) $evidence->payload_hash,
])->save();
}
return [
'contentType' => $contentType,
'blockedType' => $blockedType,
'betaType' => $betaType,
'connection' => $connection,
'contentResource' => $contentResource->refresh(),
'blockedResource' => $blockedResource->refresh(),
'betaResource' => $betaResource->refresh(),
];
}
it('renders the read-only Coverage v2 readiness surface with scoped summary and no raw payloads', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner');
seedCoverageV2ReadinessScenario($environment);
$foreignEnvironment = ManagedEnvironment::factory()->create(['workspace_id' => (int) $environment->workspace_id]);
ProviderConnection::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $foreignEnvironment->getKey(),
'display_name' => 'Foreign provider must not render',
]);
coverageV2ActingAs($user, $environment);
bindFailHardGraphClient();
$response = assertNoOutboundHttp(fn () => $this->get(CoverageV2Readiness::getUrl(tenant: $environment)));
$response->assertOk()
->assertSee('Coverage v2 Readiness')
->assertSee('Blocked')
->assertSee('Activation readiness')
->assertSee('Reason')
->assertSee('Identity conflict is the highest-priority activation blocker.')
->assertSee('Next step')
->assertSee('Inspect Spec 418 conflicting assignment filter and resolve the blocker before cutover planning.')
->assertSee('Resource types')
->assertSee('Activation blockers')
->assertSee('Identity conflict')
->assertSee('Claim blocked')
->assertSee('Spec 418 content type')
->assertSee('Spec 418 conflicting assignment filter')
->assertDontSee('raw-response-secret')
->assertDontSee('normalized-secret')
->assertDontSee('permission-secret')
->assertDontSee('Foreign provider must not render')
->assertDontSee('customer-ready')
->assertDontSee('Evidence gaps');
});
it('explains unknown readiness when no Coverage v2 resources are captured', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner');
coverageV2ActingAs($user, $environment);
$response = $this->get(CoverageV2Readiness::getUrl(tenant: $environment));
$response->assertOk()
->assertSee('Unknown')
->assertSee('Activation readiness')
->assertSee('Reason')
->assertSee('No Coverage v2 resource rows exist for this managed environment.')
->assertSee('Next step')
->assertSee('Review capture prerequisites before using Coverage v2 as activation proof.')
->assertSee('No captured Coverage v2 resources');
expect($response->getContent())
->toContain('No captured Coverage v2 resources')
->and(strpos((string) $response->getContent(), 'No captured Coverage v2 resources'))
->toBeLessThan(strpos((string) $response->getContent(), 'Resource type registry'));
});
it('derives summary counts and top blockers from Coverage v2 state only', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner');
seedCoverageV2ReadinessScenario($environment);
coverageV2ActingAs($user, $environment);
$readModel = app(CoverageV2ReadinessReadModel::class);
$summary = $readModel->summary($environment);
$blockers = $readModel->activationBlockers($environment)->pluck('blocker')->all();
expect($summary)
->toMatchArray([
'readiness_state' => 'blocked',
'resources_total' => 3,
'content_backed_count' => 1,
'activation_blocker_count' => 6,
'identity_conflict_count' => 1,
'claim_allowed_count' => 1,
'claim_blocked_count' => 2,
])
->and($summary['beta_experimental_count'])
->toBeGreaterThanOrEqual(1)
->and($summary['graph_fallback_count'])
->toBeGreaterThanOrEqual(1)
->and(array_slice($blockers, 0, 5))
->toBe([
'identity_conflict',
'claim_blocked',
'permission_blocked',
'not_captured',
'beta_experimental',
]);
});
it('renders resource type registry filters and supported-scope inclusion', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner');
$scenario = seedCoverageV2ReadinessScenario($environment);
coverageV2ActingAs($user, $environment);
Livewire::actingAs($user)
->test(CoverageV2ResourceTypesTable::class)
->assertTableColumnExists('display_name')
->assertTableColumnExists('canonical_type')
->assertTableColumnExists('source_class')
->assertTableColumnExists('support_state')
->assertTableColumnExists('default_coverage_level')
->assertTableColumnExists('supported_scope')
->assertTableColumnExists('default_claim_state')
->assertTableActionExists('inspect', fn (Action $action): bool => $action->getLabel() === 'Inspect' && ! $action->isConfirmationRequired(), $scenario['contentType'])
->searchTable('Spec 418')
->assertCanSeeTableRecords([$scenario['contentType'], $scenario['blockedType'], $scenario['betaType']])
->filterTable('supported_scope', 'spec418_scope')
->assertCanSeeTableRecords([$scenario['contentType']])
->assertCanNotSeeTableRecords([$scenario['blockedType'], $scenario['betaType']])
->removeTableFilters()
->filterTable('beta_experimental', 'yes')
->assertCanSeeTableRecords([$scenario['betaType']])
->assertCanNotSeeTableRecords([$scenario['contentType'], $scenario['blockedType']]);
});
it('renders resource instance states, filters, and one read-only inspect action', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner');
$scenario = seedCoverageV2ReadinessScenario($environment);
coverageV2ActingAs($user, $environment);
Livewire::actingAs($user)
->test(CoverageV2ResourceInstancesTable::class, ['environmentId' => (int) $environment->getKey()])
->assertTableColumnExists('source_display_name')
->assertTableColumnExists('resourceType.display_name')
->assertTableColumnExists('providerConnection.display_name')
->assertTableColumnExists('coverage_level')
->assertTableColumnExists('latest_evidence_state')
->assertTableColumnExists('latest_identity_state')
->assertTableColumnExists('latest_claim_state')
->assertTableColumnExists('latest_payload_hash')
->assertCanSeeTableRecords([$scenario['contentResource'], $scenario['blockedResource']])
->assertTableColumnFormattedStateSet('latest_claim_state', 'Claim blocked', $scenario['blockedResource'])
->assertTableColumnFormattedStateSet('latest_identity_state', 'Identity conflict', $scenario['blockedResource'])
->assertTableActionExists('inspect', fn (Action $action): bool => $action->getLabel() === 'Inspect' && ! $action->isConfirmationRequired(), $scenario['blockedResource'])
->filterTable('latest_identity_state', IdentityState::IdentityConflict->value)
->assertCanSeeTableRecords([$scenario['blockedResource']])
->assertCanNotSeeTableRecords([$scenario['contentResource']]);
});
it('redacts raw evidence payload fields from inspect disclosure', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner');
$scenario = seedCoverageV2ReadinessScenario($environment);
coverageV2ActingAs($user, $environment);
$details = app(CoverageV2ReadinessReadModel::class)
->inspectDetails($scenario['blockedResource'], $environment, $user);
$run = $scenario['blockedResource']->latestEvidence?->operationRun;
$outsider = User::factory()->create();
$outsiderDetails = app(CoverageV2ReadinessReadModel::class)
->inspectDetails($scenario['blockedResource'], $environment, $outsider);
$html = view('filament.modals.tenant-configuration.coverage-v2-resource-inspect', [
'details' => $details,
])->render();
expect($details)
->toHaveKey('identity_reason_code', 'same_scope_derived_identity_collision')
->toHaveKey('operation_run_url', OperationRunLinks::view($run, $environment))
->not->toHaveKeys(['raw_payload', 'normalized_payload', 'permission_context'])
->and($outsiderDetails['operation_run_url'] ?? null)
->toBeNull()
->and($html)
->toContain('Identity conflict')
->toContain('same_scope_derived_identity_collision')
->toContain('spec418-schema-hash')
->not->toContain('raw-response-secret')
->not->toContain('normalized-secret')
->not->toContain('permission-secret')
->not->toContain('raw_payload')
->not->toContain('normalized_payload')
->not->toContain('permission_context');
});
it('redacts raw registry metadata fields from resource type inspect disclosure', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner');
$scenario = seedCoverageV2ReadinessScenario($environment);
coverageV2ActingAs($user, $environment);
$details = app(CoverageV2ReadinessReadModel::class)
->resourceTypeInspectDetails($scenario['contentType'], 'spec418_scope');
$html = view('filament.modals.tenant-configuration.coverage-v2-resource-type-inspect', [
'details' => $details,
])->render();
expect($details)
->toHaveKey('canonical_type', 'spec418ContentType')
->not->toHaveKeys(['metadata', 'raw_payload', 'normalized_payload', 'permission_context'])
->and($html)
->toContain('Spec 418 scope')
->toContain('Content backed')
->toContain('Claim allowed')
->not->toContain('raw-response-secret')
->not->toContain('normalized-secret')
->not->toContain('permission-secret')
->not->toContain('metadata')
->not->toContain('raw_payload')
->not->toContain('normalized_payload')
->not->toContain('permission_context');
});
it('returns 404 when the actor is not a member of the requested workspace', function (): void {
[$owner, $environment] = createUserWithTenant(role: 'owner');
seedCoverageV2ReadinessScenario($environment);
$outsider = User::factory()->create();
$this->actingAs($outsider)
->get(CoverageV2Readiness::getUrl(tenant: $environment))
->assertNotFound();
});
it('returns 404 when the actor belongs to the workspace but not the requested environment', function (): void {
[$owner, $environment] = createUserWithTenant(role: 'owner');
seedCoverageV2ReadinessScenario($environment);
$otherEnvironment = ManagedEnvironment::factory()->create(['workspace_id' => (int) $environment->workspace_id]);
$outsider = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'user_id' => (int) $outsider->getKey(),
'role' => 'owner',
]);
DB::table('managed_environment_memberships')->insert([
'id' => (string) Str::uuid(),
'managed_environment_id' => (int) $otherEnvironment->getKey(),
'user_id' => (int) $outsider->getKey(),
'role' => 'owner',
'source' => 'manual',
'created_at' => now(),
'updated_at' => now(),
]);
coverageV2ActingAs($outsider, $environment);
$this->get(CoverageV2Readiness::getUrl(tenant: $environment))
->assertNotFound();
});
it('returns 403 when environment scope is valid but the capability is denied', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner');
seedCoverageV2ReadinessScenario($environment);
coverageV2ActingAs($user, $environment);
app()->instance(ManagedEnvironmentAccessScopeResolver::class, new class
{
public function decision(User $user, ManagedEnvironment $environment, ?string $requiredCapability = null): ManagedEnvironmentAccessDecision
{
return new ManagedEnvironmentAccessDecision(
workspaceId: (int) $environment->workspace_id,
managedEnvironmentId: (int) $environment->getKey(),
userId: (int) $user->getKey(),
workspaceMember: true,
workspaceRole: 'owner',
explicitScopeRowsPresent: false,
managedEnvironmentAllowed: true,
failedBoundary: 'capability',
requiredCapability: $requiredCapability,
capabilityAllowed: false,
denialHttpStatus: 403,
);
}
});
try {
$this->get(CoverageV2Readiness::getUrl(tenant: $environment))
->assertForbidden();
} finally {
app()->forgetInstance(ManagedEnvironmentAccessScopeResolver::class);
}
});