Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59bd193783 |
@@ -4,12 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Console\Tenant;
|
||||
|
||||
use KTXC\Context\TenantContext;
|
||||
use KTXC\Models\Tenant\DomainCollection;
|
||||
use KTXC\Models\Tenant\TenantConfiguration;
|
||||
use KTXC\Models\Tenant\TenantObject;
|
||||
use KTXC\Service\TenantService;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\Stores\UserAccountsStore;
|
||||
use KTXC\Stores\UserRolesStore;
|
||||
use KTXF\Utile\UUID;
|
||||
@@ -37,8 +35,6 @@ class TenantCreateCommand extends Command
|
||||
private readonly TenantService $tenantService,
|
||||
private readonly UserRolesStore $rolesStore,
|
||||
private readonly UserAccountsStore $userStore,
|
||||
private readonly UserAccountsService $userService,
|
||||
private readonly TenantContext $tenantContext,
|
||||
private readonly LoggerInterface $logger
|
||||
) {
|
||||
parent::__construct();
|
||||
@@ -101,10 +97,6 @@ class TenantCreateCommand extends Command
|
||||
$io->error('Failed to create tenant.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
if (!$this->tenantContext->resolveIdentifier($identifier)) {
|
||||
throw new \RuntimeException("Failed to initialize tenant context for '{$identifier}'.");
|
||||
}
|
||||
$identifier = $this->tenantContext->requireIdentifier();
|
||||
|
||||
$this->logger->info('Tenant created via console', [
|
||||
'identifier' => $identifier,
|
||||
@@ -142,7 +134,7 @@ class TenantCreateCommand extends Command
|
||||
if ($this->userStore->fetchByIdentity($identifier, $adminIdentity)) {
|
||||
$io->warning("User '{$adminIdentity}' already exists in tenant '{$identifier}'; skipping admin user creation.");
|
||||
} else {
|
||||
$this->userService->createUser([
|
||||
$this->userStore->createUser($identifier, [
|
||||
'identity' => $adminIdentity,
|
||||
'label' => 'Administrator',
|
||||
'enabled' => true,
|
||||
|
||||
@@ -4,8 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Console\User;
|
||||
|
||||
use KTXC\Context\TenantContext;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\Service\TenantService;
|
||||
use KTXC\Stores\UserAccountsStore;
|
||||
use KTXC\Stores\UserRolesStore;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -29,9 +28,8 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
class UserCreateCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TenantContext $tenantContext,
|
||||
private readonly TenantService $tenantService,
|
||||
private readonly UserAccountsStore $userStore,
|
||||
private readonly UserAccountsService $userService,
|
||||
private readonly UserRolesStore $rolesStore,
|
||||
private readonly LoggerInterface $logger
|
||||
) {
|
||||
@@ -61,11 +59,11 @@ class UserCreateCommand extends Command
|
||||
$io->title('Create User');
|
||||
|
||||
try {
|
||||
if (!$this->tenantContext->resolveIdentifier($tenant)) {
|
||||
// Ensure the tenant exists
|
||||
if (!$this->tenantService->fetchById($tenant)) {
|
||||
$io->error("Tenant '{$tenant}' not found.");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$tenant = $this->tenantContext->requireIdentifier();
|
||||
|
||||
// Ensure identity is unique within the tenant
|
||||
if ($this->userStore->fetchByIdentity($tenant, $identity)) {
|
||||
@@ -97,7 +95,7 @@ class UserCreateCommand extends Command
|
||||
$userData['uid'] = $input->getOption('uid');
|
||||
}
|
||||
|
||||
$user = $this->userService->createUser($userData);
|
||||
$user = $this->userStore->createUser($tenant, $userData);
|
||||
|
||||
$this->logger->info('User created via console', [
|
||||
'tenant' => $tenant,
|
||||
|
||||
@@ -4,8 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Console\User;
|
||||
|
||||
use KTXC\Context\TenantContext;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\Stores\UserAccountsStore;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
@@ -28,9 +26,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
class UserDeleteCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TenantContext $tenantContext,
|
||||
private readonly UserAccountsStore $userStore,
|
||||
private readonly UserAccountsService $userService,
|
||||
private readonly LoggerInterface $logger
|
||||
) {
|
||||
parent::__construct();
|
||||
@@ -56,12 +52,6 @@ class UserDeleteCommand extends Command
|
||||
$io->title('Delete User');
|
||||
|
||||
try {
|
||||
if (!$this->tenantContext->resolveIdentifier($tenant)) {
|
||||
$io->error("Tenant '{$tenant}' not found.");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$tenant = $this->tenantContext->requireIdentifier();
|
||||
|
||||
$user = $this->userStore->fetchByIdentity($tenant, $identity);
|
||||
|
||||
if (!$user) {
|
||||
@@ -74,7 +64,7 @@ class UserDeleteCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if (!$this->userService->deleteUser($user['uid'])) {
|
||||
if (!$this->userStore->deleteUser($tenant, $user['uid'])) {
|
||||
$io->error("Failed to delete user '{$identity}'.");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
@@ -6,19 +6,14 @@ use KTXC\Models\Identity\User;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXC\Stores\UserAccountsStore;
|
||||
use KTXC\User\Event\UserCreatedEvent;
|
||||
use KTXC\User\Event\UserDeletingEvent;
|
||||
use KTXC\User\Event\UserUpdatedEvent;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
|
||||
class UserAccountsService
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly IdentityContextInterface $identityContext,
|
||||
private readonly UserAccountsStore $userStore,
|
||||
private readonly EventDispatcherInterface $events,
|
||||
private readonly IdentityContextInterface $identityContext,
|
||||
private readonly UserAccountsStore $userStore
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -70,53 +65,17 @@ class UserAccountsService
|
||||
|
||||
public function createUser(array $userData): array
|
||||
{
|
||||
$tenantId = $this->tenantContext->requireIdentifier();
|
||||
$user = $this->userStore->createUser($tenantId, $userData);
|
||||
$this->events->dispatch(UserCreatedEvent::fromUser(
|
||||
$user,
|
||||
$tenantId,
|
||||
$this->identityContext->identifier(),
|
||||
));
|
||||
|
||||
return $user;
|
||||
return $this->userStore->createUser($this->tenantContext->identifier(), $userData);
|
||||
}
|
||||
|
||||
public function updateUser(string $userId, array $updates): bool
|
||||
public function updateUser(string $uid, array $updates): bool
|
||||
{
|
||||
$tenantId = $this->tenantContext->requireIdentifier();
|
||||
if (!$this->userStore->updateUser($tenantId, $userId, $updates)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = $this->userStore->fetchByIdentifier($tenantId, $userId);
|
||||
if ($user === null) {
|
||||
throw new \RuntimeException("Updated user '{$userId}' could not be retrieved.");
|
||||
}
|
||||
|
||||
$this->events->dispatch(UserUpdatedEvent::fromUser(
|
||||
$user,
|
||||
$tenantId,
|
||||
$this->identityContext->identifier(),
|
||||
));
|
||||
|
||||
return true;
|
||||
return $this->userStore->updateUser($this->tenantContext->identifier(), $uid, $updates);
|
||||
}
|
||||
|
||||
public function deleteUser(string $userId): bool
|
||||
public function deleteUser(string $uid): bool
|
||||
{
|
||||
$tenantId = $this->tenantContext->requireIdentifier();
|
||||
$user = $this->userStore->fetchByIdentifier($tenantId, $userId);
|
||||
if ($user === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->events->dispatch(UserDeletingEvent::fromUser(
|
||||
$user,
|
||||
$tenantId,
|
||||
$this->identityContext->identifier(),
|
||||
));
|
||||
|
||||
return $this->userStore->deleteUser($tenantId, $userId);
|
||||
return $this->userStore->deleteUser($this->tenantContext->identifier(), $uid);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -167,6 +126,10 @@ class UserAccountsService
|
||||
return $this->userStore->storeSettings($this->tenantContext->identifier(), $this->identityContext->identifier(), $settings);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Check if a profile field is editable by the user
|
||||
*
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\User\Event;
|
||||
|
||||
final class UserCreatedEvent extends UserEvent
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\User\Event;
|
||||
|
||||
final class UserDeletingEvent extends UserEvent
|
||||
{
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\User\Event;
|
||||
|
||||
use KTXF\Event\Event;
|
||||
|
||||
abstract class UserEvent extends Event
|
||||
{
|
||||
final public function __construct(
|
||||
private readonly string $userIdentifier,
|
||||
private readonly string $userIdentity,
|
||||
private readonly string $userLabel,
|
||||
private readonly bool $userEnabled,
|
||||
private readonly array $userRoles,
|
||||
private readonly string $tenantIdentifier,
|
||||
private readonly ?string $actorIdentifier = null,
|
||||
) {
|
||||
if ($userIdentifier === '') {
|
||||
throw new \InvalidArgumentException('User lifecycle events require a user ID.');
|
||||
}
|
||||
if ($userIdentity === '') {
|
||||
throw new \InvalidArgumentException('User lifecycle events require a user identity.');
|
||||
}
|
||||
if ($tenantIdentifier === '') {
|
||||
throw new \InvalidArgumentException('User lifecycle events require a tenant ID.');
|
||||
}
|
||||
parent::__construct(
|
||||
static::class,
|
||||
[
|
||||
'identifier' => $userIdentifier,
|
||||
'identity' => $userIdentity,
|
||||
'label' => $userLabel,
|
||||
'roles' => $userRoles,
|
||||
'enabled' => $userEnabled,
|
||||
],
|
||||
$tenantIdentifier,
|
||||
$actorIdentifier,
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromUser(
|
||||
array $user,
|
||||
string $tenantIdentifier,
|
||||
?string $actorIdentifier = null,
|
||||
): static {
|
||||
return new static(
|
||||
(string) ($user['uid'] ?? ''),
|
||||
(string) ($user['identity'] ?? ''),
|
||||
(string) ($user['label'] ?? $user['identity'] ?? ''),
|
||||
(bool) ($user['enabled'] ?? true),
|
||||
array_values((array) ($user['roles'] ?? [])),
|
||||
$tenantIdentifier,
|
||||
$actorIdentifier,
|
||||
);
|
||||
}
|
||||
|
||||
public function userIdentifier(): string
|
||||
{
|
||||
return $this->userIdentifier;
|
||||
}
|
||||
|
||||
public function userIdentity(): string
|
||||
{
|
||||
return $this->userIdentity;
|
||||
}
|
||||
|
||||
public function userLabel(): string
|
||||
{
|
||||
return $this->userLabel;
|
||||
}
|
||||
|
||||
public function userRoles(): array
|
||||
{
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
public function userEnabled(): bool
|
||||
{
|
||||
return $this->userEnabled;
|
||||
}
|
||||
|
||||
public function actorIdentifier(): ?string
|
||||
{
|
||||
return $this->actorIdentifier;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\User\Event;
|
||||
|
||||
final class UserUpdatedEvent extends UserEvent
|
||||
{
|
||||
}
|
||||
@@ -33,30 +33,6 @@ require_command()
|
||||
fi
|
||||
}
|
||||
|
||||
# Cron runs with a bare PATH, so nvm-installed npm (added to PATH only by
|
||||
# .bashrc sourcing nvm.sh in an interactive shell) is invisible here even
|
||||
# though it works fine when this script is run by hand. Resolve nvm's
|
||||
# current npm via bash (nvm.sh is not POSIX sh compatible) and prepend it,
|
||||
# so a later `nvm use`/`nvm install` doesn't require updating this script
|
||||
# or the crontab.
|
||||
ensure_npm_on_path()
|
||||
{
|
||||
command -v npm >/dev/null 2>&1 && return 0
|
||||
|
||||
nvm_dir=${NVM_DIR:-${HOME:-/root}/.nvm}
|
||||
[ -s "$nvm_dir/nvm.sh" ] || return 0
|
||||
command -v bash >/dev/null 2>&1 || return 0
|
||||
|
||||
npm_path=$(bash -c ". \"\$1/nvm.sh\" >/dev/null 2>&1 && command -v npm" _ "$nvm_dir" 2>/dev/null) || return 0
|
||||
[ -n "$npm_path" ] || return 0
|
||||
|
||||
PATH=$(dirname -- "$npm_path"):$PATH
|
||||
export PATH
|
||||
log "Resolved npm via nvm: $npm_path"
|
||||
}
|
||||
|
||||
ensure_npm_on_path
|
||||
|
||||
git_in()
|
||||
{
|
||||
repository=$1
|
||||
|
||||
Generated
+28
-28
@@ -642,14 +642,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.7.tgz",
|
||||
"integrity": "sha512-MSB/sBKwEWJTILvQIhg2rnIcwPpLayo3wGwvVA+dJTNeUBD9GoqQgAaSOLdI9iOPDHCm9YoVnLqpfzza98MpkQ==",
|
||||
"version": "11.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.8.tgz",
|
||||
"integrity": "sha512-A+Q7SKm5oEcy1E/cghqd7n/St4XjTqLhiiyDuieNcMrJcrHlkY5n0jp7Q9dD3txvVHzvsmBVV5M9wD5/s1zfzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/devtools-types": "11.4.7",
|
||||
"@intlify/message-compiler": "11.4.7",
|
||||
"@intlify/shared": "11.4.7"
|
||||
"@intlify/devtools-types": "11.4.8",
|
||||
"@intlify/message-compiler": "11.4.8",
|
||||
"@intlify/shared": "11.4.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22"
|
||||
@@ -659,13 +659,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/devtools-types": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.7.tgz",
|
||||
"integrity": "sha512-GSz+J+hqH+AEpAHIYya6fSufS30OaMnG39HiZX7DmGKi3+aaLvassCfsXENEc4Wr4m68q2YP0QdMdB3D9UeAXg==",
|
||||
"version": "11.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.8.tgz",
|
||||
"integrity": "sha512-MGpID+rlfzGUbNcnC20bm5NMSBHPrvx0atLTfv9dftn3kjXw1hGKDcIcwrO99tSrZEc2i+hczRL7ks8qXsHPkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.4.7",
|
||||
"@intlify/shared": "11.4.7"
|
||||
"@intlify/core-base": "11.4.8",
|
||||
"@intlify/shared": "11.4.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22"
|
||||
@@ -675,12 +675,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.7.tgz",
|
||||
"integrity": "sha512-bHxmh7n94N4N1evADeb7XTkc3jTw6Ki5biMFZVSX6Jmk+iehy8/maeH2XUsBI27rtKIK+Hzc6QnVAKggUwylKw==",
|
||||
"version": "11.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.8.tgz",
|
||||
"integrity": "sha512-vbzk17dYwduYiv52EK61+FDCyhfVg1uPUtPmiD/d45W99uJIcXywrweOBcHv7n9/iEqmXiMGT52bgJbZDQqK3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.4.7",
|
||||
"@intlify/shared": "11.4.8",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -691,9 +691,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.7.tgz",
|
||||
"integrity": "sha512-OtjPZan3No2OZZFnMUiCVsXC6+j+XRwEywaFDk0AoayAbLuPesyDloXhJZLl9JUl5vHZeQUkYSbEA8VX+CWMjg==",
|
||||
"version": "11.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.8.tgz",
|
||||
"integrity": "sha512-XbRgrv+XEuvDr7UCY55oibVrh+o4u+A0VB6nSL0F5Z8LcZxE/8j573LYG6bCrOigIcHdGpSNI7Rh5UpC5/B/eg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 22"
|
||||
@@ -6537,14 +6537,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.7.tgz",
|
||||
"integrity": "sha512-j6RyshdPPzqLiMAUpnpvZGFPM+rRoWi14Sl5yTsquvoW0/56DWyvhAj2o9TO2YXGvb6teg8T0xrYO9jR3urvdw==",
|
||||
"version": "11.4.8",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.8.tgz",
|
||||
"integrity": "sha512-0ULeHP6Z9CGvAm67S77ZEp41cfGXIREGL8qfhos2BMgcQQewtQcDKuojt6jjasAD/S8GwfTp2ySPmDSpwvrCMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.4.7",
|
||||
"@intlify/devtools-types": "11.4.7",
|
||||
"@intlify/shared": "11.4.7",
|
||||
"@intlify/core-base": "11.4.8",
|
||||
"@intlify/devtools-types": "11.4.8",
|
||||
"@intlify/shared": "11.4.8",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6642,9 +6642,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.1.8.tgz",
|
||||
"integrity": "sha512-003D/8b5462uZCD5CMRTZF3smADmOn47mzthNY0aP/xkslovR5yIc1qXjPdFN0LHcu+p3GEAVmMQabldaIQ5pg==",
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.1.6.tgz",
|
||||
"integrity": "sha512-VOsRTsNfs+FE4JXMONZkqX2yznSqC/FXG9/pgVVjsoqIjUvJg+CfFTuOlgyWVFrZYY+crk7LV9uaiN8bZREV/g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -6653,7 +6653,7 @@
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.7",
|
||||
"vite-plugin-vuetify": ">=2.1.0",
|
||||
"vue": "^3.5.0 || ^3.6.0-0",
|
||||
"vue": "^3.5.0",
|
||||
"webpack-plugin-vuetify": ">=3.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
|
||||
@@ -5,10 +5,8 @@ declare(strict_types=1);
|
||||
namespace KTXT\Unit\Console\Tenant;
|
||||
|
||||
use KTXC\Console\Tenant\TenantCreateCommand;
|
||||
use KTXC\Context\TenantContext;
|
||||
use KTXC\Models\Tenant\TenantObject;
|
||||
use KTXC\Service\TenantService;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\Stores\UserAccountsStore;
|
||||
use KTXC\Stores\UserRolesStore;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
@@ -24,7 +22,6 @@ class TenantCreateCommandTest extends TestCase
|
||||
private TenantService&MockObject $tenantService;
|
||||
private UserRolesStore $rolesStore;
|
||||
private UserAccountsStore $userStore;
|
||||
private UserAccountsService $userService;
|
||||
private CommandTester $tester;
|
||||
private ?TenantObject $deposited = null;
|
||||
|
||||
@@ -34,21 +31,12 @@ class TenantCreateCommandTest extends TestCase
|
||||
$this->rolesStore = $this->createStub(UserRolesStore::class);
|
||||
$this->rolesStore->method('createRole')->willReturn(['rid' => 'admin']);
|
||||
$this->userStore = $this->createStub(UserAccountsStore::class);
|
||||
$this->userService = $this->createStub(UserAccountsService::class);
|
||||
$this->userService->method('createUser')->willReturn(['uid' => 'admin']);
|
||||
$contextTenantService = $this->createStub(TenantService::class);
|
||||
$contextTenantService->method('fetchById')->willReturnCallback(
|
||||
fn(string $identifier): ?TenantObject => $this->deposited?->getIdentifier() === $identifier
|
||||
? $this->deposited
|
||||
: null,
|
||||
);
|
||||
$this->userStore->method('createUser')->willReturn(['uid' => 'admin']);
|
||||
$this->tester = new CommandTester(
|
||||
new TenantCreateCommand(
|
||||
$this->tenantService,
|
||||
$this->rolesStore,
|
||||
$this->userStore,
|
||||
$this->userService,
|
||||
new TenantContext($contextTenantService),
|
||||
new NullLogger(),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXT\Unit\Event;
|
||||
|
||||
use KTXC\User\Event\UserCreatedEvent;
|
||||
use KTXC\User\Event\UserDeletingEvent;
|
||||
use KTXC\User\Event\UserUpdatedEvent;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class UserEventTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
#[TestDox('User lifecycle events contain an immutable user snapshot')]
|
||||
public function containsUserSnapshot(): void
|
||||
{
|
||||
$event = UserCreatedEvent::fromUser(
|
||||
[
|
||||
'uid' => 'user-a',
|
||||
'identity' => 'person@example.test',
|
||||
'label' => 'Person',
|
||||
'enabled' => true,
|
||||
'roles' => ['member'],
|
||||
],
|
||||
'tenant-a',
|
||||
'actor-a',
|
||||
);
|
||||
|
||||
self::assertSame(UserCreatedEvent::class, $event->getName());
|
||||
self::assertSame('user-a', $event->userIdentifier());
|
||||
self::assertSame('person@example.test', $event->userIdentity());
|
||||
self::assertSame('Person', $event->userLabel());
|
||||
self::assertTrue($event->userEnabled());
|
||||
self::assertSame(['member'], $event->userRoles());
|
||||
self::assertSame('tenant-a', $event->getTenantId());
|
||||
self::assertSame('actor-a', $event->actorIdentifier());
|
||||
self::assertSame('actor-a', $event->getIdentityId());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[TestDox('Created, updated, and deleting users have distinct event names')]
|
||||
public function distinguishesLifecycleStage(): void
|
||||
{
|
||||
$user = ['uid' => 'user-a', 'identity' => 'person@example.test'];
|
||||
|
||||
self::assertSame(UserCreatedEvent::class, UserCreatedEvent::fromUser($user, 'tenant-a')->getName());
|
||||
self::assertSame(UserUpdatedEvent::class, UserUpdatedEvent::fromUser($user, 'tenant-a')->getName());
|
||||
self::assertSame(UserDeletingEvent::class, UserDeletingEvent::fromUser($user, 'tenant-a')->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[TestDox('User lifecycle events reject incomplete snapshots')]
|
||||
public function rejectsIncompleteSnapshot(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
UserCreatedEvent::fromUser(['identity' => 'person@example.test'], 'tenant-a');
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
<?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]->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'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user