create([ 'tenant_id' => $tenantId, 'status' => 'active', 'app_client_id' => null, 'app_client_secret' => null, ]); $connection = ProviderConnection::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'is_default' => true, 'status' => 'connected', 'entra_tenant_id' => $tenantId, ]); $clientId = 'provider-client-'.$tenant->getKey(); $clientSecret = 'provider-secret-'.$tenant->getKey(); ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'type' => 'client_secret', 'payload' => [ 'client_id' => $clientId, 'client_secret' => $clientSecret, ], ]); return [ 'tenant' => $tenant, 'connection' => $connection, 'client_id' => $clientId, 'client_secret' => $clientSecret, ]; } it('Spec081 smoke: inventory sync uses provider connection credentials with tenant secrets empty', function (): void { $setup = spec081TenantWithDefaultMicrosoftConnection('tenant-spec081-inventory'); $graph = \Mockery::mock(GraphClientInterface::class); $graph->shouldReceive('listPolicies') ->once() ->with( 'deviceConfiguration', \Mockery::on(function (array $options) use ($setup): bool { return ($options['tenant'] ?? null) === $setup['connection']->entra_tenant_id && ($options['client_id'] ?? null) === $setup['client_id'] && ($options['client_secret'] ?? null) === $setup['client_secret']; }), ) ->andReturn(new GraphResponse(success: true, data: [])); app()->instance(GraphClientInterface::class, $graph); $run = app(InventorySyncService::class)->syncNow( $setup['tenant'], [ 'policy_types' => ['deviceConfiguration'], 'categories' => ['Configuration'], 'include_foundations' => false, 'include_dependencies' => false, ], ); expect($run->status)->toBe('success'); }); it('Spec081 smoke: policy sync uses provider connection credentials with tenant secrets empty', function (): void { $setup = spec081TenantWithDefaultMicrosoftConnection('tenant-spec081-policy-sync'); $graph = \Mockery::mock(GraphClientInterface::class); $graph->shouldReceive('listPolicies') ->once() ->with( 'deviceConfiguration', \Mockery::on(function (array $options) use ($setup): bool { return ($options['tenant'] ?? null) === $setup['connection']->entra_tenant_id && ($options['client_id'] ?? null) === $setup['client_id'] && ($options['client_secret'] ?? null) === $setup['client_secret'] && ($options['platform'] ?? null) === 'windows'; }), ) ->andReturn(new GraphResponse(success: true, data: [ [ 'id' => 'cfg-spec081', 'displayName' => 'Spec081 Config', '@odata.type' => '#microsoft.graph.deviceConfiguration', ], ])); app()->instance(GraphClientInterface::class, $graph); $result = app(PolicySyncService::class)->syncPoliciesWithReport( $setup['tenant'], [['type' => 'deviceConfiguration', 'platform' => 'windows']], ); expect($result['failures'])->toBeArray()->toBeEmpty() ->and($result['synced'])->toHaveCount(1); }); it('Spec081 smoke: policy snapshot uses provider connection credentials with tenant secrets empty', function (): void { $setup = spec081TenantWithDefaultMicrosoftConnection('tenant-spec081-snapshot'); $policy = Policy::factory()->create([ 'tenant_id' => (int) $setup['tenant']->getKey(), 'external_id' => 'cfg-snapshot-spec081', 'policy_type' => 'deviceConfiguration', 'platform' => 'windows', ]); $graph = \Mockery::mock(GraphClientInterface::class); $graph->shouldReceive('getPolicy') ->once() ->with( 'deviceConfiguration', 'cfg-snapshot-spec081', \Mockery::on(function (array $options) use ($setup): bool { return ($options['tenant'] ?? null) === $setup['connection']->entra_tenant_id && ($options['client_id'] ?? null) === $setup['client_id'] && ($options['client_secret'] ?? null) === $setup['client_secret'] && ($options['platform'] ?? null) === 'windows'; }), ) ->andReturn(new GraphResponse(success: true, data: [ 'payload' => [ 'id' => 'cfg-snapshot-spec081', 'displayName' => 'Snapshot Policy', '@odata.type' => '#microsoft.graph.deviceConfiguration', ], ])); app()->instance(GraphClientInterface::class, $graph); $result = app(PolicySnapshotService::class)->fetch($setup['tenant'], $policy); expect($result['payload']['id'] ?? null)->toBe('cfg-snapshot-spec081'); }); it('Spec081 smoke: restore execution uses provider connection credentials with tenant secrets empty', function (): void { $setup = spec081TenantWithDefaultMicrosoftConnection('tenant-spec081-restore'); $backupSet = BackupSet::factory()->create([ 'tenant_id' => (int) $setup['tenant']->getKey(), 'status' => 'completed', ]); $policy = Policy::factory()->create([ 'tenant_id' => (int) $setup['tenant']->getKey(), 'external_id' => 'cfg-restore-spec081', 'policy_type' => 'deviceConfiguration', 'platform' => 'windows', 'display_name' => 'Restore Spec081', ]); $backupItem = BackupItem::factory()->create([ 'tenant_id' => (int) $setup['tenant']->getKey(), 'backup_set_id' => (int) $backupSet->getKey(), 'policy_id' => (int) $policy->getKey(), 'policy_identifier' => 'cfg-restore-spec081', 'policy_type' => 'deviceConfiguration', 'platform' => 'windows', 'payload' => [ 'id' => 'cfg-restore-spec081', 'displayName' => 'Restore Spec081', '@odata.type' => '#microsoft.graph.deviceConfiguration', ], 'metadata' => ['displayName' => 'Restore Spec081'], ]); $graph = \Mockery::mock(GraphClientInterface::class); $graph->shouldReceive('applyPolicy') ->once() ->with( 'deviceConfiguration', 'cfg-restore-spec081', \Mockery::type('array'), \Mockery::on(function (array $options) use ($setup): bool { return ($options['tenant'] ?? null) === $setup['connection']->entra_tenant_id && ($options['client_id'] ?? null) === $setup['client_id'] && ($options['client_secret'] ?? null) === $setup['client_secret'] && ($options['platform'] ?? null) === 'windows'; }), ) ->andReturn(new GraphResponse(success: true, data: [])); app()->instance(GraphClientInterface::class, $graph); $restoreRun = app(RestoreService::class)->execute( tenant: $setup['tenant'], backupSet: $backupSet, selectedItemIds: [(int) $backupItem->getKey()], dryRun: false, actorEmail: 'spec081@example.test', actorName: 'Spec081', ); expect($restoreRun->status)->toBe('completed'); }); it('Spec081 smoke: scope tag resolver uses provider connection credentials with tenant secrets empty', function (): void { $setup = spec081TenantWithDefaultMicrosoftConnection('tenant-spec081-scope-tags'); $graph = \Mockery::mock(GraphClientInterface::class); $graph->shouldReceive('request') ->once() ->with( 'GET', '/deviceManagement/roleScopeTags', \Mockery::on(function (array $options) use ($setup): bool { return ($options['tenant'] ?? null) === $setup['connection']->entra_tenant_id && ($options['client_id'] ?? null) === $setup['client_id'] && ($options['client_secret'] ?? null) === $setup['client_secret'] && ($options['query']['$select'] ?? null) === 'id,displayName'; }), ) ->andReturn(new GraphResponse( success: true, data: [ 'value' => [ ['id' => '0', 'displayName' => 'Default'], ], ], )); app()->instance(GraphClientInterface::class, $graph); $tags = app(ScopeTagResolver::class)->resolve(['0'], $setup['tenant']); expect($tags)->toBe([['id' => '0', 'displayName' => 'Default']]); }); it('Spec081 smoke: RBAC health uses provider connection credentials with tenant secrets empty', function (): void { $setup = spec081TenantWithDefaultMicrosoftConnection('tenant-spec081-rbac'); $tenant = $setup['tenant']; $tenant->forceFill([ 'rbac_group_id' => 'group-spec081', 'rbac_role_assignment_id' => null, 'rbac_status_reason' => null, 'rbac_last_warnings' => [], ])->save(); $graph = \Mockery::mock(GraphClientInterface::class); $graph->shouldReceive('request') ->andReturnUsing(function (string $method, string $path, array $options = []) use ($setup): GraphResponse { if ($method === 'GET' && $path === 'servicePrincipals') { expect($options['tenant'] ?? null)->toBe($setup['connection']->entra_tenant_id) ->and($options['client_id'] ?? null)->toBe($setup['client_id']) ->and($options['client_secret'] ?? null)->toBe($setup['client_secret']) ->and($options['query']['$filter'] ?? null)->toBe("appId eq '{$setup['client_id']}'"); return new GraphResponse(success: true, data: ['value' => [['id' => 'sp-spec081']]]); } if ($method === 'GET' && $path === 'groups/group-spec081') { return new GraphResponse(success: true, data: ['id' => 'group-spec081']); } if ($method === 'GET' && $path === 'groups/group-spec081/members') { return new GraphResponse(success: true, data: [ 'value' => [ ['id' => 'sp-spec081'], ], ]); } throw new RuntimeException("Unexpected Graph request: {$method} {$path}"); }); app()->instance(GraphClientInterface::class, $graph); $result = app(RbacHealthService::class)->check($tenant); expect($result['status'])->toBe('missing') ->and($result['reason'])->toBe(RbacReason::AssignmentMissing->value); });