TenantAtlas/tests/Unit/PolicySnapshotServiceTest.php
ahmido 817ad208da feat/027-enrollment-config-subtypes (#31)
expose enrollment config subtypes as their own policy types (limit/platform restrictions/notifications) with preview-only restore risk and proper Graph contracts
classify enrollment configs by their @odata.type + deviceEnrollmentConfigurationType so sync only keeps ESP in windowsEnrollmentStatusPage and the rest stay in their own types, including new restore-normalizer UI blocks + warnings
hydrate enrollment notifications: snapshot fetch now downloads each notification template + localized messages, normalized view surfaces template names/subjects/messages, and restore previews keep preview-only behavior
tenant UI tweaks: Tenant list and detail actions moved into an action group; “Open in Entra” re-added in index, and detail now has “Deactivate” + tests covering the new menu layout and actions
tests added/updated for sync, snapshots, restores, normalized settings, tenant UI, plus Pint/test suite run

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #31
2026-01-04 13:25:15 +00:00

758 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;
use Tests\TestCase;
uses(TestCase::class);
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');
});