TenantAtlas/apps/platform/tests/Feature/SupportRequests/TenantSupportRequestActionTest.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

138 lines
6.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\TenantDashboard;
use App\Models\SupportRequest;
use App\Models\Tenant;
use App\Models\User;
use App\Models\WorkspaceMembership;
use App\Services\Auth\CapabilityResolver;
use App\Support\Auth\Capabilities;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Actions\Action;
use Livewire\Livewire;
use function Pest\Laravel\mock;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
function tenantSupportRequestComponent(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);
}
it('creates a tenant support request from the dashboard', function (): void {
$tenant = Tenant::factory()->create(['name' => 'Contoso Support Tenant']);
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
tenantSupportRequestComponent($user, $tenant)
->assertActionVisible('requestSupport')
->assertActionEnabled('requestSupport')
->assertActionExists('requestSupport', fn (Action $action): bool => $action->getLabel() === 'Request support')
->mountAction('requestSupport')
->setActionData([
'severity' => SupportRequest::SEVERITY_HIGH,
'summary' => 'Policy sync failed after the latest tenant refresh.',
'reproduction_notes' => 'Open the tenant dashboard after a failed sync and request support from the header action.',
'contact_name' => 'Ops On Call',
'contact_email' => 'ops@example.test',
])
->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_TENANT)
->and($supportRequest->operation_run_id)->toBeNull()
->and($supportRequest->attachment_mode)->toBe(SupportRequest::ATTACHMENT_MODE_DIAGNOSTIC_SNAPSHOT_ATTACHED)
->and($supportRequest->severity)->toBe(SupportRequest::SEVERITY_HIGH)
->and($supportRequest->summary)->toBe('Policy sync failed after the latest tenant refresh.')
->and($supportRequest->reproduction_notes)->toContain('failed sync')
->and($supportRequest->contact_name)->toBe('Ops On Call')
->and($supportRequest->contact_email)->toBe('ops@example.test')
->and(data_get($supportRequest->context_envelope, 'primary_context.type'))->toBe('tenant')
->and(data_get($supportRequest->context_envelope, 'primary_context.tenant_id'))->toBe((int) $tenant->getKey())
->and(data_get($supportRequest->context_envelope, 'diagnostic_snapshot'))->toBeArray();
});
it('stores canonical context only when the creator cannot view support diagnostics', function (): void {
$tenant = Tenant::factory()->create();
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
mock(CapabilityResolver::class, function ($mock) use ($tenant): void {
$mock->shouldReceive('primeMemberships')->andReturnNull();
$mock->shouldReceive('isMember')
->andReturnUsing(static fn ($user, Tenant $resolvedTenant): bool => (int) $resolvedTenant->getKey() === (int) $tenant->getKey());
$mock->shouldReceive('can')
->andReturnUsing(static function ($user, Tenant $resolvedTenant, string $capability) use ($tenant): bool {
expect((int) $resolvedTenant->getKey())->toBe((int) $tenant->getKey());
return match ($capability) {
Capabilities::SUPPORT_REQUESTS_CREATE => true,
Capabilities::SUPPORT_DIAGNOSTICS_VIEW => false,
default => true,
};
});
});
tenantSupportRequestComponent($user, $tenant)
->assertActionVisible('requestSupport')
->assertActionEnabled('requestSupport')
->mountAction('requestSupport')
->setActionData([
'summary' => 'Need help reviewing the latest tenant support context.',
])
->callMountedAction()
->assertHasNoActionErrors()
->assertNotified('Support request submitted');
$supportRequest = SupportRequest::query()->sole();
expect($supportRequest->severity)->toBe(SupportRequest::SEVERITY_NORMAL)
->and($supportRequest->contact_name)->toBe($user->name)
->and($supportRequest->contact_email)->toBe($user->email)
->and($supportRequest->attachment_mode)->toBe(SupportRequest::ATTACHMENT_MODE_CANONICAL_CONTEXT_ONLY)
->and(data_get($supportRequest->context_envelope, 'diagnostic_snapshot'))->toBeNull()
->and(data_get($supportRequest->context_envelope, 'omissions.0.reason'))->toBe('omitted_without_support_diagnostics_view');
});
it('keeps tenant dashboard support requests deny-as-not-found for workspace members without tenant entitlement', function (): void {
$tenant = Tenant::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'user_id' => (int) $user->getKey(),
'role' => 'operator',
]);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
->assertNotFound();
});
it('returns forbidden for entitled tenant members without support request capability', function (): void {
$tenant = Tenant::factory()->create();
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
tenantSupportRequestComponent($user, $tenant)
->assertActionVisible('requestSupport')
->assertActionDisabled('requestSupport')
->call('authorizeTenantSupportRequest')
->assertForbidden();
expect(SupportRequest::query()->count())->toBe(0);
});