TenantAtlas/tests/Feature/Filament/WindowsUpdateProfilesRestoreTest.php
ahmido 440e63edff feat: implement tenant action taxonomy lifecycle visibility (#174)
## Summary

Implements Spec 145 for tenant action taxonomy and lifecycle-safe visibility.

This PR:
- adds a central tenant action policy surface and supporting value objects
- aligns tenant list, detail, edit, onboarding, and widget surfaces around lifecycle-safe actions
- standardizes operator-facing lifecycle wording around View, Resume onboarding, Archive, Restore, and Complete onboarding
- tightens onboarding and tenant lifecycle authorization semantics, including honest 404 vs 403 behavior
- updates related regression coverage and spec artifacts for Spec 145
- fixes follow-on full-suite regressions uncovered during validation, including onboarding browser flows, provider consent fixtures, workspace redirect DI expectations, and critical table/action/UI expectation drift

## Validation

Executed and passed:
- vendor/bin/sail bin pint --dirty --format agent
- vendor/bin/sail artisan test --compact

Result:
- 2581 passed
- 8 skipped
- 13534 assertions

## Notes

- Base branch: dev
- Feature branch commit: a33a41b
- Filament v5 / Livewire v4 compliance preserved
- No panel provider registration changes; Laravel 12 provider registration remains in bootstrap/providers.php
- No new globally searchable resource behavior added in this slice
- Destructive lifecycle actions remain confirmation-gated and authorization-protected

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #174
2026-03-16 00:57:17 +00:00

319 lines
11 KiB
PHP

<?php
use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\ProviderConnection;
use App\Models\ProviderCredential;
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);
if (! function_exists('makeTenantWithDefaultProviderConnection')) {
function makeTenantWithDefaultProviderConnection(array $attributes = []): Tenant
{
$tenant = Tenant::create(array_merge([
'tenant_id' => 'tenant-1',
'name' => 'Tenant One',
'metadata' => [],
], $attributes));
$connection = ProviderConnection::factory()->consentGranted()->create([
'tenant_id' => $tenant->id,
'provider' => 'microsoft',
'is_default' => true,
'status' => 'connected',
]);
ProviderCredential::factory()->create([
'provider_connection_id' => $connection->id,
]);
return $tenant;
}
}
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 = makeTenantWithDefaultProviderConnection();
$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');
$result = $run->results['items'][$backupItem->id] ?? null;
expect($result)->not->toBeNull();
expect($result['status'] ?? null)->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 = makeTenantWithDefaultProviderConnection();
$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');
$result = $run->results['items'][$backupItem->id] ?? null;
expect($result)->not->toBeNull();
expect($result['status'] ?? null)->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');
});
test('restore execution applies windows driver update profile with sanitized payload', function () {
$client = new WindowsUpdateProfilesRestoreGraphClient;
app()->instance(GraphClientInterface::class, $client);
$tenant = makeTenantWithDefaultProviderConnection();
$policy = Policy::create([
'tenant_id' => $tenant->id,
'external_id' => 'policy-driver',
'policy_type' => 'windowsDriverUpdateProfile',
'display_name' => 'Driver Updates A',
'platform' => 'windows',
]);
$backupSet = BackupSet::create([
'tenant_id' => $tenant->id,
'name' => 'Backup',
'status' => 'completed',
'item_count' => 1,
]);
$backupPayload = [
'id' => 'policy-driver',
'@odata.type' => '#microsoft.graph.windowsDriverUpdateProfile',
'displayName' => 'Driver Updates A',
'description' => 'Drivers rollout policy',
'approvalType' => 'automatic',
'deploymentDeferralInDays' => 7,
'deviceReporting' => 12,
'newUpdates' => 3,
'inventorySyncStatus' => [
'driverInventorySyncState' => 'success',
'lastSuccessfulSyncDateTime' => '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');
$result = $run->results['items'][$backupItem->id] ?? null;
expect($result)->not->toBeNull();
expect($result['status'] ?? null)->toBe('applied');
expect(PolicyVersion::where('policy_id', $policy->id)->count())->toBe(1);
expect($client->applyPolicyCalls)->toHaveCount(1);
expect($client->applyPolicyCalls[0]['policyType'])->toBe('windowsDriverUpdateProfile');
expect($client->applyPolicyCalls[0]['policyId'])->toBe('policy-driver');
expect($client->applyPolicyCalls[0]['options']['method'] ?? null)->toBe('PATCH');
expect($client->applyPolicyCalls[0]['payload']['approvalType'])->toBe('automatic');
expect($client->applyPolicyCalls[0]['payload']['deploymentDeferralInDays'])->toBe(7);
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('deviceReporting');
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('newUpdates');
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('inventorySyncStatus');
});