diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2810c1c..abc65e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Composer run action PHPC_CS - uses: MayMeowHQ/composer-run-action@v3 + uses: MayMeowHQ/composer-run-action@v8.4 with: composer_script: 'cs-check' @@ -21,7 +21,17 @@ jobs: steps: - uses: actions/checkout@v2 - name: Composer run action PHPStan - uses: MayMeowHQ/composer-run-action@v3 + uses: MayMeowHQ/composer-run-action@v8.4 with: composer_script: 'stan' memory_limit: '1024M' + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Composer run action PHPUnit + uses: MayMeowHQ/composer-run-action@v8.4 + with: + composer_script: 'test' + memory_limit: '1024M' diff --git a/composer.json b/composer.json index c28e59f..cbdb74c 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ "scripts": { "cs-check": "phpcs --colors -p src/ tests/", "cs-fix": "phpcbf --colors -p src/ tests/", - "stan": "phpstan analyse" + "stan": "phpstan analyse", + "test": "vendor/bin/phpunit" }, "config": { "allow-plugins": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e3c51d7..787866d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -17,15 +17,6 @@ - - - - - - - - - src/ diff --git a/src/Controller/Component/DownloadComponent.php b/src/Controller/Component/DownloadComponent.php index 2976ee6..1be223e 100644 --- a/src/Controller/Component/DownloadComponent.php +++ b/src/Controller/Component/DownloadComponent.php @@ -4,6 +4,7 @@ namespace FileUpload\Controller\Component; use Cake\Controller\Component; +use Cake\Http\Exception\HttpException; use FileUpload\File\StoredFileInterface; use FileUpload\Storage\StorageManagerInterface; @@ -17,26 +18,46 @@ class DownloadComponent extends Component * * @var array */ - protected array $_defaultConfig = [ - ]; + protected array $_defaultConfig = []; + + private ?StorageManagerInterface $storageManager = null; /** * Returns stored file info and content * * @param string $fileName Name of file without path or slashes * @return \FileUpload\File\StoredFileInterface Stored file object + * @throws \Cake\Http\Exception\HttpException When storage manager is misconfigured */ public function getFile(string $fileName): StoredFileInterface { - $sm = $this->_getStorageManager(); - - return $sm->pull($fileName); + return $this->_getStorageManager()->pull($fileName); } protected function _getStorageManager(): StorageManagerInterface { + if ($this->storageManager instanceof StorageManagerInterface) { + return $this->storageManager; + } + $sm = $this->getConfig('managerClass'); - return new $sm($this->getConfig()); + if (!is_string($sm) || $sm === '') { + throw new HttpException('Storage manager class is not configured.'); + } + + if (!class_exists($sm)) { + throw new HttpException(sprintf('Storage manager class "%s" does not exist.', $sm)); + } + + $storageManager = new $sm($this->getConfig()); + + if (!$storageManager instanceof StorageManagerInterface) { + throw new HttpException(sprintf('Storage manager must implement %s.', StorageManagerInterface::class)); + } + + $this->storageManager = $storageManager; + + return $this->storageManager; } } diff --git a/src/Controller/Component/UploadComponent.php b/src/Controller/Component/UploadComponent.php index 5966c50..c42e913 100644 --- a/src/Controller/Component/UploadComponent.php +++ b/src/Controller/Component/UploadComponent.php @@ -8,6 +8,7 @@ use Cake\Http\Exception\HttpException; use FileUpload\File\UploadedFileDecorator; use FileUpload\Storage\StorageManagerInterface; +use Psr\Http\Message\UploadedFileInterface; /** * Upload component @@ -25,31 +26,53 @@ class UploadComponent extends Component ]; /** - * Allowed stoage types - * - * @var string[] + * @var \FileUpload\Storage\StorageManagerInterface|null */ - protected array $_allowedStorageTypes = [ - 'local', 's3', - ]; + private ?StorageManagerInterface $storageManager = null; /** * Uploading File to storage and returns info of that file * - * @param \Cake\Http\ServerRequest $serverRequest Server Request - * @throws HttpException + * @param \Cake\Controller\Controller $controller Controller instance + * @throws \Cake\Http\Exception\HttpException When upload data is missing or misconfigured */ public function getFile(Controller $controller): UploadedFileDecorator { + $uploadedFile = $controller->getRequest()->getData($this->getConfig('fieldName')); + + if (!$uploadedFile instanceof UploadedFileInterface) { + throw new HttpException('Uploaded file data is missing or invalid.'); + } + $sm = $this->_getStorageManager(); - return $sm->put($controller->getRequest()->getData($this->getConfig('fieldName'))); + return $sm->put($uploadedFile); } protected function _getStorageManager(): StorageManagerInterface { + if ($this->storageManager instanceof StorageManagerInterface) { + return $this->storageManager; + } + $sm = $this->getConfig('managerClass'); - return new $sm($this->getConfig()); + if (!is_string($sm) || $sm === '') { + throw new HttpException('Storage manager class is not configured.'); + } + + if (!class_exists($sm)) { + throw new HttpException(sprintf('Storage manager class "%s" does not exist.', $sm)); + } + + $storageManager = new $sm($this->getConfig()); + + if (!$storageManager instanceof StorageManagerInterface) { + throw new HttpException(sprintf('Storage manager must implement %s.', StorageManagerInterface::class)); + } + + $this->storageManager = $storageManager; + + return $this->storageManager; } } diff --git a/src/File/PathUtils.php b/src/File/PathUtils.php index 8c29267..89552bb 100644 --- a/src/File/PathUtils.php +++ b/src/File/PathUtils.php @@ -14,6 +14,10 @@ public static function fileNameSanitize(string $filename): string $filename = pathinfo($filename, PATHINFO_FILENAME); $filename = Text::slug($filename); + if ($ext === '') { + return $filename; + } + return $filename . '.' . $ext; } } \ No newline at end of file diff --git a/src/File/StoredFile.php b/src/File/StoredFile.php index d675c46..df571e5 100644 --- a/src/File/StoredFile.php +++ b/src/File/StoredFile.php @@ -3,28 +3,57 @@ namespace FileUpload\File; +use FileUpload\Exceptions\FileContentException; + class StoredFile implements StoredFileInterface { - protected string $mimeType; + private ?string $content = null; + + private ?string $mimeType = null; + public function __construct( - protected string $file, + private string $file, ) { } - public function getContent(): string + public function getContent(): string { - $stream = fopen($this->file, 'rb'); - $content = stream_get_contents($stream); + if ($this->content !== null) { + return $this->content; + } - $finfo = finfo_open(); - $this->mimeType = finfo_buffer($finfo, $content, FILEINFO_MIME_TYPE); - finfo_close($finfo); + $content = @file_get_contents($this->file); - return $content; + if ($content === false) { + throw new FileContentException(sprintf('Cannot load content of file "%s"', $this->file)); + } + + $this->content = $content; + $this->mimeType = $this->detectMimeType($content); + + return $this->content; } public function getMimeType(): string { + if ($this->mimeType === null) { + $this->mimeType = $this->detectMimeType($this->getContent()); + } + return $this->mimeType; } + + private function detectMimeType(string $content): string + { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + + if ($finfo === false) { + return 'application/octet-stream'; + } + + $mimeType = finfo_buffer($finfo, $content) ?: 'application/octet-stream'; + finfo_close($finfo); + + return $mimeType; + } } \ No newline at end of file diff --git a/src/File/UploadedFileDecorator.php b/src/File/UploadedFileDecorator.php index 6dc73bf..653984a 100644 --- a/src/File/UploadedFileDecorator.php +++ b/src/File/UploadedFileDecorator.php @@ -3,18 +3,18 @@ namespace FileUpload\File; -use Laminas\Diactoros\UploadedFile; +use Psr\Http\Message\UploadedFileInterface; class UploadedFileDecorator { public function __construct( - protected UploadedFile $originalData, + protected UploadedFileInterface $originalData, protected string $storageType, protected array $options = [] ) { } - public function getOriginalData(): UploadedFile + public function getOriginalData(): UploadedFileInterface { return $this->originalData; } @@ -24,15 +24,17 @@ public function getStorageType(): string return $this->storageType; } - public function get(string $key): string + public function get(string $key, mixed $default = null): mixed { - return $this->options[$key] ?? ""; + return $this->options[$key] ?? $default; } public function getFileName(): string { - if (isset($this->options['fileName'])) { - return $this->options['fileName']; + $fileName = $this->get('fileName'); + + if (is_string($fileName) && $fileName !== '') { + return $fileName; } return $this->originalData->getClientFilename(); diff --git a/src/Storage/BunnyStorageManager.php b/src/Storage/BunnyStorageManager.php index 87f7be9..9fa8bc3 100644 --- a/src/Storage/BunnyStorageManager.php +++ b/src/Storage/BunnyStorageManager.php @@ -8,6 +8,7 @@ use FileUpload\File\StoredFileInterface; use FileUpload\File\UploadedFileDecorator; use Psr\Http\Message\UploadedFileInterface; +use RuntimeException; final class BunnyStorageManager extends StorageManager { @@ -21,8 +22,8 @@ final class BunnyStorageManager extends StorageManager */ public function put(UploadedFileInterface $fileObject): UploadedFileDecorator { - $accessKey = $this->getConfig('accessKey'); - $hostname = (!empty($this->getConfig('region'))) ? $this->getConfig('region') . '.' . $this->getConfig('baseHostName') : $this->getConfig('baseHostName'); + $accessKey = (string)$this->getConfig('accessKey'); + $hostname = $this->composeHostname((string)$this->getConfig('baseHostName'), (string)$this->getConfig('region')); $fileName = PathUtils::fileNameSanitize($fileObject->getClientFilename()); @@ -30,34 +31,57 @@ public function put(UploadedFileInterface $fileObject): UploadedFileDecorator $ch = curl_init(); + if ($ch === false) { + throw new RuntimeException('Failed to initialise BunnyCDN upload request.'); + } + + $streamUri = $fileObject->getStream()->getMetadata('uri'); + + if (!is_string($streamUri) || $streamUri === '') { + curl_close($ch); + throw new RuntimeException('Unable to resolve uploaded file stream Uri.'); + } + + $streamResource = fopen($streamUri, 'rb'); + + if ($streamResource === false) { + curl_close($ch); + throw new RuntimeException('Unable to open uploaded file stream.'); + } + $options = [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_PUT => true, - CURLOPT_INFILE => fopen($fileObject->getStream()->getMetadata('uri'), 'r'), + CURLOPT_INFILE => $streamResource, CURLOPT_INFILESIZE => $fileObject->getSize(), - CURLOPT_HTTPHEADER => array( + CURLOPT_HTTPHEADER => [ "AccessKey: {$accessKey}", - 'Content-Type: application/octet-stream' - ) + 'Content-Type: application/octet-stream', + ], ]; curl_setopt_array($ch, $options); $response = curl_exec($ch); + $curlError = $response === false ? curl_error($ch) : null; + curl_close($ch); + fclose($streamResource); + + if ($response === false) { + throw new RuntimeException(sprintf('Failed to upload file to BunnyCDN: %s', $curlError ?? 'unknown error')); + } - $response = json_decode($response, true); + $decodedResponse = json_decode($response, true); - if (isset($response['HttpCode']) && $response['HttpCode'] !== 201) { - throw new \RuntimeException('Failed to upload file to BunnyCDN'); + if (!is_array($decodedResponse) || (isset($decodedResponse['HttpCode']) && (int)$decodedResponse['HttpCode'] !== 201)) { + throw new RuntimeException('Failed to upload file to BunnyCDN.'); } - $file = new UploadedFileDecorator($fileObject, self::STORAGE_TYPE, [ + return new UploadedFileDecorator($fileObject, self::STORAGE_TYPE, [ 'storagePath' => $this->composeUrl($this->getConfig('cdnDomain'), $this->getConfig('storageZonePath')), 'fileName' => $fileName, ]); - - return $file; } /** @@ -75,9 +99,36 @@ public function pull(string $fileName): StoredFileInterface public function composeUrl(string ...$url): string { - $url = array_filter($url, fn ($part) => !empty($part) && $part !== ''); + $segments = array_filter($url, fn ($part) => is_string($part) && $part !== ''); + + if ($segments === []) { + return ''; + } + + $segments = array_map(static fn (string $part): string => trim($part, '/'), $segments); + + $base = array_shift($segments); + $prefix = preg_match('#^https?://#i', $base) === 1 ? '' : 'https://'; + + $path = $segments ? '/' . implode('/', $segments) : ''; + + return $prefix . $base . $path; + } + + private function composeHostname(string $baseHostName, string $region): string + { + $trimmedBase = trim($baseHostName); + + if ($trimmedBase === '') { + throw new RuntimeException('BunnyCDN base host name is not configured.'); + } + + $trimmedRegion = trim($region); + + if ($trimmedRegion === '') { + return $trimmedBase; + } - $https = 'https://'; - return $https . implode('/', $url); + return $trimmedRegion . '.' . $trimmedBase; } } \ No newline at end of file diff --git a/src/Storage/LocalStorageManager.php b/src/Storage/LocalStorageManager.php index 0d1fb2d..39f0ca6 100644 --- a/src/Storage/LocalStorageManager.php +++ b/src/Storage/LocalStorageManager.php @@ -20,11 +20,18 @@ class LocalStorageManager extends StorageManager */ public function put(UploadedFileInterface $fileObject): UploadedFileDecorator { + $storagePath = (string)$this->getConfig('storagePath'); + $normalizedPath = rtrim($storagePath, DIRECTORY_SEPARATOR . '/'); + + if ($normalizedPath !== '') { + $normalizedPath .= DIRECTORY_SEPARATOR; + } + $fileName = PathUtils::fileNameSanitize($fileObject->getClientFilename()); - $fileObject->moveTo($this->getConfig('storagePath') . $fileName); + $fileObject->moveTo($normalizedPath . $fileName); $uploadedFile = new UploadedFileDecorator($fileObject, self::STORAGE_TYPE, options: [ - 'storagePath' => $this->getConfig('storagePath'), + 'storagePath' => $storagePath, 'fileName' => $fileName, ]); @@ -40,7 +47,14 @@ public function put(UploadedFileInterface $fileObject): UploadedFileDecorator */ public function pull(string $fileName): StoredFileInterface { - $file = new StoredFile(file: $this->getConfig('storagePath') . $fileName); + $storagePath = (string)$this->getConfig('storagePath'); + $normalizedPath = rtrim($storagePath, DIRECTORY_SEPARATOR . '/'); + + if ($normalizedPath !== '') { + $normalizedPath .= DIRECTORY_SEPARATOR; + } + + $file = new StoredFile(file: $normalizedPath . $fileName); return $file; } diff --git a/src/Storage/StorageManager.php b/src/Storage/StorageManager.php index ae47d98..bdd434a 100644 --- a/src/Storage/StorageManager.php +++ b/src/Storage/StorageManager.php @@ -12,7 +12,7 @@ public function __construct(array $configurations = []) $this->configurations = $configurations; } - public function getConfig(string $key, $default = ""): string + public function getConfig(?string $key = null, mixed $default = null): mixed { if ($key === null) { return $this->configurations; diff --git a/src/Storage/StorageManagerInterface.php b/src/Storage/StorageManagerInterface.php index c44de26..9e9c94a 100644 --- a/src/Storage/StorageManagerInterface.php +++ b/src/Storage/StorageManagerInterface.php @@ -12,8 +12,7 @@ interface StorageManagerInterface public function __construct(array $configurations = []); /** - * @param \Psr\Http\Message\UploadedFileInterface $fileObject UploadedFile Object - * @return \FileUpload\File\StoredFileInterface + * @param \Psr\Http\Message\UploadedFileInterface $fileObject Uploaded file object */ public function put(UploadedFileInterface $fileObject): UploadedFileDecorator; diff --git a/tests/TestCase/Controller/Component/DownloadComponentTest.php b/tests/TestCase/Controller/Component/DownloadComponentTest.php index 18998cd..4f5ce39 100644 --- a/tests/TestCase/Controller/Component/DownloadComponentTest.php +++ b/tests/TestCase/Controller/Component/DownloadComponentTest.php @@ -1,45 +1,82 @@ Download = new DownloadComponent($registry); + $this->Download = new DownloadComponent($registry, [ + 'managerClass' => TestStorageManager::class, + ]); } - /** - * tearDown method - * - * @return void - */ public function tearDown(): void { unset($this->Download); parent::tearDown(); } + + public function testGetFileReturnsStoredFile(): void + { + TestStorageManager::reset(); + + $storedFile = $this->Download->getFile('image.png'); + + $this->assertSame('content-for-image.png', $storedFile->getContent()); + $this->assertSame('text/plain', $storedFile->getMimeType()); + + $manager = TestStorageManager::$lastInstance; + $this->assertNotNull($manager); + $this->assertSame('image.png', $manager->lastPulledFileName); + } + + public function testGetFileThrowsWhenManagerClassMissing(): void + { + $component = new DownloadComponent(new ComponentRegistry()); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Storage manager class is not configured.'); + + $component->getFile('file.txt'); + } + + public function testGetFileThrowsWhenManagerClassDoesNotExist(): void + { + $component = new DownloadComponent(new ComponentRegistry(), [ + 'managerClass' => '\\NonExistent\\StorageManager', + ]); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Storage manager class "\\NonExistent\\StorageManager" does not exist.'); + + $component->getFile('file.txt'); + } + + public function testGetFileThrowsWhenManagerDoesNotImplementInterface(): void + { + $component = new DownloadComponent(new ComponentRegistry(), [ + 'managerClass' => TestNotAStorageManager::class, + ]); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage(sprintf('Storage manager must implement %s.', StorageManagerInterface::class)); + + $component->getFile('file.txt'); + } } diff --git a/tests/TestCase/Controller/Component/TestStorageManager.php b/tests/TestCase/Controller/Component/TestStorageManager.php new file mode 100644 index 0000000..e11fbf1 --- /dev/null +++ b/tests/TestCase/Controller/Component/TestStorageManager.php @@ -0,0 +1,71 @@ +lastPutFile = $fileObject; + + return new UploadedFileDecorator($fileObject, 'stub', [ + 'fileName' => 'stub-name.txt', + ]); + } + + public function pull(string $fileName): StoredFileInterface + { + $this->lastPulledFileName = $fileName; + + return new TestStoredFile('content-for-' . $fileName, 'text/plain'); + } +} + +final class TestStoredFile implements StoredFileInterface +{ + public function __construct( + private string $content, + private string $mimeType + ) { + } + + public function getContent(): string + { + return $this->content; + } + + public function getMimeType(): string + { + return $this->mimeType; + } +} + +final class TestNotAStorageManager +{ + public function __construct(array $configurations = []) + { + } +} diff --git a/tests/TestCase/Controller/Component/UploadComponentTest.php b/tests/TestCase/Controller/Component/UploadComponentTest.php index aa05a87..a576758 100644 --- a/tests/TestCase/Controller/Component/UploadComponentTest.php +++ b/tests/TestCase/Controller/Component/UploadComponentTest.php @@ -4,8 +4,16 @@ namespace FileUpload\Test\TestCase\Controller\Component; use Cake\Controller\ComponentRegistry; +use Cake\Controller\Controller; +use Cake\Http\Exception\HttpException; +use Cake\Http\ServerRequest; use Cake\TestSuite\TestCase; use FileUpload\Controller\Component\UploadComponent; +use FileUpload\File\UploadedFileDecorator; +use FileUpload\Storage\StorageManagerInterface; +use Laminas\Diactoros\UploadedFile; +use Psr\Http\Message\UploadedFileInterface; +use RuntimeException; /** * FileUpload\Controller\Component\UploadComponent Test Case @@ -17,7 +25,7 @@ class UploadComponentTest extends TestCase * * @var \FileUpload\Controller\Component\UploadComponent */ - protected $Upload; + protected UploadComponent $Upload; /** * setUp method @@ -28,7 +36,10 @@ public function setUp(): void { parent::setUp(); $registry = new ComponentRegistry(); - $this->Upload = new UploadComponent($registry); + $this->Upload = new UploadComponent($registry, [ + 'managerClass' => TestStorageManager::class, + 'fieldName' => 'upload', + ]); } /** @@ -42,4 +53,95 @@ public function tearDown(): void parent::tearDown(); } + + public function testGetFileReturnsDecorator(): void + { + TestStorageManager::reset(); + $controller = $this->createControllerWithUpload('example.txt', 'plain/text'); + + $result = $this->Upload->getFile($controller); + + $this->assertInstanceOf(UploadedFileDecorator::class, $result); + $this->assertSame('stub', $result->getStorageType()); + $this->assertSame('stub-name.txt', $result->getFileName()); + + $manager = TestStorageManager::$lastInstance; + $this->assertNotNull($manager); + $this->assertInstanceOf(UploadedFileInterface::class, $manager->lastPutFile); + $this->assertSame('example.txt', $manager->lastPutFile->getClientFilename()); + } + + public function testGetFileThrowsWhenUploadMissing(): void + { + $request = (new ServerRequest())->withData('upload', 'invalid'); + $controller = new Controller($request); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Uploaded file data is missing or invalid.'); + + $this->Upload->getFile($controller); + } + + public function testGetFileThrowsWhenManagerClassMissing(): void + { + $component = new UploadComponent(new ComponentRegistry(), [ + 'fieldName' => 'upload', + ]); + + $controller = $this->createControllerWithUpload('file.txt', 'text/plain'); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Storage manager class is not configured.'); + + $component->getFile($controller); + } + + public function testGetFileThrowsWhenManagerClassDoesNotExist(): void + { + $component = new UploadComponent(new ComponentRegistry(), [ + 'fieldName' => 'upload', + 'managerClass' => '\\NonExistent\\StorageManager', + ]); + + $controller = $this->createControllerWithUpload('file.txt', 'text/plain'); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Storage manager class "\\NonExistent\\StorageManager" does not exist.'); + + $component->getFile($controller); + } + + public function testGetFileThrowsWhenManagerDoesNotImplementInterface(): void + { + $component = new UploadComponent(new ComponentRegistry(), [ + 'fieldName' => 'upload', + 'managerClass' => TestNotAStorageManager::class, + ]); + + $controller = $this->createControllerWithUpload('file.txt', 'text/plain'); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage(sprintf('Storage manager must implement %s.', StorageManagerInterface::class)); + + $component->getFile($controller); + } + + private function createControllerWithUpload(string $fileName, string $mimeType): Controller + { + $stream = fopen('php://temp', 'wb+'); + + if ($stream === false) { + throw new RuntimeException('Unable to initialise temporary stream.'); + } + + fwrite($stream, 'file-content'); + $size = ftell($stream); + rewind($stream); + + $uploadedFile = new UploadedFile($stream, $size ?: null, UPLOAD_ERR_OK, $fileName, $mimeType); + + $request = (new ServerRequest())->withData('upload', $uploadedFile); + + return new Controller($request); + } } diff --git a/tests/TestCase/File/PathUtilsTest.php b/tests/TestCase/File/PathUtilsTest.php new file mode 100644 index 0000000..f922e93 --- /dev/null +++ b/tests/TestCase/File/PathUtilsTest.php @@ -0,0 +1,25 @@ +assertSame('Example-File.TXT', PathUtils::fileNameSanitize('Example File.TXT')); + } + + public function testFileNameSanitizeHandlesMultipleDots(): void + { + $this->assertSame('Archive-tar.gz', PathUtils::fileNameSanitize('Archive.tar.gz')); + } + + public function testFileNameSanitizeHandlesMissingExtension(): void + { + $this->assertSame('FileName', PathUtils::fileNameSanitize('FileName')); + } +} diff --git a/tests/TestCase/File/StoredFileTest.php b/tests/TestCase/File/StoredFileTest.php new file mode 100644 index 0000000..cfcd7dc --- /dev/null +++ b/tests/TestCase/File/StoredFileTest.php @@ -0,0 +1,40 @@ +fail('Unable to create temporary file.'); + } + + file_put_contents($path, 'stored content'); + + $storedFile = new StoredFile($path); + + $firstRead = $storedFile->getContent(); + unlink($path); + $secondRead = $storedFile->getContent(); + + $this->assertSame('stored content', $firstRead); + $this->assertSame($firstRead, $secondRead); + $this->assertNotSame('', $storedFile->getMimeType()); + } + + public function testGetContentThrowsWhenFileMissing(): void + { + $storedFile = new StoredFile('missing-file-' . uniqid('', true)); + + $this->expectException(FileContentException::class); + $storedFile->getContent(); + } +} diff --git a/tests/TestCase/File/UploadedFileDecoratorTest.php b/tests/TestCase/File/UploadedFileDecoratorTest.php new file mode 100644 index 0000000..9bd63a6 --- /dev/null +++ b/tests/TestCase/File/UploadedFileDecoratorTest.php @@ -0,0 +1,45 @@ +createUploadedFile('example.txt'), 'local', [ + 'custom' => 'value', + 'fileName' => 'stored.txt', + ]); + + $this->assertSame('value', $decorator->get('custom')); + $this->assertSame('fallback', $decorator->get('missing', 'fallback')); + $this->assertSame('stored.txt', $decorator->getFileName()); + } + + public function testGetFileNameFallsBackToOriginal(): void + { + $decorator = new UploadedFileDecorator($this->createUploadedFile('original.txt'), 'local'); + + $this->assertSame('original.txt', $decorator->getFileName()); + } + + private function createUploadedFile(string $fileName): UploadedFile + { + $stream = fopen('php://temp', 'wb+'); + + if ($stream === false) { + $this->fail('Unable to allocate temporary stream.'); + } + + fwrite($stream, 'sample'); + $size = ftell($stream); + rewind($stream); + + return new UploadedFile($stream, $size ?: null, UPLOAD_ERR_OK, $fileName, 'text/plain'); + } +}