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
138 lines
6.3 KiB
PHP
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);
|
|
});
|