feat: admin dashboard widgets
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Dashboard;
|
||||
|
||||
use App\Dashboard\Contract\DashboardWidgetProviderInterface;
|
||||
use App\Dashboard\DashboardWidgetRegistry;
|
||||
use App\Model\DashboardWidget;
|
||||
use App\Security\Role;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
class DashboardWidgetRegistryTest extends TestCase
|
||||
{
|
||||
public function testSkipsProvidersWhoseRoleIsNotGranted(): void
|
||||
{
|
||||
$provider = $this->provider(Role::ADMIN, 100, new DashboardWidget('Nur für Admins', []));
|
||||
|
||||
$registry = new DashboardWidgetRegistry([$provider], $this->securityGranting([]));
|
||||
|
||||
$this->assertSame([], $registry->build());
|
||||
}
|
||||
|
||||
public function testKeepsProvidersWhoseRoleIsGranted(): void
|
||||
{
|
||||
$widget = new DashboardWidget('Gruppenbuchungen', []);
|
||||
$provider = $this->provider(Role::GROUPS_MANAGER, 100, $widget);
|
||||
|
||||
$registry = new DashboardWidgetRegistry([$provider], $this->securityGranting([Role::GROUPS_MANAGER]));
|
||||
|
||||
$this->assertSame([$widget], $registry->build());
|
||||
}
|
||||
|
||||
/**
|
||||
* The hierarchy is security.yaml's business, not the registry's: whatever isGranted() says
|
||||
* about ROLE_GROUPS_MANAGER is what an admin gets, without the registry knowing why.
|
||||
*/
|
||||
public function testRoleIsResolvedByTheSecurityCheckerAlone(): void
|
||||
{
|
||||
$widget = new DashboardWidget('Gruppenbuchungen', []);
|
||||
$provider = $this->provider(Role::GROUPS_MANAGER, 100, $widget);
|
||||
|
||||
$security = $this->createMock(Security::class);
|
||||
$security->expects($this->once())
|
||||
->method('isGranted')
|
||||
->with(Role::GROUPS_MANAGER)
|
||||
->willReturn(true)
|
||||
;
|
||||
|
||||
$registry = new DashboardWidgetRegistry([$provider], $security);
|
||||
|
||||
$this->assertSame([$widget], $registry->build());
|
||||
}
|
||||
|
||||
public function testDropsProvidersReturningNullButKeepsEmptyWidgets(): void
|
||||
{
|
||||
$empty = new DashboardWidget('Leer, aber da', []);
|
||||
|
||||
$registry = new DashboardWidgetRegistry([
|
||||
$this->provider(Role::ADMIN, 100, null),
|
||||
$this->provider(Role::ADMIN, 90, $empty),
|
||||
], $this->securityGranting([Role::ADMIN]));
|
||||
|
||||
$this->assertSame([$empty], $registry->build());
|
||||
}
|
||||
|
||||
public function testReturnsWidgetsInPriorityOrder(): void
|
||||
{
|
||||
$low = new DashboardWidget('Zuletzt', []);
|
||||
$high = new DashboardWidget('Zuerst', []);
|
||||
|
||||
$registry = new DashboardWidgetRegistry([
|
||||
$this->provider(Role::ADMIN, 10, $low),
|
||||
$this->provider(Role::ADMIN, 100, $high),
|
||||
], $this->securityGranting([Role::ADMIN]));
|
||||
|
||||
$this->assertSame([$high, $low], $registry->build());
|
||||
}
|
||||
|
||||
private function provider(string $role, int $priority, ?DashboardWidget $widget): DashboardWidgetProviderInterface
|
||||
{
|
||||
$provider = $this->createMock(DashboardWidgetProviderInterface::class);
|
||||
$provider->method('getRequiredRole')->willReturn($role);
|
||||
$provider->method('getPriority')->willReturn($priority);
|
||||
$provider->method('build')->willReturn($widget);
|
||||
|
||||
return $provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $grantedRoles
|
||||
*/
|
||||
private function securityGranting(array $grantedRoles): Security
|
||||
{
|
||||
$security = $this->createMock(Security::class);
|
||||
$security->method('isGranted')->willReturnCallback(
|
||||
static fn (mixed $role): bool => \in_array($role, $grantedRoles, true),
|
||||
);
|
||||
|
||||
return $security;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Dashboard;
|
||||
|
||||
use App\Dashboard\Widget\PendingRoleApprovalsWidgetProvider;
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Security\Role;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class PendingRoleApprovalsWidgetProviderTest extends TestCase
|
||||
{
|
||||
public function testEntryNamesTheAccountAndWhatItIsNominatedFor(): void
|
||||
{
|
||||
$user = (new User('[email protected]'))
|
||||
->setFirstName('Rita')
|
||||
->setLastName('Vorschlag')
|
||||
->setRoles([Role::TEAMER, Role::pending(Role::GROUPS_ADMIN)])
|
||||
;
|
||||
|
||||
$widget = $this->provider([$user])->build();
|
||||
|
||||
$this->assertNotNull($widget);
|
||||
$this->assertCount(1, $widget->entries);
|
||||
$this->assertStringContainsString('Rita Vorschlag', $widget->entries[0]->label);
|
||||
$this->assertStringContainsString(Role::labels()[Role::GROUPS_ADMIN], $widget->entries[0]->label);
|
||||
}
|
||||
|
||||
/**
|
||||
* The approval modal cannot be linked to directly, so the entry leads to the user list
|
||||
* filtered down to that account — which needs the filter marker to bind at all.
|
||||
*/
|
||||
public function testEntryLinksToTheUserListFilteredToThatAccount(): void
|
||||
{
|
||||
$user = (new User('[email protected]'))
|
||||
->setRoles([Role::pending(Role::ADMIN)])
|
||||
;
|
||||
|
||||
$widget = $this->provider([$user])->build();
|
||||
|
||||
$this->assertNotNull($widget);
|
||||
$this->assertSame('/admin/user?f=1&q=nominee%40example.com', $widget->entries[0]->url);
|
||||
}
|
||||
|
||||
public function testRendersAsAnEmptyCardRatherThanDisappearingWhenNothingIsPending(): void
|
||||
{
|
||||
$widget = $this->provider([])->build();
|
||||
|
||||
$this->assertNotNull($widget);
|
||||
$this->assertSame([], $widget->entries);
|
||||
$this->assertSame('Keine offenen Rollenfreigaben.', $widget->emptyText);
|
||||
}
|
||||
|
||||
public function testRequiresTheRoleThatMayActuallyApprove(): void
|
||||
{
|
||||
$this->assertSame(Role::ADMIN, $this->provider([])->getRequiredRole());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User[] $pending
|
||||
*/
|
||||
private function provider(array $pending): PendingRoleApprovalsWidgetProvider
|
||||
{
|
||||
$repository = $this->createMock(UserRepository::class);
|
||||
$repository->method('findWithPendingRoles')->willReturn($pending);
|
||||
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator->method('generate')->willReturnCallback(
|
||||
static function (string $route, array $parameters = []): string {
|
||||
$path = '/'.str_replace('_', '/', substr($route, \strlen('app_')));
|
||||
|
||||
return [] === $parameters ? $path : $path.'?'.http_build_query($parameters);
|
||||
},
|
||||
);
|
||||
|
||||
return new PendingRoleApprovalsWidgetProvider($repository, $urlGenerator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Dashboard;
|
||||
|
||||
use App\Dashboard\Widget\RecentLogEntriesWidgetProvider;
|
||||
use App\Entity\LogEntry;
|
||||
use App\Repository\LogEntryRepository;
|
||||
use App\Security\Role;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class RecentLogEntriesWidgetProviderTest extends TestCase
|
||||
{
|
||||
public function testEntriesStayDisplayOnly(): void
|
||||
{
|
||||
$widget = $this->provider([$this->logEntry('bpn', 'Verbindung wiederhergestellt')])->build();
|
||||
|
||||
$this->assertNotNull($widget);
|
||||
$this->assertNull($widget->entries[0]->url);
|
||||
}
|
||||
|
||||
public function testHeaderLinksToTheFullLog(): void
|
||||
{
|
||||
$widget = $this->provider([$this->logEntry('bpn', 'Verbindung wiederhergestellt')])->build();
|
||||
|
||||
$this->assertNotNull($widget);
|
||||
$this->assertSame('/admin/log', $widget->actionUrl);
|
||||
$this->assertSame('Alle Logeinträge', $widget->actionLabel);
|
||||
}
|
||||
|
||||
public function testLabelNamesWhenWhereAndWhat(): void
|
||||
{
|
||||
$widget = $this->provider([$this->logEntry('auth', 'Anmeldung fehlgeschlagen')])->build();
|
||||
|
||||
$this->assertNotNull($widget);
|
||||
$this->assertSame('24.08.2026 09:30 auth: Anmeldung fehlgeschlagen', $widget->entries[0]->label);
|
||||
}
|
||||
|
||||
public function testLongMessagesAreCutToFitOneLine(): void
|
||||
{
|
||||
$widget = $this->provider([$this->logEntry('core', str_repeat('a', 80))])->build();
|
||||
|
||||
$this->assertNotNull($widget);
|
||||
$this->assertStringEndsWith(str_repeat('a', 60).'…', $widget->entries[0]->label);
|
||||
}
|
||||
|
||||
public function testAsksForFiveEntriesAndRequiresAdmin(): void
|
||||
{
|
||||
$repository = $this->createMock(LogEntryRepository::class);
|
||||
$repository->expects($this->once())
|
||||
->method('findLatest')
|
||||
->with(5)
|
||||
->willReturn([])
|
||||
;
|
||||
|
||||
$provider = new RecentLogEntriesWidgetProvider($repository, $this->urlGenerator());
|
||||
|
||||
$this->assertSame(Role::ADMIN, $provider->getRequiredRole());
|
||||
$this->assertNotNull($provider->build());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LogEntry[] $entries
|
||||
*/
|
||||
private function provider(array $entries): RecentLogEntriesWidgetProvider
|
||||
{
|
||||
$repository = $this->createMock(LogEntryRepository::class);
|
||||
$repository->method('findLatest')->willReturn($entries);
|
||||
|
||||
return new RecentLogEntriesWidgetProvider($repository, $this->urlGenerator());
|
||||
}
|
||||
|
||||
private function urlGenerator(): UrlGeneratorInterface
|
||||
{
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator->method('generate')->willReturnCallback(
|
||||
static fn (string $route): string => '/'.str_replace('_', '/', substr($route, \strlen('app_'))),
|
||||
);
|
||||
|
||||
return $urlGenerator;
|
||||
}
|
||||
|
||||
private function logEntry(string $channel, string $message): LogEntry
|
||||
{
|
||||
return (new LogEntry($channel, $message))
|
||||
->setCreatedAt(new \DateTimeImmutable('2026-08-24 09:30:00'))
|
||||
;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user