TenantAtlas/apps/platform/tests/Feature/SupportRequests/TenantSupportRequestActionTest.php
Ahmed Darrazi 3ca722a125
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m30s
fix(ui): render support diagnostics as labeled menu item; add support-request scaffold and tests
2026-04-27 13:23:35 +02: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);
});