feat: Provisionner automatiquement un nouvel établissement
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

Lorsqu'un super-admin crée un établissement via l'interface, le système
doit automatiquement créer la base tenant, exécuter les migrations,
créer le premier utilisateur admin et envoyer l'invitation — le tout
de manière asynchrone pour ne pas bloquer la réponse HTTP.

Ce mécanisme rend chaque établissement opérationnel dès sa création
sans intervention manuelle sur l'infrastructure.
This commit is contained in:
2026-04-08 13:55:41 +02:00
parent bec211ebf0
commit 531548c65f
34 changed files with 1928 additions and 69 deletions

View File

@@ -37,7 +37,7 @@ final class CreateEstablishmentHandlerTest extends TestCase
}
#[Test]
public function createsEstablishmentAndReturnsResult(): void
public function createsEstablishmentAndReturnsIt(): void
{
$command = new CreateEstablishmentCommand(
name: 'École Alpha',
@@ -46,13 +46,13 @@ final class CreateEstablishmentHandlerTest extends TestCase
superAdminId: self::SUPER_ADMIN_ID,
);
$result = ($this->handler)($command);
$establishment = ($this->handler)($command);
self::assertNotEmpty($result->establishmentId);
self::assertNotEmpty($result->tenantId);
self::assertSame('École Alpha', $result->name);
self::assertSame('ecole-alpha', $result->subdomain);
self::assertStringStartsWith('classeo_tenant_', $result->databaseName);
self::assertNotEmpty((string) $establishment->id);
self::assertNotEmpty((string) $establishment->tenantId);
self::assertSame('École Alpha', $establishment->name);
self::assertSame('ecole-alpha', $establishment->subdomain);
self::assertStringStartsWith('classeo_tenant_', $establishment->databaseName);
}
#[Test]
@@ -65,10 +65,10 @@ final class CreateEstablishmentHandlerTest extends TestCase
superAdminId: self::SUPER_ADMIN_ID,
);
$result = ($this->handler)($command);
$establishment = ($this->handler)($command);
$establishments = $this->repository->findAll();
self::assertCount(1, $establishments);
self::assertSame($result->establishmentId, (string) $establishments[0]->id);
self::assertSame((string) $establishment->id, (string) $establishments[0]->id);
}
}

View File

@@ -40,6 +40,7 @@ final class GetEstablishmentsHandlerTest extends TestCase
$this->repository->save(Establishment::creer(
name: 'École Alpha',
subdomain: 'ecole-alpha',
adminEmail: 'admin@ecole-alpha.fr',
createdBy: SuperAdminId::fromString(self::SUPER_ADMIN_ID),
createdAt: new DateTimeImmutable('2026-02-16 10:00:00'),
));
@@ -47,6 +48,7 @@ final class GetEstablishmentsHandlerTest extends TestCase
$this->repository->save(Establishment::creer(
name: 'École Beta',
subdomain: 'ecole-beta',
adminEmail: 'admin@ecole-beta.fr',
createdBy: SuperAdminId::fromString(self::SUPER_ADMIN_ID),
createdAt: new DateTimeImmutable('2026-02-16 11:00:00'),
));
@@ -56,6 +58,6 @@ final class GetEstablishmentsHandlerTest extends TestCase
self::assertCount(2, $result);
self::assertSame('École Alpha', $result[0]->name);
self::assertSame('ecole-alpha', $result[0]->subdomain);
self::assertSame('active', $result[0]->status);
self::assertSame('provisioning', $result[0]->status);
}
}

View File

@@ -23,11 +23,11 @@ final class EstablishmentTest extends TestCase
private const string SUBDOMAIN = 'ecole-alpha';
#[Test]
public function creerCreatesActiveEstablishment(): void
public function creerCreatesProvisioningEstablishment(): void
{
$establishment = $this->createEstablishment();
self::assertSame(EstablishmentStatus::ACTIF, $establishment->status);
self::assertSame(EstablishmentStatus::PROVISIONING, $establishment->status);
self::assertSame(self::ESTABLISHMENT_NAME, $establishment->name);
self::assertSame(self::SUBDOMAIN, $establishment->subdomain);
self::assertNull($establishment->lastActivityAt);
@@ -59,10 +59,21 @@ final class EstablishmentTest extends TestCase
self::assertStringStartsWith('classeo_tenant_', $establishment->databaseName);
}
#[Test]
public function activerChangesStatusToActif(): void
{
$establishment = $this->createEstablishment();
self::assertSame(EstablishmentStatus::PROVISIONING, $establishment->status);
$establishment->activer();
self::assertSame(EstablishmentStatus::ACTIF, $establishment->status);
}
#[Test]
public function desactiverChangesStatusToInactif(): void
{
$establishment = $this->createEstablishment();
$establishment->activer();
$establishment->desactiver(new DateTimeImmutable('2026-02-16 12:00:00'));
@@ -73,6 +84,7 @@ final class EstablishmentTest extends TestCase
public function desactiverRecordsEtablissementDesactiveEvent(): void
{
$establishment = $this->createEstablishment();
$establishment->activer();
$establishment->pullDomainEvents(); // Clear creation event
$establishment->desactiver(new DateTimeImmutable('2026-02-16 12:00:00'));
@@ -86,6 +98,7 @@ final class EstablishmentTest extends TestCase
public function desactiverThrowsWhenAlreadyInactive(): void
{
$establishment = $this->createEstablishment();
$establishment->activer();
$establishment->desactiver(new DateTimeImmutable('2026-02-16 12:00:00'));
$this->expectException(EstablishmentDejaInactifException::class);
@@ -141,6 +154,7 @@ final class EstablishmentTest extends TestCase
return Establishment::creer(
name: self::ESTABLISHMENT_NAME,
subdomain: self::SUBDOMAIN,
adminEmail: 'admin@ecole-alpha.fr',
createdBy: SuperAdminId::fromString(self::SUPER_ADMIN_ID),
createdAt: new DateTimeImmutable('2026-02-16 10:00:00'),
);

View File

@@ -7,6 +7,7 @@ namespace App\Tests\Unit\SuperAdmin\Infrastructure\Api\Processor;
use ApiPlatform\Metadata\Post;
use App\Shared\Domain\Clock;
use App\SuperAdmin\Application\Command\CreateEstablishment\CreateEstablishmentHandler;
use App\SuperAdmin\Application\Command\ProvisionEstablishment\ProvisionEstablishmentCommand;
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
use App\SuperAdmin\Infrastructure\Api\Processor\CreateEstablishmentProcessor;
use App\SuperAdmin\Infrastructure\Api\Resource\EstablishmentResource;
@@ -16,13 +17,15 @@ use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
final class CreateEstablishmentProcessorTest extends TestCase
{
private const string SUPER_ADMIN_ID = '550e8400-e29b-41d4-a716-446655440001';
#[Test]
public function processCreatesEstablishmentAndReturnsResource(): void
public function processCreatesEstablishmentAndDispatchesProvisioning(): void
{
$repository = new InMemoryEstablishmentRepository();
$clock = new class implements Clock {
@@ -42,7 +45,16 @@ final class CreateEstablishmentProcessorTest extends TestCase
$security = $this->createMock(Security::class);
$security->method('getUser')->willReturn($securityUser);
$processor = new CreateEstablishmentProcessor($handler, $security);
$dispatched = [];
$commandBus = $this->createMock(MessageBusInterface::class);
$commandBus->method('dispatch')
->willReturnCallback(static function (object $message) use (&$dispatched): Envelope {
$dispatched[] = $message;
return new Envelope($message);
});
$processor = new CreateEstablishmentProcessor($handler, $security, $commandBus);
$input = new EstablishmentResource();
$input->name = 'École Gamma';
@@ -55,6 +67,12 @@ final class CreateEstablishmentProcessorTest extends TestCase
self::assertNotNull($result->tenantId);
self::assertSame('École Gamma', $result->name);
self::assertSame('ecole-gamma', $result->subdomain);
self::assertSame('active', $result->status);
self::assertSame('provisioning', $result->status);
self::assertCount(1, $dispatched);
self::assertInstanceOf(ProvisionEstablishmentCommand::class, $dispatched[0]);
self::assertSame('admin@ecole-gamma.fr', $dispatched[0]->adminEmail);
self::assertSame('ecole-gamma', $dispatched[0]->subdomain);
self::assertSame('École Gamma', $dispatched[0]->establishmentName);
}
}

View File

@@ -37,6 +37,7 @@ final class EstablishmentCollectionProviderTest extends TestCase
$repository->save(Establishment::creer(
name: 'École Alpha',
subdomain: 'ecole-alpha',
adminEmail: 'admin@ecole-alpha.fr',
createdBy: SuperAdminId::fromString(self::SUPER_ADMIN_ID),
createdAt: new DateTimeImmutable('2026-02-16 10:00:00'),
));
@@ -49,6 +50,6 @@ final class EstablishmentCollectionProviderTest extends TestCase
self::assertCount(1, $result);
self::assertSame('École Alpha', $result[0]->name);
self::assertSame('ecole-alpha', $result[0]->subdomain);
self::assertSame('active', $result[0]->status);
self::assertSame('provisioning', $result[0]->status);
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\SuperAdmin\Infrastructure\Provisioning;
use App\SuperAdmin\Application\Port\TenantProvisioner;
use App\SuperAdmin\Infrastructure\Provisioning\DatabaseTenantProvisioner;
use App\SuperAdmin\Infrastructure\Provisioning\TenantDatabaseCreator;
use App\SuperAdmin\Infrastructure\Provisioning\TenantMigrator;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use RuntimeException;
final class DatabaseTenantProvisionerTest extends TestCase
{
#[Test]
public function itCallsCreatorThenMigratorInOrder(): void
{
$steps = [];
$connection = $this->createMock(Connection::class);
$connection->method('fetchOne')->willReturn(false);
$connection->method('executeStatement')->willReturnCallback(
static function () use (&$steps): int {
$steps[] = 'create';
return 1;
},
);
$creator = new TenantDatabaseCreator($connection, new NullLogger());
// TenantMigrator is final — we wrap via the TenantProvisioner interface
// to verify the creator is called. Migration subprocess cannot be tested unitarily.
$provisioner = new class($creator, $steps) implements TenantProvisioner {
/** @param string[] $steps */
public function __construct(
private readonly TenantDatabaseCreator $creator,
private array &$steps,
) {
}
public function provision(string $databaseName): void
{
$this->creator->create($databaseName);
$this->steps[] = 'migrate';
}
};
$provisioner->provision('classeo_tenant_test');
self::assertSame(['create', 'migrate'], $steps);
}
#[Test]
public function itPropagatesCreationFailure(): void
{
$connection = $this->createMock(Connection::class);
$connection->method('fetchOne')->willThrowException(new RuntimeException('Connection refused'));
$creator = new TenantDatabaseCreator($connection, new NullLogger());
$migrator = new TenantMigrator('/tmp', 'postgresql://u:p@h/db', new NullLogger());
$provisioner = new DatabaseTenantProvisioner($creator, $migrator);
$this->expectException(RuntimeException::class);
$provisioner->provision('classeo_tenant_test');
}
}

View File

@@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\SuperAdmin\Infrastructure\Provisioning;
use App\Administration\Application\Command\InviteUser\InviteUserHandler;
use App\Administration\Domain\Event\InvitationRenvoyee;
use App\Administration\Domain\Event\UtilisateurInvite;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use App\SuperAdmin\Application\Command\ProvisionEstablishment\ProvisionEstablishmentCommand;
use App\SuperAdmin\Application\Port\TenantProvisioner;
use App\SuperAdmin\Domain\Model\Establishment\Establishment;
use App\SuperAdmin\Domain\Model\Establishment\EstablishmentId;
use App\SuperAdmin\Domain\Model\Establishment\EstablishmentStatus;
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
use App\SuperAdmin\Infrastructure\Persistence\InMemory\InMemoryEstablishmentRepository;
use App\SuperAdmin\Infrastructure\Provisioning\ProvisionEstablishmentHandler;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use RuntimeException;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
final class ProvisionEstablishmentHandlerTest extends TestCase
{
private const string MASTER_URL = 'postgresql://classeo:secret@db:5432/classeo_master?serverVersion=18';
private const string ESTABLISHMENT_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
#[Test]
public function itProvisionsTenantDatabase(): void
{
$provisioner = $this->createMock(TenantProvisioner::class);
$provisioner->expects(self::once())
->method('provision')
->with('classeo_tenant_abc123');
$handler = $this->buildHandler(provisioner: $provisioner);
$handler($this->command());
}
#[Test]
public function itCreatesAdminUser(): void
{
$userRepository = new InMemoryUserRepository();
$handler = $this->buildHandler(userRepository: $userRepository);
$handler($this->command());
$users = $userRepository->findAllByTenant(TenantId::fromString(self::TENANT_ID));
self::assertCount(1, $users);
self::assertSame('admin@ecole-gamma.fr', (string) $users[0]->email);
}
#[Test]
public function itDispatchesInvitationEvent(): void
{
$dispatched = [];
$eventBus = $this->spyEventBus($dispatched);
$handler = $this->buildHandler(eventBus: $eventBus);
$handler($this->command());
self::assertNotEmpty($dispatched);
self::assertInstanceOf(UtilisateurInvite::class, $dispatched[0]);
}
#[Test]
public function itActivatesEstablishmentAfterProvisioning(): void
{
$establishmentRepo = $this->establishmentRepoWithProvisioningEstablishment();
$handler = $this->buildHandler(establishmentRepository: $establishmentRepo);
$handler($this->command());
$establishment = $establishmentRepo->get(
EstablishmentId::fromString(self::ESTABLISHMENT_ID),
);
self::assertSame(EstablishmentStatus::ACTIF, $establishment->status);
}
#[Test]
public function itIsIdempotentWhenAdminAlreadyExists(): void
{
$userRepository = new InMemoryUserRepository();
$dispatched = [];
$eventBus = $this->spyEventBus($dispatched);
$handler = $this->buildHandler(userRepository: $userRepository, eventBus: $eventBus);
// First call creates the admin
$handler($this->command());
self::assertCount(1, $dispatched);
self::assertInstanceOf(UtilisateurInvite::class, $dispatched[0]);
// Second call is idempotent — re-sends invitation
$dispatched = [];
$handler($this->command());
self::assertCount(1, $dispatched);
self::assertInstanceOf(InvitationRenvoyee::class, $dispatched[0]);
}
#[Test]
public function itSwitchesDatabaseAndRestores(): void
{
$switcher = new SpyDatabaseSwitcher();
$handler = $this->buildHandler(databaseSwitcher: $switcher);
$handler($this->command());
self::assertCount(1, $switcher->switchedTo);
self::assertStringContainsString('classeo_tenant_abc123', $switcher->switchedTo[0]);
self::assertTrue($switcher->restoredToDefault);
}
#[Test]
public function itPreservesQueryParametersInDatabaseUrl(): void
{
$switcher = new SpyDatabaseSwitcher();
$handler = $this->buildHandler(databaseSwitcher: $switcher);
$handler($this->command());
self::assertStringContainsString('?serverVersion=18', $switcher->switchedTo[0]);
}
#[Test]
public function itRestoresDatabaseEvenOnFailure(): void
{
$switcher = new SpyDatabaseSwitcher();
$eventBus = $this->createMock(MessageBusInterface::class);
$eventBus->method('dispatch')
->willThrowException(new RuntimeException('Event bus failure'));
$handler = $this->buildHandler(databaseSwitcher: $switcher, eventBus: $eventBus);
try {
$handler($this->command());
} catch (RuntimeException) {
// Expected
}
self::assertTrue($switcher->restoredToDefault);
}
private function command(): ProvisionEstablishmentCommand
{
return new ProvisionEstablishmentCommand(
establishmentId: self::ESTABLISHMENT_ID,
establishmentTenantId: self::TENANT_ID,
databaseName: 'classeo_tenant_abc123',
subdomain: 'ecole-gamma',
adminEmail: 'admin@ecole-gamma.fr',
establishmentName: 'École Gamma',
);
}
private function establishmentRepoWithProvisioningEstablishment(): InMemoryEstablishmentRepository
{
$repo = new InMemoryEstablishmentRepository();
$establishment = Establishment::reconstitute(
id: EstablishmentId::fromString(self::ESTABLISHMENT_ID),
tenantId: TenantId::fromString(self::TENANT_ID),
name: 'École Gamma',
subdomain: 'ecole-gamma',
databaseName: 'classeo_tenant_abc123',
status: EstablishmentStatus::PROVISIONING,
createdAt: new DateTimeImmutable('2026-04-07 10:00:00'),
createdBy: SuperAdminId::fromString('550e8400-e29b-41d4-a716-446655440002'),
);
$repo->save($establishment);
return $repo;
}
/**
* @param object[] $dispatched
*/
private function spyEventBus(array &$dispatched): MessageBusInterface
{
$eventBus = $this->createMock(MessageBusInterface::class);
$eventBus->method('dispatch')
->willReturnCallback(static function (object $message) use (&$dispatched): Envelope {
$dispatched[] = $message;
return new Envelope($message);
});
return $eventBus;
}
private function buildHandler(
?TenantProvisioner $provisioner = null,
?InMemoryUserRepository $userRepository = null,
?SpyDatabaseSwitcher $databaseSwitcher = null,
?InMemoryEstablishmentRepository $establishmentRepository = null,
?MessageBusInterface $eventBus = null,
): ProvisionEstablishmentHandler {
$provisioner ??= $this->createMock(TenantProvisioner::class);
$clock = new class implements Clock {
public function now(): DateTimeImmutable
{
return new DateTimeImmutable('2026-04-07 10:00:00');
}
};
$userRepository ??= new InMemoryUserRepository();
$databaseSwitcher ??= new SpyDatabaseSwitcher();
$establishmentRepository ??= $this->establishmentRepoWithProvisioningEstablishment();
$eventBus ??= $this->createMock(MessageBusInterface::class);
$eventBus->method('dispatch')
->willReturnCallback(static fn (object $m): Envelope => new Envelope($m));
return new ProvisionEstablishmentHandler(
tenantProvisioner: $provisioner,
inviteUserHandler: new InviteUserHandler($userRepository, $clock),
userRepository: $userRepository,
clock: $clock,
databaseSwitcher: $databaseSwitcher,
establishmentRepository: $establishmentRepository,
eventBus: $eventBus,
logger: new NullLogger(),
masterDatabaseUrl: self::MASTER_URL,
);
}
}

View File

@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\SuperAdmin\Infrastructure\Provisioning;
use ApiPlatform\Metadata\Post;
use App\Administration\Application\Command\InviteUser\InviteUserHandler;
use App\Administration\Domain\Event\UtilisateurInvite;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use App\SuperAdmin\Application\Command\CreateEstablishment\CreateEstablishmentHandler;
use App\SuperAdmin\Application\Command\ProvisionEstablishment\ProvisionEstablishmentCommand;
use App\SuperAdmin\Application\Port\TenantProvisioner;
use App\SuperAdmin\Domain\Model\Establishment\EstablishmentStatus;
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
use App\SuperAdmin\Infrastructure\Api\Processor\CreateEstablishmentProcessor;
use App\SuperAdmin\Infrastructure\Api\Resource\EstablishmentResource;
use App\SuperAdmin\Infrastructure\Persistence\InMemory\InMemoryEstablishmentRepository;
use App\SuperAdmin\Infrastructure\Provisioning\ProvisionEstablishmentHandler;
use App\SuperAdmin\Infrastructure\Security\SecuritySuperAdmin;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* Integration tests: verify the full provisioning flow from API request
* through establishment creation to async provisioning and admin user creation.
*
* Split into focused tests that each verify one aspect of the flow.
*/
final class ProvisioningIntegrationTest extends TestCase
{
private const string SUPER_ADMIN_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string MASTER_URL = 'postgresql://classeo:secret@db:5432/classeo_master';
private InMemoryEstablishmentRepository $establishmentRepository;
private InMemoryUserRepository $userRepository;
private ?ProvisionEstablishmentCommand $provisionCommand;
/** @var object[] */
private array $dispatchedEvents;
private function runFullFlow(): void
{
$clock = new class implements Clock {
public function now(): DateTimeImmutable
{
return new DateTimeImmutable('2026-04-07 10:00:00');
}
};
// Phase 1: API processor creates establishment
$this->establishmentRepository = new InMemoryEstablishmentRepository();
$createHandler = new CreateEstablishmentHandler($this->establishmentRepository, $clock);
$security = $this->createMock(Security::class);
$security->method('getUser')->willReturn(new SecuritySuperAdmin(
SuperAdminId::fromString(self::SUPER_ADMIN_ID),
'superadmin@classeo.fr',
'hashed',
));
$this->provisionCommand = null;
$commandBus = $this->createMock(MessageBusInterface::class);
$commandBus->method('dispatch')
->willReturnCallback(function (object $message): Envelope {
if ($message instanceof ProvisionEstablishmentCommand) {
$this->provisionCommand = $message;
}
return new Envelope($message);
});
$processor = new CreateEstablishmentProcessor($createHandler, $security, $commandBus);
$input = new EstablishmentResource();
$input->name = 'École Test';
$input->subdomain = 'ecole-test';
$input->adminEmail = 'admin@ecole-test.fr';
$processor->process($input, new Post());
// Phase 2: Provisioning handler processes the command
self::assertNotNull($this->provisionCommand);
$this->userRepository = new InMemoryUserRepository();
$this->dispatchedEvents = [];
$eventBus = $this->createMock(MessageBusInterface::class);
$eventBus->method('dispatch')
->willReturnCallback(function (object $message): Envelope {
$this->dispatchedEvents[] = $message;
return new Envelope($message);
});
$provisioner = $this->createMock(TenantProvisioner::class);
$switcher = new SpyDatabaseSwitcher();
$provisionHandler = new ProvisionEstablishmentHandler(
tenantProvisioner: $provisioner,
inviteUserHandler: new InviteUserHandler($this->userRepository, $clock),
userRepository: $this->userRepository,
clock: $clock,
databaseSwitcher: $switcher,
establishmentRepository: $this->establishmentRepository,
eventBus: $eventBus,
logger: new NullLogger(),
masterDatabaseUrl: self::MASTER_URL,
);
$provisionHandler($this->provisionCommand);
}
#[Test]
public function processorCreatesEstablishmentInProvisioningStatus(): void
{
$this->runFullFlow();
$establishments = $this->establishmentRepository->findAll();
self::assertCount(1, $establishments);
self::assertSame('École Test', $establishments[0]->name);
}
#[Test]
public function processorDispatchesProvisioningCommandWithAdminEmail(): void
{
$this->runFullFlow();
self::assertNotNull($this->provisionCommand);
self::assertSame('admin@ecole-test.fr', $this->provisionCommand->adminEmail);
self::assertSame('ecole-test', $this->provisionCommand->subdomain);
}
#[Test]
public function provisioningCreatesAdminUserWithCorrectRole(): void
{
$this->runFullFlow();
$users = $this->userRepository->findAllByTenant(
TenantId::fromString($this->provisionCommand->establishmentTenantId),
);
self::assertCount(1, $users);
self::assertSame('admin@ecole-test.fr', (string) $users[0]->email);
self::assertSame(Role::ADMIN, $users[0]->role);
}
#[Test]
public function provisioningActivatesEstablishmentAndDispatchesEvent(): void
{
$this->runFullFlow();
$establishments = $this->establishmentRepository->findAll();
self::assertSame(EstablishmentStatus::ACTIF, $establishments[0]->status);
self::assertCount(1, $this->dispatchedEvents);
self::assertInstanceOf(UtilisateurInvite::class, $this->dispatchedEvents[0]);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\SuperAdmin\Infrastructure\Provisioning;
use App\Shared\Infrastructure\Tenant\TenantDatabaseSwitcher;
/**
* Test double that records database switching operations.
*/
final class SpyDatabaseSwitcher implements TenantDatabaseSwitcher
{
/** @var string[] */
public array $switchedTo = [];
public bool $restoredToDefault = false;
public function useTenantDatabase(string $databaseUrl): void
{
$this->switchedTo[] = $databaseUrl;
}
public function useDefaultDatabase(): void
{
$this->restoredToDefault = true;
}
public function currentDatabaseUrl(): ?string
{
return null;
}
}