TenantAtlas/tests/Unit/PolicySnapshotServiceTest.php
ahmido 93dbd3b13d fix(tests): remove per-file TestCase uses (#45)
What Changed

Removed per-file uses(TestCase::class ...) bindings in Unit tests to avoid Pest v4 “folder already uses the test case” discovery failure (kept RefreshDatabase where needed).
Updated the backup scheduling job test to pass the newly required BulkOperationService when manually calling RunBackupScheduleJob::handle().
Where

Unit (bulk cleanup across 56 files)
RunBackupScheduleJobTest.php
Verification

./vendor/bin/sail test → 443 passed, 5 skipped

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #45
2026-01-08 00:41:46 +00:00

756 lines
29 KiB
PHP

<?php
use App\Models\Policy;
use App\Models\Tenant;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphResponse;
use App\Services\Intune\PolicySnapshotService;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
class PolicySnapshotGraphClient 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 === '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');
});