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