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', 'windowsDriverUpdateProfile']); 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'); expect($byType['windowsDriverUpdateProfile']['endpoint'] ?? null) ->toBe('deviceManagement/windowsDriverUpdateProfiles'); }); it('syncs windows driver update profiles 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('windowsDriverUpdateProfile', mockery::type('array')) ->andReturn(new GraphResponse( success: true, data: [ [ 'id' => 'wdp-1', 'displayName' => 'Driver Updates A', '@odata.type' => '#microsoft.graph.windowsDriverUpdateProfile', 'approvalType' => 'automatic', ], ], )); $service = app(PolicySyncService::class); $service->syncPolicies($tenant, [ ['type' => 'windowsDriverUpdateProfile', 'platform' => 'windows'], ]); expect(Policy::query()->where('tenant_id', $tenant->id)->where('policy_type', 'windowsDriverUpdateProfile')->count()) ->toBe(1); }); 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'); });