feat/012-windows-update-rings (#18)
Created a safe session branch, committed everything, fast-forward merged back into feat/012-windows-update-rings, then pushed.
Commit: 074a656 feat(rings): update rings + update profiles
Push is done; upstream tracking is se
Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #18
This commit is contained in:
parent
b048131f81
commit
286d3c596b
@ -104,13 +104,17 @@ public function makeCurrent(): void
|
|||||||
DB::transaction(function () {
|
DB::transaction(function () {
|
||||||
static::activeQuery()->update(['is_current' => false]);
|
static::activeQuery()->update(['is_current' => false]);
|
||||||
|
|
||||||
$this->forceFill(['is_current' => true])->save();
|
static::query()
|
||||||
|
->whereKey($this->getKey())
|
||||||
|
->update(['is_current' => true]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->forceFill(['is_current' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function current(): self
|
public static function current(): self
|
||||||
{
|
{
|
||||||
$envTenantId = env('INTUNE_TENANT_ID') ?: null;
|
$envTenantId = getenv('INTUNE_TENANT_ID') ?: null;
|
||||||
|
|
||||||
if ($envTenantId) {
|
if ($envTenantId) {
|
||||||
$tenant = static::activeQuery()
|
$tenant = static::activeQuery()
|
||||||
|
|||||||
@ -10,6 +10,9 @@
|
|||||||
use App\Services\Intune\DeviceConfigurationPolicyNormalizer;
|
use App\Services\Intune\DeviceConfigurationPolicyNormalizer;
|
||||||
use App\Services\Intune\GroupPolicyConfigurationNormalizer;
|
use App\Services\Intune\GroupPolicyConfigurationNormalizer;
|
||||||
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
|
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
|
||||||
|
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
|
||||||
|
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
|
||||||
|
use App\Services\Intune\WindowsUpdateRingNormalizer;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@ -40,6 +43,9 @@ public function register(): void
|
|||||||
DeviceConfigurationPolicyNormalizer::class,
|
DeviceConfigurationPolicyNormalizer::class,
|
||||||
GroupPolicyConfigurationNormalizer::class,
|
GroupPolicyConfigurationNormalizer::class,
|
||||||
SettingsCatalogPolicyNormalizer::class,
|
SettingsCatalogPolicyNormalizer::class,
|
||||||
|
WindowsFeatureUpdateProfileNormalizer::class,
|
||||||
|
WindowsQualityUpdateProfileNormalizer::class,
|
||||||
|
WindowsUpdateRingNormalizer::class,
|
||||||
],
|
],
|
||||||
'policy-type-normalizers'
|
'policy-type-normalizers'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -77,6 +77,16 @@ public function fetch(Tenant $tenant, Policy $policy, ?string $actorEmail = null
|
|||||||
$metadata = Arr::except($response->data, ['payload']);
|
$metadata = Arr::except($response->data, ['payload']);
|
||||||
$metadataWarnings = $metadata['warnings'] ?? [];
|
$metadataWarnings = $metadata['warnings'] ?? [];
|
||||||
|
|
||||||
|
if ($policy->policy_type === 'windowsUpdateRing') {
|
||||||
|
[$payload, $metadata] = $this->hydrateWindowsUpdateRing(
|
||||||
|
tenantIdentifier: $tenantIdentifier,
|
||||||
|
tenant: $tenant,
|
||||||
|
policyId: $policy->external_id,
|
||||||
|
payload: is_array($payload) ? $payload : [],
|
||||||
|
metadata: $metadata,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ($policy->policy_type === 'settingsCatalogPolicy') {
|
if ($policy->policy_type === 'settingsCatalogPolicy') {
|
||||||
[$payload, $metadata] = $this->hydrateSettingsCatalog(
|
[$payload, $metadata] = $this->hydrateSettingsCatalog(
|
||||||
tenantIdentifier: $tenantIdentifier,
|
tenantIdentifier: $tenantIdentifier,
|
||||||
@ -152,6 +162,57 @@ public function fetch(Tenant $tenant, Policy $policy, ?string $actorEmail = null
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydrate Windows Update Ring payload via derived type cast to capture
|
||||||
|
* windowsUpdateForBusinessConfiguration-specific properties.
|
||||||
|
*
|
||||||
|
* @return array{0:array,1:array}
|
||||||
|
*/
|
||||||
|
private function hydrateWindowsUpdateRing(string $tenantIdentifier, Tenant $tenant, string $policyId, array $payload, array $metadata): array
|
||||||
|
{
|
||||||
|
$odataType = $payload['@odata.type'] ?? null;
|
||||||
|
$castSegment = $this->deriveTypeCastSegment($odataType);
|
||||||
|
|
||||||
|
if ($castSegment === null) {
|
||||||
|
$metadata['properties_hydration'] = 'skipped';
|
||||||
|
|
||||||
|
return [$payload, $metadata];
|
||||||
|
}
|
||||||
|
|
||||||
|
$castPath = sprintf('deviceManagement/deviceConfigurations/%s/%s', urlencode($policyId), $castSegment);
|
||||||
|
|
||||||
|
$response = $this->graphClient->request('GET', $castPath, [
|
||||||
|
'tenant' => $tenantIdentifier,
|
||||||
|
'client_id' => $tenant->app_client_id,
|
||||||
|
'client_secret' => $tenant->app_client_secret,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->failed() || ! is_array($response->data)) {
|
||||||
|
$metadata['properties_hydration'] = 'failed';
|
||||||
|
|
||||||
|
return [$payload, $metadata];
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadata['properties_hydration'] = 'complete';
|
||||||
|
|
||||||
|
return [array_merge($payload, $response->data), $metadata];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deriveTypeCastSegment(mixed $odataType): ?string
|
||||||
|
{
|
||||||
|
if (! is_string($odataType) || $odataType === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! str_starts_with($odataType, '#')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$segment = ltrim($odataType, '#');
|
||||||
|
|
||||||
|
return $segment !== '' ? $segment : null;
|
||||||
|
}
|
||||||
|
|
||||||
private function isMetadataOnlyPolicyType(string $policyType): bool
|
private function isMetadataOnlyPolicyType(string $policyType): bool
|
||||||
{
|
{
|
||||||
foreach (config('tenantpilot.supported_policy_types', []) as $type) {
|
foreach (config('tenantpilot.supported_policy_types', []) as $type) {
|
||||||
|
|||||||
@ -555,6 +555,23 @@ public function execute(
|
|||||||
$payload,
|
$payload,
|
||||||
$graphOptions + ['method' => $updateMethod]
|
$graphOptions + ['method' => $updateMethod]
|
||||||
);
|
);
|
||||||
|
} elseif ($item->policy_type === 'windowsUpdateRing') {
|
||||||
|
$odataType = $this->resolvePayloadString($originalPayload, ['@odata.type']);
|
||||||
|
$castSegment = $odataType && str_starts_with($odataType, '#')
|
||||||
|
? ltrim($odataType, '#')
|
||||||
|
: 'microsoft.graph.windowsUpdateForBusinessConfiguration';
|
||||||
|
|
||||||
|
$updatePath = sprintf(
|
||||||
|
'deviceManagement/deviceConfigurations/%s/%s',
|
||||||
|
urlencode($item->policy_identifier),
|
||||||
|
$castSegment,
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->graphClient->request(
|
||||||
|
$updateMethod,
|
||||||
|
$updatePath,
|
||||||
|
['json' => $payload] + Arr::except($graphOptions, ['platform'])
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
$response = $this->graphClient->applyPolicy(
|
$response = $this->graphClient->applyPolicy(
|
||||||
$item->policy_type,
|
$item->policy_type,
|
||||||
|
|||||||
107
app/Services/Intune/WindowsFeatureUpdateProfileNormalizer.php
Normal file
107
app/Services/Intune/WindowsFeatureUpdateProfileNormalizer.php
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Intune;
|
||||||
|
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class WindowsFeatureUpdateProfileNormalizer implements PolicyTypeNormalizer
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DefaultPolicyNormalizer $defaultNormalizer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function supports(string $policyType): bool
|
||||||
|
{
|
||||||
|
return $policyType === 'windowsFeatureUpdateProfile';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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'][] = $this->buildFeatureUpdateBlock($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 buildFeatureUpdateBlock(array $snapshot): ?array
|
||||||
|
{
|
||||||
|
$entries = [];
|
||||||
|
|
||||||
|
$displayName = Arr::get($snapshot, 'displayName');
|
||||||
|
|
||||||
|
if (is_string($displayName) && $displayName !== '') {
|
||||||
|
$entries[] = ['key' => 'Name', 'value' => $displayName];
|
||||||
|
}
|
||||||
|
|
||||||
|
$version = Arr::get($snapshot, 'featureUpdateVersion');
|
||||||
|
|
||||||
|
if (is_string($version) && $version !== '') {
|
||||||
|
$entries[] = ['key' => 'Feature update version', 'value' => $version];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rollout = Arr::get($snapshot, 'rolloutSettings');
|
||||||
|
|
||||||
|
if (is_array($rollout)) {
|
||||||
|
$start = $this->formatDateTime($rollout['offerStartDateTimeInUTC'] ?? null);
|
||||||
|
$end = $this->formatDateTime($rollout['offerEndDateTimeInUTC'] ?? null);
|
||||||
|
$interval = $rollout['offerIntervalInDays'] ?? null;
|
||||||
|
|
||||||
|
if ($start !== null) {
|
||||||
|
$entries[] = ['key' => 'Rollout start', 'value' => $start];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($end !== null) {
|
||||||
|
$entries[] = ['key' => 'Rollout end', 'value' => $end];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($interval !== null) {
|
||||||
|
$entries[] = ['key' => 'Rollout interval (days)', 'value' => $interval];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entries === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'keyValue',
|
||||||
|
'title' => 'Feature 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Intune;
|
||||||
|
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class WindowsQualityUpdateProfileNormalizer implements PolicyTypeNormalizer
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DefaultPolicyNormalizer $defaultNormalizer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function supports(string $policyType): bool
|
||||||
|
{
|
||||||
|
return $policyType === 'windowsQualityUpdateProfile';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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->buildQualityUpdateBlock($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 buildQualityUpdateBlock(array $snapshot): ?array
|
||||||
|
{
|
||||||
|
$entries = [];
|
||||||
|
|
||||||
|
$displayName = Arr::get($snapshot, 'displayName');
|
||||||
|
|
||||||
|
if (is_string($displayName) && $displayName !== '') {
|
||||||
|
$entries[] = ['key' => 'Name', 'value' => $displayName];
|
||||||
|
}
|
||||||
|
|
||||||
|
$release = Arr::get($snapshot, 'releaseDateDisplayName');
|
||||||
|
|
||||||
|
if (is_string($release) && $release !== '') {
|
||||||
|
$entries[] = ['key' => 'Release', 'value' => $release];
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = Arr::get($snapshot, 'deployableContentDisplayName');
|
||||||
|
|
||||||
|
if (is_string($content) && $content !== '') {
|
||||||
|
$entries[] = ['key' => 'Deployable content', 'value' => $content];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entries === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'keyValue',
|
||||||
|
'title' => 'Quality Update Profile',
|
||||||
|
'entries' => $entries,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,14 @@ protected static function odataTypeMap(): array
|
|||||||
'windows' => '#microsoft.graph.windowsUpdateForBusinessConfiguration',
|
'windows' => '#microsoft.graph.windowsUpdateForBusinessConfiguration',
|
||||||
'all' => '#microsoft.graph.windowsUpdateForBusinessConfiguration',
|
'all' => '#microsoft.graph.windowsUpdateForBusinessConfiguration',
|
||||||
],
|
],
|
||||||
|
'windowsFeatureUpdateProfile' => [
|
||||||
|
'windows' => '#microsoft.graph.windowsFeatureUpdateProfile',
|
||||||
|
'all' => '#microsoft.graph.windowsFeatureUpdateProfile',
|
||||||
|
],
|
||||||
|
'windowsQualityUpdateProfile' => [
|
||||||
|
'windows' => '#microsoft.graph.windowsQualityUpdateProfile',
|
||||||
|
'all' => '#microsoft.graph.windowsQualityUpdateProfile',
|
||||||
|
],
|
||||||
'deviceCompliancePolicy' => [
|
'deviceCompliancePolicy' => [
|
||||||
'windows' => '#microsoft.graph.windows10CompliancePolicy',
|
'windows' => '#microsoft.graph.windows10CompliancePolicy',
|
||||||
'ios' => '#microsoft.graph.iosCompliancePolicy',
|
'ios' => '#microsoft.graph.iosCompliancePolicy',
|
||||||
|
|||||||
@ -143,6 +143,13 @@
|
|||||||
'update_method' => 'PATCH',
|
'update_method' => 'PATCH',
|
||||||
'id_field' => 'id',
|
'id_field' => 'id',
|
||||||
'hydration' => 'properties',
|
'hydration' => 'properties',
|
||||||
|
'update_strip_keys' => [
|
||||||
|
'version',
|
||||||
|
'qualityUpdatesPauseStartDate',
|
||||||
|
'featureUpdatesPauseStartDate',
|
||||||
|
'qualityUpdatesWillBeRolledBack',
|
||||||
|
'featureUpdatesWillBeRolledBack',
|
||||||
|
],
|
||||||
'assignments_list_path' => '/deviceManagement/deviceConfigurations/{id}/assignments',
|
'assignments_list_path' => '/deviceManagement/deviceConfigurations/{id}/assignments',
|
||||||
'assignments_create_path' => '/deviceManagement/deviceConfigurations/{id}/assign',
|
'assignments_create_path' => '/deviceManagement/deviceConfigurations/{id}/assign',
|
||||||
'assignments_create_method' => 'POST',
|
'assignments_create_method' => 'POST',
|
||||||
@ -153,6 +160,52 @@
|
|||||||
'supports_scope_tags' => true,
|
'supports_scope_tags' => true,
|
||||||
'scope_tag_field' => 'roleScopeTagIds',
|
'scope_tag_field' => 'roleScopeTagIds',
|
||||||
],
|
],
|
||||||
|
'windowsFeatureUpdateProfile' => [
|
||||||
|
'resource' => 'deviceManagement/windowsFeatureUpdateProfiles',
|
||||||
|
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'createdDateTime', 'lastModifiedDateTime'],
|
||||||
|
'allowed_expand' => [],
|
||||||
|
'type_family' => [
|
||||||
|
'#microsoft.graph.windowsFeatureUpdateProfile',
|
||||||
|
],
|
||||||
|
'create_method' => 'POST',
|
||||||
|
'update_method' => 'PATCH',
|
||||||
|
'id_field' => 'id',
|
||||||
|
'hydration' => 'properties',
|
||||||
|
'update_strip_keys' => [
|
||||||
|
'deployableContentDisplayName',
|
||||||
|
'endOfSupportDate',
|
||||||
|
],
|
||||||
|
'assignments_list_path' => '/deviceManagement/windowsFeatureUpdateProfiles/{id}/assignments',
|
||||||
|
'assignments_create_path' => '/deviceManagement/windowsFeatureUpdateProfiles/{id}/assign',
|
||||||
|
'assignments_create_method' => 'POST',
|
||||||
|
'assignments_update_path' => '/deviceManagement/windowsFeatureUpdateProfiles/{id}/assignments/{assignmentId}',
|
||||||
|
'assignments_update_method' => 'PATCH',
|
||||||
|
'assignments_delete_path' => '/deviceManagement/windowsFeatureUpdateProfiles/{id}/assignments/{assignmentId}',
|
||||||
|
'assignments_delete_method' => 'DELETE',
|
||||||
|
],
|
||||||
|
'windowsQualityUpdateProfile' => [
|
||||||
|
'resource' => 'deviceManagement/windowsQualityUpdateProfiles',
|
||||||
|
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'createdDateTime', 'lastModifiedDateTime'],
|
||||||
|
'allowed_expand' => [],
|
||||||
|
'type_family' => [
|
||||||
|
'#microsoft.graph.windowsQualityUpdateProfile',
|
||||||
|
],
|
||||||
|
'create_method' => 'POST',
|
||||||
|
'update_method' => 'PATCH',
|
||||||
|
'id_field' => 'id',
|
||||||
|
'hydration' => 'properties',
|
||||||
|
'update_strip_keys' => [
|
||||||
|
'releaseDateDisplayName',
|
||||||
|
'deployableContentDisplayName',
|
||||||
|
],
|
||||||
|
'assignments_list_path' => '/deviceManagement/windowsQualityUpdateProfiles/{id}/assignments',
|
||||||
|
'assignments_create_path' => '/deviceManagement/windowsQualityUpdateProfiles/{id}/assign',
|
||||||
|
'assignments_create_method' => 'POST',
|
||||||
|
'assignments_update_path' => '/deviceManagement/windowsQualityUpdateProfiles/{id}/assignments/{assignmentId}',
|
||||||
|
'assignments_update_method' => 'PATCH',
|
||||||
|
'assignments_delete_path' => '/deviceManagement/windowsQualityUpdateProfiles/{id}/assignments/{assignmentId}',
|
||||||
|
'assignments_delete_method' => 'DELETE',
|
||||||
|
],
|
||||||
'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'],
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
'category' => 'Configuration',
|
'category' => 'Configuration',
|
||||||
'platform' => 'all',
|
'platform' => 'all',
|
||||||
'endpoint' => 'deviceManagement/deviceConfigurations',
|
'endpoint' => 'deviceManagement/deviceConfigurations',
|
||||||
'filter' => "@odata.type ne '#microsoft.graph.windowsUpdateForBusinessConfiguration'",
|
'filter' => "not isof('microsoft.graph.windowsUpdateForBusinessConfiguration')",
|
||||||
'backup' => 'full',
|
'backup' => 'full',
|
||||||
'restore' => 'enabled',
|
'restore' => 'enabled',
|
||||||
'risk' => 'medium',
|
'risk' => 'medium',
|
||||||
@ -39,11 +39,31 @@
|
|||||||
'category' => 'Update Management',
|
'category' => 'Update Management',
|
||||||
'platform' => 'windows',
|
'platform' => 'windows',
|
||||||
'endpoint' => 'deviceManagement/deviceConfigurations',
|
'endpoint' => 'deviceManagement/deviceConfigurations',
|
||||||
'filter' => "@odata.type eq '#microsoft.graph.windowsUpdateForBusinessConfiguration'",
|
'filter' => "isof('microsoft.graph.windowsUpdateForBusinessConfiguration')",
|
||||||
'backup' => 'full',
|
'backup' => 'full',
|
||||||
'restore' => 'enabled',
|
'restore' => 'enabled',
|
||||||
'risk' => 'medium-high',
|
'risk' => 'medium-high',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'type' => 'windowsFeatureUpdateProfile',
|
||||||
|
'label' => 'Feature Updates (Windows)',
|
||||||
|
'category' => 'Update Management',
|
||||||
|
'platform' => 'windows',
|
||||||
|
'endpoint' => 'deviceManagement/windowsFeatureUpdateProfiles',
|
||||||
|
'backup' => 'full',
|
||||||
|
'restore' => 'enabled',
|
||||||
|
'risk' => 'high',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'windowsQualityUpdateProfile',
|
||||||
|
'label' => 'Quality Updates (Windows)',
|
||||||
|
'category' => 'Update Management',
|
||||||
|
'platform' => 'windows',
|
||||||
|
'endpoint' => 'deviceManagement/windowsQualityUpdateProfiles',
|
||||||
|
'backup' => 'full',
|
||||||
|
'restore' => 'enabled',
|
||||||
|
'risk' => 'high',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'type' => 'deviceCompliancePolicy',
|
'type' => 'deviceCompliancePolicy',
|
||||||
'label' => 'Device Compliance',
|
'label' => 'Device Compliance',
|
||||||
@ -130,7 +150,7 @@
|
|||||||
'category' => 'Enrollment',
|
'category' => 'Enrollment',
|
||||||
'platform' => 'all',
|
'platform' => 'all',
|
||||||
'endpoint' => 'deviceManagement/deviceEnrollmentConfigurations',
|
'endpoint' => 'deviceManagement/deviceEnrollmentConfigurations',
|
||||||
'filter' => "@odata.type eq '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration'",
|
'filter' => "isof('microsoft.graph.windows10EnrollmentCompletionPageConfiguration')",
|
||||||
'backup' => 'full',
|
'backup' => 'full',
|
||||||
'restore' => 'enabled',
|
'restore' => 'enabled',
|
||||||
'risk' => 'medium',
|
'risk' => 'medium',
|
||||||
|
|||||||
@ -18,7 +18,9 @@
|
|||||||
</include>
|
</include>
|
||||||
</source>
|
</source>
|
||||||
<php>
|
<php>
|
||||||
|
<ini name="memory_limit" value="512M"/>
|
||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="INTUNE_TENANT_ID" value="" force="true"/>
|
||||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
|
|||||||
18
specs/012-windows-update-rings/plan.md
Normal file
18
specs/012-windows-update-rings/plan.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Implementation Plan: Windows Update Rings (012)
|
||||||
|
|
||||||
|
**Branch**: `feat/012-windows-update-rings`
|
||||||
|
**Date**: 2025-12-31
|
||||||
|
**Spec Source**: [spec.md](./spec.md)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
Make `windowsUpdateRing` snapshots/restores accurate by correctly capturing and applying its settings, and present a readable normalized view in Filament.
|
||||||
|
|
||||||
|
Also add coverage for Windows Feature Update Profiles (`windowsFeatureUpdateProfile`) and Windows Quality Update Profiles (`windowsQualityUpdateProfile`) so they can be synced, snapshotted, restored, and displayed in a readable normalized format.
|
||||||
|
|
||||||
|
## Execution Steps
|
||||||
|
1. **Graph contract verification**: Ensure `config/graph_contracts.php` entry for `windowsUpdateRing` is correct and complete.
|
||||||
|
2. **Snapshot capture hydration**: Extend `PolicySnapshotService` to correctly hydrate `windowsUpdateForBusinessConfiguration` settings into the policy payload.
|
||||||
|
3. **Restore**: Extend `RestoreService` to apply `windowsUpdateRing` settings from a snapshot to the target policy in Intune.
|
||||||
|
4. **UI normalization**: Add a dedicated normalizer for `windowsUpdateRing` that renders configured settings as readable rows in the Filament UI.
|
||||||
|
5. **Feature/Quality Update Profiles**: Add Graph contract + supported types, and normalizers for `windowsFeatureUpdateProfile` and `windowsQualityUpdateProfile`.
|
||||||
|
6. **Tests + formatting**: Add targeted Pest tests for sync filters/types, snapshot/normalized display (as applicable), and restore payload sanitization. Run `./vendor/bin/pint --dirty` and the affected tests.
|
||||||
77
specs/012-windows-update-rings/spec.md
Normal file
77
specs/012-windows-update-rings/spec.md
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# Feature Specification: Windows Update Rings (012)
|
||||||
|
|
||||||
|
**Feature Branch**: `feat/012-windows-update-rings`
|
||||||
|
**Created**: 2025-12-31
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: `config/graph_contracts.php` (windowsUpdateRing scope)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Add reliable coverage for **Windows Update Rings** (`windowsUpdateRing`) in the existing inventory/backup/version/restore flows.
|
||||||
|
|
||||||
|
This feature also extends coverage to **Windows Feature Update Profiles** ("Feature Updates"), which are managed under the `deviceManagement/windowsFeatureUpdateProfiles` endpoint and have `@odata.type` `#microsoft.graph.windowsFeatureUpdateProfile`.
|
||||||
|
|
||||||
|
This feature also extends coverage to **Windows Quality Update Profiles** ("Quality Updates"), which are managed under the `deviceManagement/windowsQualityUpdateProfiles` endpoint and have `@odata.type` `#microsoft.graph.windowsQualityUpdateProfile`.
|
||||||
|
|
||||||
|
This policy type is defined in `graph_contracts.php` and uses the `deviceManagement/deviceConfigurations` endpoint, identified by the `@odata.type` `#microsoft.graph.windowsUpdateForBusinessConfiguration`. This feature will focus on implementing the necessary UI normalization and ensuring the sync, backup, versioning, and restore flows function correctly for this policy type.
|
||||||
|
|
||||||
|
## In Scope
|
||||||
|
- Policy type: `windowsUpdateRing`
|
||||||
|
- Sync: Policies with `@odata.type` of `#microsoft.graph.windowsUpdateForBusinessConfiguration` should be correctly identified and synced as `windowsUpdateRing` policies.
|
||||||
|
- Snapshot capture: Full snapshot of all settings within a Windows Update Ring policy.
|
||||||
|
- Restore: Restore a Windows Update Ring policy from a snapshot.
|
||||||
|
- UI: Display the settings of a Windows Update Ring policy in a readable, normalized format.
|
||||||
|
|
||||||
|
- Policy type: `windowsFeatureUpdateProfile`
|
||||||
|
- Sync: Feature Update Profiles should be listed and synced from `deviceManagement/windowsFeatureUpdateProfiles`.
|
||||||
|
- Snapshot capture: Full snapshot of the Feature Update Profile payload.
|
||||||
|
- Restore: Restore a Feature Update Profile from a snapshot.
|
||||||
|
- UI: Display the key settings of a Feature Update Profile in a readable, normalized format.
|
||||||
|
|
||||||
|
- Policy type: `windowsQualityUpdateProfile`
|
||||||
|
- Sync: Quality Update Profiles should be listed and synced from `deviceManagement/windowsQualityUpdateProfiles`.
|
||||||
|
- Snapshot capture: Full snapshot of the Quality Update Profile payload.
|
||||||
|
- Restore: Restore a Quality Update Profile from a snapshot.
|
||||||
|
- UI: Display the key settings of a Quality Update Profile in a readable, normalized format.
|
||||||
|
|
||||||
|
## Out of Scope (v1)
|
||||||
|
- Advanced analytics or reporting on update compliance.
|
||||||
|
- Per-setting partial restore.
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 — Inventory + readable view
|
||||||
|
As an admin, I can see my Windows Update Ring policies in the policy list and view their configured settings in a clear, understandable format.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
1. Windows Update Ring policies are listed in the main policy table with the correct type name.
|
||||||
|
2. The policy detail view shows a structured list/table of configured settings (e.g., "Quality update deferral period", "Automatic update behavior").
|
||||||
|
3. Policy Versions store the snapshot and render the settings in the “Normalized settings” view.
|
||||||
|
|
||||||
|
### User Story 2 — Backup/Version capture
|
||||||
|
As an admin, when I back up or create a new version of a Windows Update Ring policy, the snapshot contains all its settings.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
1. The backup/version payload in the `snapshot` column contains all the properties of the `windowsUpdateForBusinessConfiguration` object.
|
||||||
|
|
||||||
|
### User Story 3 — Restore settings
|
||||||
|
As an admin, I can restore a Windows Update Ring policy from a backup or a previous version.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
1. The restore operation correctly applies the settings from the snapshot to the target policy in Intune.
|
||||||
|
2. The restore process is audited.
|
||||||
|
|
||||||
|
### User Story 4 — Feature Updates inventory + readable view
|
||||||
|
As an admin, I can see my Windows Feature Update Profiles in the policy list and view their configured rollout/version settings in a clear, understandable format.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
1. Feature Update Profiles are listed in the main policy table with the correct type name.
|
||||||
|
2. The policy detail view shows a structured list/table of configured settings (e.g., feature update version, rollout window).
|
||||||
|
3. Policy Versions store the snapshot and render the settings in the “Normalized settings” view.
|
||||||
|
|
||||||
|
### User Story 5 — Quality Updates inventory + readable view
|
||||||
|
As an admin, I can see my Windows Quality Update Profiles in the policy list and view their configured release/content settings in a clear, understandable format.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
1. Quality Update Profiles are listed in the main policy table with the correct type name.
|
||||||
|
2. The policy detail view shows a structured list/table of configured settings (e.g., release, deployable content).
|
||||||
|
3. Policy Versions store the snapshot and render the settings in the “Normalized settings” view.
|
||||||
26
specs/012-windows-update-rings/tasks.md
Normal file
26
specs/012-windows-update-rings/tasks.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Tasks: Windows Update Rings (012)
|
||||||
|
|
||||||
|
**Branch**: `feat/012-windows-update-rings` | **Date**: 2025-12-31
|
||||||
|
**Input**: [spec.md](./spec.md), [plan.md](./plan.md)
|
||||||
|
|
||||||
|
## Phase 1: Contracts + Snapshot Hydration
|
||||||
|
- [X] T001 Verify `config/graph_contracts.php` for `windowsUpdateRing` (resource, allowed_select, type_family, etc.).
|
||||||
|
- [X] T002 Extend `PolicySnapshotService` to hydrate `windowsUpdateForBusinessConfiguration` settings.
|
||||||
|
- [X] T001b Fix Graph filters for update rings (`isof(...)`) and add `windowsFeatureUpdateProfile` support.
|
||||||
|
|
||||||
|
## Phase 2: Restore
|
||||||
|
- [X] T003 Implement restore apply for `windowsUpdateRing` settings in `RestoreService.php`.
|
||||||
|
|
||||||
|
## Phase 3: UI Normalization
|
||||||
|
- [X] T004 Add `WindowsUpdateRingNormalizer` and register it (Policy “Normalized settings” is readable).
|
||||||
|
- [X] T004b Add `WindowsFeatureUpdateProfileNormalizer` and register it (Policy “Normalized settings” is readable).
|
||||||
|
- [X] T004c Add `WindowsQualityUpdateProfileNormalizer` and register it (Policy “Normalized settings” is readable).
|
||||||
|
|
||||||
|
## Phase 4: Tests + Verification
|
||||||
|
- [X] T005 Add tests for sync filters + supported types.
|
||||||
|
- [X] T006 Add tests for restore apply.
|
||||||
|
- [X] T007 Run tests (targeted).
|
||||||
|
- [X] T008 Run Pint (`./vendor/bin/pint --dirty`).
|
||||||
|
|
||||||
|
## Open TODOs (Follow-up)
|
||||||
|
- None yet.
|
||||||
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
test('progress widget shows running operations for current tenant and user', function () {
|
test('progress widget shows running operations for current tenant and user', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
// Own running op
|
// Own running op
|
||||||
@ -39,9 +40,6 @@
|
|||||||
'status' => 'running',
|
'status' => 'running',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// $tenant->makeCurrent();
|
|
||||||
$tenant->forceFill(['is_current' => true])->save();
|
|
||||||
|
|
||||||
auth()->login($user); // Login user explicitly for auth()->id() call in component
|
auth()->login($user); // Login user explicitly for auth()->id() call in component
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
test('policy detail shows app protection settings in readable sections', function () {
|
test('policy detail shows app protection settings in readable sections', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
|
|||||||
@ -73,7 +73,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
});
|
});
|
||||||
|
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
'name' => 'Tenant',
|
'name' => 'Tenant',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'name' => 'Set 1',
|
'name' => 'Set 1',
|
||||||
@ -60,6 +62,8 @@
|
|||||||
'name' => 'Tenant 2',
|
'name' => 'Tenant 2',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'name' => 'Set with restore',
|
'name' => 'Set with restore',
|
||||||
@ -93,6 +97,8 @@
|
|||||||
'name' => 'Tenant Force',
|
'name' => 'Tenant Force',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'name' => 'Set force',
|
'name' => 'Set force',
|
||||||
@ -132,6 +138,8 @@
|
|||||||
'name' => 'Tenant Restore Backup Set',
|
'name' => 'Tenant Restore Backup Set',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'name' => 'Set restore',
|
'name' => 'Set restore',
|
||||||
@ -171,6 +179,8 @@
|
|||||||
'name' => 'Tenant Restore Run',
|
'name' => 'Tenant Restore Run',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'name' => 'Set RR',
|
'name' => 'Set RR',
|
||||||
@ -207,6 +217,8 @@
|
|||||||
'name' => 'Tenant Restore Restore Run',
|
'name' => 'Tenant Restore Restore Run',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'name' => 'Set for restore run restore',
|
'name' => 'Set for restore run restore',
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
test('malformed snapshot renders warning on policy and version detail', function () {
|
test('malformed snapshot renders warning on policy and version detail', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
|
|||||||
@ -50,7 +50,7 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
|||||||
});
|
});
|
||||||
|
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
test('policies are listed for the active tenant', function () {
|
test('policies are listed for the active tenant', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -12,13 +12,12 @@
|
|||||||
|
|
||||||
test('policy detail shows normalized settings section', function () {
|
test('policy detail shows normalized settings section', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
|
||||||
$tenant->makeCurrent();
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$policy = Policy::create([
|
$policy = Policy::create([
|
||||||
|
|||||||
@ -12,13 +12,12 @@
|
|||||||
|
|
||||||
test('policy version detail renders tabs and scroll-safe blocks', function () {
|
test('policy version detail renders tabs and scroll-safe blocks', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
|
||||||
$tenant->makeCurrent();
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$policy = Policy::create([
|
$policy = Policy::create([
|
||||||
|
|||||||
@ -12,13 +12,12 @@
|
|||||||
|
|
||||||
test('policy version view shows scope tags even when assignments are missing', function () {
|
test('policy version view shows scope tags even when assignments are missing', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
|
||||||
$tenant->makeCurrent();
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$policy = Policy::create([
|
$policy = Policy::create([
|
||||||
|
|||||||
@ -12,13 +12,12 @@
|
|||||||
|
|
||||||
test('policy version detail shows raw and normalized settings', function () {
|
test('policy version detail shows raw and normalized settings', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
|
||||||
$tenant->makeCurrent();
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$policy = Policy::create([
|
$policy = Policy::create([
|
||||||
|
|||||||
@ -11,11 +11,13 @@
|
|||||||
|
|
||||||
test('policy versions render with timeline data', function () {
|
test('policy versions render with timeline data', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$policy = Policy::create([
|
$policy = Policy::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'external_id' => 'policy-1',
|
'external_id' => 'policy-1',
|
||||||
|
|||||||
@ -13,13 +13,12 @@
|
|||||||
|
|
||||||
it('shows Settings tab for Settings Catalog policy', function () {
|
it('shows Settings tab for Settings Catalog policy', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Test Tenant',
|
'name' => 'Test Tenant',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
|
||||||
$tenant->makeCurrent();
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$policy = Policy::create([
|
$policy = Policy::create([
|
||||||
@ -86,13 +85,12 @@
|
|||||||
|
|
||||||
it('shows display names instead of definition IDs', function () {
|
it('shows display names instead of definition IDs', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Test Tenant',
|
'name' => 'Test Tenant',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
|
||||||
$tenant->makeCurrent();
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$policy = Policy::create([
|
$policy = Policy::create([
|
||||||
@ -143,13 +141,12 @@
|
|||||||
|
|
||||||
it('shows fallback prettified labels when definitions not cached', function () {
|
it('shows fallback prettified labels when definitions not cached', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Test Tenant',
|
'name' => 'Test Tenant',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
|
||||||
$tenant->makeCurrent();
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$policy = Policy::create([
|
$policy = Policy::create([
|
||||||
@ -195,13 +192,12 @@
|
|||||||
|
|
||||||
it('shows tabbed layout for non-Settings Catalog policies', function () {
|
it('shows tabbed layout for non-Settings Catalog policies', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Test Tenant',
|
'name' => 'Test Tenant',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
|
||||||
$tenant->makeCurrent();
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$policy = Policy::create([
|
$policy = Policy::create([
|
||||||
@ -242,7 +238,7 @@
|
|||||||
// T034: Test display names shown (not definition IDs)
|
// T034: Test display names shown (not definition IDs)
|
||||||
it('displays setting display names instead of raw definition IDs', function () {
|
it('displays setting display names instead of raw definition IDs', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Test Tenant',
|
'name' => 'Test Tenant',
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
@ -296,7 +292,7 @@
|
|||||||
// T035: Test values formatted correctly
|
// T035: Test values formatted correctly
|
||||||
it('formats setting values correctly based on type', function () {
|
it('formats setting values correctly based on type', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Test Tenant',
|
'name' => 'Test Tenant',
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
@ -370,7 +366,7 @@
|
|||||||
// T036: Test search/filter functionality
|
// T036: Test search/filter functionality
|
||||||
it('search filters settings in real-time', function () {
|
it('search filters settings in real-time', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Test Tenant',
|
'name' => 'Test Tenant',
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
@ -433,7 +429,7 @@
|
|||||||
// T037: Test graceful degradation for missing definitions
|
// T037: Test graceful degradation for missing definitions
|
||||||
it('shows prettified fallback labels when definitions are not cached', function () {
|
it('shows prettified fallback labels when definitions are not cached', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Test Tenant',
|
'name' => 'Test Tenant',
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -13,13 +13,12 @@
|
|||||||
|
|
||||||
test('settings catalog policies render a normalized settings table', function () {
|
test('settings catalog policies render a normalized settings table', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
|
||||||
$tenant->makeCurrent();
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$policy = Policy::create([
|
$policy = Policy::create([
|
||||||
|
|||||||
@ -75,16 +75,11 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
app()->instance(GraphClientInterface::class, new SettingsCatalogFakeGraphClient($responses));
|
app()->instance(GraphClientInterface::class, new SettingsCatalogFakeGraphClient($responses));
|
||||||
|
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
|
||||||
$_ENV['INTUNE_TENANT_ID'] = $tenant->tenant_id;
|
|
||||||
$_SERVER['INTUNE_TENANT_ID'] = $tenant->tenant_id;
|
|
||||||
|
|
||||||
$tenant->makeCurrent();
|
$tenant->makeCurrent();
|
||||||
expect(Tenant::current()->id)->toBe($tenant->id);
|
expect(Tenant::current()->id)->toBe($tenant->id);
|
||||||
|
|
||||||
|
|||||||
@ -13,13 +13,12 @@
|
|||||||
|
|
||||||
test('settings catalog settings render as a filament table with details action', function () {
|
test('settings catalog settings render as a filament table with details action', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),
|
'tenant_id' => 'local-tenant',
|
||||||
'name' => 'Tenant One',
|
'name' => 'Tenant One',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
|
||||||
$tenant->makeCurrent();
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
$policy = Policy::create([
|
$policy = Policy::create([
|
||||||
|
|||||||
213
tests/Feature/Filament/WindowsUpdateProfilesRestoreTest.php
Normal file
213
tests/Feature/Filament/WindowsUpdateProfilesRestoreTest.php
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
<?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);
|
||||||
|
|
||||||
|
class WindowsUpdateProfilesRestoreGraphClient implements GraphClientInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<int, array{policyType:string,policyId:string,payload:array,options:array}>
|
||||||
|
*/
|
||||||
|
public array $applyPolicyCalls = [];
|
||||||
|
|
||||||
|
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->applyPolicyCalls[] = [
|
||||||
|
'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, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('restore execution applies windows feature 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-feature',
|
||||||
|
'policy_type' => 'windowsFeatureUpdateProfile',
|
||||||
|
'display_name' => 'Feature Updates A',
|
||||||
|
'platform' => 'windows',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupSet = BackupSet::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'name' => 'Backup',
|
||||||
|
'status' => 'completed',
|
||||||
|
'item_count' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupPayload = [
|
||||||
|
'id' => 'policy-feature',
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsFeatureUpdateProfile',
|
||||||
|
'displayName' => 'Feature Updates A',
|
||||||
|
'featureUpdateVersion' => 'Windows 11, version 23H2',
|
||||||
|
'deployableContentDisplayName' => 'Some Content',
|
||||||
|
'endOfSupportDate' => '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('windowsFeatureUpdateProfile');
|
||||||
|
expect($client->applyPolicyCalls[0]['policyId'])->toBe('policy-feature');
|
||||||
|
expect($client->applyPolicyCalls[0]['options']['method'] ?? null)->toBe('PATCH');
|
||||||
|
|
||||||
|
expect($client->applyPolicyCalls[0]['payload']['featureUpdateVersion'])->toBe('Windows 11, version 23H2');
|
||||||
|
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('deployableContentDisplayName');
|
||||||
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('endOfSupportDate');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore execution applies windows quality 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-quality',
|
||||||
|
'policy_type' => 'windowsQualityUpdateProfile',
|
||||||
|
'display_name' => 'Quality Updates A',
|
||||||
|
'platform' => 'windows',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupSet = BackupSet::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'name' => 'Backup',
|
||||||
|
'status' => 'completed',
|
||||||
|
'item_count' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupPayload = [
|
||||||
|
'id' => 'policy-quality',
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsQualityUpdateProfile',
|
||||||
|
'displayName' => 'Quality Updates A',
|
||||||
|
'qualityUpdateCveIds' => ['CVE-2025-0001'],
|
||||||
|
'deployableContentDisplayName' => 'Some Content',
|
||||||
|
'releaseDateDisplayName' => 'January 2026',
|
||||||
|
'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('windowsQualityUpdateProfile');
|
||||||
|
expect($client->applyPolicyCalls[0]['policyId'])->toBe('policy-quality');
|
||||||
|
expect($client->applyPolicyCalls[0]['options']['method'] ?? null)->toBe('PATCH');
|
||||||
|
|
||||||
|
expect($client->applyPolicyCalls[0]['payload']['qualityUpdateCveIds'])->toBe(['CVE-2025-0001']);
|
||||||
|
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('deployableContentDisplayName');
|
||||||
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('releaseDateDisplayName');
|
||||||
|
});
|
||||||
77
tests/Feature/Filament/WindowsUpdateRingPolicyTest.php
Normal file
77
tests/Feature/Filament/WindowsUpdateRingPolicyTest.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?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' => 'local-tenant',
|
||||||
|
'name' => 'Tenant One',
|
||||||
|
'metadata' => [],
|
||||||
|
'is_current' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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');
|
||||||
|
});
|
||||||
151
tests/Feature/Filament/WindowsUpdateRingRestoreTest.php
Normal file
151
tests/Feature/Filament/WindowsUpdateRingRestoreTest.php
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<?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 array $requests = [];
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$this->requests[] = [
|
||||||
|
'method' => strtoupper($method),
|
||||||
|
'path' => $path,
|
||||||
|
'payload' => $options['json'] ?? null,
|
||||||
|
];
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
'id' => 'policy-wuring',
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsUpdateForBusinessConfiguration',
|
||||||
|
'automaticUpdateMode' => 'autoInstallAtMaintenanceTime',
|
||||||
|
'featureUpdatesDeferralPeriodInDays' => 14,
|
||||||
|
'version' => 7,
|
||||||
|
'qualityUpdatesPauseStartDate' => '2025-01-01T00:00:00Z',
|
||||||
|
'featureUpdatesPauseStartDate' => '2025-01-02T00:00:00Z',
|
||||||
|
'qualityUpdatesWillBeRolledBack' => false,
|
||||||
|
'featureUpdatesWillBeRolledBack' => false,
|
||||||
|
'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->requests)->toHaveCount(1);
|
||||||
|
expect($client->requests[0]['method'])->toBe('PATCH');
|
||||||
|
expect($client->requests[0]['path'])->toBe('deviceManagement/deviceConfigurations/policy-wuring/microsoft.graph.windowsUpdateForBusinessConfiguration');
|
||||||
|
|
||||||
|
expect($client->requests[0]['payload']['automaticUpdateMode'])->toBe('autoInstallAtMaintenanceTime');
|
||||||
|
expect($client->requests[0]['payload']['featureUpdatesDeferralPeriodInDays'])->toBe(14);
|
||||||
|
expect($client->requests[0]['payload']['roleScopeTagIds'])->toBe(['0']);
|
||||||
|
|
||||||
|
expect($client->requests[0]['payload'])->not->toHaveKey('id');
|
||||||
|
expect($client->requests[0]['payload'])->not->toHaveKey('@odata.type');
|
||||||
|
expect($client->requests[0]['payload'])->not->toHaveKey('version');
|
||||||
|
expect($client->requests[0]['payload'])->not->toHaveKey('qualityUpdatesPauseStartDate');
|
||||||
|
expect($client->requests[0]['payload'])->not->toHaveKey('featureUpdatesPauseStartDate');
|
||||||
|
expect($client->requests[0]['payload'])->not->toHaveKey('qualityUpdatesWillBeRolledBack');
|
||||||
|
expect($client->requests[0]['payload'])->not->toHaveKey('featureUpdatesWillBeRolledBack');
|
||||||
|
});
|
||||||
@ -49,7 +49,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
|
|
||||||
test('sync skips managed app configurations from app protection inventory', function () {
|
test('sync skips managed app configurations from app protection inventory', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'test-tenant'),
|
'tenant_id' => 'test-tenant',
|
||||||
'name' => 'Test Tenant',
|
'name' => 'Test Tenant',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
|
|||||||
@ -49,7 +49,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
|
|
||||||
test('sync revives ignored policies when they exist in Intune', function () {
|
test('sync revives ignored policies when they exist in Intune', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'test-tenant'),
|
'tenant_id' => 'test-tenant',
|
||||||
'name' => 'Test Tenant',
|
'name' => 'Test Tenant',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
@ -94,7 +94,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
|
|
||||||
test('sync creates new policies even if ignored ones exist with same external_id', function () {
|
test('sync creates new policies even if ignored ones exist with same external_id', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => env('INTUNE_TENANT_ID', 'test-tenant-2'),
|
'tenant_id' => 'test-tenant-2',
|
||||||
'name' => 'Test Tenant 2',
|
'name' => 'Test Tenant 2',
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
'is_current' => true,
|
'is_current' => true,
|
||||||
|
|||||||
77
tests/Feature/PolicySyncServiceTest.php
Normal file
77
tests/Feature/PolicySyncServiceTest.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Policy;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Services\Graph\GraphClientInterface;
|
||||||
|
use App\Services\Graph\GraphLogger;
|
||||||
|
use App\Services\Graph\GraphResponse;
|
||||||
|
use App\Services\Intune\PolicySyncService;
|
||||||
|
|
||||||
|
use function Pest\Laravel\mock;
|
||||||
|
|
||||||
|
it('marks targeted managed app configurations as ignored during sync', function () {
|
||||||
|
$tenant = Tenant::factory()->create([
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$policy = Policy::factory()->create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'external_id' => 'policy-1',
|
||||||
|
'policy_type' => 'appProtectionPolicy',
|
||||||
|
'ignored_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$logger = mock(GraphLogger::class);
|
||||||
|
|
||||||
|
$logger->shouldReceive('logRequest')
|
||||||
|
->zeroOrMoreTimes()
|
||||||
|
->andReturnNull();
|
||||||
|
|
||||||
|
$logger->shouldReceive('logResponse')
|
||||||
|
->zeroOrMoreTimes()
|
||||||
|
->andReturnNull();
|
||||||
|
|
||||||
|
mock(GraphClientInterface::class)
|
||||||
|
->shouldReceive('listPolicies')
|
||||||
|
->once()
|
||||||
|
->andReturn(new GraphResponse(
|
||||||
|
success: true,
|
||||||
|
data: [
|
||||||
|
[
|
||||||
|
'id' => 'policy-1',
|
||||||
|
'displayName' => 'Ignored policy',
|
||||||
|
'@odata.type' => '#microsoft.graph.targetedManagedAppConfiguration',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
$service = app(PolicySyncService::class);
|
||||||
|
|
||||||
|
$synced = $service->syncPolicies($tenant, [
|
||||||
|
['type' => 'appProtectionPolicy'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$policy->refresh();
|
||||||
|
|
||||||
|
expect($policy->ignored_at)->not->toBeNull();
|
||||||
|
expect($synced)->toBeArray()->toBeEmpty();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses isof filters for windows update rings and supports feature/quality update profiles', function () {
|
||||||
|
$supported = config('tenantpilot.supported_policy_types');
|
||||||
|
$byType = collect($supported)->keyBy('type');
|
||||||
|
|
||||||
|
expect($byType)->toHaveKeys(['deviceConfiguration', 'windowsUpdateRing', 'windowsFeatureUpdateProfile', 'windowsQualityUpdateProfile']);
|
||||||
|
|
||||||
|
expect($byType['deviceConfiguration']['filter'] ?? null)
|
||||||
|
->toBe("not isof('microsoft.graph.windowsUpdateForBusinessConfiguration')");
|
||||||
|
|
||||||
|
expect($byType['windowsUpdateRing']['filter'] ?? null)
|
||||||
|
->toBe("isof('microsoft.graph.windowsUpdateForBusinessConfiguration')");
|
||||||
|
|
||||||
|
expect($byType['windowsFeatureUpdateProfile']['endpoint'] ?? null)
|
||||||
|
->toBe('deviceManagement/windowsFeatureUpdateProfiles');
|
||||||
|
|
||||||
|
expect($byType['windowsQualityUpdateProfile']['endpoint'] ?? null)
|
||||||
|
->toBe('deviceManagement/windowsQualityUpdateProfiles');
|
||||||
|
});
|
||||||
@ -4,4 +4,13 @@
|
|||||||
|
|
||||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
|
|
||||||
abstract class TestCase extends BaseTestCase {}
|
abstract class TestCase extends BaseTestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
putenv('INTUNE_TENANT_ID');
|
||||||
|
unset($_ENV['INTUNE_TENANT_ID'], $_SERVER['INTUNE_TENANT_ID']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -115,7 +115,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
expect($result['items'][1]['source_id'])->toBe('filter-2');
|
expect($result['items'][1]['source_id'])->toBe('filter-2');
|
||||||
|
|
||||||
expect($client->requests[0]['path'])->toBe('deviceManagement/assignmentFilters');
|
expect($client->requests[0]['path'])->toBe('deviceManagement/assignmentFilters');
|
||||||
expect($client->requests[0]['options']['query']['$select'])->toBe(['id', 'displayName']);
|
expect($client->requests[0]['options']['query']['$select'])->toBe('id,displayName');
|
||||||
expect($client->requests[1]['path'])->toBe('deviceManagement/assignmentFilters?$skiptoken=abc');
|
expect($client->requests[1]['path'])->toBe('deviceManagement/assignmentFilters?$skiptoken=abc');
|
||||||
expect($client->requests[1]['options']['query'])->toBe([]);
|
expect($client->requests[1]['options']['query'])->toBe([]);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -169,3 +169,85 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
expect($client->requests[0][3]['select'])->toContain('roleScopeTagIds');
|
expect($client->requests[0][3]['select'])->toContain('roleScopeTagIds');
|
||||||
expect($client->requests[0][3]['select'])->not->toContain('@odata.type');
|
expect($client->requests[0][3]['select'])->not->toContain('@odata.type');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class WindowsUpdateRingSnapshotGraphClient 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, $options];
|
||||||
|
|
||||||
|
return new GraphResponse(success: true, data: [
|
||||||
|
'payload' => [
|
||||||
|
'id' => $policyId,
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsUpdateForBusinessConfiguration',
|
||||||
|
'displayName' => 'Ring A',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ($method === 'GET' && $path === 'deviceManagement/deviceConfigurations/policy-wuring/microsoft.graph.windowsUpdateForBusinessConfiguration') {
|
||||||
|
return new GraphResponse(success: true, data: [
|
||||||
|
'automaticUpdateMode' => 'autoInstallAtMaintenanceTime',
|
||||||
|
'featureUpdatesDeferralPeriodInDays' => 14,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GraphResponse(success: true, data: []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('hydrates windows update ring snapshots via derived type cast endpoint', function () {
|
||||||
|
$client = new WindowsUpdateRingSnapshotGraphClient;
|
||||||
|
app()->instance(GraphClientInterface::class, $client);
|
||||||
|
|
||||||
|
$tenant = Tenant::factory()->create([
|
||||||
|
'tenant_id' => 'tenant-wuring',
|
||||||
|
'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' => 'policy-wuring',
|
||||||
|
'policy_type' => 'windowsUpdateRing',
|
||||||
|
'display_name' => 'Ring A',
|
||||||
|
'platform' => 'windows',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = app(PolicySnapshotService::class);
|
||||||
|
$result = $service->fetch($tenant, $policy);
|
||||||
|
|
||||||
|
expect($result)->toHaveKey('payload');
|
||||||
|
expect($result['payload']['@odata.type'])->toBe('#microsoft.graph.windowsUpdateForBusinessConfiguration');
|
||||||
|
expect($result['payload']['automaticUpdateMode'])->toBe('autoInstallAtMaintenanceTime');
|
||||||
|
expect($result['payload']['featureUpdatesDeferralPeriodInDays'])->toBe(14);
|
||||||
|
expect($result['metadata']['properties_hydration'] ?? null)->toBe('complete');
|
||||||
|
});
|
||||||
|
|||||||
@ -108,3 +108,27 @@ function restoreIntuneTenantId(string|false $original): void
|
|||||||
|
|
||||||
restoreIntuneTenantId($originalEnv);
|
restoreIntuneTenantId($originalEnv);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('makeCurrent keeps tenant current when already current', function () {
|
||||||
|
$originalEnv = getenv('INTUNE_TENANT_ID');
|
||||||
|
putenv('INTUNE_TENANT_ID=');
|
||||||
|
|
||||||
|
$current = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-current',
|
||||||
|
'name' => 'Already Current',
|
||||||
|
'is_current' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$other = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-other',
|
||||||
|
'name' => 'Other Tenant',
|
||||||
|
'is_current' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$current->makeCurrent();
|
||||||
|
|
||||||
|
expect($current->fresh()->is_current)->toBeTrue();
|
||||||
|
expect($other->fresh()->is_current)->toBeFalse();
|
||||||
|
|
||||||
|
restoreIntuneTenantId($originalEnv);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user