Pest v4 discovery fails when unit tests re-bind the test case with uses(TestCase::class). Remove per-file bindings and keep RefreshDatabase where needed. Also update RunBackupScheduleJobTest to pass BulkOperationService when calling handle() manually.
756 lines
29 KiB
PHP
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');
|
|
});
|