'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]->getTenantId()); 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')); } }