Files
Classeo/backend/tests/Unit/Scolarite/Infrastructure/Storage/S3FileStorageTest.php
Mathias STRASSER 713e408773
Some checks failed
CI / Naming Conventions (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Build Check (push) Has been cancelled
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.
2026-04-10 15:24:27 +02:00

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;
}
}