TenantAtlas/apps/platform/tests/Unit/Providers/ProviderOperationStartResultPresenterTest.php
ahmido a089350f98
Some checks failed
Main Confidence / confidence (push) Failing after 49s
feat: unify provider-backed action dispatch gating (#255)
## Summary
- unify provider-backed action starts behind the shared provider dispatch gate and shared start-result presenter
- align tenant, onboarding, provider-connection, restore, directory, and monitoring surfaces with the same blocked, deduped, scope-busy, and accepted semantics
- include the spec kit artifacts for spec 216 and the regression fixes that brought the full suite back to green

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/RestoreRunIdempotencyTest.php tests/Feature/ExecuteRestoreRunJobTest.php tests/Feature/Restore/RestoreRunProviderStartTest.php tests/Feature/Hardening/ExecuteRestoreRunJobGateTest.php tests/Feature/Hardening/BlockedWriteAuditLogTest.php tests/Feature/Onboarding/OnboardingDraftLifecycleTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec177InventoryCoverageTruthSmokeTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact`

## Notes
- branch: `216-provider-dispatch-gate`
- commit: `34230be7`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #255
2026-04-20 06:52:38 +00:00

149 lines
5.8 KiB
PHP

<?php
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Services\Providers\ProviderOperationStartResult;
use App\Support\OperationRunLinks;
use App\Support\OpsUx\ProviderOperationStartResultPresenter;
use App\Support\ReasonTranslation\NextStepOption;
use App\Support\ReasonTranslation\ReasonResolutionEnvelope;
use Filament\Actions\Action;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('builds queued notifications for accepted provider-backed starts', function (): void {
$tenant = Tenant::factory()->create();
$run = OperationRun::factory()->create([
'tenant_id' => $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => 'queued',
'context' => [
'provider_connection_id' => 123,
],
]);
$presenter = app(ProviderOperationStartResultPresenter::class);
$notification = $presenter->notification(
result: ProviderOperationStartResult::started($run, true),
blockedTitle: 'Verification blocked',
runUrl: OperationRunLinks::tenantlessView($run),
extraActions: [
Action::make('manage_connections')
->label('Manage Provider Connections')
->url('/provider-connections'),
],
);
$actions = collect($notification->getActions());
expect($notification->getTitle())->toBe('Provider connection check queued')
->and($notification->getBody())->toBe('Queued for execution. Open the operation for progress and next steps.')
->and($actions->map(fn (Action $action): string => (string) $action->getName())->all())->toBe([
'view_run',
'manage_connections',
])
->and($actions->map(fn (Action $action): string => (string) $action->getLabel())->all())->toBe([
'Open operation',
'Manage Provider Connections',
]);
});
it('builds already-running notifications for deduped provider-backed starts', function (): void {
$tenant = Tenant::factory()->create();
$run = OperationRun::factory()->create([
'tenant_id' => $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => 'running',
'context' => [
'provider_connection_id' => 123,
],
]);
$presenter = app(ProviderOperationStartResultPresenter::class);
$notification = $presenter->notification(
result: ProviderOperationStartResult::deduped($run),
blockedTitle: 'Verification blocked',
runUrl: OperationRunLinks::tenantlessView($run),
);
expect($notification->getTitle())->toBe('Provider connection check already running')
->and($notification->getBody())->toBe('A matching operation is already queued or running. Open the operation for progress and next steps.');
});
it('builds scope-busy notifications for conflicting provider-backed starts', function (): void {
$tenant = Tenant::factory()->create();
$run = OperationRun::factory()->create([
'tenant_id' => $tenant->getKey(),
'type' => 'inventory_sync',
'status' => 'running',
'context' => [
'provider_connection_id' => 123,
],
]);
$presenter = app(ProviderOperationStartResultPresenter::class);
$notification = $presenter->notification(
result: ProviderOperationStartResult::scopeBusy($run),
blockedTitle: 'Inventory sync blocked',
runUrl: OperationRunLinks::tenantlessView($run),
);
expect($notification->getTitle())->toBe('Scope busy')
->and($notification->getBody())->toBe('Another provider-backed operation is already running for this scope. Open the active operation for progress and next steps.');
});
it('builds blocked notifications from translated reason detail and first next step', function (): void {
$tenant = Tenant::factory()->create();
$reasonEnvelope = new ReasonResolutionEnvelope(
internalCode: 'provider_consent_missing',
operatorLabel: 'Admin consent required',
shortExplanation: 'Grant admin consent for this provider connection before retrying.',
actionability: 'prerequisite_missing',
nextSteps: [
NextStepOption::link('Grant admin consent', '/provider-connections/1/consent'),
NextStepOption::link('Open provider settings', '/provider-connections/1'),
],
);
$run = OperationRun::factory()->create([
'tenant_id' => $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => 'completed',
'outcome' => 'blocked',
'context' => [
'reason_code' => 'provider_consent_missing',
'reason_translation' => $reasonEnvelope->toArray(),
],
]);
$presenter = app(ProviderOperationStartResultPresenter::class);
$notification = $presenter->notification(
result: ProviderOperationStartResult::blocked($run),
blockedTitle: 'Verification blocked',
runUrl: OperationRunLinks::tenantlessView($run),
extraActions: [
Action::make('manage_connections')
->label('Manage Provider Connections')
->url('/provider-connections'),
],
);
$actions = collect($notification->getActions());
expect($notification->getTitle())->toBe('Verification blocked')
->and($notification->getBody())->toBe("Admin consent required\nGrant admin consent for this provider connection before retrying.\nNext step: Grant admin consent.")
->and($actions->map(fn (Action $action): string => (string) $action->getName())->all())->toBe([
'view_run',
'next_step_0',
'manage_connections',
])
->and($actions->map(fn (Action $action): string => (string) $action->getLabel())->all())->toBe([
'Open operation',
'Grant admin consent',
'Manage Provider Connections',
]);
});