feat/018-driver-updates-wufb #27
@ -6,9 +6,9 @@ # Requirements Checklist (018)
|
|||||||
- [x] `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).
|
||||||
- [x] 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).
|
||||||
- [x] 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.
|
- [x] Snapshot capture stores a complete payload for backups and versions.
|
||||||
- [ ] Restore preview is available and respects the configured restore mode.
|
- [x] Restore preview is available and respects the configured restore mode.
|
||||||
- [x] Restore execution applies only patchable properties and records audit logs.
|
- [x] Restore execution applies only patchable properties and records audit logs.
|
||||||
- [x] 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.
|
- [x] Pest tests cover sync + snapshot + restore + normalized display.
|
||||||
- [x] Pint run (`./vendor/bin/pint --dirty`) on touched files.
|
- [x] Pint run (`./vendor/bin/pint --dirty`) on touched files.
|
||||||
|
|||||||
@ -15,8 +15,8 @@ ## Phase 2: Research & Design
|
|||||||
|
|
||||||
## Phase 3: Tests (TDD)
|
## Phase 3: Tests (TDD)
|
||||||
- [x] 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.
|
- [x] T007 Add snapshot/version capture test asserting full payload is stored.
|
||||||
- [ ] T008 Add restore preview test for this type (entries + restore_mode shown).
|
- [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] 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.
|
- [x] T010 Add normalized display test for key fields.
|
||||||
|
|
||||||
|
|||||||
@ -104,6 +104,77 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
expect($policyPreview['action'])->toBe('update');
|
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 () {
|
test('restore preview warns about missing compliance notification templates', function () {
|
||||||
app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface
|
app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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: [
|
return new GraphResponse(success: true, data: [
|
||||||
'payload' => [
|
'payload' => [
|
||||||
'id' => $policyId,
|
'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');
|
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 () {
|
test('falls back to metadata-only snapshot when mamAppConfiguration returns 500', function () {
|
||||||
$client = Mockery::mock(\App\Services\Graph\GraphClientInterface::class);
|
$client = Mockery::mock(\App\Services\Graph\GraphClientInterface::class);
|
||||||
$client->shouldReceive('getPolicy')
|
$client->shouldReceive('getPolicy')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user