requests[] = ['getPolicy', $policyType, $policyId, $options]; if ($policyType === 'mobileApp') { return new GraphResponse(success: true, data: [ 'payload' => [ 'id' => $policyId, 'displayName' => 'Contoso Portal', 'publisher' => 'Contoso', 'description' => 'Company Portal', '@odata.type' => '#microsoft.graph.win32LobApp', 'createdDateTime' => '2025-01-01T00:00:00Z', 'lastModifiedDateTime' => '2025-01-02T00:00:00Z', 'roleScopeTagIds' => ['0', 'tag-1', 'tag-2'], 'installCommandLine' => 'setup.exe /quiet', 'largeIcon' => ['type' => 'image/png', 'value' => '...'], ], ]); } if ($policyType === 'windowsDriverUpdateProfile') { return new GraphResponse(success: true, data: [ 'payload' => [ 'id' => $policyId, 'displayName' => 'Driver Updates A', 'description' => 'Drivers rollout policy', '@odata.type' => '#microsoft.graph.windowsDriverUpdateProfile', 'approvalType' => 'automatic', 'deploymentDeferralInDays' => 7, 'deviceReporting' => 12, 'newUpdates' => 3, 'roleScopeTagIds' => ['0'], 'inventorySyncStatus' => [ '@odata.type' => '#microsoft.graph.windowsDriverUpdateProfileInventorySyncStatus', 'driverInventorySyncState' => 'success', 'lastSuccessfulSyncDateTime' => '2026-01-01T00:00:00Z', ], ], ]); } return new GraphResponse(success: true, data: [ 'payload' => [ 'id' => $policyId, 'displayName' => 'Compliance Alpha', '@odata.type' => '#microsoft.graph.windows10CompliancePolicy', ], ]); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function request(string $method, string $path, array $options = []): GraphResponse { $this->requests[] = [$method, $path]; if (str_contains($path, 'scheduledActionsForRule')) { return new GraphResponse(success: true, data: [ 'value' => [ [ 'ruleName' => 'Default rule', 'scheduledActionConfigurations' => [ [ 'actionType' => 'notification', 'notificationTemplateId' => 'template-123', ], ], ], ], ]); } return new GraphResponse(success: true, data: []); } } class ConfigurationPolicySettingsSnapshotGraphClient implements GraphClientInterface { public array $requests = []; public function listPolicies(string $policyType, array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { $this->requests[] = ['getPolicy', $policyType, $policyId, $options]; return new GraphResponse(success: true, data: [ 'payload' => [ 'id' => $policyId, 'name' => 'Endpoint Security Alpha', '@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy', ], ]); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function request(string $method, string $path, array $options = []): GraphResponse { $this->requests[] = [$method, $path, $options]; if ($method === 'GET' && str_contains($path, 'deviceManagement/configurationPolicies/') && str_ends_with($path, '/settings')) { return new GraphResponse(success: true, data: [ 'value' => [ [ 'id' => 'setting-1', 'settingInstance' => [ '@odata.type' => '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance', 'settingDefinitionId' => 'device_vendor_msft_policy_config_firewall_policy_alpha', 'simpleSettingValue' => [ 'value' => true, ], ], ], ], ]); } return new GraphResponse(success: true, data: []); } } class EnrollmentNotificationSnapshotGraphClient implements GraphClientInterface { public array $requests = []; public function listPolicies(string $policyType, array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { $this->requests[] = ['getPolicy', $policyType, $policyId, $options]; if ($policyType === 'deviceEnrollmentNotificationConfiguration') { return new GraphResponse(success: true, data: [ 'payload' => [ 'id' => $policyId, 'displayName' => 'Enrollment Notifications', '@odata.type' => '#microsoft.graph.deviceEnrollmentNotificationConfiguration', 'priority' => 1, 'version' => 1, 'platformType' => 'windows', 'brandingOptions' => 'none', 'templateType' => '0', 'notificationMessageTemplateId' => '00000000-0000-0000-0000-000000000000', 'notificationTemplates' => [ 'Email_email-template-1', 'Push_push-template-1', ], 'deviceEnrollmentConfigurationType' => 'enrollmentNotificationsConfiguration', ], ]); } return new GraphResponse(success: true, data: [ 'payload' => [ 'id' => $policyId, 'displayName' => 'Policy', '@odata.type' => '#microsoft.graph.deviceConfiguration', ], ]); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function request(string $method, string $path, array $options = []): GraphResponse { $this->requests[] = [$method, $path, $options]; if ($method === 'GET' && str_contains($path, 'deviceManagement/notificationMessageTemplates/email-template-1/localizedNotificationMessages')) { return new GraphResponse(success: true, data: [ 'value' => [ [ 'id' => 'email-template-1_en-us', 'locale' => 'en-us', 'subject' => 'Email Subject', 'messageTemplate' => 'Email Body', 'isDefault' => true, ], ], ]); } if ($method === 'GET' && str_contains($path, 'deviceManagement/notificationMessageTemplates/push-template-1/localizedNotificationMessages')) { return new GraphResponse(success: true, data: [ 'value' => [ [ 'id' => 'push-template-1_en-us', 'locale' => 'en-us', 'subject' => 'Push Subject', 'messageTemplate' => 'Push Body', 'isDefault' => true, ], ], ]); } if ($method === 'GET' && str_contains($path, 'deviceManagement/notificationMessageTemplates/email-template-1')) { return new GraphResponse(success: true, data: [ '@odata.context' => 'https://graph.microsoft.com/beta/$metadata#deviceManagement/notificationMessageTemplates/$entity', 'id' => 'email-template-1', 'displayName' => 'Email Template', 'defaultLocale' => 'en-us', 'brandingOptions' => 'none', ]); } if ($method === 'GET' && str_contains($path, 'deviceManagement/notificationMessageTemplates/push-template-1')) { return new GraphResponse(success: true, data: [ '@odata.context' => 'https://graph.microsoft.com/beta/$metadata#deviceManagement/notificationMessageTemplates/$entity', 'id' => 'push-template-1', 'displayName' => 'Push Template', 'defaultLocale' => 'en-us', 'brandingOptions' => 'none', ]); } return new GraphResponse(success: true, data: []); } } it('hydrates compliance policy scheduled actions into snapshots', function () { $client = new PolicySnapshotGraphClient; app()->instance(GraphClientInterface::class, $client); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-compliance', 'app_client_id' => 'client-123', 'app_client_secret' => 'secret-123', 'is_current' => true, ]); $tenant->makeCurrent(); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'compliance-123', 'policy_type' => 'deviceCompliancePolicy', 'display_name' => 'Compliance Alpha', 'platform' => 'windows', ]); $service = app(PolicySnapshotService::class); $result = $service->fetch($tenant, $policy); expect($result)->toHaveKey('payload'); expect($result['payload'])->toHaveKey('scheduledActionsForRule'); expect($result['payload']['scheduledActionsForRule'])->toHaveCount(1); expect($result['payload']['scheduledActionsForRule'][0]['scheduledActionConfigurations'][0]['notificationTemplateId']) ->toBe('template-123'); expect($result['metadata']['compliance_actions_hydration'])->toBe('complete'); expect($client->requests[0][0])->toBe('getPolicy'); expect($client->requests[0][1])->toBe('deviceCompliancePolicy'); expect($client->requests[0][2])->toBe('compliance-123'); expect($client->requests[0][3]['expand'] ?? null) ->toBe('scheduledActionsForRule($expand=scheduledActionConfigurations)'); }); it('hydrates configuration policy settings into snapshots', function (string $policyType) { $client = new ConfigurationPolicySettingsSnapshotGraphClient; app()->instance(GraphClientInterface::class, $client); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-endpoint-security', 'app_client_id' => 'client-123', 'app_client_secret' => 'secret-123', 'is_current' => true, ]); $tenant->makeCurrent(); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'esp-123', 'policy_type' => $policyType, 'display_name' => 'Endpoint Security Alpha', 'platform' => 'windows', ]); $service = app(PolicySnapshotService::class); $result = $service->fetch($tenant, $policy); expect($result)->toHaveKey('payload'); expect($result['payload'])->toHaveKey('settings'); expect($result['payload']['settings'])->toHaveCount(1); expect($result['metadata']['settings_hydration'] ?? null)->toBe('complete'); $paths = collect($client->requests) ->filter(fn (array $entry): bool => ($entry[0] ?? null) === 'GET') ->map(fn (array $entry): string => (string) ($entry[1] ?? '')) ->values(); expect($paths->contains(fn (string $path): bool => str_contains($path, 'deviceManagement/configurationPolicies/esp-123/settings')))->toBeTrue(); })->with([ 'endpointSecurityPolicy', 'securityBaselinePolicy', ]); it('hydrates enrollment notification templates into snapshots', function () { $client = new EnrollmentNotificationSnapshotGraphClient; app()->instance(GraphClientInterface::class, $client); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-enrollment-notifications', 'app_client_id' => 'client-123', 'app_client_secret' => 'secret-123', 'is_current' => true, ]); $tenant->makeCurrent(); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'enroll-notify-123', 'policy_type' => 'deviceEnrollmentNotificationConfiguration', 'display_name' => 'Enrollment Notifications', 'platform' => 'all', ]); $service = app(PolicySnapshotService::class); $result = $service->fetch($tenant, $policy); expect($result)->toHaveKey('payload'); expect($result['payload'])->toHaveKey('notificationTemplateSnapshots'); expect($result['payload']['notificationTemplateSnapshots'])->toHaveCount(2); expect($result['metadata']['enrollment_notification_templates_hydration'] ?? null)->toBe('complete'); $email = collect($result['payload']['notificationTemplateSnapshots'])->firstWhere('channel', 'Email'); expect($email)->not->toBeNull() ->and($email['template_id'] ?? null)->toBe('email-template-1') ->and($email['template']['displayName'] ?? null)->toBe('Email Template') ->and($email['localized_notification_messages'][0]['subject'] ?? null)->toBe('Email Subject'); $push = collect($result['payload']['notificationTemplateSnapshots'])->firstWhere('channel', 'Push'); expect($push)->not->toBeNull() ->and($push['template_id'] ?? null)->toBe('push-template-1') ->and($push['template']['displayName'] ?? null)->toBe('Push Template') ->and($push['localized_notification_messages'][0]['subject'] ?? null)->toBe('Push Subject'); }); it('filters mobile app snapshots to metadata-only keys', function () { $client = new PolicySnapshotGraphClient; app()->instance(GraphClientInterface::class, $client); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-apps', 'app_client_id' => 'client-123', 'app_client_secret' => 'secret-123', 'is_current' => true, ]); $tenant->makeCurrent(); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'app-123', 'policy_type' => 'mobileApp', 'display_name' => 'Contoso Portal', 'platform' => 'all', ]); $service = app(PolicySnapshotService::class); $result = $service->fetch($tenant, $policy); expect($result['payload'])->toHaveKeys([ 'id', 'displayName', 'publisher', 'description', '@odata.type', 'createdDateTime', 'lastModifiedDateTime', 'roleScopeTagIds', ]); expect($result['payload']['roleScopeTagIds'])->toBe(['0', 'tag-1', 'tag-2']); expect($result['payload'])->not->toHaveKey('installCommandLine'); expect($result['payload'])->not->toHaveKey('largeIcon'); expect($client->requests[0][0])->toBe('getPolicy'); expect($client->requests[0][1])->toBe('mobileApp'); expect($client->requests[0][2])->toBe('app-123'); expect($client->requests[0][3]['select'] ?? null)->toBeArray(); expect($client->requests[0][3]['select'])->toContain('displayName'); expect($client->requests[0][3]['select'])->toContain('roleScopeTagIds'); expect($client->requests[0][3]['select'])->not->toContain('@odata.type'); }); it('captures windows driver update profile snapshots with full payload', function () { $client = new PolicySnapshotGraphClient; app()->instance(GraphClientInterface::class, $client); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-driver', 'app_client_id' => 'client-123', 'app_client_secret' => 'secret-123', 'is_current' => true, ]); $tenant->makeCurrent(); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'wdp-123', 'policy_type' => 'windowsDriverUpdateProfile', 'display_name' => 'Driver Updates A', 'platform' => 'windows', ]); $service = app(PolicySnapshotService::class); $result = $service->fetch($tenant, $policy); expect($result)->toHaveKey('payload'); expect($result['payload']['approvalType'] ?? null)->toBe('automatic'); expect($result['payload']['deploymentDeferralInDays'] ?? null)->toBe(7); expect($result['payload']['deviceReporting'] ?? null)->toBe(12); expect($result['payload']['newUpdates'] ?? null)->toBe(3); expect($result['payload']['inventorySyncStatus']['driverInventorySyncState'] ?? null)->toBe('success'); expect($client->requests[0][0])->toBe('getPolicy'); expect($client->requests[0][1])->toBe('windowsDriverUpdateProfile'); expect($client->requests[0][2])->toBe('wdp-123'); }); test('falls back to metadata-only snapshot when mamAppConfiguration returns 500', function () { $client = Mockery::mock(\App\Services\Graph\GraphClientInterface::class); $client->shouldReceive('getPolicy') ->once() ->andThrow(new \App\Services\Graph\GraphException('InternalServerError: upstream', 500)); app()->instance(\App\Services\Graph\GraphClientInterface::class, $client); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-mam-fallback', 'app_client_id' => 'client-123', 'app_client_secret' => 'secret-123', 'is_current' => true, ]); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'A_fallback-policy', 'policy_type' => 'mamAppConfiguration', 'display_name' => 'MAM Config Alpha', 'platform' => 'iOS', ]); $service = app(\App\Services\Intune\PolicySnapshotService::class); $result = $service->fetch($tenant, $policy); expect($result)->toHaveKey('payload'); expect($result)->toHaveKey('metadata'); expect($result)->toHaveKey('warnings'); expect($result['payload']['id'])->toBe('A_fallback-policy'); expect($result['payload']['displayName'])->toBe('MAM Config Alpha'); expect($result['payload']['@odata.type'])->toBe('#microsoft.graph.targetedManagedAppConfiguration'); expect($result['payload']['platform'])->toBe('iOS'); expect($result['metadata']['source'])->toBe('metadata_only'); expect($result['metadata']['original_status'])->toBe(500); expect($result['warnings'])->toHaveCount(1); expect($result['warnings'][0])->toContain('Snapshot captured from local metadata only'); expect($result['warnings'][0])->toContain('Restore preview available, full restore not possible'); }); test('does not fallback to metadata for non-5xx errors', function () { $client = Mockery::mock(\App\Services\Graph\GraphClientInterface::class); $client->shouldReceive('getPolicy') ->once() ->andThrow(new \App\Services\Graph\GraphException('NotFound', 404)); app()->instance(\App\Services\Graph\GraphClientInterface::class, $client); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-404', 'app_client_id' => 'client-123', 'app_client_secret' => 'secret-123', 'is_current' => true, ]); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'A_missing', 'policy_type' => 'mamAppConfiguration', 'display_name' => 'Missing Policy', 'platform' => 'iOS', ]); $service = app(\App\Services\Intune\PolicySnapshotService::class); $result = $service->fetch($tenant, $policy); expect($result)->toHaveKey('failure'); expect($result['failure']['status'])->toBe(404); expect($result['failure']['reason'])->toContain('NotFound'); }); test('falls back to metadata-only when graph client returns failed response for mamAppConfiguration', function () { $client = Mockery::mock(\App\Services\Graph\GraphClientInterface::class); $client->shouldReceive('getPolicy') ->once() ->andReturn(new \App\Services\Graph\GraphResponse( success: false, data: [ 'error' => [ 'code' => 'InternalServerError', 'message' => 'Upstream MAM failure', ], ], status: 500, errors: [['code' => 'InternalServerError', 'message' => 'Upstream MAM failure']], meta: [ 'client_request_id' => 'client-req-1', 'request_id' => 'req-1', ], )); app()->instance(\App\Services\Graph\GraphClientInterface::class, $client); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-mam-fallback-response', 'app_client_id' => 'client-123', 'app_client_secret' => 'secret-123', 'is_current' => true, ]); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'A_resp_fallback', 'policy_type' => 'mamAppConfiguration', 'display_name' => 'MAM Config Response', 'platform' => 'iOS', ]); $service = app(\App\Services\Intune\PolicySnapshotService::class); $result = $service->fetch($tenant, $policy); expect($result)->toHaveKey('payload'); expect($result['metadata']['source'])->toBe('metadata_only'); expect($result['metadata']['original_status'])->toBe(500); expect($result['metadata']['original_failure'])->toContain('InternalServerError'); }); class WindowsUpdateRingSnapshotGraphClient implements GraphClientInterface { public array $requests = []; public function listPolicies(string $policyType, array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { $this->requests[] = ['getPolicy', $policyType, $policyId, $options]; return new GraphResponse(success: true, data: [ 'payload' => [ 'id' => $policyId, '@odata.type' => '#microsoft.graph.windowsUpdateForBusinessConfiguration', 'displayName' => 'Ring A', ], ]); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function request(string $method, string $path, array $options = []): GraphResponse { $this->requests[] = [$method, $path]; if ($method === 'GET' && $path === 'deviceManagement/deviceConfigurations/policy-wuring/microsoft.graph.windowsUpdateForBusinessConfiguration') { return new GraphResponse(success: true, data: [ 'automaticUpdateMode' => 'autoInstallAtMaintenanceTime', 'featureUpdatesDeferralPeriodInDays' => 14, ]); } return new GraphResponse(success: true, data: []); } } it('hydrates windows update ring snapshots via derived type cast endpoint', function () { $client = new WindowsUpdateRingSnapshotGraphClient; app()->instance(GraphClientInterface::class, $client); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-wuring', 'app_client_id' => 'client-123', 'app_client_secret' => 'secret-123', 'is_current' => true, ]); $tenant->makeCurrent(); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'policy-wuring', 'policy_type' => 'windowsUpdateRing', 'display_name' => 'Ring A', 'platform' => 'windows', ]); $service = app(PolicySnapshotService::class); $result = $service->fetch($tenant, $policy); expect($result)->toHaveKey('payload'); expect($result['payload']['@odata.type'])->toBe('#microsoft.graph.windowsUpdateForBusinessConfiguration'); expect($result['payload']['automaticUpdateMode'])->toBe('autoInstallAtMaintenanceTime'); expect($result['payload']['featureUpdatesDeferralPeriodInDays'])->toBe(14); expect($result['metadata']['properties_hydration'] ?? null)->toBe('complete'); }); class FailedSnapshotGraphClient implements GraphClientInterface { public function listPolicies(string $policyType, array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { return new GraphResponse( success: false, data: [], status: 500, errors: [], warnings: [], meta: [ 'error_code' => 'InternalServerError', 'error_message' => 'An internal server error has occurred', 'request_id' => 'req-123', 'client_request_id' => 'client-456', ], ); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } public function request(string $method, string $path, array $options = []): GraphResponse { return new GraphResponse(success: true, data: []); } } it('returns actionable reasons when graph snapshot fails', function () { app()->instance(GraphClientInterface::class, new FailedSnapshotGraphClient); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-failure', 'app_client_id' => 'client-123', 'app_client_secret' => 'secret-123', 'is_current' => true, ]); $tenant->makeCurrent(); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'external_id' => 'mam-123', 'policy_type' => 'deviceCompliancePolicy', 'display_name' => 'Compliance Config', 'platform' => 'mobile', ]); $service = app(PolicySnapshotService::class); $result = $service->fetch($tenant, $policy); expect($result)->toHaveKey('failure'); expect($result['failure']['status'])->toBe(500); expect($result['failure']['reason'])->toContain('InternalServerError'); expect($result['failure']['reason'])->toContain('An internal server error has occurred'); expect($result['failure']['reason'])->toContain('client_request_id=client-456'); expect($result['failure']['reason'])->toContain('request_id=req-123'); });