'#microsoft.graph.windows10CompliancePolicy', 'passwordRequired' => true, 'passwordMinimumLength' => 8, 'defenderEnabled' => true, 'bitLockerEnabled' => false, 'osMinimumVersion' => '10.0.19045', 'activeFirewallRequired' => true, 'scheduledActionsForRule' => [ [ 'ruleName' => 'Default rule', 'scheduledActionConfigurations' => [ ['actionType' => 'notification'], ], ], ], 'scheduledActionsForRule@odata.context' => 'https://graph.microsoft.com/beta/$metadata#deviceManagement/deviceCompliancePolicies', 'customSetting' => 'Custom value', ]; $normalized = $normalizer->normalize($snapshot, 'deviceCompliancePolicy', 'windows'); $settings = collect($normalized['settings']); $passwordBlock = $settings->firstWhere('title', 'Password & Access'); expect($passwordBlock)->not->toBeNull(); expect(collect($passwordBlock['rows'])->pluck('label')->all()) ->toContain('Password required', 'Password minimum length'); $additionalBlock = $settings->firstWhere('title', 'Additional Settings'); expect($additionalBlock)->not->toBeNull(); expect(collect($additionalBlock['rows'])->pluck('label')->all()) ->toContain('Custom Setting'); expect(collect($additionalBlock['rows'])->pluck('label')->all()) ->not->toContain('Scheduled Actions For Rule'); expect(collect($additionalBlock['rows'])->pluck('label')->all()) ->not->toContain('Scheduled Actions For Rule@Odata.context'); expect($settings->pluck('title')->all())->not->toContain('General'); }); it('flattens compliance noncompliance actions into a canonical diff signal', function () { $normalizer = app(CompliancePolicyNormalizer::class); $snapshot = [ '@odata.type' => '#microsoft.graph.windows10CompliancePolicy', 'passwordRequired' => true, 'scheduledActionsForRule' => [ [ 'ruleName' => null, 'scheduledActionConfigurations' => [ [ 'actionType' => 'block', 'gracePeriodHours' => 240, 'notificationTemplateId' => '00000000-0000-0000-0000-000000000000', ], [ 'actionType' => 'notification', 'gracePeriodHours' => 48, 'notificationTemplateId' => 'template-123', ], [ 'actionType' => 'retire', 'gracePeriodHours' => 2664, 'notificationTemplateId' => 'template-retire', ], ], ], ], 'scheduledActionsForRule@odata.context' => 'https://graph.microsoft.com/beta/$metadata#deviceManagement/deviceCompliancePolicies', ]; $flat = $normalizer->flattenForDiff($snapshot, 'deviceCompliancePolicy', 'windows'); expect($flat)->toHaveKey('Password & Access > Password required'); expect($flat['Password & Access > Password required'])->toBeTrue(); expect($flat)->toHaveKey('Actions for noncompliance > Mark device noncompliant > Grace period'); expect($flat['Actions for noncompliance > Mark device noncompliant > Grace period'])->toBe('10 days (240 hours)'); expect($flat)->toHaveKey('Actions for noncompliance > Mark device noncompliant > Notification template ID'); expect($flat['Actions for noncompliance > Mark device noncompliant > Notification template ID'])->toBeNull(); expect($flat)->toHaveKey('Actions for noncompliance > Send notification > Grace period'); expect($flat['Actions for noncompliance > Send notification > Grace period'])->toBe('2 days (48 hours)'); expect($flat['Actions for noncompliance > Send notification > Notification template ID'])->toBe('template-123'); expect($flat)->toHaveKey('Actions for noncompliance > Add device to retire list > Grace period'); expect($flat['Actions for noncompliance > Add device to retire list > Grace period'])->toBe('111 days (2664 hours)'); expect($flat['Actions for noncompliance > Add device to retire list > Notification template ID'])->toBe('template-retire'); expect(array_keys($flat))->not->toContain('scheduledActionsForRule'); expect(array_keys($flat))->not->toContain('scheduledActionsForRule@odata.context'); }); it('ignores noncompliance action ids and ordering when flattening for diff', function () { $normalizer = app(CompliancePolicyNormalizer::class); $firstSnapshot = [ '@odata.type' => '#microsoft.graph.windows10CompliancePolicy', 'scheduledActionsForRule' => [ [ 'ruleName' => null, 'scheduledActionConfigurations' => [ [ 'id' => 'config-a', 'actionType' => 'notification', 'gracePeriodHours' => 48, 'notificationTemplateId' => 'template-b', ], [ 'id' => 'config-b', 'actionType' => 'notification', 'gracePeriodHours' => 24, 'notificationTemplateId' => 'template-a', ], ], ], ], ]; $secondSnapshot = [ '@odata.type' => '#microsoft.graph.windows10CompliancePolicy', 'scheduledActionsForRule' => [ [ 'ruleName' => null, 'scheduledActionConfigurations' => [ [ 'id' => 'config-z', 'actionType' => 'notification', 'gracePeriodHours' => 24, 'notificationTemplateId' => 'template-a', ], [ 'id' => 'config-y', 'actionType' => 'notification', 'gracePeriodHours' => 48, 'notificationTemplateId' => 'template-b', ], ], ], ], ]; $firstFlat = $normalizer->flattenForDiff($firstSnapshot, 'deviceCompliancePolicy', 'windows'); $secondFlat = $normalizer->flattenForDiff($secondSnapshot, 'deviceCompliancePolicy', 'windows'); expect($firstFlat)->toBe($secondFlat); expect($firstFlat)->toHaveKey('Actions for noncompliance > Send notification #1 > Grace period'); expect($firstFlat)->toHaveKey('Actions for noncompliance > Send notification #2 > Grace period'); });