Some checks failed
Main Confidence / confidence (push) Failing after 1m29s
## Summary - add the first in-app support request flow with an immutable `SupportRequest` record, canonical context builder, submission service, and generated internal reference - expose contextual support-request actions from the tenant dashboard and operation run surfaces, including audit logging and support-safe diagnostic capture rules - add Pest coverage plus the `specs/246-support-request-context` artifacts for the new support-request slice ## Testing - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/SupportRequests/OperationRunSupportRequestActionTest.php tests/Feature/SupportRequests/SupportRequestAuditTest.php tests/Feature/SupportRequests/SupportRequestAuthorizationTest.php tests/Feature/SupportRequests/TenantSupportRequestActionTest.php tests/Unit/Support/SupportRequests/SupportRequestContextBuilderTest.php tests/Unit/Support/SupportRequests/SupportRequestReferenceTest.php` ## Notes - this PR supersedes the earlier session-branch PR opened from `246-support-request-context-session-1777289015` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #285
167 lines
7.0 KiB
PHP
167 lines
7.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
|
|
use App\Models\OperationRun;
|
|
use App\Models\SupportRequest;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Filament\Actions\Action;
|
|
use Filament\Actions\ActionGroup;
|
|
use Filament\Facades\Filament;
|
|
use Livewire\Livewire;
|
|
|
|
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
|
|
|
|
function operationSupportRequestComponent(User $user, OperationRun $run): \Livewire\Features\SupportTesting\Testable
|
|
{
|
|
test()->actingAs($user);
|
|
Filament::setTenant(null, true);
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $run->workspace_id);
|
|
|
|
return Livewire::actingAs($user)->test(TenantlessOperationRunViewer::class, ['run' => $run]);
|
|
}
|
|
|
|
function operationSupportRequestHeaderActions(\Livewire\Features\SupportTesting\Testable $component): array
|
|
{
|
|
$instance = $component->instance();
|
|
|
|
if ($instance->getCachedHeaderActions() === []) {
|
|
$instance->cacheInteractsWithHeaderActions();
|
|
}
|
|
|
|
return $instance->getCachedHeaderActions();
|
|
}
|
|
|
|
function operationSupportRequestHeaderPrimaryNames(\Livewire\Features\SupportTesting\Testable $component): array
|
|
{
|
|
return collect(operationSupportRequestHeaderActions($component))
|
|
->reject(static fn ($action): bool => $action instanceof ActionGroup)
|
|
->map(static fn ($action): ?string => $action instanceof Action ? $action->getName() : null)
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
function operationSupportRequestHeaderMoreActionNames(\Livewire\Features\SupportTesting\Testable $component): array
|
|
{
|
|
$moreGroup = collect(operationSupportRequestHeaderActions($component))
|
|
->first(static fn ($action): bool => $action instanceof ActionGroup && $action->getLabel() === 'More');
|
|
|
|
return collect($moreGroup?->getActions() ?? [])
|
|
->map(static fn ($action): ?string => $action instanceof Action ? $action->getName() : null)
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
function operationSupportRequestHeaderMoreAction(\Livewire\Features\SupportTesting\Testable $component, string $name): ?Action
|
|
{
|
|
$moreGroup = collect(operationSupportRequestHeaderActions($component))
|
|
->first(static fn ($action): bool => $action instanceof ActionGroup && $action->getLabel() === 'More');
|
|
|
|
$action = collect($moreGroup?->getActions() ?? [])
|
|
->first(static fn ($action): bool => $action instanceof Action && $action->getName() === $name);
|
|
|
|
return $action instanceof Action ? $action : null;
|
|
}
|
|
|
|
it('creates a run-scoped support request from the tenantless operation viewer', function (): void {
|
|
$tenant = Tenant::factory()->create(['name' => 'Contoso Support Tenant']);
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'operator');
|
|
|
|
$run = OperationRun::factory()
|
|
->forTenant($tenant)
|
|
->create([
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'summary_counts' => [
|
|
'total' => 0,
|
|
'processed' => 0,
|
|
],
|
|
'failure_summary' => [[
|
|
'message' => 'Run failed after provider validation.',
|
|
]],
|
|
'completed_at' => now()->subMinutes(10),
|
|
]);
|
|
|
|
$component = operationSupportRequestComponent($user, $run);
|
|
|
|
expect(operationSupportRequestHeaderPrimaryNames($component))
|
|
->not->toContain('openSupportDiagnostics')
|
|
->not->toContain('requestSupport')
|
|
->and(operationSupportRequestHeaderMoreActionNames($component))
|
|
->toEqualCanonicalizing(['openSupportDiagnostics', 'requestSupport'])
|
|
->and(operationSupportRequestHeaderMoreAction($component, 'openSupportDiagnostics')?->isIconButton())
|
|
->toBeFalse();
|
|
|
|
$component
|
|
->assertActionVisible('openSupportDiagnostics')
|
|
->assertActionEnabled('openSupportDiagnostics')
|
|
->assertActionVisible('requestSupport')
|
|
->assertActionEnabled('requestSupport')
|
|
->assertActionExists('requestSupport', fn (Action $action): bool => $action->getLabel() === 'Request support')
|
|
->mountAction('requestSupport')
|
|
->setActionData([
|
|
'severity' => SupportRequest::SEVERITY_BLOCKING,
|
|
'summary' => 'This failed operation needs support escalation.',
|
|
'reproduction_notes' => 'Open the canonical run detail and submit the request from the grouped secondary action.',
|
|
])
|
|
->callMountedAction()
|
|
->assertHasNoActionErrors()
|
|
->assertNotified('Support request submitted');
|
|
|
|
$supportRequest = SupportRequest::query()->sole();
|
|
|
|
expect($supportRequest->internal_reference)->toMatch('/^SR-[0-9A-HJKMNP-TV-Z]{26}$/')
|
|
->and($supportRequest->workspace_id)->toBe((int) $tenant->workspace_id)
|
|
->and($supportRequest->tenant_id)->toBe((int) $tenant->getKey())
|
|
->and($supportRequest->initiated_by_user_id)->toBe((int) $user->getKey())
|
|
->and($supportRequest->primary_context_type)->toBe(SupportRequest::PRIMARY_CONTEXT_OPERATION_RUN)
|
|
->and($supportRequest->operation_run_id)->toBe((int) $run->getKey())
|
|
->and($supportRequest->attachment_mode)->toBe(SupportRequest::ATTACHMENT_MODE_DIAGNOSTIC_SNAPSHOT_ATTACHED)
|
|
->and($supportRequest->severity)->toBe(SupportRequest::SEVERITY_BLOCKING)
|
|
->and($supportRequest->summary)->toBe('This failed operation needs support escalation.')
|
|
->and(data_get($supportRequest->context_envelope, 'primary_context.type'))->toBe('operation_run')
|
|
->and(data_get($supportRequest->context_envelope, 'primary_context.operation_run_id'))->toBe((int) $run->getKey())
|
|
->and(data_get($supportRequest->context_envelope, 'diagnostic_snapshot'))->toBeArray();
|
|
});
|
|
|
|
it('keeps tenantless operation detail deny-as-not-found for workspace members without tenant entitlement', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$tenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
]);
|
|
|
|
$user = User::factory()->create();
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'user_id' => (int) $user->getKey(),
|
|
'role' => 'owner',
|
|
]);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
|
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
|
->assertNotFound();
|
|
});
|