Adds Pest test suites for the four critical admin modals using test double components, following the existing LivewireModalTest.php pattern. Covers tab navigation, form validation, CRUD flows, filter combinations, and state management. Fixes #7. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
602 lines
21 KiB
PHP
602 lines
21 KiB
PHP
<?php
|
|
|
|
/*
|
|
* Core PHP Framework
|
|
*
|
|
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
|
* See LICENSE file for details.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Livewire\Attributes\Computed;
|
|
use Livewire\Component;
|
|
use Livewire\Livewire;
|
|
|
|
/**
|
|
* Tests for PlatformUser modal component patterns.
|
|
*
|
|
* These tests verify the PlatformUser admin panel behaviour using test doubles:
|
|
* tab navigation, tier management, verification toggling, delete confirmation
|
|
* flows, package/entitlement modals, and GDPR data export.
|
|
*/
|
|
|
|
// =============================================================================
|
|
// Test Double Components
|
|
// =============================================================================
|
|
|
|
/**
|
|
* PlatformUser-style component for admin user management.
|
|
*/
|
|
class PlatformUserModalDouble extends Component
|
|
{
|
|
// User data
|
|
public int $userId = 0;
|
|
|
|
public string $userName = '';
|
|
|
|
public string $userEmail = '';
|
|
|
|
// Editable fields
|
|
public string $editingTier = 'free';
|
|
|
|
public bool $editingVerified = false;
|
|
|
|
// Action state
|
|
public string $actionMessage = '';
|
|
|
|
public string $actionType = '';
|
|
|
|
// Tab navigation
|
|
public string $activeTab = 'overview';
|
|
|
|
// Delete confirmation flow
|
|
public bool $showDeleteConfirm = false;
|
|
|
|
public bool $immediateDelete = false;
|
|
|
|
public string $deleteReason = '';
|
|
|
|
// Package provisioning modal
|
|
public bool $showPackageModal = false;
|
|
|
|
public ?int $selectedWorkspaceId = null;
|
|
|
|
public string $selectedPackageCode = '';
|
|
|
|
// Entitlement provisioning modal
|
|
public bool $showEntitlementModal = false;
|
|
|
|
public ?int $entitlementWorkspaceId = null;
|
|
|
|
public string $entitlementFeatureCode = '';
|
|
|
|
public string $entitlementType = 'enable';
|
|
|
|
public ?int $entitlementLimit = null;
|
|
|
|
public string $entitlementDuration = 'permanent';
|
|
|
|
public ?string $entitlementExpiresAt = null;
|
|
|
|
// Export tracking
|
|
public bool $dataExported = false;
|
|
|
|
public function mount(int $id = 1): void
|
|
{
|
|
$this->userId = $id;
|
|
$this->userName = 'Test User';
|
|
$this->userEmail = 'test@example.com';
|
|
$this->editingTier = 'free';
|
|
$this->editingVerified = false;
|
|
}
|
|
|
|
public function setTab(string $tab): void
|
|
{
|
|
if (in_array($tab, ['overview', 'workspaces', 'entitlements', 'data', 'danger'])) {
|
|
$this->activeTab = $tab;
|
|
}
|
|
}
|
|
|
|
public function saveTier(): void
|
|
{
|
|
$this->actionMessage = "Tier updated to {$this->editingTier}.";
|
|
$this->actionType = 'success';
|
|
}
|
|
|
|
public function saveVerification(): void
|
|
{
|
|
$this->actionMessage = $this->editingVerified
|
|
? 'Email marked as verified.'
|
|
: 'Email verification removed.';
|
|
$this->actionType = 'success';
|
|
}
|
|
|
|
public function resendVerification(): void
|
|
{
|
|
if ($this->editingVerified) {
|
|
$this->actionMessage = 'User email is already verified.';
|
|
$this->actionType = 'warning';
|
|
|
|
return;
|
|
}
|
|
|
|
$this->actionMessage = 'Verification email sent.';
|
|
$this->actionType = 'success';
|
|
}
|
|
|
|
public function confirmDelete(bool $immediate = false): void
|
|
{
|
|
$this->immediateDelete = $immediate;
|
|
$this->showDeleteConfirm = true;
|
|
$this->deleteReason = '';
|
|
}
|
|
|
|
public function cancelDelete(): void
|
|
{
|
|
$this->showDeleteConfirm = false;
|
|
$this->immediateDelete = false;
|
|
$this->deleteReason = '';
|
|
}
|
|
|
|
public function scheduleDelete(): void
|
|
{
|
|
if ($this->immediateDelete) {
|
|
$this->actionMessage = 'User has been permanently deleted.';
|
|
$this->actionType = 'success';
|
|
} else {
|
|
$this->actionMessage = 'Account deletion scheduled. Will be deleted in 7 days unless cancelled.';
|
|
$this->actionType = 'warning';
|
|
}
|
|
|
|
$this->showDeleteConfirm = false;
|
|
}
|
|
|
|
public function anonymizeUser(): void
|
|
{
|
|
$this->userName = 'Anonymized User';
|
|
$this->userEmail = "anon_{$this->userId}@anonymized.local";
|
|
$this->editingTier = 'free';
|
|
$this->editingVerified = false;
|
|
|
|
$this->actionMessage = 'User data has been anonymized.';
|
|
$this->actionType = 'success';
|
|
}
|
|
|
|
public function openPackageModal(int $workspaceId): void
|
|
{
|
|
$this->selectedWorkspaceId = $workspaceId;
|
|
$this->selectedPackageCode = '';
|
|
$this->showPackageModal = true;
|
|
}
|
|
|
|
public function closePackageModal(): void
|
|
{
|
|
$this->showPackageModal = false;
|
|
$this->selectedWorkspaceId = null;
|
|
$this->selectedPackageCode = '';
|
|
}
|
|
|
|
public function provisionPackage(): void
|
|
{
|
|
if (! $this->selectedWorkspaceId || ! $this->selectedPackageCode) {
|
|
$this->actionMessage = 'Please select a workspace and package.';
|
|
$this->actionType = 'warning';
|
|
|
|
return;
|
|
}
|
|
|
|
$this->actionMessage = "Package provisioned to workspace.";
|
|
$this->actionType = 'success';
|
|
$this->closePackageModal();
|
|
}
|
|
|
|
public function openEntitlementModal(int $workspaceId): void
|
|
{
|
|
$this->entitlementWorkspaceId = $workspaceId;
|
|
$this->entitlementFeatureCode = '';
|
|
$this->entitlementType = 'enable';
|
|
$this->entitlementLimit = null;
|
|
$this->entitlementDuration = 'permanent';
|
|
$this->entitlementExpiresAt = null;
|
|
$this->showEntitlementModal = true;
|
|
}
|
|
|
|
public function closeEntitlementModal(): void
|
|
{
|
|
$this->showEntitlementModal = false;
|
|
$this->entitlementWorkspaceId = null;
|
|
$this->entitlementFeatureCode = '';
|
|
}
|
|
|
|
public function provisionEntitlement(): void
|
|
{
|
|
if (! $this->entitlementWorkspaceId || ! $this->entitlementFeatureCode) {
|
|
$this->actionMessage = 'Please select a workspace and feature.';
|
|
$this->actionType = 'warning';
|
|
|
|
return;
|
|
}
|
|
|
|
$this->actionMessage = "Entitlement added to workspace.";
|
|
$this->actionType = 'success';
|
|
$this->closeEntitlementModal();
|
|
}
|
|
|
|
public function exportUserData(): void
|
|
{
|
|
$this->dataExported = true;
|
|
}
|
|
|
|
#[Computed]
|
|
public function collectUserData(): array
|
|
{
|
|
return [
|
|
'export_info' => [
|
|
'exported_at' => now()->toIso8601String(),
|
|
'exported_by' => 'Platform Administrator',
|
|
],
|
|
'account' => [
|
|
'id' => $this->userId,
|
|
'name' => $this->userName,
|
|
'email' => $this->userEmail,
|
|
'tier' => $this->editingTier,
|
|
],
|
|
];
|
|
}
|
|
|
|
public function render(): string
|
|
{
|
|
return <<<'HTML'
|
|
<div>
|
|
<h1>User: {{ $userName }}</h1>
|
|
<span>Tab: {{ $activeTab }}</span>
|
|
<span>Tier: {{ $editingTier }}</span>
|
|
@if($showDeleteConfirm)
|
|
<div class="delete-confirm">Delete Confirm</div>
|
|
@endif
|
|
@if($showPackageModal)
|
|
<div class="package-modal">Package Modal</div>
|
|
@endif
|
|
@if($showEntitlementModal)
|
|
<div class="entitlement-modal">Entitlement Modal</div>
|
|
@endif
|
|
@if($actionMessage)
|
|
<div class="action-message {{ $actionType }}">{{ $actionMessage }}</div>
|
|
@endif
|
|
</div>
|
|
HTML;
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Tab Navigation Tests
|
|
// =============================================================================
|
|
|
|
describe('PlatformUser tab navigation', function () {
|
|
it('defaults to overview tab', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->assertSet('activeTab', 'overview');
|
|
});
|
|
|
|
it('switches to valid tabs', function () {
|
|
$validTabs = ['overview', 'workspaces', 'entitlements', 'data', 'danger'];
|
|
|
|
foreach ($validTabs as $tab) {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('setTab', $tab)
|
|
->assertSet('activeTab', $tab);
|
|
}
|
|
});
|
|
|
|
it('ignores invalid tab names', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('setTab', 'nonexistent')
|
|
->assertSet('activeTab', 'overview');
|
|
});
|
|
|
|
it('renders current tab label', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('setTab', 'workspaces')
|
|
->assertSee('Tab: workspaces');
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Tier Management Tests
|
|
// =============================================================================
|
|
|
|
describe('PlatformUser tier management', function () {
|
|
it('loads user tier on mount', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->assertSet('editingTier', 'free');
|
|
});
|
|
|
|
it('can change tier to apollo', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->set('editingTier', 'apollo')
|
|
->call('saveTier')
|
|
->assertSet('actionMessage', 'Tier updated to apollo.')
|
|
->assertSet('actionType', 'success');
|
|
});
|
|
|
|
it('can change tier to hades', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->set('editingTier', 'hades')
|
|
->call('saveTier')
|
|
->assertSet('actionMessage', 'Tier updated to hades.')
|
|
->assertSet('actionType', 'success');
|
|
});
|
|
|
|
it('renders tier value', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->set('editingTier', 'apollo')
|
|
->assertSee('Tier: apollo');
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Verification Management Tests
|
|
// =============================================================================
|
|
|
|
describe('PlatformUser verification management', function () {
|
|
it('can mark email as verified', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->set('editingVerified', true)
|
|
->call('saveVerification')
|
|
->assertSet('actionMessage', 'Email marked as verified.')
|
|
->assertSet('actionType', 'success');
|
|
});
|
|
|
|
it('can remove email verification', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->set('editingVerified', false)
|
|
->call('saveVerification')
|
|
->assertSet('actionMessage', 'Email verification removed.')
|
|
->assertSet('actionType', 'success');
|
|
});
|
|
|
|
it('warns when resending to already verified user', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->set('editingVerified', true)
|
|
->call('resendVerification')
|
|
->assertSet('actionMessage', 'User email is already verified.')
|
|
->assertSet('actionType', 'warning');
|
|
});
|
|
|
|
it('sends verification email for unverified user', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->set('editingVerified', false)
|
|
->call('resendVerification')
|
|
->assertSet('actionMessage', 'Verification email sent.')
|
|
->assertSet('actionType', 'success');
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Delete Confirmation Flow Tests
|
|
// =============================================================================
|
|
|
|
describe('PlatformUser delete confirmation flow', function () {
|
|
it('opens delete confirmation with scheduled mode', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('confirmDelete', false)
|
|
->assertSet('showDeleteConfirm', true)
|
|
->assertSet('immediateDelete', false)
|
|
->assertSet('deleteReason', '')
|
|
->assertSee('Delete Confirm');
|
|
});
|
|
|
|
it('opens delete confirmation with immediate mode', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('confirmDelete', true)
|
|
->assertSet('showDeleteConfirm', true)
|
|
->assertSet('immediateDelete', true);
|
|
});
|
|
|
|
it('cancels delete confirmation', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('confirmDelete', true)
|
|
->set('deleteReason', 'GDPR request')
|
|
->call('cancelDelete')
|
|
->assertSet('showDeleteConfirm', false)
|
|
->assertSet('immediateDelete', false)
|
|
->assertSet('deleteReason', '')
|
|
->assertDontSee('Delete Confirm');
|
|
});
|
|
|
|
it('schedules deletion (non-immediate)', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('confirmDelete', false)
|
|
->set('deleteReason', 'User request')
|
|
->call('scheduleDelete')
|
|
->assertSet('showDeleteConfirm', false)
|
|
->assertSet('actionMessage', 'Account deletion scheduled. Will be deleted in 7 days unless cancelled.')
|
|
->assertSet('actionType', 'warning');
|
|
});
|
|
|
|
it('executes immediate deletion', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('confirmDelete', true)
|
|
->call('scheduleDelete')
|
|
->assertSet('showDeleteConfirm', false)
|
|
->assertSet('actionMessage', 'User has been permanently deleted.')
|
|
->assertSet('actionType', 'success');
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Anonymization Tests
|
|
// =============================================================================
|
|
|
|
describe('PlatformUser anonymization', function () {
|
|
it('anonymizes user data', function () {
|
|
Livewire::test(PlatformUserModalDouble::class, ['id' => 42])
|
|
->call('anonymizeUser')
|
|
->assertSet('userName', 'Anonymized User')
|
|
->assertSet('userEmail', 'anon_42@anonymized.local')
|
|
->assertSet('editingTier', 'free')
|
|
->assertSet('editingVerified', false)
|
|
->assertSet('actionType', 'success');
|
|
});
|
|
|
|
it('displays anonymized name after anonymization', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('anonymizeUser')
|
|
->assertSee('Anonymized User');
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Package Modal Tests
|
|
// =============================================================================
|
|
|
|
describe('PlatformUser package modal', function () {
|
|
it('opens package modal for a workspace', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('openPackageModal', 5)
|
|
->assertSet('showPackageModal', true)
|
|
->assertSet('selectedWorkspaceId', 5)
|
|
->assertSet('selectedPackageCode', '')
|
|
->assertSee('Package Modal');
|
|
});
|
|
|
|
it('closes package modal and resets state', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('openPackageModal', 5)
|
|
->set('selectedPackageCode', 'pro-plan')
|
|
->call('closePackageModal')
|
|
->assertSet('showPackageModal', false)
|
|
->assertSet('selectedWorkspaceId', null)
|
|
->assertSet('selectedPackageCode', '')
|
|
->assertDontSee('Package Modal');
|
|
});
|
|
|
|
it('warns when provisioning without workspace or package', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('openPackageModal', 5)
|
|
->call('provisionPackage')
|
|
->assertSet('actionMessage', 'Please select a workspace and package.')
|
|
->assertSet('actionType', 'warning');
|
|
});
|
|
|
|
it('provisions package and closes modal on success', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('openPackageModal', 5)
|
|
->set('selectedPackageCode', 'pro-plan')
|
|
->call('provisionPackage')
|
|
->assertSet('showPackageModal', false)
|
|
->assertSet('actionType', 'success');
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Entitlement Modal Tests
|
|
// =============================================================================
|
|
|
|
describe('PlatformUser entitlement modal', function () {
|
|
it('opens entitlement modal with defaults', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('openEntitlementModal', 7)
|
|
->assertSet('showEntitlementModal', true)
|
|
->assertSet('entitlementWorkspaceId', 7)
|
|
->assertSet('entitlementFeatureCode', '')
|
|
->assertSet('entitlementType', 'enable')
|
|
->assertSet('entitlementLimit', null)
|
|
->assertSet('entitlementDuration', 'permanent')
|
|
->assertSet('entitlementExpiresAt', null)
|
|
->assertSee('Entitlement Modal');
|
|
});
|
|
|
|
it('closes entitlement modal and resets state', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('openEntitlementModal', 7)
|
|
->set('entitlementFeatureCode', 'core.srv.bio')
|
|
->call('closeEntitlementModal')
|
|
->assertSet('showEntitlementModal', false)
|
|
->assertSet('entitlementWorkspaceId', null)
|
|
->assertSet('entitlementFeatureCode', '')
|
|
->assertDontSee('Entitlement Modal');
|
|
});
|
|
|
|
it('warns when provisioning without workspace or feature', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('openEntitlementModal', 7)
|
|
->call('provisionEntitlement')
|
|
->assertSet('actionMessage', 'Please select a workspace and feature.')
|
|
->assertSet('actionType', 'warning');
|
|
});
|
|
|
|
it('provisions entitlement and closes modal on success', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('openEntitlementModal', 7)
|
|
->set('entitlementFeatureCode', 'core.srv.bio')
|
|
->call('provisionEntitlement')
|
|
->assertSet('showEntitlementModal', false)
|
|
->assertSet('actionType', 'success');
|
|
});
|
|
|
|
it('supports different entitlement types', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('openEntitlementModal', 7)
|
|
->set('entitlementType', 'add_limit')
|
|
->set('entitlementLimit', 100)
|
|
->assertSet('entitlementType', 'add_limit')
|
|
->assertSet('entitlementLimit', 100);
|
|
});
|
|
|
|
it('supports duration-based entitlements', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->call('openEntitlementModal', 7)
|
|
->set('entitlementDuration', 'duration')
|
|
->set('entitlementExpiresAt', '2026-12-31')
|
|
->assertSet('entitlementDuration', 'duration')
|
|
->assertSet('entitlementExpiresAt', '2026-12-31');
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Data Export Tests
|
|
// =============================================================================
|
|
|
|
describe('PlatformUser data export', function () {
|
|
it('collects user data for export', function () {
|
|
Livewire::test(PlatformUserModalDouble::class, ['id' => 42])
|
|
->assertSet(fn ($component) => $component->collectUserData['account']['id'] === 42
|
|
&& $component->collectUserData['account']['name'] === 'Test User'
|
|
&& $component->collectUserData['account']['email'] === 'test@example.com');
|
|
});
|
|
|
|
it('triggers data export', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->assertSet('dataExported', false)
|
|
->call('exportUserData')
|
|
->assertSet('dataExported', true);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Action Message Tests
|
|
// =============================================================================
|
|
|
|
describe('PlatformUser action messages', function () {
|
|
it('clears action message between operations', function () {
|
|
$component = Livewire::test(PlatformUserModalDouble::class)
|
|
->set('editingTier', 'apollo')
|
|
->call('saveTier')
|
|
->assertSet('actionType', 'success');
|
|
|
|
// Next operation replaces the message
|
|
$component->set('editingVerified', true)
|
|
->call('resendVerification')
|
|
->assertSet('actionType', 'warning')
|
|
->assertSet('actionMessage', 'User email is already verified.');
|
|
});
|
|
|
|
it('renders action message in view', function () {
|
|
Livewire::test(PlatformUserModalDouble::class)
|
|
->set('editingTier', 'hades')
|
|
->call('saveTier')
|
|
->assertSee('Tier updated to hades.');
|
|
});
|
|
});
|