TenantAtlas/tests/Feature/PolicySyncServiceTest.php
ahmido 412dd7ad66 feat/017-policy-types-mam-endpoint-security-baselines (#23)
Hydrate configurationPolicies/{id}/settings for endpoint security/baseline policies so snapshots include real rule data.
Treat those types like Settings Catalog policies in the normalizer so they show the searchable settings table, recognizable categories, and readable choice values (firewall-specific formatting + interface badge parsing).
Improve “General” tab cards: badge lists for platforms/technologies, template reference summary (name/family/version/ID), and ISO timestamps rendered as YYYY‑MM‑DD HH:MM:SS; added regression test for the view.

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #23
2026-01-03 02:06:35 +00:00

297 lines
9.6 KiB
PHP

<?php
use App\Models\Policy;
use App\Models\PolicyVersion;
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 function Pest\Laravel\mock;
it('marks targeted managed app configurations as ignored during sync', function () {
$tenant = Tenant::factory()->create([
'status' => 'active',
]);
$policy = Policy::factory()->create([
'tenant_id' => $tenant->id,
'external_id' => 'policy-1',
'policy_type' => 'appProtectionPolicy',
'ignored_at' => null,
]);
$logger = mock(GraphLogger::class);
$logger->shouldReceive('logRequest')
->zeroOrMoreTimes()
->andReturnNull();
$logger->shouldReceive('logResponse')
->zeroOrMoreTimes()
->andReturnNull();
mock(GraphClientInterface::class)
->shouldReceive('listPolicies')
->once()
->andReturn(new GraphResponse(
success: true,
data: [
[
'id' => 'policy-1',
'displayName' => 'Ignored policy',
'@odata.type' => '#microsoft.graph.targetedManagedAppConfiguration',
],
],
));
$service = app(PolicySyncService::class);
$synced = $service->syncPolicies($tenant, [
['type' => 'appProtectionPolicy'],
]);
$policy->refresh();
expect($policy->ignored_at)->not->toBeNull();
expect($synced)->toBeArray()->toBeEmpty();
});
it('uses isof filters for windows update rings and supports feature/quality update profiles', function () {
$supported = config('tenantpilot.supported_policy_types');
$byType = collect($supported)->keyBy('type');
expect($byType)->toHaveKeys(['deviceConfiguration', 'windowsUpdateRing', 'windowsFeatureUpdateProfile', 'windowsQualityUpdateProfile']);
expect($byType['deviceConfiguration']['filter'] ?? null)
->toBe("not isof('microsoft.graph.windowsUpdateForBusinessConfiguration')");
expect($byType['windowsUpdateRing']['filter'] ?? null)
->toBe("isof('microsoft.graph.windowsUpdateForBusinessConfiguration')");
expect($byType['windowsFeatureUpdateProfile']['endpoint'] ?? null)
->toBe('deviceManagement/windowsFeatureUpdateProfiles');
expect($byType['windowsQualityUpdateProfile']['endpoint'] ?? null)
->toBe('deviceManagement/windowsQualityUpdateProfiles');
});
it('includes managed device app configurations in supported types', function () {
$supported = config('tenantpilot.supported_policy_types');
$byType = collect($supported)->keyBy('type');
expect($byType)->toHaveKey('managedDeviceAppConfiguration');
expect($byType['managedDeviceAppConfiguration']['endpoint'] ?? null)
->toBe('deviceAppManagement/mobileAppConfigurations');
expect($byType['managedDeviceAppConfiguration']['filter'] ?? null)
->toBe("microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig eq false or isof('microsoft.graph.androidManagedStoreAppConfiguration') eq false");
});
it('syncs managed device app configurations from Graph', function () {
$tenant = Tenant::factory()->create([
'status' => 'active',
]);
$logger = mock(GraphLogger::class);
$logger->shouldReceive('logRequest')
->zeroOrMoreTimes()
->andReturnNull();
$logger->shouldReceive('logResponse')
->zeroOrMoreTimes()
->andReturnNull();
mock(GraphClientInterface::class)
->shouldReceive('listPolicies')
->once()
->with('managedDeviceAppConfiguration', mockery::type('array'))
->andReturn(new GraphResponse(
success: true,
data: [
[
'id' => 'madc-1',
'displayName' => 'MAM Device Config',
'@odata.type' => '#microsoft.graph.managedDeviceMobileAppConfiguration',
],
],
));
$service = app(PolicySyncService::class);
$service->syncPolicies($tenant, [
['type' => 'managedDeviceAppConfiguration', 'platform' => 'mobile'],
]);
expect(Policy::query()->where('tenant_id', $tenant->id)->where('policy_type', 'managedDeviceAppConfiguration')->count())
->toBe(1);
});
it('classifies configuration policies into settings catalog, endpoint security, and security baseline types', function () {
$tenant = Tenant::factory()->create([
'status' => 'active',
]);
$logger = mock(GraphLogger::class);
$logger->shouldReceive('logRequest')
->zeroOrMoreTimes()
->andReturnNull();
$logger->shouldReceive('logResponse')
->zeroOrMoreTimes()
->andReturnNull();
$graphResponse = new GraphResponse(
success: true,
data: [
[
'id' => 'scp-1',
'name' => 'Settings Catalog Alpha',
'@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy',
'technologies' => ['mdm'],
'templateReference' => null,
],
[
'id' => 'esp-1',
'name' => 'Endpoint Security Beta',
'@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy',
'technologies' => 'mdm',
'templateReference' => [
'templateFamily' => 'endpointSecurityDiskEncryption',
'templateDisplayName' => 'BitLocker',
],
],
[
'id' => 'sb-1',
'name' => 'Security Baseline Gamma',
'@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy',
'technologies' => ['mdm'],
'templateReference' => [
'templateFamily' => 'securityBaseline',
],
],
],
);
$calledTypes = [];
mock(GraphClientInterface::class)
->shouldReceive('listPolicies')
->times(3)
->andReturnUsing(function (string $policyType) use (&$calledTypes, $graphResponse) {
$calledTypes[] = $policyType;
return $graphResponse;
});
$service = app(PolicySyncService::class);
$service->syncPolicies($tenant, [
['type' => 'settingsCatalogPolicy', 'platform' => 'windows'],
['type' => 'endpointSecurityPolicy', 'platform' => 'windows'],
['type' => 'securityBaselinePolicy', 'platform' => 'windows'],
]);
expect($calledTypes)->toMatchArray([
'settingsCatalogPolicy',
'endpointSecurityPolicy',
'securityBaselinePolicy',
]);
expect(Policy::query()->where('tenant_id', $tenant->id)->where('policy_type', 'settingsCatalogPolicy')->count())
->toBe(1);
expect(Policy::query()->where('tenant_id', $tenant->id)->where('policy_type', 'endpointSecurityPolicy')->count())
->toBe(1);
expect(Policy::query()->where('tenant_id', $tenant->id)->where('policy_type', 'securityBaselinePolicy')->count())
->toBe(1);
});
it('reclassifies configuration policies when canonical type changes', function () {
$tenant = Tenant::factory()->create([
'status' => 'active',
]);
$policy = Policy::factory()->create([
'tenant_id' => $tenant->id,
'external_id' => 'esp-1',
'policy_type' => 'settingsCatalogPolicy',
'platform' => 'windows',
'display_name' => 'Misclassified',
'ignored_at' => null,
]);
$version = PolicyVersion::factory()->create([
'tenant_id' => $tenant->id,
'policy_id' => $policy->id,
'policy_type' => 'settingsCatalogPolicy',
'platform' => 'windows',
]);
$logger = mock(GraphLogger::class);
$logger->shouldReceive('logRequest')
->zeroOrMoreTimes()
->andReturnNull();
$logger->shouldReceive('logResponse')
->zeroOrMoreTimes()
->andReturnNull();
$graphResponse = new GraphResponse(
success: true,
data: [
[
'id' => 'esp-1',
'name' => 'Endpoint Security Beta',
'@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy',
'technologies' => 'mdm',
'templateReference' => [
'templateFamily' => 'endpointSecurityDiskEncryption',
'templateDisplayName' => 'BitLocker',
],
],
],
);
mock(GraphClientInterface::class)
->shouldReceive('listPolicies')
->times(3)
->andReturn($graphResponse);
$service = app(PolicySyncService::class);
$service->syncPolicies($tenant, [
['type' => 'settingsCatalogPolicy', 'platform' => 'windows'],
['type' => 'endpointSecurityPolicy', 'platform' => 'windows'],
['type' => 'securityBaselinePolicy', 'platform' => 'windows'],
]);
expect(Policy::query()
->where('tenant_id', $tenant->id)
->where('external_id', 'esp-1')
->whereNull('ignored_at')
->count())->toBe(1);
expect(Policy::query()
->where('tenant_id', $tenant->id)
->where('external_id', 'esp-1')
->where('policy_type', 'endpointSecurityPolicy')
->whereNull('ignored_at')
->count())->toBe(1);
expect(Policy::query()
->where('tenant_id', $tenant->id)
->where('external_id', 'esp-1')
->where('policy_type', 'settingsCatalogPolicy')
->whereNull('ignored_at')
->count())->toBe(0);
$version->refresh();
expect($version->policy_type)->toBe('endpointSecurityPolicy');
});