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.
167 lines
6.3 KiB
PHP
167 lines
6.3 KiB
PHP
<?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]);
|
|
}
|
|
}
|