create([ 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => fake()->uuid(), ]); ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'payload' => [ 'tenant_id' => (string) $connection->entra_tenant_id, 'client_id' => fake()->uuid(), 'client_secret' => fake()->sha1(), ], ]); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'user_id' => (int) $user->getKey(), 'type' => 'provider.connection.check', 'status' => 'running', 'outcome' => 'pending', 'context' => [ 'provider_connection_id' => (int) $connection->getKey(), ], ]); $this->mock(GraphClientInterface::class, function ($mock): void { $mock->shouldReceive('getOrganization') ->once() ->andReturn(new GraphResponse(false, [], 401, ['Bearer super-secret-token'])); }); $job = new ProviderConnectionHealthCheckJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), providerConnectionId: (int) $connection->getKey(), operationRun: $run, ); $job->handle( healthCheck: app(MicrosoftProviderHealthCheck::class), runs: app(OperationRunService::class), ); $run = $run->fresh(); expect($run)->not->toBeNull(); expect($run->status)->toBe('completed'); expect($run->outcome)->toBe('failed'); $context = is_array($run->context) ? $run->context : []; $report = $context['verification_report'] ?? null; expect($report)->toBeArray(); expect(VerificationReportSchema::isValidReport($report))->toBeTrue(); expect(json_encode($report))->not->toContain('Bearer '); expect($report['checks'][0]['reason_code'] ?? null)->toBe('authentication_failed'); foreach (($report['checks'] ?? []) as $check) { expect($check)->toBeArray(); foreach (($check['evidence'] ?? []) as $pointer) { expect($pointer)->toBeArray(); expect(array_keys($pointer))->toEqualCanonicalizing(['kind', 'value']); } } $audit = AuditLog::query() ->where('workspace_id', (int) $tenant->workspace_id) ->where('action', AuditActionId::VerificationCompleted->value) ->latest('id') ->first(); expect($audit)->not->toBeNull(); expect($audit?->metadata)->toMatchArray([ 'operation_run_id' => (int) $run->getKey(), ]); }); it('writes a verification report for successful provider connection checks', function (): void { [$user, $tenant] = createUserWithTenant(role: 'operator'); $connection = ProviderConnection::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => fake()->uuid(), ]); ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'payload' => [ 'tenant_id' => (string) $connection->entra_tenant_id, 'client_id' => fake()->uuid(), 'client_secret' => fake()->sha1(), ], ]); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'user_id' => (int) $user->getKey(), 'type' => 'provider.connection.check', 'status' => 'running', 'outcome' => 'pending', 'context' => [ 'provider_connection_id' => (int) $connection->getKey(), ], ]); $this->mock(GraphClientInterface::class, function ($mock): void { $mock->shouldReceive('getOrganization') ->once() ->andReturn(new GraphResponse(true, [ 'id' => 'org_123', 'displayName' => 'Org 123', ], 200)); $required = collect(config('intune_permissions.permissions', [])) ->filter(fn (mixed $row): bool => is_array($row)) ->map(fn (array $row): string => (string) ($row['key'] ?? '')) ->filter() ->values() ->all(); $mock->shouldReceive('getServicePrincipalPermissions') ->once() ->andReturn(new GraphResponse(true, [ 'permissions' => $required, ], 200)); }); $job = new ProviderConnectionHealthCheckJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), providerConnectionId: (int) $connection->getKey(), operationRun: $run, ); $job->handle( healthCheck: app(MicrosoftProviderHealthCheck::class), runs: app(OperationRunService::class), ); $run = $run->fresh(); expect($run)->not->toBeNull(); expect($run->status)->toBe('completed'); expect($run->outcome)->toBe('succeeded'); $context = is_array($run->context) ? $run->context : []; $report = $context['verification_report'] ?? null; expect($report)->toBeArray(); expect(VerificationReportSchema::isValidReport($report))->toBeTrue(); $checks = is_array($report['checks'] ?? null) ? $report['checks'] : []; $checkKeys = collect($checks) ->filter(fn ($check) => is_array($check)) ->map(fn (array $check): string => (string) ($check['key'] ?? '')) ->filter() ->values() ->all(); expect($checkKeys)->toContain('provider.connection.check'); expect($checkKeys)->toContain('permissions.admin_consent'); $counts = is_array($report['summary']['counts'] ?? null) ? $report['summary']['counts'] : []; expect((int) ($counts['total'] ?? 0))->toBe(count($checks)); expect((int) ($counts['total'] ?? 0))->toBeLessThanOrEqual(7); }); it('degrades permission clusters to warnings when live permissions refresh is throttled', function (): void { [$user, $tenant] = createUserWithTenant(role: 'operator'); $connection = ProviderConnection::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => fake()->uuid(), ]); ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'payload' => [ 'tenant_id' => (string) $connection->entra_tenant_id, 'client_id' => fake()->uuid(), 'client_secret' => fake()->sha1(), ], ]); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'user_id' => (int) $user->getKey(), 'type' => 'provider.connection.check', 'status' => 'running', 'outcome' => 'pending', 'context' => [ 'provider_connection_id' => (int) $connection->getKey(), ], ]); $this->mock(GraphClientInterface::class, function ($mock): void { $mock->shouldReceive('getOrganization') ->once() ->andReturn(new GraphResponse(true, [ 'id' => 'org_123', 'displayName' => 'Org 123', ], 200)); $mock->shouldReceive('getServicePrincipalPermissions') ->once() ->andReturn(new GraphResponse(false, [], 429, ['Too Many Requests'])); }); $job = new ProviderConnectionHealthCheckJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), providerConnectionId: (int) $connection->getKey(), operationRun: $run, ); $job->handle( healthCheck: app(MicrosoftProviderHealthCheck::class), runs: app(OperationRunService::class), ); $run = $run->fresh(); expect($run?->outcome)->toBe('succeeded'); $context = is_array($run?->context) ? $run->context : []; $report = $context['verification_report'] ?? null; expect($report)->toBeArray(); expect(VerificationReportSchema::isValidReport($report))->toBeTrue(); $checks = is_array($report['checks'] ?? null) ? $report['checks'] : []; $adminConsentCheck = collect($checks) ->filter(fn (mixed $check): bool => is_array($check)) ->firstWhere('key', 'permissions.admin_consent'); expect($adminConsentCheck)->toBeArray(); expect($adminConsentCheck['status'] ?? null)->toBe('warn'); expect($adminConsentCheck['blocking'] ?? null)->toBeFalse(); expect($adminConsentCheck['reason_code'] ?? null)->toBe('throttled'); expect((string) ($adminConsentCheck['message'] ?? ''))->toContain('Unable to refresh observed permissions inventory'); expect(TenantPermission::query()->where('tenant_id', (int) $tenant->getKey())->count())->toBe(0); }); it('degrades permission clusters to warnings when live permissions refresh cannot map assignments', function (): void { [$user, $tenant] = createUserWithTenant(role: 'operator'); $connection = ProviderConnection::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => fake()->uuid(), ]); ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'payload' => [ 'tenant_id' => (string) $connection->entra_tenant_id, 'client_id' => fake()->uuid(), 'client_secret' => fake()->sha1(), ], ]); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'user_id' => (int) $user->getKey(), 'type' => 'provider.connection.check', 'status' => 'running', 'outcome' => 'pending', 'context' => [ 'provider_connection_id' => (int) $connection->getKey(), ], ]); $this->mock(GraphClientInterface::class, function ($mock): void { $mock->shouldReceive('getOrganization') ->once() ->andReturn(new GraphResponse(true, [ 'id' => 'org_123', 'displayName' => 'Org 123', ], 200)); $mock->shouldReceive('getServicePrincipalPermissions') ->twice() ->andReturn(new GraphResponse(true, [ 'permissions' => [], 'diagnostics' => [ 'assignments_total' => 1, 'mapped_total' => 0, 'graph_roles_total' => 1, ], ], 200)); }); $job = new ProviderConnectionHealthCheckJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), providerConnectionId: (int) $connection->getKey(), operationRun: $run, ); $job->handle( healthCheck: app(MicrosoftProviderHealthCheck::class), runs: app(OperationRunService::class), ); $credential = ProviderCredential::query() ->where('provider_connection_id', (int) $connection->getKey()) ->first(); $payload = is_array($credential?->payload) ? $credential->payload : []; $comparison = app(\App\Services\Intune\TenantPermissionService::class)->compare( $tenant, persist: true, liveCheck: true, useConfiguredStub: false, graphOptions: [ 'tenant' => (string) $connection->entra_tenant_id, 'client_id' => (string) ($payload['client_id'] ?? ''), 'client_secret' => (string) ($payload['client_secret'] ?? ''), ], ); expect($comparison['live_check']['reason_code'] ?? null)->toBe('permission_mapping_failed'); $run = $run->fresh(); $context = is_array($run?->context) ? $run->context : []; $report = $context['verification_report'] ?? null; expect($report)->toBeArray(); expect(VerificationReportSchema::isValidReport($report))->toBeTrue(); $checks = is_array($report['checks'] ?? null) ? $report['checks'] : []; $adminConsentCheck = collect($checks) ->filter(fn (mixed $check): bool => is_array($check)) ->firstWhere('key', 'permissions.admin_consent'); expect($adminConsentCheck)->toBeArray(); expect($adminConsentCheck['status'] ?? null)->toBe('warn'); expect($adminConsentCheck['blocking'] ?? null)->toBeFalse(); expect($adminConsentCheck['reason_code'] ?? null)->toBeIn(['permission_mapping_failed', 'unknown_error']); expect((string) ($adminConsentCheck['message'] ?? ''))->toContain('Unable to refresh observed permissions inventory'); expect(TenantPermission::query()->where('tenant_id', (int) $tenant->getKey())->count())->toBe(0); });