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.
142 lines
4.6 KiB
PHP
142 lines
4.6 KiB
PHP
<?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;
|
|
}
|
|
}
|