feat(007): device config & compliance snapshot/restore improvements #9
@ -282,6 +282,7 @@ private function ignoredKeys(): array
|
||||
'settingCount',
|
||||
'settingsCount',
|
||||
'templateReference',
|
||||
'scheduledActionsForRule',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -73,6 +73,16 @@ public function fetch(Tenant $tenant, Policy $policy, ?string $actorEmail = null
|
||||
);
|
||||
}
|
||||
|
||||
if ($policy->policy_type === 'deviceCompliancePolicy') {
|
||||
[$payload, $metadata] = $this->hydrateComplianceActions(
|
||||
tenantIdentifier: $tenantIdentifier,
|
||||
tenant: $tenant,
|
||||
policyId: $policy->external_id,
|
||||
payload: is_array($payload) ? $payload : [],
|
||||
metadata: $metadata
|
||||
);
|
||||
}
|
||||
|
||||
if ($response->failed()) {
|
||||
$reason = $response->warnings[0] ?? 'Graph request failed';
|
||||
$failure = [
|
||||
@ -174,6 +184,60 @@ private function hydrateSettingsCatalog(string $tenantIdentifier, Tenant $tenant
|
||||
return [$payload, $metadata];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate compliance policies with scheduled actions (notification templates).
|
||||
*
|
||||
* @return array{0:array,1:array}
|
||||
*/
|
||||
private function hydrateComplianceActions(string $tenantIdentifier, Tenant $tenant, string $policyId, array $payload, array $metadata): array
|
||||
{
|
||||
$path = sprintf('deviceManagement/deviceCompliancePolicies/%s/scheduledActionsForRule', urlencode($policyId));
|
||||
$options = [
|
||||
'tenant' => $tenantIdentifier,
|
||||
'client_id' => $tenant->app_client_id,
|
||||
'client_secret' => $tenant->app_client_secret,
|
||||
];
|
||||
|
||||
$actions = [];
|
||||
$nextPath = $path;
|
||||
$hydrationStatus = 'complete';
|
||||
|
||||
while ($nextPath) {
|
||||
$response = $this->graphClient->request('GET', $nextPath, $options);
|
||||
|
||||
if ($response->failed()) {
|
||||
$hydrationStatus = 'failed';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$data = $response->data;
|
||||
$pageItems = $data['value'] ?? (is_array($data) ? $data : []);
|
||||
|
||||
foreach ($pageItems as $item) {
|
||||
if (is_array($item)) {
|
||||
$actions[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$nextLink = $data['@odata.nextLink'] ?? null;
|
||||
|
||||
if (! $nextLink) {
|
||||
break;
|
||||
}
|
||||
|
||||
$nextPath = $this->stripGraphBaseUrl((string) $nextLink);
|
||||
}
|
||||
|
||||
if (! empty($actions)) {
|
||||
$payload['scheduledActionsForRule'] = $actions;
|
||||
}
|
||||
|
||||
$metadata['compliance_actions_hydration'] = $hydrationStatus;
|
||||
|
||||
return [$payload, $metadata];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all settingDefinitionId from settings array, including nested children.
|
||||
*/
|
||||
|
||||
@ -15,6 +15,14 @@
|
||||
'bitLockerEnabled' => false,
|
||||
'osMinimumVersion' => '10.0.19045',
|
||||
'activeFirewallRequired' => true,
|
||||
'scheduledActionsForRule' => [
|
||||
[
|
||||
'ruleName' => 'Default rule',
|
||||
'scheduledActionConfigurations' => [
|
||||
['actionType' => 'notification'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'customSetting' => 'Custom value',
|
||||
];
|
||||
|
||||
@ -31,6 +39,8 @@
|
||||
expect($additionalBlock)->not->toBeNull();
|
||||
expect(collect($additionalBlock['rows'])->pluck('label')->all())
|
||||
->toContain('Custom Setting');
|
||||
expect(collect($additionalBlock['rows'])->pluck('label')->all())
|
||||
->not->toContain('Scheduled Actions For Rule');
|
||||
|
||||
expect($settings->pluck('title')->all())->not->toContain('General');
|
||||
});
|
||||
|
||||
105
tests/Unit/PolicySnapshotServiceTest.php
Normal file
105
tests/Unit/PolicySnapshotServiceTest.php
Normal file
@ -0,0 +1,105 @@
|
||||
<?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];
|
||||
|
||||
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: []);
|
||||
}
|
||||
}
|
||||
|
||||
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)->toContain(['getPolicy', 'deviceCompliancePolicy', 'compliance-123']);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user