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
205 lines
8.7 KiB
PHP
205 lines
8.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
|
|
use App\Filament\Pages\TenantDashboard;
|
|
use App\Models\AuditLog;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\SupportRequest;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\Providers\ProviderReasonCodes;
|
|
use App\Support\Providers\ProviderVerificationStatus;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Filament\Facades\Filament;
|
|
use Livewire\Livewire;
|
|
|
|
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
|
|
|
|
function supportRequestAuditTenantComponent(User $user, Tenant $tenant): \Livewire\Features\SupportTesting\Testable
|
|
{
|
|
test()->actingAs($user);
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
|
setTenantPanelContext($tenant);
|
|
|
|
return Livewire::actingAs($user)->test(TenantDashboard::class);
|
|
}
|
|
|
|
function supportRequestAuditOperationComponent(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]);
|
|
}
|
|
|
|
it('records a redacted audit entry for tenant-scoped support requests', function (): void {
|
|
$tenant = Tenant::factory()->create(['name' => 'Audit Support Tenant']);
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
|
|
ProviderConnection::factory()
|
|
->withCredential()
|
|
->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'display_name' => 'Audit Microsoft connection',
|
|
'verification_status' => ProviderVerificationStatus::Blocked->value,
|
|
'last_error_reason_code' => ProviderReasonCodes::ProviderPermissionMissing,
|
|
'last_error_message' => 'tenant-provider-secret',
|
|
]);
|
|
|
|
supportRequestAuditTenantComponent($user, $tenant)
|
|
->mountAction('requestSupport')
|
|
->setActionData([
|
|
'severity' => SupportRequest::SEVERITY_HIGH,
|
|
'summary' => 'Need tenant support audit proof.',
|
|
])
|
|
->callMountedAction()
|
|
->assertHasNoActionErrors();
|
|
|
|
$supportRequest = SupportRequest::query()->sole();
|
|
|
|
$audit = AuditLog::query()
|
|
->where('action', AuditActionId::SupportRequestCreated->value)
|
|
->latest('id')
|
|
->first();
|
|
|
|
expect($audit)->not->toBeNull()
|
|
->and($audit?->workspace_id)->toBe((int) $tenant->workspace_id)
|
|
->and($audit?->tenant_id)->toBe((int) $tenant->getKey())
|
|
->and($audit?->resource_type)->toBe('support_request')
|
|
->and($audit?->resource_id)->toBe((string) $supportRequest->getKey())
|
|
->and($audit?->target_label)->toBe($supportRequest->internal_reference)
|
|
->and($audit?->operation_run_id)->toBeNull()
|
|
->and(data_get($audit?->metadata, 'internal_reference'))->toBe($supportRequest->internal_reference)
|
|
->and(data_get($audit?->metadata, 'primary_context_type'))->toBe(SupportRequest::PRIMARY_CONTEXT_TENANT)
|
|
->and(data_get($audit?->metadata, 'primary_context_id'))->toBe((string) $tenant->getKey())
|
|
->and(data_get($audit?->metadata, 'attachment_mode'))->toBe(SupportRequest::ATTACHMENT_MODE_DIAGNOSTIC_SNAPSHOT_ATTACHED)
|
|
->and(data_get($audit?->metadata, 'redaction_mode'))->toBe('default_redacted')
|
|
->and((string) json_encode($audit?->metadata))->not->toContain('tenant-provider-secret');
|
|
});
|
|
|
|
it('records a redacted audit entry for run-scoped support requests', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'operator');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'summary_counts' => [
|
|
'total' => 0,
|
|
'processed' => 0,
|
|
],
|
|
'context' => [
|
|
'raw_response_body' => 'run-provider-secret',
|
|
],
|
|
'failure_summary' => [[
|
|
'message' => 'Run failed after provider validation.',
|
|
]],
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
supportRequestAuditOperationComponent($user, $run)
|
|
->mountAction('requestSupport')
|
|
->setActionData([
|
|
'severity' => SupportRequest::SEVERITY_BLOCKING,
|
|
'summary' => 'Need run support audit proof.',
|
|
])
|
|
->callMountedAction()
|
|
->assertHasNoActionErrors();
|
|
|
|
$supportRequest = SupportRequest::query()->sole();
|
|
|
|
$audit = AuditLog::query()
|
|
->where('action', AuditActionId::SupportRequestCreated->value)
|
|
->latest('id')
|
|
->first();
|
|
|
|
expect($audit)->not->toBeNull()
|
|
->and($audit?->workspace_id)->toBe((int) $tenant->workspace_id)
|
|
->and($audit?->tenant_id)->toBe((int) $tenant->getKey())
|
|
->and($audit?->resource_type)->toBe('support_request')
|
|
->and($audit?->resource_id)->toBe((string) $supportRequest->getKey())
|
|
->and($audit?->target_label)->toBe($supportRequest->internal_reference)
|
|
->and($audit?->operation_run_id)->toBe((int) $run->getKey())
|
|
->and(data_get($audit?->metadata, 'internal_reference'))->toBe($supportRequest->internal_reference)
|
|
->and(data_get($audit?->metadata, 'primary_context_type'))->toBe(SupportRequest::PRIMARY_CONTEXT_OPERATION_RUN)
|
|
->and(data_get($audit?->metadata, 'primary_context_id'))->toBe((string) $run->getKey())
|
|
->and(data_get($audit?->metadata, 'attachment_mode'))->toBe(SupportRequest::ATTACHMENT_MODE_DIAGNOSTIC_SNAPSHOT_ATTACHED)
|
|
->and(data_get($audit?->metadata, 'redaction_mode'))->toBe('default_redacted')
|
|
->and((string) json_encode($audit?->metadata))->not->toContain('run-provider-secret');
|
|
});
|
|
|
|
it('creates distinct support references for duplicate submissions without outbound http or operation-run side effects', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'operator');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'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(),
|
|
]);
|
|
|
|
$component = supportRequestAuditOperationComponent($user, $run);
|
|
$existingRunCount = OperationRun::query()->count();
|
|
|
|
assertNoOutboundHttp(function () use ($component): void {
|
|
$component
|
|
->mountAction('requestSupport')
|
|
->setActionData([
|
|
'severity' => SupportRequest::SEVERITY_HIGH,
|
|
'summary' => 'Duplicate run support request.',
|
|
])
|
|
->callMountedAction()
|
|
->assertHasNoActionErrors()
|
|
->mountAction('requestSupport')
|
|
->setActionData([
|
|
'severity' => SupportRequest::SEVERITY_HIGH,
|
|
'summary' => 'Duplicate run support request.',
|
|
])
|
|
->callMountedAction()
|
|
->assertHasNoActionErrors();
|
|
});
|
|
|
|
$supportRequests = SupportRequest::query()
|
|
->orderBy('id')
|
|
->get();
|
|
|
|
$auditReferences = AuditLog::query()
|
|
->where('action', AuditActionId::SupportRequestCreated->value)
|
|
->orderBy('id')
|
|
->pluck('target_label');
|
|
|
|
expect($supportRequests)->toHaveCount(2)
|
|
->and($supportRequests->pluck('summary')->all())->toBe([
|
|
'Duplicate run support request.',
|
|
'Duplicate run support request.',
|
|
])
|
|
->and($supportRequests->pluck('internal_reference')->unique())->toHaveCount(2)
|
|
->and($supportRequests->pluck('operation_run_id')->unique()->all())->toBe([(int) $run->getKey()])
|
|
->and($auditReferences->all())->toBe($supportRequests->pluck('internal_reference')->all())
|
|
->and(OperationRun::query()->count())->toBe($existingRunCount)
|
|
->and($run->fresh()?->status)->toBe(OperationRunStatus::Completed->value)
|
|
->and($run->fresh()?->outcome)->toBe(OperationRunOutcome::Failed->value);
|
|
});
|