217 lines
7.7 KiB
PHP
217 lines
7.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\BackupSchedule;
|
|
use App\Models\BackupSet;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Services\AdapterRunReconciler;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('reconciles legacy backup schedule runs from a matching complete backup set in Spec362', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$schedule = spec362BackupSchedule($tenant);
|
|
$backupSet = BackupSet::factory()->for($tenant)->create([
|
|
'status' => 'completed',
|
|
'item_count' => 2,
|
|
'metadata' => [],
|
|
]);
|
|
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'backup_schedule_run',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subMinutes(20),
|
|
'context' => [
|
|
'backup_schedule_id' => (int) $schedule->getKey(),
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
],
|
|
]);
|
|
|
|
$result = app(AdapterRunReconciler::class)->reconcile([
|
|
'type' => 'backup_schedule_run',
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'older_than_minutes' => 10,
|
|
'limit' => 10,
|
|
'dry_run' => false,
|
|
]);
|
|
|
|
expect($result['candidates'] ?? null)->toBe(1)
|
|
->and($result['reconciled'] ?? null)->toBe(1);
|
|
|
|
$run->refresh();
|
|
$backupSet->refresh();
|
|
|
|
expect($run->status)->toBe(OperationRunStatus::Completed->value)
|
|
->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value)
|
|
->and($run->reconciliationAdapter())->toBe('backup_schedule_execution')
|
|
->and($run->reconciledRelatedBackupSetId())->toBe((int) $backupSet->getKey())
|
|
->and($run->summary_counts)->toMatchArray([
|
|
'total' => 2,
|
|
'processed' => 2,
|
|
'succeeded' => 2,
|
|
'failed' => 0,
|
|
'created' => 1,
|
|
'updated' => 2,
|
|
'items' => 2,
|
|
])
|
|
->and($backupSet->status)->toBe('completed');
|
|
|
|
expect(array_keys(OperationRunLinks::related($run->fresh(), $tenant)))
|
|
->toContain('Backup Schedules', 'Backup Sets', 'Backup Set');
|
|
});
|
|
|
|
it('marks backup schedule runs partially succeeded when the backup set is usable but incomplete in Spec362', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$schedule = spec362BackupSchedule($tenant);
|
|
$backupSet = BackupSet::factory()->for($tenant)->create([
|
|
'status' => 'partial',
|
|
'item_count' => 2,
|
|
'metadata' => [
|
|
'failures' => [
|
|
['policy_id' => 'policy-1'],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'backup.schedule.execute',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subMinutes(20),
|
|
'context' => [
|
|
'backup_schedule_id' => (int) $schedule->getKey(),
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
],
|
|
]);
|
|
|
|
$change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, false);
|
|
|
|
expect($change['applied'] ?? null)->toBeTrue();
|
|
|
|
$run->refresh();
|
|
|
|
expect($run->outcome)->toBe(OperationRunOutcome::PartiallySucceeded->value)
|
|
->and($run->reconciliationDecision())->toBe('reconciled_partially_succeeded')
|
|
->and($run->summary_counts)->toMatchArray([
|
|
'total' => 3,
|
|
'processed' => 3,
|
|
'succeeded' => 2,
|
|
'failed' => 1,
|
|
'created' => 1,
|
|
'updated' => 2,
|
|
'items' => 3,
|
|
]);
|
|
});
|
|
|
|
it('marks backup schedule runs blocked when the schedule was archived before any current backup set existed in Spec362', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$schedule = spec362BackupSchedule($tenant);
|
|
$schedule->delete();
|
|
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'backup.schedule.execute',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subMinutes(20),
|
|
'context' => [
|
|
'backup_schedule_id' => (int) $schedule->getKey(),
|
|
],
|
|
]);
|
|
|
|
$change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, false);
|
|
|
|
expect($change['applied'] ?? null)->toBeTrue();
|
|
|
|
$run->refresh();
|
|
|
|
expect($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
|
|
->and($run->reconciliationDecision())->toBe('blocked')
|
|
->and((string) data_get($run->failure_summary, '0.message'))->toContain('archived');
|
|
});
|
|
|
|
it('fails closed when a backup set crosses the queued tenant scope in Spec362', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
$foreignTenant = ManagedEnvironment::factory()->create();
|
|
|
|
$schedule = spec362BackupSchedule($tenant);
|
|
$foreignBackupSet = BackupSet::factory()->for($foreignTenant)->create();
|
|
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'backup.schedule.execute',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subMinutes(20),
|
|
'context' => [
|
|
'backup_schedule_id' => (int) $schedule->getKey(),
|
|
'backup_set_id' => (int) $foreignBackupSet->getKey(),
|
|
],
|
|
]);
|
|
|
|
$change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, true);
|
|
|
|
expect($change['applied'] ?? null)->toBeFalse()
|
|
->and($change['decision'] ?? null)->toBe('not_reconciled')
|
|
->and((string) ($change['reason_message'] ?? ''))->toContain('queued backup schedule scope safely');
|
|
|
|
$run->refresh();
|
|
|
|
expect($run->status)->toBe(OperationRunStatus::Queued->value)
|
|
->and($run->outcome)->toBe(OperationRunOutcome::Pending->value);
|
|
});
|
|
|
|
it('keeps backup schedule runs active when no current backup set proof exists yet in Spec362', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$schedule = spec362BackupSchedule($tenant);
|
|
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'backup.schedule.execute',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subMinutes(20),
|
|
'context' => [
|
|
'backup_schedule_id' => (int) $schedule->getKey(),
|
|
],
|
|
]);
|
|
|
|
$change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, true);
|
|
|
|
expect($change['applied'] ?? null)->toBeFalse()
|
|
->and($change['decision'] ?? null)->toBe('not_reconciled')
|
|
->and((string) ($change['reason_message'] ?? ''))->toContain('No current backup set proof');
|
|
|
|
$run->refresh();
|
|
|
|
expect($run->status)->toBe(OperationRunStatus::Queued->value)
|
|
->and($run->outcome)->toBe(OperationRunOutcome::Pending->value);
|
|
});
|
|
|
|
function spec362BackupSchedule(ManagedEnvironment $tenant, array $overrides = []): BackupSchedule
|
|
{
|
|
return BackupSchedule::query()->create(array_merge([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'name' => 'Spec362 Daily backup',
|
|
'is_enabled' => true,
|
|
'timezone' => 'UTC',
|
|
'frequency' => 'daily',
|
|
'time_of_day' => '10:00:00',
|
|
'days_of_week' => null,
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'include_foundations' => true,
|
|
'retention_keep_last' => 30,
|
|
'last_run_at' => null,
|
|
'last_run_status' => null,
|
|
'next_run_at' => null,
|
|
], $overrides));
|
|
}
|