62b416f13e
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
192 lines
7.5 KiB
PHP
192 lines
7.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXT\Unit\Service;
|
|
|
|
use KTXC\Context\IdentityContextInterface;
|
|
use KTXC\Context\TenantContextInterface;
|
|
use KTXC\Service\UserAccountsService;
|
|
use KTXC\Stores\UserAccountsStore;
|
|
use KTXC\User\Event\UserCreatedEvent;
|
|
use KTXC\User\Event\UserDeletingEvent;
|
|
use KTXC\User\Event\UserUpdatedEvent;
|
|
use KTXF\Event\EventDispatcherInterface;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\Attributes\TestDox;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class UserAccountsServiceTest extends TestCase
|
|
{
|
|
#[Test]
|
|
#[TestDox('Successful user creation and deletion emit complete lifecycle events')]
|
|
public function emitsLifecycleEvents(): void
|
|
{
|
|
$user = [
|
|
'uid' => 'user-a',
|
|
'identity' => 'person@example.test',
|
|
'label' => 'Person',
|
|
'enabled' => true,
|
|
'roles' => ['member'],
|
|
];
|
|
|
|
$tenant = $this->createStub(TenantContextInterface::class);
|
|
$tenant->method('requireIdentifier')->willReturn('tenant-a');
|
|
$identity = $this->createStub(IdentityContextInterface::class);
|
|
$identity->method('identifier')->willReturn('actor-a');
|
|
$store = $this->createMock(UserAccountsStore::class);
|
|
$store->expects($this->once())
|
|
->method('createUser')
|
|
->with('tenant-a', ['identity' => 'person@example.test'])
|
|
->willReturn($user);
|
|
$store->expects($this->once())
|
|
->method('fetchByIdentifier')
|
|
->with('tenant-a', 'user-a')
|
|
->willReturn($user);
|
|
$operations = [];
|
|
$store->expects($this->once())
|
|
->method('deleteUser')
|
|
->with('tenant-a', 'user-a')
|
|
->willReturnCallback(static function () use (&$operations): bool {
|
|
$operations[] = 'delete';
|
|
return true;
|
|
});
|
|
|
|
$emitted = [];
|
|
$events = $this->createMock(EventDispatcherInterface::class);
|
|
$events->expects($this->exactly(2))
|
|
->method('dispatch')
|
|
->willReturnCallback(static function ($event) use (&$emitted, &$operations): void {
|
|
$emitted[] = $event;
|
|
if ($event instanceof UserDeletingEvent) {
|
|
$operations[] = 'event';
|
|
}
|
|
});
|
|
|
|
$service = new UserAccountsService($tenant, $identity, $store, $events);
|
|
|
|
self::assertSame($user, $service->createUser(['identity' => 'person@example.test']));
|
|
self::assertTrue($service->deleteUser('user-a'));
|
|
self::assertInstanceOf(UserCreatedEvent::class, $emitted[0]);
|
|
self::assertInstanceOf(UserDeletingEvent::class, $emitted[1]);
|
|
self::assertSame('actor-a', $emitted[0]->actorIdentifier());
|
|
self::assertSame('tenant-a', $emitted[1]->tenantIdentifier());
|
|
self::assertSame(['event', 'delete'], $operations);
|
|
}
|
|
|
|
#[Test]
|
|
#[TestDox('Deleting event precedes a failed persistence attempt')]
|
|
public function emitsBeforeFailedDeletion(): void
|
|
{
|
|
$store = $this->createStub(UserAccountsStore::class);
|
|
$store->method('fetchByIdentifier')->willReturn([
|
|
'uid' => 'user-a',
|
|
'identity' => 'person@example.test',
|
|
]);
|
|
$store->method('deleteUser')->willReturn(false);
|
|
$events = $this->createMock(EventDispatcherInterface::class);
|
|
$events->expects($this->once())
|
|
->method('dispatch')
|
|
->with(self::isInstanceOf(UserDeletingEvent::class));
|
|
|
|
$tenant = $this->createStub(TenantContextInterface::class);
|
|
$tenant->method('requireIdentifier')->willReturn('tenant-a');
|
|
$service = new UserAccountsService(
|
|
$tenant,
|
|
$this->createStub(IdentityContextInterface::class),
|
|
$store,
|
|
$events,
|
|
);
|
|
|
|
self::assertFalse($service->deleteUser('user-a'));
|
|
}
|
|
|
|
#[Test]
|
|
#[TestDox('Successful updates emit the persisted user snapshot after storage')]
|
|
public function emitsAfterSuccessfulUpdate(): void
|
|
{
|
|
$updatedUser = [
|
|
'uid' => 'user-a',
|
|
'identity' => 'person@example.test',
|
|
'label' => 'Updated Person',
|
|
'enabled' => true,
|
|
'roles' => ['admin'],
|
|
];
|
|
$operations = [];
|
|
$store = $this->createMock(UserAccountsStore::class);
|
|
$store->expects($this->once())
|
|
->method('updateUser')
|
|
->with('tenant-a', 'user-a', ['label' => 'Updated Person'])
|
|
->willReturnCallback(static function () use (&$operations): bool {
|
|
$operations[] = 'update';
|
|
return true;
|
|
});
|
|
$store->expects($this->once())
|
|
->method('fetchByIdentifier')
|
|
->with('tenant-a', 'user-a')
|
|
->willReturnCallback(static function () use (&$operations, $updatedUser): array {
|
|
$operations[] = 'fetch';
|
|
return $updatedUser;
|
|
});
|
|
$events = $this->createMock(EventDispatcherInterface::class);
|
|
$events->expects($this->once())
|
|
->method('dispatch')
|
|
->with(self::callback(static function ($event) use (&$operations): bool {
|
|
$operations[] = 'event';
|
|
return $event instanceof UserUpdatedEvent
|
|
&& $event->userLabel() === 'Updated Person'
|
|
&& $event->userRoles() === ['admin'];
|
|
}));
|
|
$tenant = $this->createStub(TenantContextInterface::class);
|
|
$tenant->method('requireIdentifier')->willReturn('tenant-a');
|
|
$identity = $this->createStub(IdentityContextInterface::class);
|
|
$identity->method('identifier')->willReturn('actor-a');
|
|
$service = new UserAccountsService($tenant, $identity, $store, $events);
|
|
|
|
self::assertTrue($service->updateUser('user-a', ['label' => 'Updated Person']));
|
|
self::assertSame(['update', 'fetch', 'event'], $operations);
|
|
}
|
|
|
|
#[Test]
|
|
#[TestDox('Unchanged users do not emit an updated event')]
|
|
public function ignoresUnchangedUser(): void
|
|
{
|
|
$store = $this->createMock(UserAccountsStore::class);
|
|
$store->expects($this->once())->method('updateUser')->willReturn(false);
|
|
$store->expects($this->never())->method('fetchByIdentifier');
|
|
$events = $this->createMock(EventDispatcherInterface::class);
|
|
$events->expects($this->never())->method('dispatch');
|
|
$tenant = $this->createStub(TenantContextInterface::class);
|
|
$tenant->method('requireIdentifier')->willReturn('tenant-a');
|
|
$service = new UserAccountsService(
|
|
$tenant,
|
|
$this->createStub(IdentityContextInterface::class),
|
|
$store,
|
|
$events,
|
|
);
|
|
|
|
self::assertFalse($service->updateUser('user-a', ['label' => 'Person']));
|
|
}
|
|
|
|
#[Test]
|
|
#[TestDox('Missing users do not emit a deleting event')]
|
|
public function ignoresMissingUser(): void
|
|
{
|
|
$store = $this->createStub(UserAccountsStore::class);
|
|
$store->method('fetchByIdentifier')->willReturn(null);
|
|
$events = $this->createMock(EventDispatcherInterface::class);
|
|
$events->expects($this->never())->method('dispatch');
|
|
|
|
$tenant = $this->createStub(TenantContextInterface::class);
|
|
$tenant->method('requireIdentifier')->willReturn('tenant-a');
|
|
$service = new UserAccountsService(
|
|
$tenant,
|
|
$this->createStub(IdentityContextInterface::class),
|
|
$store,
|
|
$events,
|
|
);
|
|
|
|
self::assertFalse($service->deleteUser('missing'));
|
|
}
|
|
}
|