Implemented sync capture backup operation semantics as requested. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #433
229 lines
8.3 KiB
PHP
229 lines
8.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\BackupSet;
|
|
use App\Models\BaselineProfile;
|
|
use App\Models\BaselineSnapshot;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\User;
|
|
use App\Services\OperationRunService;
|
|
use App\Support\Inventory\InventoryCoverage;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
pest()->browser()->timeout(60_000);
|
|
|
|
it('Spec362 smokes selected-family reconciled truth across the existing operations hub and detail pages', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
|
|
spec362AuthenticateBrowser($this, $user, $environment);
|
|
|
|
[$inventoryRun, $baselineRun, $backupRun] = spec362BrowserSeedSelectedFamilyRuns($environment, $user);
|
|
|
|
visit(OperationRunLinks::index($environment))
|
|
->resize(1440, 1100)
|
|
->waitForText('Operations Hub')
|
|
->assertSee('Inventory sync')
|
|
->assertSee('Baseline capture')
|
|
->assertSee('Backup schedule run')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
|
|
visit(OperationRunLinks::tenantlessView($inventoryRun))
|
|
->waitForText('Monitoring detail')
|
|
->assertSee('Inventory sync')
|
|
->assertSee('Completed with follow-up')
|
|
->assertSee('Automatically reconciled')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
|
|
visit(OperationRunLinks::tenantlessView($baselineRun))
|
|
->waitForText('Monitoring detail')
|
|
->assertSee('Baseline capture')
|
|
->assertSee('Completed successfully')
|
|
->assertSee('Automatically reconciled')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
|
|
visit(OperationRunLinks::tenantlessView($backupRun))
|
|
->waitForText('Monitoring detail')
|
|
->assertSee('Backup schedule run')
|
|
->assertSee('Completed with follow-up')
|
|
->assertSee('Automatically reconciled')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
});
|
|
|
|
function spec362AuthenticateBrowser(mixed $test, User $user, ManagedEnvironment $environment): void
|
|
{
|
|
$workspaceId = (int) $environment->workspace_id;
|
|
|
|
$test->actingAs($user)->withSession([
|
|
WorkspaceContext::SESSION_KEY => $workspaceId,
|
|
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
|
(string) $workspaceId => (int) $environment->getKey(),
|
|
],
|
|
]);
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
|
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
|
(string) $workspaceId => (int) $environment->getKey(),
|
|
]);
|
|
|
|
setAdminPanelContext($environment);
|
|
}
|
|
|
|
/**
|
|
* @return array{0: OperationRun, 1: OperationRun, 2: OperationRun}
|
|
*/
|
|
function spec362BrowserSeedSelectedFamilyRuns(ManagedEnvironment $environment, User $user): array
|
|
{
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
]);
|
|
$snapshot = BaselineSnapshot::factory()->complete()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'summary_jsonb' => ['total_items' => 2],
|
|
]);
|
|
$backupSet = BackupSet::factory()->for($environment)->create([
|
|
'status' => 'partial',
|
|
'item_count' => 2,
|
|
'metadata' => [
|
|
'failures' => [
|
|
['policy_id' => 'policy-1'],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$inventoryRun = OperationRun::factory()->forTenant($environment)->create([
|
|
'user_id' => (int) $user->getKey(),
|
|
'initiator_name' => $user->name,
|
|
'type' => 'inventory.sync',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'policy_types' => ['conditionalAccessPolicy', 'deviceConfiguration'],
|
|
'inventory' => [
|
|
'coverage' => InventoryCoverage::buildPayload([
|
|
'conditionalAccessPolicy' => 'succeeded',
|
|
'deviceConfiguration' => 'skipped',
|
|
], []),
|
|
],
|
|
],
|
|
]);
|
|
|
|
$baselineRun = OperationRun::factory()->forTenant($environment)->create([
|
|
'user_id' => (int) $user->getKey(),
|
|
'initiator_name' => $user->name,
|
|
'type' => 'baseline.capture',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
],
|
|
]);
|
|
|
|
$backupRun = OperationRun::factory()->forTenant($environment)->create([
|
|
'user_id' => (int) $user->getKey(),
|
|
'initiator_name' => $user->name,
|
|
'type' => 'backup.schedule.execute',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
],
|
|
]);
|
|
|
|
$service = app(OperationRunService::class);
|
|
|
|
$inventoryRun = $service->updateRunWithReconciliation(
|
|
run: $inventoryRun,
|
|
status: OperationRunStatus::Completed->value,
|
|
outcome: OperationRunOutcome::PartiallySucceeded->value,
|
|
summaryCounts: [
|
|
'total' => 2,
|
|
'processed' => 2,
|
|
'succeeded' => 1,
|
|
'failed' => 0,
|
|
'skipped' => 1,
|
|
],
|
|
failures: [[
|
|
'code' => 'inventory.partial',
|
|
'reason_code' => 'run.adapter_out_of_sync',
|
|
'message' => 'The inventory sync recorded usable coverage, but some selected policy types did not complete cleanly.',
|
|
]],
|
|
reasonCode: 'run.adapter_out_of_sync',
|
|
reasonMessage: 'The inventory sync recorded usable coverage, but some selected policy types did not complete cleanly.',
|
|
source: 'adapter_reconciler',
|
|
evidence: ['adapter' => 'inventory_sync'],
|
|
adapter: 'inventory_sync',
|
|
decision: 'reconciled_partially_succeeded',
|
|
)->fresh();
|
|
|
|
$baselineRun = $service->updateRunWithReconciliation(
|
|
run: $baselineRun,
|
|
status: OperationRunStatus::Completed->value,
|
|
outcome: OperationRunOutcome::Succeeded->value,
|
|
summaryCounts: [
|
|
'total' => 2,
|
|
'processed' => 2,
|
|
'succeeded' => 2,
|
|
'failed' => 0,
|
|
],
|
|
failures: [],
|
|
reasonCode: 'run.adapter_out_of_sync',
|
|
reasonMessage: 'The baseline capture already produced a complete snapshot before the run finished updating.',
|
|
source: 'adapter_reconciler',
|
|
evidence: ['adapter' => 'baseline_capture'],
|
|
adapter: 'baseline_capture',
|
|
decision: 'reconciled_succeeded',
|
|
related: [
|
|
'type' => 'baseline_snapshot',
|
|
'id' => (int) $snapshot->getKey(),
|
|
'lifecycle_state' => 'complete',
|
|
],
|
|
)->fresh();
|
|
|
|
$backupRun = $service->updateRunWithReconciliation(
|
|
run: $backupRun,
|
|
status: OperationRunStatus::Completed->value,
|
|
outcome: OperationRunOutcome::PartiallySucceeded->value,
|
|
summaryCounts: [
|
|
'total' => 3,
|
|
'processed' => 3,
|
|
'succeeded' => 2,
|
|
'failed' => 1,
|
|
'created' => 1,
|
|
'updated' => 2,
|
|
'items' => 3,
|
|
],
|
|
failures: [[
|
|
'code' => 'backup_schedule.partial',
|
|
'reason_code' => 'run.adapter_out_of_sync',
|
|
'message' => 'The backup schedule produced a usable backup set, but some captures did not finish cleanly.',
|
|
]],
|
|
reasonCode: 'run.adapter_out_of_sync',
|
|
reasonMessage: 'The backup schedule produced a usable backup set, but some captures did not finish cleanly.',
|
|
source: 'adapter_reconciler',
|
|
evidence: ['adapter' => 'backup_schedule_execution'],
|
|
adapter: 'backup_schedule_execution',
|
|
decision: 'reconciled_partially_succeeded',
|
|
related: [
|
|
'type' => 'backup_set',
|
|
'id' => (int) $backupSet->getKey(),
|
|
'status' => 'partial',
|
|
],
|
|
)->fresh();
|
|
|
|
return [$inventoryRun, $baselineRun, $backupRun];
|
|
}
|