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,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Infrastructure\Storage;
|
||||
|
||||
use App\Scolarite\Infrastructure\Storage\S3FileStorage;
|
||||
|
||||
use function fopen;
|
||||
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\UnableToDeleteFile;
|
||||
use League\Flysystem\UnableToReadFile;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ReflectionClass;
|
||||
use RuntimeException;
|
||||
|
||||
final class S3FileStorageTest extends TestCase
|
||||
{
|
||||
private Filesystem $filesystem;
|
||||
private LoggerInterface $logger;
|
||||
private S3FileStorage $storage;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->filesystem = $this->createMock(Filesystem::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
$this->storage = $this->createStorageWithMockedFilesystem($this->filesystem, $this->logger);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadWritesStringContentToFilesystem(): void
|
||||
{
|
||||
$this->filesystem->expects(self::once())
|
||||
->method('write')
|
||||
->with('homework/abc/file.pdf', 'fake content', ['ContentType' => 'application/pdf']);
|
||||
|
||||
$result = $this->storage->upload('homework/abc/file.pdf', 'fake content', 'application/pdf');
|
||||
|
||||
self::assertSame('homework/abc/file.pdf', $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uploadWritesStreamContentToFilesystem(): void
|
||||
{
|
||||
/** @var resource $stream */
|
||||
$stream = fopen('php://memory', 'r+');
|
||||
|
||||
$this->filesystem->expects(self::once())
|
||||
->method('writeStream')
|
||||
->with('homework/abc/file.pdf', $stream, ['ContentType' => 'application/pdf']);
|
||||
|
||||
$result = $this->storage->upload('homework/abc/file.pdf', $stream, 'application/pdf');
|
||||
|
||||
self::assertSame('homework/abc/file.pdf', $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function deleteRemovesFileFromFilesystem(): void
|
||||
{
|
||||
$this->filesystem->expects(self::once())
|
||||
->method('delete')
|
||||
->with('homework/abc/file.pdf');
|
||||
|
||||
$this->logger->expects(self::never())
|
||||
->method('warning');
|
||||
|
||||
$this->storage->delete('homework/abc/file.pdf');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function deleteLogsWarningOnFailure(): void
|
||||
{
|
||||
$this->filesystem->expects(self::once())
|
||||
->method('delete')
|
||||
->willThrowException(UnableToDeleteFile::atLocation('homework/abc/file.pdf'));
|
||||
|
||||
$this->logger->expects(self::once())
|
||||
->method('warning')
|
||||
->with(
|
||||
'S3 delete failed, possible orphan blob: {path}',
|
||||
self::callback(static fn (array $context): bool => $context['path'] === 'homework/abc/file.pdf'),
|
||||
);
|
||||
|
||||
$this->storage->delete('homework/abc/file.pdf');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function readStreamReturnsResourceFromFilesystem(): void
|
||||
{
|
||||
/** @var resource $expectedStream */
|
||||
$expectedStream = fopen('php://memory', 'r+');
|
||||
|
||||
$this->filesystem->expects(self::once())
|
||||
->method('readStream')
|
||||
->with('homework/abc/file.pdf')
|
||||
->willReturn($expectedStream);
|
||||
|
||||
$result = $this->storage->readStream('homework/abc/file.pdf');
|
||||
|
||||
self::assertSame($expectedStream, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function readStreamThrowsRuntimeExceptionOnMissingFile(): void
|
||||
{
|
||||
$this->filesystem->expects(self::once())
|
||||
->method('readStream')
|
||||
->with('homework/abc/missing.pdf')
|
||||
->willThrowException(UnableToReadFile::fromLocation('homework/abc/missing.pdf'));
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Impossible de lire le fichier : homework/abc/missing.pdf');
|
||||
|
||||
$this->storage->readStream('homework/abc/missing.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an S3FileStorage instance with a mocked Filesystem injected via reflection.
|
||||
*
|
||||
* S3FileStorage is `final readonly` and its constructor creates a real S3Client,
|
||||
* so we bypass it with newInstanceWithoutConstructor() and inject mocks directly.
|
||||
* If the class gains new properties, this method must be updated.
|
||||
*/
|
||||
private function createStorageWithMockedFilesystem(Filesystem $filesystem, LoggerInterface $logger): S3FileStorage
|
||||
{
|
||||
$reflection = new ReflectionClass(S3FileStorage::class);
|
||||
$storage = $reflection->newInstanceWithoutConstructor();
|
||||
|
||||
$fsProp = $reflection->getProperty('filesystem');
|
||||
$fsProp->setValue($storage, $filesystem);
|
||||
|
||||
$loggerProp = $reflection->getProperty('logger');
|
||||
$loggerProp->setValue($storage, $logger);
|
||||
|
||||
return $storage;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user