create($attributes + [ 'status' => 'active', 'app_client_id' => null, 'app_client_secret' => null, ]); $connection = ProviderConnection::factory()->consentGranted()->create([ 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'is_default' => true, 'consent_status' => 'granted', 'entra_tenant_id' => (string) ($tenant->tenant_id ?: 'tenant-'.$tenant->getKey()), ]); ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'type' => 'client_secret', 'payload' => [ 'client_id' => 'provider-client-'.$tenant->getKey(), 'client_secret' => 'provider-secret-'.$tenant->getKey(), ], ]); return $tenant; } it('marks previously observed policies missing when provider list omits them', function (): void { $tenant = tenantWithDefaultMicrosoftConnectionForProviderMissing(); $present = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'policy-present', 'policy_type' => 'deviceConfiguration', 'display_name' => 'Old present', 'ignored_at' => null, 'missing_from_provider_at' => null, ]); $missing = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'policy-missing', 'policy_type' => 'deviceConfiguration', 'display_name' => 'Missing from provider', 'ignored_at' => null, 'missing_from_provider_at' => null, ]); mock(GraphLogger::class) ->shouldReceive('logRequest', 'logResponse') ->zeroOrMoreTimes() ->andReturnNull(); mock(GraphClientInterface::class) ->shouldReceive('listPolicies') ->once() ->with('deviceConfiguration', mockery::type('array')) ->andReturn(new GraphResponse( success: true, data: [ [ 'id' => 'policy-present', 'displayName' => 'Provider present', 'platform' => 'windows', ], ], )); app(PolicySyncService::class)->syncPolicies($tenant, [ ['type' => 'deviceConfiguration', 'platform' => 'windows'], ]); $present->refresh(); $missing->refresh(); expect($present->display_name)->toBe('Provider present') ->and($present->ignored_at)->toBeNull() ->and($present->missing_from_provider_at)->toBeNull() ->and($missing->ignored_at)->toBeNull() ->and($missing->missing_from_provider_at)->not->toBeNull() ->and($missing->visibilityState())->toBe(Policy::VISIBILITY_PROVIDER_MISSING); expect(AuditLog::query() ->where('tenant_id', $tenant->id) ->where('action', AuditActionId::PolicyProviderMissingDetected->value) ->where('resource_id', (string) $missing->getKey()) ->exists())->toBeTrue(); }); it('clears provider missing on reappearance without clearing local ignore', function (): void { $tenant = tenantWithDefaultMicrosoftConnectionForProviderMissing(); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'policy-returned', 'policy_type' => 'deviceConfiguration', 'display_name' => 'Returned policy', 'ignored_at' => now()->subDay(), 'missing_from_provider_at' => now()->subDay(), ]); mock(GraphLogger::class) ->shouldReceive('logRequest', 'logResponse') ->zeroOrMoreTimes() ->andReturnNull(); mock(GraphClientInterface::class) ->shouldReceive('listPolicies') ->once() ->with('deviceConfiguration', mockery::type('array')) ->andReturn(new GraphResponse( success: true, data: [ [ 'id' => 'policy-returned', 'displayName' => 'Returned from provider', 'platform' => 'windows', ], ], )); app(PolicySyncService::class)->syncPolicies($tenant, [ ['type' => 'deviceConfiguration', 'platform' => 'windows'], ]); $policy->refresh(); expect($policy->display_name)->toBe('Returned from provider') ->and($policy->ignored_at)->not->toBeNull() ->and($policy->missing_from_provider_at)->toBeNull() ->and($policy->visibilityState())->toBe(Policy::VISIBILITY_IGNORED_LOCALLY); expect(AuditLog::query() ->where('tenant_id', $tenant->id) ->where('action', AuditActionId::PolicyProviderMissingCleared->value) ->where('resource_id', (string) $policy->getKey()) ->exists())->toBeTrue(); });