feat: Provisionner automatiquement un nouvel établissement
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:
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Shared\Infrastructure\Tenant;
|
||||
|
||||
use App\Shared\Infrastructure\Tenant\DoctrineTenantRegistry;
|
||||
use App\Shared\Infrastructure\Tenant\TenantId;
|
||||
use App\Shared\Infrastructure\Tenant\TenantNotFoundException;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(DoctrineTenantRegistry::class)]
|
||||
final class DoctrineTenantRegistryTest extends TestCase
|
||||
{
|
||||
private const string MASTER_URL = 'postgresql://classeo:secret@db:5432/classeo_master';
|
||||
private const string TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
||||
private const string SUBDOMAIN = 'ecole-alpha';
|
||||
private const string DB_NAME = 'classeo_tenant_a1b2c3d4e5f67890abcdef1234567890';
|
||||
|
||||
#[Test]
|
||||
public function itResolvesConfigBySubdomain(): void
|
||||
{
|
||||
$registry = $this->registryWith([
|
||||
['tenant_id' => self::TENANT_ID, 'subdomain' => self::SUBDOMAIN, 'database_name' => self::DB_NAME],
|
||||
]);
|
||||
|
||||
$config = $registry->getBySubdomain(self::SUBDOMAIN);
|
||||
|
||||
self::assertSame(self::SUBDOMAIN, $config->subdomain);
|
||||
self::assertSame(self::TENANT_ID, (string) $config->tenantId);
|
||||
self::assertSame('postgresql://classeo:secret@db:5432/' . self::DB_NAME, $config->databaseUrl);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itResolvesConfigByTenantId(): void
|
||||
{
|
||||
$registry = $this->registryWith([
|
||||
['tenant_id' => self::TENANT_ID, 'subdomain' => self::SUBDOMAIN, 'database_name' => self::DB_NAME],
|
||||
]);
|
||||
|
||||
$config = $registry->getConfig(TenantId::fromString(self::TENANT_ID));
|
||||
|
||||
self::assertSame(self::SUBDOMAIN, $config->subdomain);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsForUnknownSubdomain(): void
|
||||
{
|
||||
$registry = $this->registryWith([]);
|
||||
|
||||
$this->expectException(TenantNotFoundException::class);
|
||||
$registry->getBySubdomain('inexistant');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsForUnknownTenantId(): void
|
||||
{
|
||||
$registry = $this->registryWith([]);
|
||||
|
||||
$this->expectException(TenantNotFoundException::class);
|
||||
$registry->getConfig(TenantId::fromString(self::TENANT_ID));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itChecksExistence(): void
|
||||
{
|
||||
$registry = $this->registryWith([
|
||||
['tenant_id' => self::TENANT_ID, 'subdomain' => self::SUBDOMAIN, 'database_name' => self::DB_NAME],
|
||||
]);
|
||||
|
||||
self::assertTrue($registry->exists(self::SUBDOMAIN));
|
||||
self::assertFalse($registry->exists('inexistant'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsAllConfigs(): void
|
||||
{
|
||||
$registry = $this->registryWith([
|
||||
['tenant_id' => self::TENANT_ID, 'subdomain' => self::SUBDOMAIN, 'database_name' => self::DB_NAME],
|
||||
['tenant_id' => 'b2c3d4e5-f6a7-8901-bcde-f12345678901', 'subdomain' => 'ecole-beta', 'database_name' => 'classeo_tenant_beta'],
|
||||
]);
|
||||
|
||||
$configs = $registry->getAllConfigs();
|
||||
|
||||
self::assertCount(2, $configs);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itQueriesDatabaseOnlyOnce(): void
|
||||
{
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$connection->expects(self::once())
|
||||
->method('fetchAllAssociative')
|
||||
->willReturn([
|
||||
['tenant_id' => self::TENANT_ID, 'subdomain' => self::SUBDOMAIN, 'database_name' => self::DB_NAME],
|
||||
]);
|
||||
|
||||
$registry = new DoctrineTenantRegistry($connection, self::MASTER_URL);
|
||||
|
||||
$registry->getBySubdomain(self::SUBDOMAIN);
|
||||
$registry->getConfig(TenantId::fromString(self::TENANT_ID));
|
||||
$registry->exists(self::SUBDOMAIN);
|
||||
$registry->getAllConfigs();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array{tenant_id: string, subdomain: string, database_name: string}> $rows
|
||||
*/
|
||||
private function registryWith(array $rows): DoctrineTenantRegistry
|
||||
{
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$connection->method('fetchAllAssociative')->willReturn($rows);
|
||||
|
||||
return new DoctrineTenantRegistry($connection, self::MASTER_URL);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user