merge: agent session work
This commit is contained in:
commit
6910e40b91
@ -103,7 +103,7 @@ public function settingsWriteMethod(string $policyType): ?string
|
|||||||
return strtoupper($method);
|
return strtoupper($method);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function settingsWritePath(string $policyType, string $policyId, string $settingId): ?string
|
public function settingsWritePath(string $policyType, string $policyId, ?string $settingId = null): ?string
|
||||||
{
|
{
|
||||||
$contract = $this->get($policyType);
|
$contract = $this->get($policyType);
|
||||||
$write = $contract['settings_write'] ?? null;
|
$write = $contract['settings_write'] ?? null;
|
||||||
@ -113,11 +113,26 @@ public function settingsWritePath(string $policyType, string $policyId, string $
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return str_replace(
|
if ($settingId === null && str_contains($template, '{settingId}')) {
|
||||||
['{id}', '{settingId}'],
|
return null;
|
||||||
[urlencode($policyId), urlencode($settingId)],
|
}
|
||||||
$template
|
|
||||||
);
|
$path = str_replace('{id}', urlencode($policyId), $template);
|
||||||
|
|
||||||
|
if ($settingId !== null) {
|
||||||
|
$path = str_replace('{settingId}', urlencode($settingId), $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function settingsWriteBodyShape(string $policyType): string
|
||||||
|
{
|
||||||
|
$contract = $this->get($policyType);
|
||||||
|
$write = $contract['settings_write'] ?? null;
|
||||||
|
$shape = is_array($write) ? ($write['body_shape'] ?? 'collection') : 'collection';
|
||||||
|
|
||||||
|
return is_string($shape) && $shape !== '' ? $shape : 'collection';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -151,19 +166,21 @@ private function sanitizeSettingsItem(array $item): array
|
|||||||
|
|
||||||
// First pass: collect information and process items
|
// First pass: collect information and process items
|
||||||
foreach ($item as $key => $value) {
|
foreach ($item as $key => $value) {
|
||||||
if (strtolower($key) === 'id') {
|
$normalizedKey = strtolower((string) $key);
|
||||||
|
|
||||||
|
if ($normalizedKey === 'id') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($key === '@odata.type') {
|
if ($normalizedKey === '@odata.type') {
|
||||||
$existingOdataType = $value;
|
$existingOdataType = $value;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($key === 'settingInstance' && is_array($value)) {
|
if ($normalizedKey === 'settinginstance' && is_array($value)) {
|
||||||
$hasSettingInstance = true;
|
$hasSettingInstance = true;
|
||||||
$result[$key] = $this->preserveOdataTypesRecursively($value);
|
$result['settingInstance'] = $this->preserveOdataTypesRecursively($value);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -406,109 +406,106 @@ private function applySettingsCatalogPolicySettings(
|
|||||||
array $context,
|
array $context,
|
||||||
): array {
|
): array {
|
||||||
$method = $this->contracts->settingsWriteMethod('settingsCatalogPolicy');
|
$method = $this->contracts->settingsWriteMethod('settingsCatalogPolicy');
|
||||||
$issues = [];
|
$path = $this->contracts->settingsWritePath('settingsCatalogPolicy', $policyId);
|
||||||
$applied = 0;
|
$bodyShape = strtolower($this->contracts->settingsWriteBodyShape('settingsCatalogPolicy'));
|
||||||
$failed = 0;
|
|
||||||
$manualRequired = 0;
|
|
||||||
|
|
||||||
foreach ($settings as $setting) {
|
$buildIssues = function (string $reason) use ($settings): array {
|
||||||
if (! is_array($setting)) {
|
$issues = [];
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$settingId = $this->resolveSettingsCatalogSettingId($setting);
|
foreach ($settings as $setting) {
|
||||||
$path = ($method && $settingId)
|
if (! is_array($setting)) {
|
||||||
? $this->contracts->settingsWritePath('settingsCatalogPolicy', $policyId, $settingId)
|
continue;
|
||||||
: null;
|
}
|
||||||
|
|
||||||
if (! $method || ! $path || ! $settingId) {
|
|
||||||
$manualRequired++;
|
|
||||||
$issues[] = array_filter([
|
|
||||||
'setting_id' => $settingId,
|
|
||||||
'status' => 'manual_required',
|
|
||||||
'reason' => ! $settingId
|
|
||||||
? 'Setting id missing (cannot apply automatically).'
|
|
||||||
: 'Settings write contract is not configured (cannot apply automatically).',
|
|
||||||
], static fn ($value) => $value !== null && $value !== '');
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sanitized = $this->contracts->sanitizeSettingsApplyPayload('settingsCatalogPolicy', [$setting])[0] ?? null;
|
|
||||||
|
|
||||||
if (! is_array($sanitized) || $sanitized === []) {
|
|
||||||
$manualRequired++;
|
|
||||||
$issues[] = [
|
$issues[] = [
|
||||||
'setting_id' => $settingId,
|
'setting_id' => $this->resolveSettingsCatalogSettingId($setting),
|
||||||
'status' => 'manual_required',
|
'status' => 'manual_required',
|
||||||
'reason' => 'Setting payload could not be sanitized (empty payload).',
|
'reason' => $reason,
|
||||||
];
|
];
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->graphLogger->logRequest('apply_setting', $context + [
|
return $issues;
|
||||||
'setting_id' => $settingId,
|
};
|
||||||
'endpoint' => $path,
|
|
||||||
'method' => $method,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->graphClient->request($method, $path, ['json' => $sanitized] + Arr::except($graphOptions, ['platform']));
|
if (! $method || ! $path) {
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'total' => count($settings),
|
||||||
|
'applied' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
'manual_required' => count($settings),
|
||||||
|
'issues' => $buildIssues('Settings write contract is not configured (cannot apply automatically).'),
|
||||||
|
],
|
||||||
|
'manual_required',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$this->graphLogger->logResponse('apply_setting', $response, $context + [
|
$sanitized = $this->contracts->sanitizeSettingsApplyPayload('settingsCatalogPolicy', $settings);
|
||||||
'setting_id' => $settingId,
|
|
||||||
'endpoint' => $path,
|
|
||||||
'method' => $method,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($response->successful()) {
|
if (! is_array($sanitized) || $sanitized === []) {
|
||||||
$applied++;
|
return [
|
||||||
|
[
|
||||||
|
'total' => count($settings),
|
||||||
|
'applied' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
'manual_required' => count($settings),
|
||||||
|
'issues' => $buildIssues('Settings payload could not be sanitized (empty payload).'),
|
||||||
|
],
|
||||||
|
'manual_required',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
$payload = match ($bodyShape) {
|
||||||
}
|
'wrapped' => ['settings' => $sanitized],
|
||||||
|
default => $sanitized,
|
||||||
|
};
|
||||||
|
|
||||||
if ($response->status === 404) {
|
$this->graphLogger->logRequest('apply_settings_bulk', $context + [
|
||||||
$manualRequired++;
|
'endpoint' => $path,
|
||||||
$issues[] = [
|
'method' => $method,
|
||||||
'setting_id' => $settingId,
|
'settings_count' => count($sanitized),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->graphClient->request($method, $path, ['json' => $payload] + Arr::except($graphOptions, ['platform']));
|
||||||
|
|
||||||
|
$this->graphLogger->logResponse('apply_settings_bulk', $response, $context + [
|
||||||
|
'endpoint' => $path,
|
||||||
|
'method' => $method,
|
||||||
|
'settings_count' => count($sanitized),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->successful()) {
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'total' => count($settings),
|
||||||
|
'applied' => count($settings),
|
||||||
|
'failed' => 0,
|
||||||
|
'manual_required' => 0,
|
||||||
|
'issues' => [],
|
||||||
|
],
|
||||||
|
'applied',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'total' => count($settings),
|
||||||
|
'applied' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
'manual_required' => count($settings),
|
||||||
|
'issues' => [[
|
||||||
|
'setting_id' => null,
|
||||||
'status' => 'manual_required',
|
'status' => 'manual_required',
|
||||||
'reason' => 'Setting not found on target policy (404).',
|
'reason' => 'Graph bulk apply failed',
|
||||||
|
'http_status' => $response->status,
|
||||||
'graph_error_message' => $response->meta['error_message'] ?? null,
|
'graph_error_message' => $response->meta['error_message'] ?? null,
|
||||||
'graph_error_code' => $response->meta['error_code'] ?? null,
|
'graph_error_code' => $response->meta['error_code'] ?? null,
|
||||||
'graph_request_id' => $response->meta['request_id'] ?? null,
|
'graph_request_id' => $response->meta['request_id'] ?? null,
|
||||||
'graph_client_request_id' => $response->meta['client_request_id'] ?? null,
|
'graph_client_request_id' => $response->meta['client_request_id'] ?? null,
|
||||||
];
|
]],
|
||||||
|
],
|
||||||
continue;
|
'manual_required',
|
||||||
}
|
|
||||||
|
|
||||||
$failed++;
|
|
||||||
$issues[] = [
|
|
||||||
'setting_id' => $settingId,
|
|
||||||
'status' => 'failed',
|
|
||||||
'reason' => 'Graph apply failed',
|
|
||||||
'graph_error_message' => $response->meta['error_message'] ?? null,
|
|
||||||
'graph_error_code' => $response->meta['error_code'] ?? null,
|
|
||||||
'graph_request_id' => $response->meta['request_id'] ?? null,
|
|
||||||
'graph_client_request_id' => $response->meta['client_request_id'] ?? null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$summary = [
|
|
||||||
'total' => count($settings),
|
|
||||||
'applied' => $applied,
|
|
||||||
'failed' => $failed,
|
|
||||||
'manual_required' => $manualRequired,
|
|
||||||
'issues' => $issues,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$status = match (true) {
|
|
||||||
$manualRequired > 0 => 'manual_required',
|
|
||||||
$failed > 0 => 'partial',
|
|
||||||
default => 'applied',
|
|
||||||
};
|
|
||||||
|
|
||||||
return [$summary, $status];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertActiveContext(Tenant $tenant, BackupSet $backupSet): void
|
private function assertActiveContext(Tenant $tenant, BackupSet $backupSet): void
|
||||||
|
|||||||
@ -63,8 +63,10 @@
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
'settings_write' => [
|
'settings_write' => [
|
||||||
'path_template' => 'deviceManagement/configurationPolicies/{id}/settings/{settingId}',
|
'path_template' => 'deviceManagement/configurationPolicies/{id}/settings',
|
||||||
'method' => 'PATCH',
|
'method' => 'POST',
|
||||||
|
'bulk' => true,
|
||||||
|
'body_shape' => 'collection',
|
||||||
],
|
],
|
||||||
'update_strategy' => 'settings_catalog_policy_with_settings',
|
'update_strategy' => 'settings_catalog_policy_with_settings',
|
||||||
],
|
],
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
test('settings catalog restore marks manual_required when a setting PATCH returns 404', function () {
|
test('settings catalog restore marks manual_required when bulk apply returns 404', function () {
|
||||||
$policyResponse = new GraphResponse(
|
$policyResponse = new GraphResponse(
|
||||||
success: true,
|
success: true,
|
||||||
data: [],
|
data: [],
|
||||||
@ -162,13 +162,16 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
expect($run->status)->toBe('partial');
|
expect($run->status)->toBe('partial');
|
||||||
expect($run->results[0]['status'])->toBe('manual_required');
|
expect($run->results[0]['status'])->toBe('manual_required');
|
||||||
expect($run->results[0]['settings_apply']['manual_required'])->toBe(1);
|
expect($run->results[0]['settings_apply']['manual_required'])->toBe(1);
|
||||||
|
expect($run->results[0]['settings_apply']['failed'])->toBe(0);
|
||||||
expect($run->results[0]['settings_apply']['issues'][0]['graph_request_id'])->toBe('req-setting-404');
|
expect($run->results[0]['settings_apply']['issues'][0]['graph_request_id'])->toBe('req-setting-404');
|
||||||
|
|
||||||
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('settings');
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('settings');
|
||||||
expect($client->requestCalls[0]['path'])->toBe('deviceManagement/configurationPolicies/scp-3/settings/setting-404');
|
expect($client->requestCalls[0]['method'])->toBe('POST');
|
||||||
|
expect($client->requestCalls[0]['path'])->toBe('deviceManagement/configurationPolicies/scp-3/settings');
|
||||||
|
|
||||||
$response = $this->get(route('filament.admin.resources.restore-runs.view', ['record' => $run]));
|
$response = $this->get(route('filament.admin.resources.restore-runs.view', ['record' => $run]));
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('Setting not found on target policy (404).');
|
$response->assertSee('Graph bulk apply failed');
|
||||||
|
$response->assertSee('Setting missing');
|
||||||
$response->assertSee('req-setting-404');
|
$response->assertSee('req-setting-404');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -77,7 +77,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('restore marks settings catalog policy as partial when a setting PATCH fails', function () {
|
test('restore marks settings catalog policy as manual_required when bulk settings apply fails', function () {
|
||||||
$policyResponse = new GraphResponse(
|
$policyResponse = new GraphResponse(
|
||||||
success: true,
|
success: true,
|
||||||
data: [],
|
data: [],
|
||||||
@ -175,8 +175,9 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
)->refresh();
|
)->refresh();
|
||||||
|
|
||||||
expect($run->status)->toBe('partial');
|
expect($run->status)->toBe('partial');
|
||||||
expect($run->results[0]['status'])->toBe('partial');
|
expect($run->results[0]['status'])->toBe('manual_required');
|
||||||
expect($run->results[0]['settings_apply']['failed'])->toBe(1);
|
expect($run->results[0]['settings_apply']['manual_required'])->toBe(1);
|
||||||
|
expect($run->results[0]['settings_apply']['failed'])->toBe(0);
|
||||||
expect($run->results[0]['settings_apply']['issues'][0]['graph_error_message'])->toContain('settings are read-only');
|
expect($run->results[0]['settings_apply']['issues'][0]['graph_error_message'])->toContain('settings are read-only');
|
||||||
expect($run->results[0]['settings_apply']['issues'][0]['graph_request_id'])->toBe('req-123');
|
expect($run->results[0]['settings_apply']['issues'][0]['graph_request_id'])->toBe('req-123');
|
||||||
expect($run->results[0]['settings_apply']['issues'][0]['graph_client_request_id'])->toBe('client-abc');
|
expect($run->results[0]['settings_apply']['issues'][0]['graph_client_request_id'])->toBe('client-abc');
|
||||||
@ -191,12 +192,13 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('settings');
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('settings');
|
||||||
|
|
||||||
expect($client->requestCalls)->toHaveCount(1);
|
expect($client->requestCalls)->toHaveCount(1);
|
||||||
expect($client->requestCalls[0]['method'])->toBe('PATCH');
|
expect($client->requestCalls[0]['method'])->toBe('POST');
|
||||||
expect($client->requestCalls[0]['path'])->toBe('deviceManagement/configurationPolicies/scp-1/settings/setting-1');
|
expect($client->requestCalls[0]['path'])->toBe('deviceManagement/configurationPolicies/scp-1/settings');
|
||||||
expect($client->requestCalls[0]['payload'])->toBeArray();
|
expect($client->requestCalls[0]['payload'])->toBeArray();
|
||||||
expect($client->requestCalls[0]['payload'])->toHaveKey('@odata.type');
|
expect($client->requestCalls[0]['payload'][0])->toHaveKey('@odata.type');
|
||||||
expect($client->requestCalls[0]['payload'])->not->toHaveKey('id');
|
expect($client->requestCalls[0]['payload'][0])->not->toHaveKey('id');
|
||||||
expect($client->requestCalls[0]['payload']['settingInstance']['@odata.type'])->toBe('#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance');
|
expect($client->requestCalls[0]['payload'][0]['settingInstance']['@odata.type'])
|
||||||
|
->toBe('#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance');
|
||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->get(route('filament.admin.resources.restore-runs.view', ['record' => $run]));
|
->get(route('filament.admin.resources.restore-runs.view', ['record' => $run]));
|
||||||
@ -292,13 +294,14 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
expect($client->applyPolicyCalls[0]['payload']['description'])->toBe('desc');
|
expect($client->applyPolicyCalls[0]['payload']['description'])->toBe('desc');
|
||||||
|
|
||||||
expect($client->requestCalls)->toHaveCount(1);
|
expect($client->requestCalls)->toHaveCount(1);
|
||||||
expect($client->requestCalls[0]['method'])->toBe('PATCH');
|
expect($client->requestCalls[0]['method'])->toBe('POST');
|
||||||
expect($client->requestCalls[0]['path'])->toBe('deviceManagement/configurationPolicies/scp-2/settings/setting-1');
|
expect($client->requestCalls[0]['path'])->toBe('deviceManagement/configurationPolicies/scp-2/settings');
|
||||||
|
|
||||||
// Ensure we preserved settingInstance @odata.type and stripped ids in the per-setting call
|
// Ensure we preserved settingInstance @odata.type and stripped ids in the bulk call
|
||||||
expect($client->requestCalls[0]['payload'])->toHaveKey('@odata.type');
|
expect($client->requestCalls[0]['payload'][0])->toHaveKey('@odata.type');
|
||||||
expect($client->requestCalls[0]['payload']['@odata.type'])->toBe('#microsoft.graph.deviceManagementConfigurationSetting');
|
expect($client->requestCalls[0]['payload'][0]['@odata.type'])->toBe('#microsoft.graph.deviceManagementConfigurationSetting');
|
||||||
expect($client->requestCalls[0]['payload'])->not->toHaveKey('id');
|
expect($client->requestCalls[0]['payload'][0])->not->toHaveKey('id');
|
||||||
expect($client->requestCalls[0]['payload']['settingInstance'])->toHaveKey('@odata.type');
|
expect($client->requestCalls[0]['payload'][0]['settingInstance'])->toHaveKey('@odata.type');
|
||||||
expect($client->requestCalls[0]['payload']['settingInstance']['@odata.type'])->toBe('#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance');
|
expect($client->requestCalls[0]['payload'][0]['settingInstance']['@odata.type'])
|
||||||
|
->toBe('#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -20,6 +20,32 @@
|
|||||||
->toBe('deviceManagement/configurationPolicies/policy-1/settings/setting-9');
|
->toBe('deviceManagement/configurationPolicies/policy-1/settings/setting-9');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns null when settings write path requires a setting id', function () {
|
||||||
|
config()->set('graph_contracts.types.settingsCatalogPolicy', [
|
||||||
|
'settings_write' => [
|
||||||
|
'path_template' => 'deviceManagement/configurationPolicies/{id}/settings/{settingId}',
|
||||||
|
'method' => 'PATCH',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$registry = app(GraphContractRegistry::class);
|
||||||
|
|
||||||
|
expect($registry->settingsWritePath('settingsCatalogPolicy', 'policy-1'))->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults settings write body shape to collection', function () {
|
||||||
|
config()->set('graph_contracts.types.settingsCatalogPolicy', [
|
||||||
|
'settings_write' => [
|
||||||
|
'path_template' => 'deviceManagement/configurationPolicies/{id}/settings',
|
||||||
|
'method' => 'POST',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$registry = app(GraphContractRegistry::class);
|
||||||
|
|
||||||
|
expect($registry->settingsWriteBodyShape('settingsCatalogPolicy'))->toBe('collection');
|
||||||
|
});
|
||||||
|
|
||||||
it('returns null when settings write contract is missing', function () {
|
it('returns null when settings write contract is missing', function () {
|
||||||
config()->set('graph_contracts.types.settingsCatalogPolicy', []);
|
config()->set('graph_contracts.types.settingsCatalogPolicy', []);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user