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); } });