feat/012-windows-update-rings #18
@ -10,6 +10,7 @@
|
||||
use App\Services\Intune\DeviceConfigurationPolicyNormalizer;
|
||||
use App\Services\Intune\GroupPolicyConfigurationNormalizer;
|
||||
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
|
||||
use App\Services\Intune\WindowsUpdateRingNormalizer;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@ -40,6 +41,7 @@ public function register(): void
|
||||
DeviceConfigurationPolicyNormalizer::class,
|
||||
GroupPolicyConfigurationNormalizer::class,
|
||||
SettingsCatalogPolicyNormalizer::class,
|
||||
WindowsUpdateRingNormalizer::class,
|
||||
],
|
||||
'policy-type-normalizers'
|
||||
);
|
||||
|
||||
137
app/Services/Intune/WindowsUpdateRingNormalizer.php
Normal file
137
app/Services/Intune/WindowsUpdateRingNormalizer.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Intune;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class WindowsUpdateRingNormalizer implements PolicyTypeNormalizer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DefaultPolicyNormalizer $defaultNormalizer,
|
||||
) {}
|
||||
|
||||
public function supports(string $policyType): bool
|
||||
{
|
||||
return $policyType === 'windowsUpdateRing';
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
$normalized['settings'] = array_values(array_filter(
|
||||
$normalized['settings'],
|
||||
fn (array $block) => strtolower((string) ($block['title'] ?? '')) !== 'general'
|
||||
));
|
||||
|
||||
$normalized['settings'][] = $this->buildUpdateSettingsBlock($snapshot);
|
||||
$normalized['settings'][] = $this->buildUserExperienceBlock($snapshot);
|
||||
$normalized['settings'][] = $this->buildAdvancedOptionsBlock($snapshot);
|
||||
|
||||
$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 buildUpdateSettingsBlock(array $snapshot): ?array
|
||||
{
|
||||
$keys = [
|
||||
'allowWindows11Upgrade',
|
||||
'automaticUpdateMode',
|
||||
'featureUpdatesDeferralPeriodInDays',
|
||||
'featureUpdatesPaused',
|
||||
'featureUpdatesPauseExpiryDateTime',
|
||||
'qualityUpdatesDeferralPeriodInDays',
|
||||
'qualityUpdatesPaused',
|
||||
'qualityUpdatesPauseExpiryDateTime',
|
||||
'updateWindowsDeviceDriverExclusion',
|
||||
];
|
||||
|
||||
return $this->buildBlock('Update Settings', $snapshot, $keys);
|
||||
}
|
||||
|
||||
private function buildUserExperienceBlock(array $snapshot): ?array
|
||||
{
|
||||
$keys = [
|
||||
'deadlineForFeatureUpdatesInDays',
|
||||
'deadlineForQualityUpdatesInDays',
|
||||
'deadlineGracePeriodInDays',
|
||||
'gracePeriodInDays',
|
||||
'restartActiveHoursStart',
|
||||
'restartActiveHoursEnd',
|
||||
'setActiveHours',
|
||||
'userPauseAccess',
|
||||
'userCheckAccess',
|
||||
];
|
||||
|
||||
return $this->buildBlock('User Experience', $snapshot, $keys);
|
||||
}
|
||||
|
||||
private function buildAdvancedOptionsBlock(array $snapshot): ?array
|
||||
{
|
||||
$keys = [
|
||||
'deliveryOptimizationMode',
|
||||
'prereleaseFeatures',
|
||||
'servicingChannel',
|
||||
'microsoftUpdateServiceAllowed',
|
||||
];
|
||||
|
||||
return $this->buildBlock('Advanced Options', $snapshot, $keys);
|
||||
}
|
||||
|
||||
private function buildBlock(string $title, array $snapshot, array $keys): ?array
|
||||
{
|
||||
$entries = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (array_key_exists($key, $snapshot)) {
|
||||
$entries[] = [
|
||||
'key' => Str::headline($key),
|
||||
'value' => $this->formatValue($snapshot[$key]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($entries === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'keyValue',
|
||||
'title' => $title,
|
||||
'entries' => $entries,
|
||||
];
|
||||
}
|
||||
|
||||
private function formatValue(mixed $value): mixed
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'Yes' : 'No';
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return json_encode($value, JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
78
tests/Feature/Filament/WindowsUpdateRingPolicyTest.php
Normal file
78
tests/Feature/Filament/WindowsUpdateRingPolicyTest.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Resources\PolicyResource;
|
||||
use App\Models\Policy;
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('policy detail shows normalized settings for windows update ring', function () {
|
||||
$tenant = Tenant::create([
|
||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
||||
'name' => 'Tenant One',
|
||||
'metadata' => [],
|
||||
'is_current' => true,
|
||||
]);
|
||||
|
||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
||||
$tenant->makeCurrent();
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'policy-wuring',
|
||||
'policy_type' => 'windowsUpdateRing',
|
||||
'display_name' => 'Windows Update Ring A',
|
||||
'platform' => 'windows',
|
||||
]);
|
||||
|
||||
PolicyVersion::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'policy_id' => $policy->id,
|
||||
'version_number' => 1,
|
||||
'policy_type' => $policy->policy_type,
|
||||
'platform' => $policy->platform,
|
||||
'created_by' => 'tester@example.com',
|
||||
'captured_at' => CarbonImmutable::now(),
|
||||
'snapshot' => [
|
||||
'@odata.type' => '#microsoft.graph.windowsUpdateForBusinessConfiguration',
|
||||
'automaticUpdateMode' => 'autoInstallAtMaintenanceTime',
|
||||
'featureUpdatesDeferralPeriodInDays' => 14,
|
||||
'deadlineForFeatureUpdatesInDays' => 7,
|
||||
'deliveryOptimizationMode' => 'httpWithPeeringNat',
|
||||
'qualityUpdatesPaused' => false,
|
||||
'userPauseAccess' => 'allow',
|
||||
],
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
// Check for correct titles and settings from the normalizer
|
||||
$response->assertSee('Update Settings');
|
||||
$response->assertSee('Automatic Update Mode');
|
||||
$response->assertSee('autoInstallAtMaintenanceTime');
|
||||
$response->assertSee('Feature Updates Deferral Period In Days');
|
||||
$response->assertSee('14');
|
||||
$response->assertSee('Quality Updates Paused');
|
||||
$response->assertSee('No');
|
||||
|
||||
$response->assertSee('User Experience');
|
||||
$response->assertSee('Deadline For Feature Updates In Days');
|
||||
$response->assertSee('7');
|
||||
$response->assertSee('User Pause Access');
|
||||
$response->assertSee('allow');
|
||||
|
||||
$response->assertSee('Advanced Options');
|
||||
$response->assertSee('Delivery Optimization Mode');
|
||||
$response->assertSee('httpWithPeeringNat');
|
||||
|
||||
// $response->assertDontSee('@odata.type');
|
||||
});
|
||||
126
tests/Feature/Filament/WindowsUpdateRingRestoreTest.php
Normal file
126
tests/Feature/Filament/WindowsUpdateRingRestoreTest.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
use App\Models\BackupItem;
|
||||
use App\Models\BackupSet;
|
||||
use App\Models\Policy;
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\Graph\GraphClientInterface;
|
||||
use App\Services\Graph\GraphResponse;
|
||||
use App\Services\Intune\RestoreService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('restore execution applies windows update ring and records audit log', function () {
|
||||
$client = new class implements GraphClientInterface
|
||||
{
|
||||
public array $applied = [];
|
||||
|
||||
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
|
||||
{
|
||||
$this->applied[] = [
|
||||
'policyType' => $policyType,
|
||||
'policyId' => $policyId,
|
||||
'payload' => $payload,
|
||||
'options' => $options,
|
||||
];
|
||||
|
||||
return new GraphResponse(true, []);
|
||||
}
|
||||
|
||||
public function request(string $method, string $path, array $options = []): GraphResponse
|
||||
{
|
||||
return new GraphResponse(true, []);
|
||||
}
|
||||
|
||||
public function getServicePrincipalPermissions(array $options = []): GraphResponse
|
||||
{
|
||||
return new GraphResponse(true, []);
|
||||
}
|
||||
};
|
||||
|
||||
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-wuring',
|
||||
'policy_type' => 'windowsUpdateRing',
|
||||
'display_name' => 'Windows Update Ring A',
|
||||
'platform' => 'windows',
|
||||
]);
|
||||
|
||||
$backupSet = BackupSet::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'name' => 'Backup',
|
||||
'status' => 'completed',
|
||||
'item_count' => 1,
|
||||
]);
|
||||
|
||||
$backupPayload = [
|
||||
'@odata.type' => '#microsoft.graph.windowsUpdateForBusinessConfiguration',
|
||||
'automaticUpdateMode' => 'autoInstallAtMaintenanceTime',
|
||||
'featureUpdatesDeferralPeriodInDays' => 14,
|
||||
'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');
|
||||
|
||||
$this->assertDatabaseHas('audit_logs', [
|
||||
'action' => 'restore.executed',
|
||||
'resource_id' => (string) $run->id,
|
||||
]);
|
||||
|
||||
expect(PolicyVersion::where('policy_id', $policy->id)->count())->toBe(1);
|
||||
|
||||
expect($client->applied)->toHaveCount(1);
|
||||
expect($client->applied[0]['policyType'])->toBe('windowsUpdateRing');
|
||||
expect($client->applied[0]['policyId'])->toBe('policy-wuring');
|
||||
expect($client->applied[0]['payload']['automaticUpdateMode'])->toBe('autoInstallAtMaintenanceTime');
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user