## Summary - separate provider-missing policy presence from local ignore semantics by introducing `missing_from_provider_at` - update policy, backup, and restore surfaces so current-state capture stays honest while historical restore continuity remains available - add focused sync, Filament, backup, restore, localization, and badge coverage for the new provider-missing behavior ## Scope - policy sync and model truth - policy resource visibility, badges, labels, and action gating - backup/export eligibility and restore continuity messaging - spec 261 artifacts and focused tests ## Validation - feature-specific Pest coverage is included in the branch - validation was not re-run as part of this commit/push/PR handoff Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #316
158 lines
5.2 KiB
PHP
158 lines
5.2 KiB
PHP
<?php
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\Policy;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ProviderCredential;
|
|
use App\Models\Tenant;
|
|
use App\Services\Graph\GraphClientInterface;
|
|
use App\Services\Graph\GraphLogger;
|
|
use App\Services\Graph\GraphResponse;
|
|
use App\Services\Intune\PolicySyncService;
|
|
use App\Support\Audit\AuditActionId;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
use function Pest\Laravel\mock;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function tenantWithDefaultMicrosoftConnectionForProviderMissing(array $attributes = []): Tenant
|
|
{
|
|
$tenant = Tenant::factory()->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();
|
|
});
|