Implements RBAC-based write gating for Intune restore flows, UI affordances, and audit logging; adds tests and specs.
126 lines
4.0 KiB
PHP
126 lines
4.0 KiB
PHP
<?php
|
|
|
|
use App\Jobs\ExecuteRestoreRunJob;
|
|
use App\Models\BackupSet;
|
|
use App\Models\RestoreRun;
|
|
use App\Models\Tenant;
|
|
use App\Services\Intune\AuditLogger;
|
|
use App\Services\Intune\RestoreService;
|
|
use App\Services\OperationRunService;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\RestoreRunStatus;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Mockery\MockInterface;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
config()->set('tenantpilot.hardening.intune_write_gate.enabled', true);
|
|
config()->set('tenantpilot.hardening.intune_write_gate.freshness_threshold_hours', 24);
|
|
});
|
|
|
|
test('execute restore run job marks run failed when rbac_status is not_configured', function () {
|
|
$tenant = Tenant::factory()->create([
|
|
'rbac_status' => 'not_configured',
|
|
'rbac_last_checked_at' => null,
|
|
]);
|
|
|
|
$backupSet = BackupSet::create([
|
|
'tenant_id' => $tenant->id,
|
|
'name' => 'Backup',
|
|
'status' => 'completed',
|
|
'item_count' => 0,
|
|
]);
|
|
|
|
$restoreRun = RestoreRun::create([
|
|
'tenant_id' => $tenant->id,
|
|
'backup_set_id' => $backupSet->id,
|
|
'requested_by' => 'actor@example.com',
|
|
'is_dry_run' => false,
|
|
'status' => RestoreRunStatus::Queued->value,
|
|
'requested_items' => null,
|
|
'preview' => [],
|
|
'results' => null,
|
|
'metadata' => [],
|
|
]);
|
|
|
|
$this->mock(RestoreService::class, function (MockInterface $mock) {
|
|
$mock->shouldNotReceive('executeForRun');
|
|
});
|
|
|
|
$operationRun = app(OperationRunService::class)->ensureRun(
|
|
tenant: $tenant,
|
|
type: 'restore.execute',
|
|
inputs: [
|
|
'restore_run_id' => $restoreRun->id,
|
|
'backup_set_id' => $backupSet->id,
|
|
'is_dry_run' => false,
|
|
],
|
|
);
|
|
|
|
$job = new ExecuteRestoreRunJob($restoreRun->id, 'actor@example.com', 'Actor', $operationRun);
|
|
$job->handle(app(RestoreService::class), app(AuditLogger::class));
|
|
|
|
$restoreRun->refresh();
|
|
$operationRun->refresh();
|
|
|
|
expect($restoreRun->status)->toBe(RestoreRunStatus::Failed->value)
|
|
->and($restoreRun->failure_reason)->toContain('not configured')
|
|
->and($operationRun->outcome)->toBe(OperationRunOutcome::Failed->value);
|
|
|
|
$failures = is_array($operationRun->failure_summary) ? $operationRun->failure_summary : [];
|
|
$reasonCodes = array_column($failures, 'reason_code');
|
|
|
|
expect($reasonCodes)->toContain('intune_rbac.not_configured');
|
|
});
|
|
|
|
test('execute restore run job marks run failed when rbac_status is stale', function () {
|
|
$tenant = Tenant::factory()->create([
|
|
'rbac_status' => 'ok',
|
|
'rbac_last_checked_at' => now()->subHours(48),
|
|
]);
|
|
|
|
$backupSet = BackupSet::create([
|
|
'tenant_id' => $tenant->id,
|
|
'name' => 'Backup',
|
|
'status' => 'completed',
|
|
'item_count' => 0,
|
|
]);
|
|
|
|
$restoreRun = RestoreRun::create([
|
|
'tenant_id' => $tenant->id,
|
|
'backup_set_id' => $backupSet->id,
|
|
'requested_by' => 'actor@example.com',
|
|
'is_dry_run' => false,
|
|
'status' => RestoreRunStatus::Queued->value,
|
|
'requested_items' => null,
|
|
'preview' => [],
|
|
'results' => null,
|
|
'metadata' => [],
|
|
]);
|
|
|
|
$this->mock(RestoreService::class, function (MockInterface $mock) {
|
|
$mock->shouldNotReceive('executeForRun');
|
|
});
|
|
|
|
$operationRun = app(OperationRunService::class)->ensureRun(
|
|
tenant: $tenant,
|
|
type: 'restore.execute',
|
|
inputs: [
|
|
'restore_run_id' => $restoreRun->id,
|
|
'backup_set_id' => $backupSet->id,
|
|
'is_dry_run' => false,
|
|
],
|
|
);
|
|
|
|
$job = new ExecuteRestoreRunJob($restoreRun->id, 'actor@example.com', 'Actor', $operationRun);
|
|
$job->handle(app(RestoreService::class), app(AuditLogger::class));
|
|
|
|
$operationRun->refresh();
|
|
|
|
$failures = is_array($operationRun->failure_summary) ? $operationRun->failure_summary : [];
|
|
$reasonCodes = array_column($failures, 'reason_code');
|
|
|
|
expect($reasonCodes)->toContain('intune_rbac.stale');
|
|
});
|