diff --git a/specs/018-driver-updates-wufb/checklists/requirements.md b/specs/018-driver-updates-wufb/checklists/requirements.md index 1a0204c..d6c149e 100644 --- a/specs/018-driver-updates-wufb/checklists/requirements.md +++ b/specs/018-driver-updates-wufb/checklists/requirements.md @@ -6,9 +6,9 @@ # Requirements Checklist (018) - [x] `windowsDriverUpdateProfile` is added to `config/tenantpilot.php` (metadata, endpoint, backup/restore mode, risk). - [x] Graph contract exists in `config/graph_contracts.php` (resource, type family, create/update methods, assignments paths). - [x] Sync lists and stores driver update profiles in the Policies inventory. -- [ ] Snapshot capture stores a complete payload for backups and versions. -- [ ] Restore preview is available and respects the configured restore mode. +- [x] Snapshot capture stores a complete payload for backups and versions. +- [x] Restore preview is available and respects the configured restore mode. - [x] Restore execution applies only patchable properties and records audit logs. - [x] Normalized settings view is readable for admins (no raw-only UX). -- [ ] Pest tests cover sync + snapshot + restore + normalized display. +- [x] Pest tests cover sync + snapshot + restore + normalized display. - [x] Pint run (`./vendor/bin/pint --dirty`) on touched files. diff --git a/specs/018-driver-updates-wufb/tasks.md b/specs/018-driver-updates-wufb/tasks.md index f7b8e38..d1e0a9a 100644 --- a/specs/018-driver-updates-wufb/tasks.md +++ b/specs/018-driver-updates-wufb/tasks.md @@ -15,8 +15,8 @@ ## Phase 2: Research & Design ## Phase 3: Tests (TDD) - [x] T006 Add sync test ensuring `windowsDriverUpdateProfile` policies are imported and typed correctly. -- [ ] T007 Add snapshot/version capture test asserting full payload is stored. -- [ ] T008 Add restore preview test for this type (entries + restore_mode shown). +- [x] T007 Add snapshot/version capture test asserting full payload is stored. +- [x] T008 Add restore preview test for this type (entries + restore_mode shown). - [x] T009 Add restore execution test asserting only patchable properties are sent and failures are reported with Graph metadata. - [x] T010 Add normalized display test for key fields. diff --git a/tests/Feature/Filament/RestorePreviewTest.php b/tests/Feature/Filament/RestorePreviewTest.php index a4ccb0a..929e83e 100644 --- a/tests/Feature/Filament/RestorePreviewTest.php +++ b/tests/Feature/Filament/RestorePreviewTest.php @@ -104,6 +104,77 @@ public function request(string $method, string $path, array $options = []): Grap expect($policyPreview['action'])->toBe('update'); }); +test('restore preview shows enabled restore mode for windows driver update profiles', function () { + app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface + { + public function listPolicies(string $policyType, array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } + + public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse + { + return new GraphResponse(true, ['payload' => []]); + } + + public function getOrganization(array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } + + public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } + + public function getServicePrincipalPermissions(array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } + + public function request(string $method, string $path, array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } + }); + + $tenant = Tenant::create([ + 'tenant_id' => 'tenant-driver-preview', + 'name' => 'Tenant Preview', + 'metadata' => [], + ]); + + $backupSet = BackupSet::create([ + 'tenant_id' => $tenant->id, + 'name' => 'Backup', + 'status' => 'completed', + 'item_count' => 1, + ]); + + $backupItem = BackupItem::create([ + 'tenant_id' => $tenant->id, + 'backup_set_id' => $backupSet->id, + 'policy_id' => null, + 'policy_identifier' => 'wdp-1', + 'policy_type' => 'windowsDriverUpdateProfile', + 'platform' => 'windows', + 'payload' => [ + '@odata.type' => '#microsoft.graph.windowsDriverUpdateProfile', + 'displayName' => 'Driver Updates A', + ], + ]); + + $service = app(RestoreService::class); + $preview = $service->preview($tenant, $backupSet, [$backupItem->id]); + + expect($preview)->toHaveCount(1); + + $policyPreview = $preview[0] ?? []; + expect($policyPreview['policy_type'] ?? null)->toBe('windowsDriverUpdateProfile'); + expect($policyPreview['action'] ?? null)->toBe('create'); + expect($policyPreview['restore_mode'] ?? null)->toBe('enabled'); +}); + test('restore preview warns about missing compliance notification templates', function () { app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface { diff --git a/tests/Unit/PolicySnapshotServiceTest.php b/tests/Unit/PolicySnapshotServiceTest.php index 9367f93..9256adf 100644 --- a/tests/Unit/PolicySnapshotServiceTest.php +++ b/tests/Unit/PolicySnapshotServiceTest.php @@ -41,6 +41,27 @@ public function getPolicy(string $policyType, string $policyId, array $options = ]); } + 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, @@ -271,6 +292,41 @@ public function request(string $method, string $path, array $options = []): Grap 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')