feat(018): add windows driver update profiles
This commit is contained in:
parent
cb5cf9b3bd
commit
a7d715c89e
@ -42,6 +42,10 @@ ## Scope
|
|||||||
name: "Quality Updates (Windows)"
|
name: "Quality Updates (Windows)"
|
||||||
graph_resource: "deviceManagement/windowsQualityUpdateProfiles"
|
graph_resource: "deviceManagement/windowsQualityUpdateProfiles"
|
||||||
|
|
||||||
|
- key: windowsDriverUpdateProfile
|
||||||
|
name: "Driver Updates (Windows)"
|
||||||
|
graph_resource: "deviceManagement/windowsDriverUpdateProfiles"
|
||||||
|
|
||||||
- key: deviceCompliancePolicy
|
- key: deviceCompliancePolicy
|
||||||
name: "Device Compliance"
|
name: "Device Compliance"
|
||||||
graph_resource: "deviceManagement/deviceCompliancePolicies"
|
graph_resource: "deviceManagement/deviceCompliancePolicies"
|
||||||
@ -158,6 +162,11 @@ ## Scope
|
|||||||
restore: enabled
|
restore: enabled
|
||||||
risk: high
|
risk: high
|
||||||
|
|
||||||
|
windowsDriverUpdateProfile:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: high
|
||||||
|
|
||||||
deviceCompliancePolicy:
|
deviceCompliancePolicy:
|
||||||
backup: full
|
backup: full
|
||||||
restore: enabled
|
restore: enabled
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
use App\Services\Intune\ManagedDeviceAppConfigurationNormalizer;
|
use App\Services\Intune\ManagedDeviceAppConfigurationNormalizer;
|
||||||
use App\Services\Intune\ScriptsPolicyNormalizer;
|
use App\Services\Intune\ScriptsPolicyNormalizer;
|
||||||
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
|
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
|
||||||
|
use App\Services\Intune\WindowsDriverUpdateProfileNormalizer;
|
||||||
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
|
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
|
||||||
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
|
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
|
||||||
use App\Services\Intune\WindowsUpdateRingNormalizer;
|
use App\Services\Intune\WindowsUpdateRingNormalizer;
|
||||||
@ -49,6 +50,7 @@ public function register(): void
|
|||||||
ManagedDeviceAppConfigurationNormalizer::class,
|
ManagedDeviceAppConfigurationNormalizer::class,
|
||||||
ScriptsPolicyNormalizer::class,
|
ScriptsPolicyNormalizer::class,
|
||||||
SettingsCatalogPolicyNormalizer::class,
|
SettingsCatalogPolicyNormalizer::class,
|
||||||
|
WindowsDriverUpdateProfileNormalizer::class,
|
||||||
WindowsFeatureUpdateProfileNormalizer::class,
|
WindowsFeatureUpdateProfileNormalizer::class,
|
||||||
WindowsQualityUpdateProfileNormalizer::class,
|
WindowsQualityUpdateProfileNormalizer::class,
|
||||||
WindowsUpdateRingNormalizer::class,
|
WindowsUpdateRingNormalizer::class,
|
||||||
|
|||||||
125
app/Services/Intune/WindowsDriverUpdateProfileNormalizer.php
Normal file
125
app/Services/Intune/WindowsDriverUpdateProfileNormalizer.php
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Intune;
|
||||||
|
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class WindowsDriverUpdateProfileNormalizer implements PolicyTypeNormalizer
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DefaultPolicyNormalizer $defaultNormalizer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function supports(string $policyType): bool
|
||||||
|
{
|
||||||
|
return $policyType === 'windowsDriverUpdateProfile';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{status: string, settings: array<int, array<string, mixed>>, settings_table?: array<string, mixed>, warnings: array<int, string>}
|
||||||
|
*/
|
||||||
|
public function normalize(?array $snapshot, string $policyType, ?string $platform = null): array
|
||||||
|
{
|
||||||
|
$snapshot = $snapshot ?? [];
|
||||||
|
$normalized = $this->defaultNormalizer->normalize($snapshot, $policyType, $platform);
|
||||||
|
|
||||||
|
if ($snapshot === []) {
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
$block = $this->buildDriverUpdateBlock($snapshot);
|
||||||
|
|
||||||
|
if ($block !== null) {
|
||||||
|
$normalized['settings'][] = $block;
|
||||||
|
$normalized['settings'] = array_values(array_filter($normalized['settings']));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function flattenForDiff(?array $snapshot, string $policyType, ?string $platform = null): array
|
||||||
|
{
|
||||||
|
$snapshot = $snapshot ?? [];
|
||||||
|
$normalized = $this->normalize($snapshot, $policyType, $platform);
|
||||||
|
|
||||||
|
return $this->defaultNormalizer->flattenNormalizedForDiff($normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildDriverUpdateBlock(array $snapshot): ?array
|
||||||
|
{
|
||||||
|
$entries = [];
|
||||||
|
|
||||||
|
$displayName = Arr::get($snapshot, 'displayName');
|
||||||
|
|
||||||
|
if (is_string($displayName) && $displayName !== '') {
|
||||||
|
$entries[] = ['key' => 'Name', 'value' => $displayName];
|
||||||
|
}
|
||||||
|
|
||||||
|
$approvalType = Arr::get($snapshot, 'approvalType');
|
||||||
|
|
||||||
|
if (is_string($approvalType) && $approvalType !== '') {
|
||||||
|
$entries[] = ['key' => 'Approval type', 'value' => $approvalType];
|
||||||
|
}
|
||||||
|
|
||||||
|
$deferral = Arr::get($snapshot, 'deploymentDeferralInDays');
|
||||||
|
|
||||||
|
if (is_int($deferral) || (is_numeric($deferral) && (string) (int) $deferral === (string) $deferral)) {
|
||||||
|
$entries[] = ['key' => 'Deployment deferral (days)', 'value' => (int) $deferral];
|
||||||
|
}
|
||||||
|
|
||||||
|
$deviceReporting = Arr::get($snapshot, 'deviceReporting');
|
||||||
|
|
||||||
|
if (is_int($deviceReporting) || (is_numeric($deviceReporting) && (string) (int) $deviceReporting === (string) $deviceReporting)) {
|
||||||
|
$entries[] = ['key' => 'Devices reporting', 'value' => (int) $deviceReporting];
|
||||||
|
}
|
||||||
|
|
||||||
|
$newUpdates = Arr::get($snapshot, 'newUpdates');
|
||||||
|
|
||||||
|
if (is_int($newUpdates) || (is_numeric($newUpdates) && (string) (int) $newUpdates === (string) $newUpdates)) {
|
||||||
|
$entries[] = ['key' => 'New driver updates', 'value' => (int) $newUpdates];
|
||||||
|
}
|
||||||
|
|
||||||
|
$inventorySyncStatus = Arr::get($snapshot, 'inventorySyncStatus');
|
||||||
|
|
||||||
|
if (is_array($inventorySyncStatus)) {
|
||||||
|
$state = Arr::get($inventorySyncStatus, 'driverInventorySyncState');
|
||||||
|
|
||||||
|
if (is_string($state) && $state !== '') {
|
||||||
|
$entries[] = ['key' => 'Inventory sync state', 'value' => $state];
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastSuccessful = $this->formatDateTime(Arr::get($inventorySyncStatus, 'lastSuccessfulSyncDateTime'));
|
||||||
|
|
||||||
|
if ($lastSuccessful !== null) {
|
||||||
|
$entries[] = ['key' => 'Last successful inventory sync', 'value' => $lastSuccessful];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entries === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'keyValue',
|
||||||
|
'title' => 'Driver Update Profile',
|
||||||
|
'entries' => $entries,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatDateTime(mixed $value): ?string
|
||||||
|
{
|
||||||
|
if (! is_string($value) || $value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return CarbonImmutable::parse($value)->toDateTimeString();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -296,6 +296,42 @@
|
|||||||
'assignments_delete_path' => '/deviceManagement/windowsQualityUpdateProfiles/{id}/assignments/{assignmentId}',
|
'assignments_delete_path' => '/deviceManagement/windowsQualityUpdateProfiles/{id}/assignments/{assignmentId}',
|
||||||
'assignments_delete_method' => 'DELETE',
|
'assignments_delete_method' => 'DELETE',
|
||||||
],
|
],
|
||||||
|
'windowsDriverUpdateProfile' => [
|
||||||
|
'resource' => 'deviceManagement/windowsDriverUpdateProfiles',
|
||||||
|
'allowed_select' => [
|
||||||
|
'id',
|
||||||
|
'displayName',
|
||||||
|
'description',
|
||||||
|
'@odata.type',
|
||||||
|
'createdDateTime',
|
||||||
|
'lastModifiedDateTime',
|
||||||
|
'approvalType',
|
||||||
|
'deploymentDeferralInDays',
|
||||||
|
'roleScopeTagIds',
|
||||||
|
],
|
||||||
|
'allowed_expand' => [],
|
||||||
|
'type_family' => [
|
||||||
|
'#microsoft.graph.windowsDriverUpdateProfile',
|
||||||
|
],
|
||||||
|
'create_method' => 'POST',
|
||||||
|
'update_method' => 'PATCH',
|
||||||
|
'id_field' => 'id',
|
||||||
|
'hydration' => 'properties',
|
||||||
|
'update_strip_keys' => [
|
||||||
|
'deviceReporting',
|
||||||
|
'newUpdates',
|
||||||
|
'inventorySyncStatus',
|
||||||
|
],
|
||||||
|
'assignments_list_path' => '/deviceManagement/windowsDriverUpdateProfiles/{id}/assignments',
|
||||||
|
'assignments_create_path' => '/deviceManagement/windowsDriverUpdateProfiles/{id}/assign',
|
||||||
|
'assignments_create_method' => 'POST',
|
||||||
|
'assignments_update_path' => '/deviceManagement/windowsDriverUpdateProfiles/{id}/assignments/{assignmentId}',
|
||||||
|
'assignments_update_method' => 'PATCH',
|
||||||
|
'assignments_delete_path' => '/deviceManagement/windowsDriverUpdateProfiles/{id}/assignments/{assignmentId}',
|
||||||
|
'assignments_delete_method' => 'DELETE',
|
||||||
|
'supports_scope_tags' => true,
|
||||||
|
'scope_tag_field' => 'roleScopeTagIds',
|
||||||
|
],
|
||||||
'deviceCompliancePolicy' => [
|
'deviceCompliancePolicy' => [
|
||||||
'resource' => 'deviceManagement/deviceCompliancePolicies',
|
'resource' => 'deviceManagement/deviceCompliancePolicies',
|
||||||
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'version', 'lastModifiedDateTime'],
|
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'version', 'lastModifiedDateTime'],
|
||||||
|
|||||||
@ -64,6 +64,16 @@
|
|||||||
'restore' => 'enabled',
|
'restore' => 'enabled',
|
||||||
'risk' => 'high',
|
'risk' => 'high',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'type' => 'windowsDriverUpdateProfile',
|
||||||
|
'label' => 'Driver Updates (Windows)',
|
||||||
|
'category' => 'Update Management',
|
||||||
|
'platform' => 'windows',
|
||||||
|
'endpoint' => 'deviceManagement/windowsDriverUpdateProfiles',
|
||||||
|
'backup' => 'full',
|
||||||
|
'restore' => 'enabled',
|
||||||
|
'risk' => 'high',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'type' => 'deviceCompliancePolicy',
|
'type' => 'deviceCompliancePolicy',
|
||||||
'label' => 'Device Compliance',
|
'label' => 'Device Compliance',
|
||||||
|
|||||||
@ -3,13 +3,12 @@ # Requirements Checklist (018)
|
|||||||
**Created**: 2026-01-03
|
**Created**: 2026-01-03
|
||||||
**Feature**: [spec.md](../spec.md)
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
- [ ] `windowsDriverUpdateProfile` is added to `config/tenantpilot.php` (metadata, endpoint, backup/restore mode, risk).
|
- [x] `windowsDriverUpdateProfile` is added to `config/tenantpilot.php` (metadata, endpoint, backup/restore mode, risk).
|
||||||
- [ ] Graph contract exists in `config/graph_contracts.php` (resource, type family, create/update methods, assignments paths).
|
- [x] Graph contract exists in `config/graph_contracts.php` (resource, type family, create/update methods, assignments paths).
|
||||||
- [ ] Sync lists and stores driver update profiles in the Policies inventory.
|
- [x] Sync lists and stores driver update profiles in the Policies inventory.
|
||||||
- [ ] Snapshot capture stores a complete payload for backups and versions.
|
- [ ] Snapshot capture stores a complete payload for backups and versions.
|
||||||
- [ ] Restore preview is available and respects the configured restore mode.
|
- [ ] Restore preview is available and respects the configured restore mode.
|
||||||
- [ ] Restore execution applies only patchable properties and records audit logs.
|
- [x] Restore execution applies only patchable properties and records audit logs.
|
||||||
- [ ] Normalized settings view is readable for admins (no raw-only UX).
|
- [x] Normalized settings view is readable for admins (no raw-only UX).
|
||||||
- [ ] Pest tests cover sync + snapshot + restore + normalized display.
|
- [ ] Pest tests cover sync + snapshot + restore + normalized display.
|
||||||
- [ ] Pint run (`./vendor/bin/pint --dirty`) on touched files.
|
- [x] Pint run (`./vendor/bin/pint --dirty`) on touched files.
|
||||||
|
|
||||||
|
|||||||
@ -27,12 +27,15 @@ ## Out of Scope (v1)
|
|||||||
- Advanced reporting on driver compliance.
|
- Advanced reporting on driver compliance.
|
||||||
- Partial per-setting restore.
|
- Partial per-setting restore.
|
||||||
|
|
||||||
## Graph API Assumptions (to verify)
|
## Graph API Details (confirmed)
|
||||||
- **Resource**: `deviceManagement/windowsDriverUpdateProfiles`
|
- **Resource**: `deviceManagement/windowsDriverUpdateProfiles`
|
||||||
- **@odata.type**: `#microsoft.graph.windowsDriverUpdateProfile`
|
- **@odata.type**: `#microsoft.graph.windowsDriverUpdateProfile`
|
||||||
- **Assignments**: standard pattern with:
|
- **Patchable fields**: `displayName`, `description`, `approvalType`, `deploymentDeferralInDays`, `roleScopeTagIds`
|
||||||
|
- **Read-only fields (strip on PATCH)**: `deviceReporting`, `newUpdates`, `inventorySyncStatus`, `createdDateTime`, `lastModifiedDateTime`
|
||||||
|
- **Assignments**:
|
||||||
- list: `/deviceManagement/windowsDriverUpdateProfiles/{id}/assignments`
|
- list: `/deviceManagement/windowsDriverUpdateProfiles/{id}/assignments`
|
||||||
- assign action: `/deviceManagement/windowsDriverUpdateProfiles/{id}/assign`
|
- assign action: `/deviceManagement/windowsDriverUpdateProfiles/{id}/assign`
|
||||||
|
- update/delete: `/deviceManagement/windowsDriverUpdateProfiles/{id}/assignments/{assignmentId}`
|
||||||
|
|
||||||
## User Scenarios & Testing
|
## User Scenarios & Testing
|
||||||
|
|
||||||
@ -74,4 +77,3 @@ ### Non-Functional Requirements
|
|||||||
- **NFR-001**: Preserve tenant isolation and least privilege.
|
- **NFR-001**: Preserve tenant isolation and least privilege.
|
||||||
- **NFR-002**: Keep restore safe-by-default (preview/confirmation/audit).
|
- **NFR-002**: Keep restore safe-by-default (preview/confirmation/audit).
|
||||||
- **NFR-003**: No new external services or dependencies.
|
- **NFR-003**: No new external services or dependencies.
|
||||||
|
|
||||||
|
|||||||
@ -8,25 +8,25 @@ ## Phase 1: Setup
|
|||||||
- [x] T001 Create/confirm spec, plan, tasks, checklist.
|
- [x] T001 Create/confirm spec, plan, tasks, checklist.
|
||||||
|
|
||||||
## Phase 2: Research & Design
|
## Phase 2: Research & Design
|
||||||
- [ ] T002 Verify Graph resource + `@odata.type` for driver update profiles.
|
- [x] T002 Verify Graph resource + `@odata.type` for driver update profiles.
|
||||||
- [ ] T003 Verify PATCHable fields and define `update_strip_keys` / `update_whitelist`.
|
- [x] T003 Verify PATCHable fields and define `update_strip_keys` / `update_whitelist`.
|
||||||
- [ ] T004 Verify assignment endpoints (`/assignments`, `/assign`) for this resource.
|
- [x] T004 Verify assignment endpoints (`/assignments`, `/assign`) for this resource.
|
||||||
- [ ] T005 Decide restore mode (`enabled` vs `preview-only`) based on risk + patchability.
|
- [x] T005 Decide restore mode (`enabled` vs `preview-only`) based on risk + patchability.
|
||||||
|
|
||||||
## Phase 3: Tests (TDD)
|
## Phase 3: Tests (TDD)
|
||||||
- [ ] T006 Add sync test ensuring `windowsDriverUpdateProfile` policies are imported and typed correctly.
|
- [x] T006 Add sync test ensuring `windowsDriverUpdateProfile` policies are imported and typed correctly.
|
||||||
- [ ] T007 Add snapshot/version capture test asserting full payload is stored.
|
- [ ] T007 Add snapshot/version capture test asserting full payload is stored.
|
||||||
- [ ] T008 Add restore preview test for this type (entries + restore_mode shown).
|
- [ ] T008 Add restore preview test for this type (entries + restore_mode shown).
|
||||||
- [ ] T009 Add restore execution test asserting only patchable properties are sent and failures are reported with Graph metadata.
|
- [x] T009 Add restore execution test asserting only patchable properties are sent and failures are reported with Graph metadata.
|
||||||
- [ ] T010 Add normalized display test for key fields.
|
- [x] T010 Add normalized display test for key fields.
|
||||||
|
|
||||||
## Phase 4: Implementation
|
## Phase 4: Implementation
|
||||||
- [ ] T011 Add `windowsDriverUpdateProfile` to `config/tenantpilot.php`.
|
- [x] T011 Add `windowsDriverUpdateProfile` to `config/tenantpilot.php`.
|
||||||
- [ ] T012 Add Graph contract entry in `config/graph_contracts.php`.
|
- [x] T012 Add Graph contract entry in `config/graph_contracts.php`.
|
||||||
- [ ] T013 Implement any required snapshot hydration (if Graph uses subresources).
|
- [ ] T013 Implement any required snapshot hydration (if Graph uses subresources).
|
||||||
- [ ] T014 Implement restore apply support in `RestoreService` (contract-driven sanitization).
|
- [x] T014 Implement restore apply support in `RestoreService` (contract-driven sanitization).
|
||||||
- [ ] T015 Add a `WindowsDriverUpdateProfileNormalizer` and register it.
|
- [x] T015 Add a `WindowsDriverUpdateProfileNormalizer` and register it.
|
||||||
|
|
||||||
## Phase 5: Verification
|
## Phase 5: Verification
|
||||||
- [ ] T016 Run targeted tests.
|
- [x] T016 Run targeted tests.
|
||||||
- [ ] T017 Run Pint (`./vendor/bin/pint --dirty`).
|
- [x] T017 Run Pint (`./vendor/bin/pint --dirty`).
|
||||||
|
|||||||
@ -211,3 +211,88 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
|||||||
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('deployableContentDisplayName');
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('deployableContentDisplayName');
|
||||||
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('releaseDateDisplayName');
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('releaseDateDisplayName');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('restore execution applies windows driver update profile with sanitized payload', function () {
|
||||||
|
$client = new WindowsUpdateProfilesRestoreGraphClient;
|
||||||
|
app()->instance(GraphClientInterface::class, $client);
|
||||||
|
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-1',
|
||||||
|
'name' => 'Tenant One',
|
||||||
|
'metadata' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$policy = Policy::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'external_id' => 'policy-driver',
|
||||||
|
'policy_type' => 'windowsDriverUpdateProfile',
|
||||||
|
'display_name' => 'Driver Updates A',
|
||||||
|
'platform' => 'windows',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupSet = BackupSet::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'name' => 'Backup',
|
||||||
|
'status' => 'completed',
|
||||||
|
'item_count' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupPayload = [
|
||||||
|
'id' => 'policy-driver',
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsDriverUpdateProfile',
|
||||||
|
'displayName' => 'Driver Updates A',
|
||||||
|
'description' => 'Drivers rollout policy',
|
||||||
|
'approvalType' => 'automatic',
|
||||||
|
'deploymentDeferralInDays' => 7,
|
||||||
|
'deviceReporting' => 12,
|
||||||
|
'newUpdates' => 3,
|
||||||
|
'inventorySyncStatus' => [
|
||||||
|
'driverInventorySyncState' => 'success',
|
||||||
|
'lastSuccessfulSyncDateTime' => '2026-01-01T00:00:00Z',
|
||||||
|
],
|
||||||
|
'roleScopeTagIds' => ['0'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$backupItem = BackupItem::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
'policy_id' => $policy->id,
|
||||||
|
'policy_identifier' => $policy->external_id,
|
||||||
|
'policy_type' => $policy->policy_type,
|
||||||
|
'platform' => $policy->platform,
|
||||||
|
'payload' => $backupPayload,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::factory()->create(['email' => 'tester@example.com']);
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$service = app(RestoreService::class);
|
||||||
|
$run = $service->execute(
|
||||||
|
tenant: $tenant,
|
||||||
|
backupSet: $backupSet,
|
||||||
|
selectedItemIds: [$backupItem->id],
|
||||||
|
dryRun: false,
|
||||||
|
actorEmail: $user->email,
|
||||||
|
actorName: $user->name,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($run->status)->toBe('completed');
|
||||||
|
expect($run->results[0]['status'])->toBe('applied');
|
||||||
|
|
||||||
|
expect(PolicyVersion::where('policy_id', $policy->id)->count())->toBe(1);
|
||||||
|
|
||||||
|
expect($client->applyPolicyCalls)->toHaveCount(1);
|
||||||
|
expect($client->applyPolicyCalls[0]['policyType'])->toBe('windowsDriverUpdateProfile');
|
||||||
|
expect($client->applyPolicyCalls[0]['policyId'])->toBe('policy-driver');
|
||||||
|
expect($client->applyPolicyCalls[0]['options']['method'] ?? null)->toBe('PATCH');
|
||||||
|
|
||||||
|
expect($client->applyPolicyCalls[0]['payload']['approvalType'])->toBe('automatic');
|
||||||
|
expect($client->applyPolicyCalls[0]['payload']['deploymentDeferralInDays'])->toBe(7);
|
||||||
|
expect($client->applyPolicyCalls[0]['payload']['roleScopeTagIds'])->toBe(['0']);
|
||||||
|
|
||||||
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('id');
|
||||||
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('@odata.type');
|
||||||
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('deviceReporting');
|
||||||
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('newUpdates');
|
||||||
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('inventorySyncStatus');
|
||||||
|
});
|
||||||
|
|||||||
@ -62,7 +62,7 @@
|
|||||||
$supported = config('tenantpilot.supported_policy_types');
|
$supported = config('tenantpilot.supported_policy_types');
|
||||||
$byType = collect($supported)->keyBy('type');
|
$byType = collect($supported)->keyBy('type');
|
||||||
|
|
||||||
expect($byType)->toHaveKeys(['deviceConfiguration', 'windowsUpdateRing', 'windowsFeatureUpdateProfile', 'windowsQualityUpdateProfile']);
|
expect($byType)->toHaveKeys(['deviceConfiguration', 'windowsUpdateRing', 'windowsFeatureUpdateProfile', 'windowsQualityUpdateProfile', 'windowsDriverUpdateProfile']);
|
||||||
|
|
||||||
expect($byType['deviceConfiguration']['filter'] ?? null)
|
expect($byType['deviceConfiguration']['filter'] ?? null)
|
||||||
->toBe("not isof('microsoft.graph.windowsUpdateForBusinessConfiguration')");
|
->toBe("not isof('microsoft.graph.windowsUpdateForBusinessConfiguration')");
|
||||||
@ -75,6 +75,50 @@
|
|||||||
|
|
||||||
expect($byType['windowsQualityUpdateProfile']['endpoint'] ?? null)
|
expect($byType['windowsQualityUpdateProfile']['endpoint'] ?? null)
|
||||||
->toBe('deviceManagement/windowsQualityUpdateProfiles');
|
->toBe('deviceManagement/windowsQualityUpdateProfiles');
|
||||||
|
|
||||||
|
expect($byType['windowsDriverUpdateProfile']['endpoint'] ?? null)
|
||||||
|
->toBe('deviceManagement/windowsDriverUpdateProfiles');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs windows driver update profiles from Graph', function () {
|
||||||
|
$tenant = Tenant::factory()->create([
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$logger = mock(GraphLogger::class);
|
||||||
|
|
||||||
|
$logger->shouldReceive('logRequest')
|
||||||
|
->zeroOrMoreTimes()
|
||||||
|
->andReturnNull();
|
||||||
|
|
||||||
|
$logger->shouldReceive('logResponse')
|
||||||
|
->zeroOrMoreTimes()
|
||||||
|
->andReturnNull();
|
||||||
|
|
||||||
|
mock(GraphClientInterface::class)
|
||||||
|
->shouldReceive('listPolicies')
|
||||||
|
->once()
|
||||||
|
->with('windowsDriverUpdateProfile', mockery::type('array'))
|
||||||
|
->andReturn(new GraphResponse(
|
||||||
|
success: true,
|
||||||
|
data: [
|
||||||
|
[
|
||||||
|
'id' => 'wdp-1',
|
||||||
|
'displayName' => 'Driver Updates A',
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsDriverUpdateProfile',
|
||||||
|
'approvalType' => 'automatic',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
$service = app(PolicySyncService::class);
|
||||||
|
|
||||||
|
$service->syncPolicies($tenant, [
|
||||||
|
['type' => 'windowsDriverUpdateProfile', 'platform' => 'windows'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(Policy::query()->where('tenant_id', $tenant->id)->where('policy_type', 'windowsDriverUpdateProfile')->count())
|
||||||
|
->toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('includes managed device app configurations in supported types', function () {
|
it('includes managed device app configurations in supported types', function () {
|
||||||
|
|||||||
38
tests/Unit/WindowsDriverUpdateProfileNormalizerTest.php
Normal file
38
tests/Unit/WindowsDriverUpdateProfileNormalizerTest.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Services\Intune\PolicyNormalizer;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class);
|
||||||
|
|
||||||
|
it('normalizes windows driver update profiles into readable settings', function () {
|
||||||
|
$normalizer = app(PolicyNormalizer::class);
|
||||||
|
|
||||||
|
$snapshot = [
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsDriverUpdateProfile',
|
||||||
|
'displayName' => 'Driver Updates A',
|
||||||
|
'description' => 'Drivers rollout policy',
|
||||||
|
'approvalType' => 'automatic',
|
||||||
|
'deploymentDeferralInDays' => 7,
|
||||||
|
'deviceReporting' => 12,
|
||||||
|
'newUpdates' => 3,
|
||||||
|
'inventorySyncStatus' => [
|
||||||
|
'driverInventorySyncState' => 'success',
|
||||||
|
'lastSuccessfulSyncDateTime' => '2026-01-01T00:00:00Z',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $normalizer->normalize($snapshot, 'windowsDriverUpdateProfile', 'windows');
|
||||||
|
|
||||||
|
expect($result['status'])->toBe('success');
|
||||||
|
expect($result['settings'])->toBeArray()->not->toBeEmpty();
|
||||||
|
|
||||||
|
$driverBlock = collect($result['settings'])
|
||||||
|
->first(fn (array $block) => ($block['title'] ?? null) === 'Driver Update Profile');
|
||||||
|
|
||||||
|
expect($driverBlock)->not->toBeNull();
|
||||||
|
|
||||||
|
$keys = collect($driverBlock['entries'] ?? [])->pluck('key')->all();
|
||||||
|
|
||||||
|
expect($keys)->toContain('Approval type', 'Deployment deferral (days)', 'Devices reporting', 'New driver updates');
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user