TenantAtlas/apps/platform/tests/Feature/SupportRequests/SupportRequestAuditTest.php
ahmido 6e3736a53f
Some checks failed
Main Confidence / confidence (push) Failing after 1m29s
Add in-app support request with context (#285)
## 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
2026-04-27 12:51:39 +00:00

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);
});