diff --git a/CHANGELOG.md b/CHANGELOG.md index 353f8e8f..b31e978d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Telegram Bot API for PHP Change Log +## 0.15 under development + +- Chg #187: Change `TransportInterface::downloadFile()` signature — remove `$stream` parameter, method now returns + `resource` (readable stream) instead of `string`. +- Chg #187: `TelegramBotApi::downloadFile()` now returns `DownloadedFile` instead of `string`. +- Chg #187: Remove `TelegramBotApi::downloadFileTo()` method. +- Chg #187: Remove `TransportInterface::downloadFileTo()` method. +- Chg #187: Move `SaveFileException` from `Phptg\BotApi\Transport` to `Phptg\BotApi` namespace. + ## 0.14.1 February 9, 2026 - New #186: Add `SetMyProfilePhoto`, `RemoveMyProfilePhoto` and `GetUserProfileAudios` methods. diff --git a/README.md b/README.md index 0c87c786..a578b80a 100644 --- a/README.md +++ b/README.md @@ -119,26 +119,23 @@ $url = $api->makeFileUrl('photos/file_2'); #### File downloading -Use `TelegramBotApi::downloadFile()` and `TelegramBotApi::downloadFileTo()` methods to download a file from the Telegram -server. For example: +Use `TelegramBotApi::downloadFile()` method to download a file from the Telegram server. The method returns +a `DownloadedFile` instance with `getStream()`, `getBody()` and `saveTo()` methods. For example: ```php /** * @var \Phptg\BotApi\TelegramBotApi $api * @var \Phptg\BotApi\Type\File $file */ - -// Get file content by `File` instance -$fileContent = $api->downloadFile($file); -// Get file content by file path -$fileContent = $api->downloadFile('photos/file_2'); +// Get file content as string +$content = $api->downloadFile($file)->getBody(); -// Download and save file by `File` instance -$fileContent = $api->downloadFileTo($file, '/local/path/to/file.jpg'); +// Get file content as stream +$stream = $api->downloadFile('photos/file_2')->getStream(); -// Download and save file by file path -$fileContent = $api->downloadFileTo('photos/file_2', '/local/path/to/file.jpg'); +// Download and save file +$api->downloadFile($file)->saveTo('/local/path/to/file.jpg'); ``` ### Guides diff --git a/composer.json b/composer.json index 2ffafe18..c82641e9 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "phpunit/phpunit": "^11.5.46", "psr/log": "^3.0.2", "yiisoft/files": "^2.1", - "yiisoft/test-support": "^3.1.0" + "yiisoft/test-support": "^3.2.0" }, "suggest": { "ext-curl": "To use `CurlTransport`.", diff --git a/infection.json.dist b/infection.json.dist index b80ad28c..48e25a2a 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -5,11 +5,12 @@ ] }, "logs": { - "text": "php:\/\/stderr", + "text": "runtime/infection/run.log", "stryker": { "report": "master" } }, + "tmpDir": "runtime/infection", "mutators": { "@default": true }, diff --git a/src/DownloadedFile.php b/src/DownloadedFile.php new file mode 100644 index 00000000..230f0d07 --- /dev/null +++ b/src/DownloadedFile.php @@ -0,0 +1,64 @@ +stream; + } + + /** + * Saves the file content to the specified path. + * + * @throws SaveFileException If an error occurred while saving the file. + */ + public function saveTo(string $path): void + { + if (file_exists($path)) { + throw new SaveFileException("File already exists: $path"); + } + + set_error_handler( + static function (int $errorNumber, string $errorString): bool { + throw new SaveFileException($errorString); + }, + ); + try { + file_put_contents($path, $this->stream); + } finally { + restore_error_handler(); + } + } + + /** + * Returns the file content as a string. + */ + public function getBody(): string + { + /** + * @var string We expect the stream to be valid, so `stream_get_contents()` returns string. + */ + return stream_get_contents($this->stream); + } +} diff --git a/src/Transport/SaveFileException.php b/src/SaveFileException.php similarity index 79% rename from src/Transport/SaveFileException.php rename to src/SaveFileException.php index a934e52b..77377945 100644 --- a/src/Transport/SaveFileException.php +++ b/src/SaveFileException.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Phptg\BotApi\Transport; +namespace Phptg\BotApi; use RuntimeException; diff --git a/src/TelegramBotApi.php b/src/TelegramBotApi.php index 2ebac77d..db0024df 100644 --- a/src/TelegramBotApi.php +++ b/src/TelegramBotApi.php @@ -177,7 +177,6 @@ use Phptg\BotApi\Transport\CurlTransport; use Phptg\BotApi\Transport\DownloadFileException; use Phptg\BotApi\Transport\NativeTransport; -use Phptg\BotApi\Transport\SaveFileException; use Phptg\BotApi\Transport\TransportInterface; use Phptg\BotApi\Type\AcceptedGiftTypes; use Phptg\BotApi\Type\BotCommand; @@ -310,38 +309,47 @@ public function makeFileUrl(string|File $file): string } /** - * Downloads a file from the Telegram servers and returns its content. + * Downloads a file from the Telegram servers and returns a {@see DownloadedFile} instance + * that provides access to the file content as a stream, a string or allows saving it to a local path. * - * @param string|File $file File path or {@see File} object. + * @param string|File $file The file path (as returned by the Telegram API) or a {@see File} object. * - * @return string The file content. + * @return DownloadedFile The {@see DownloadedFile} instance with a seekable stream ready for reading. * * @throws DownloadFileException If an error occurred while downloading the file. - * @throws LogicException If the file path is not specified in `File` object. */ - public function downloadFile(string|File $file): string + public function downloadFile(string|File $file): DownloadedFile { - return $this->transport->downloadFile( + $stream = $this->transport->downloadFile( $this->makeFileUrl($file), ); - } - /** - * Downloads a file from the Telegram servers and saves it to a file. - * - * @param string|File $file File path or {@see File} object. - * @param string $savePath The path to save the file. - * - * @throws DownloadFileException If an error occurred while downloading the file. - * @throws SaveFileException If an error occurred while saving the file. - * @throws LogicException If the file path is not specified in `File` object. - */ - public function downloadFileTo(string|File $file, string $savePath): void - { - $this->transport->downloadFileTo( - $this->makeFileUrl($file), - $savePath, - ); + $uri = stream_get_meta_data($stream)['uri'] ?? null; + if ($uri === 'php://temp' || $uri === 'php://memory') { + return new DownloadedFile($stream); + } + + /** + * @var resource $temp `php://temp` always opens successfully. + */ + $temp = fopen('php://temp', 'r+b'); + + set_error_handler( + static function (int $errorNumber, string $errorString) use ($temp): never { + fclose($temp); + throw new DownloadFileException($errorString); + }, + ); + try { + stream_copy_to_stream($stream, $temp); + } finally { + restore_error_handler(); + fclose($stream); + } + + rewind($temp); + + return new DownloadedFile($temp); } /** diff --git a/src/Transport/CurlTransport.php b/src/Transport/CurlTransport.php index f121b2d7..dc2b20ba 100644 --- a/src/Transport/CurlTransport.php +++ b/src/Transport/CurlTransport.php @@ -78,53 +78,16 @@ public function postWithFiles(string $url, array $data, array $files): ApiRespon return $this->send($options); } - public function downloadFile(string $url): string + public function downloadFile(string $url): mixed { - $options = [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FAILONERROR => true, - CURLOPT_SHARE => $this->curlShareHandle, - ]; - - try { - $curl = $this->curl->init(); - } catch (CurlException $exception) { - throw new DownloadFileException($exception->getMessage(), previous: $exception); - } - - try { - $this->curl->setopt_array($curl, $options); - - /** - * @var string $result `curl_exec` returns string because `CURLOPT_RETURNTRANSFER` is set to `true`. - */ - $result = $this->curl->exec($curl); - } catch (CurlException $exception) { - throw new DownloadFileException($exception->getMessage(), previous: $exception); - } finally { - $this->curl->close($curl); - } - - return $result; - } - - public function downloadFileTo(string $url, string $savePath): void - { - set_error_handler( - static function (int $errorNumber, string $errorString): bool { - throw new SaveFileException($errorString); - }, - ); - try { - $fileHandler = fopen($savePath, 'wb'); - } finally { - restore_error_handler(); - } + /** + * @var resource $stream `php://temp` always opens successfully. + */ + $stream = fopen('php://temp', 'r+b'); $options = [ CURLOPT_URL => $url, - CURLOPT_FILE => $fileHandler, + CURLOPT_FILE => $stream, CURLOPT_FAILONERROR => true, CURLOPT_SHARE => $this->curlShareHandle, ]; @@ -143,6 +106,10 @@ static function (int $errorNumber, string $errorString): bool { } finally { $this->curl->close($curl); } + + rewind($stream); + + return $stream; } private function send(array $options): ApiResponse diff --git a/src/Transport/NativeTransport.php b/src/Transport/NativeTransport.php index ef30443e..da1de87b 100644 --- a/src/Transport/NativeTransport.php +++ b/src/Transport/NativeTransport.php @@ -77,7 +77,7 @@ public function postWithFiles(string $url, array $data, array $files): ApiRespon ); } - public function downloadFile(string $url): string + public function downloadFile(string $url): mixed { set_error_handler( static function (int $errorNumber, string $errorString): bool { @@ -86,28 +86,14 @@ static function (int $errorNumber, string $errorString): bool { ); try { /** - * @var string We throw exception on error, so `file_get_contents()` returns string. + * @var resource $stream We throw exception on error, so `fopen()` returns resource. */ - return file_get_contents($url); + $stream = fopen($url, 'rb'); } finally { restore_error_handler(); } - } - - public function downloadFileTo(string $url, string $savePath): void - { - $content = $this->downloadFile($url); - set_error_handler( - static function (int $errorNumber, string $errorString): bool { - throw new SaveFileException($errorString); - }, - ); - try { - file_put_contents($savePath, $content); - } finally { - restore_error_handler(); - } + return $stream; } private function send(string $url, array $options): ApiResponse diff --git a/src/Transport/PsrTransport.php b/src/Transport/PsrTransport.php index 89fb8973..8a70406b 100644 --- a/src/Transport/PsrTransport.php +++ b/src/Transport/PsrTransport.php @@ -75,28 +75,45 @@ public function postWithFiles(string $url, array $data, array $files): ApiRespon return $this->send($request); } - public function downloadFile(string $url): string + public function downloadFile(string $url): mixed { - return $this->internalDownload($url)->getContents(); - } + $request = $this->requestFactory->createRequest('GET', $url); - public function downloadFileTo(string $url, string $savePath): void - { - $body = $this->internalDownload($url); + try { + $response = $this->client->sendRequest($request); + } catch (ClientExceptionInterface $exception) { + throw new DownloadFileException($exception->getMessage(), previous: $exception); + } - $content = $body->detach(); - $content ??= $body->getContents(); + $body = $response->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + + $resource = $body->detach(); + if ($resource !== null) { + return $resource; + } + + /** + * @var resource $stream `php://temp` always opens successfully. + */ + $stream = fopen('php://temp', 'r+b'); set_error_handler( static function (int $errorNumber, string $errorString): bool { - throw new SaveFileException($errorString); + throw new DownloadFileException($errorString); }, ); try { - file_put_contents($savePath, $content); + fwrite($stream, (string) $body); } finally { restore_error_handler(); } + + rewind($stream); + + return $stream; } private function send(RequestInterface $request): ApiResponse @@ -113,25 +130,4 @@ private function send(RequestInterface $request): ApiResponse $body->getContents(), ); } - - /** - * @throws DownloadFileException - */ - private function internalDownload(string $url): StreamInterface - { - $request = $this->requestFactory->createRequest('GET', $url); - - try { - $response = $this->client->sendRequest($request); - } catch (ClientExceptionInterface $exception) { - throw new DownloadFileException($exception->getMessage(), previous: $exception); - } - - $body = $response->getBody(); - if ($body->isSeekable()) { - $body->rewind(); - } - - return $body; - } } diff --git a/src/Transport/TransportInterface.php b/src/Transport/TransportInterface.php index c860ea3a..3da3d053 100644 --- a/src/Transport/TransportInterface.php +++ b/src/Transport/TransportInterface.php @@ -52,24 +52,14 @@ public function post(string $url, string $body, array $headers): ApiResponse; public function postWithFiles(string $url, array $data, array $files): ApiResponse; /** - * Downloads a file by URL. + * Downloads a file by URL and returns a readable stream with its content. The returned resource is intended for + * a single read — implementations are not required to return a seekable stream. * * @param string $url The URL of the file to download. * - * @return string The file content. + * @return resource A readable stream. * * @throws DownloadFileException If an error occurred while downloading the file. */ - public function downloadFile(string $url): string; - - /** - * Downloads a file by URL and saves it to a file. - * - * @param string $url The URL of the file to download. - * @param string $savePath The path to save the file. - * - * @throws DownloadFileException If an error occurred while downloading the file. - * @throws SaveFileException If an error occurred while saving the file. - */ - public function downloadFileTo(string $url, string $savePath): void; + public function downloadFile(string $url): mixed; } diff --git a/tests/DownloadedFile/DownloadedFileTest.php b/tests/DownloadedFile/DownloadedFileTest.php new file mode 100644 index 00000000..a0234899 --- /dev/null +++ b/tests/DownloadedFile/DownloadedFileTest.php @@ -0,0 +1,35 @@ +getStream()); + } + + public function testGetBody(): void + { + $stream = fopen('php://temp', 'r+b'); + fwrite($stream, 'hello-content'); + rewind($stream); + + $file = new DownloadedFile($stream); + + assertSame('hello-content', $file->getBody()); + } +} diff --git a/tests/DownloadedFile/SaveTo/.gitignore b/tests/DownloadedFile/SaveTo/.gitignore new file mode 100644 index 00000000..341707b0 --- /dev/null +++ b/tests/DownloadedFile/SaveTo/.gitignore @@ -0,0 +1 @@ +/test.txt diff --git a/tests/DownloadedFile/SaveTo/SaveToTest.php b/tests/DownloadedFile/SaveTo/SaveToTest.php new file mode 100644 index 00000000..613d3cd6 --- /dev/null +++ b/tests/DownloadedFile/SaveTo/SaveToTest.php @@ -0,0 +1,56 @@ +saveTo(self::FILE); + + assertFileExists(self::FILE); + assertSame('hello-content', file_get_contents(self::FILE)); + } + + public function testFileAlreadyExists(): void + { + $stream = fopen('php://temp', 'r+b'); + $file = new DownloadedFile($stream); + + touch(self::FILE); + + $this->expectException(SaveFileException::class); + $this->expectExceptionMessage('File already exists: ' . self::FILE); + $file->saveTo(self::FILE); + } + + public function testError(): void + { + $stream = fopen('php://temp', 'r+b'); + $file = new DownloadedFile($stream); + + $this->expectException(SaveFileException::class); + $file->saveTo(__DIR__ . '/non-existent-directory/file.txt'); + } +} diff --git a/tests/Support/TransportMock.php b/tests/Support/TransportMock.php index 512c76a1..5e900f85 100644 --- a/tests/Support/TransportMock.php +++ b/tests/Support/TransportMock.php @@ -10,7 +10,6 @@ final class TransportMock implements TransportInterface { private ?string $url = null; - private array $savedFiles = []; private ?string $sentBody = null; private ?array $sentHeaders = null; @@ -18,8 +17,12 @@ final class TransportMock implements TransportInterface private ?array $sentData = null; private ?array $sentFiles = null; + /** + * @param resource|null $downloadFileResource + */ public function __construct( private readonly ApiResponse $response = new ApiResponse(200, '{"ok":true,"result":true}'), + private readonly mixed $downloadFileResource = null, ) {} public static function successResult(mixed $result): self @@ -54,19 +57,17 @@ public function postWithFiles(string $url, array $data, array $files): ApiRespon return $this->response; } - public function downloadFile(string $url): string + public function downloadFile(string $url): mixed { - return $url; - } - - public function downloadFileTo(string $url, string $savePath): void - { - $this->savedFiles[] = [$url, $savePath]; - } - - public function savedFiles(): array - { - return $this->savedFiles; + if ($this->downloadFileResource !== null) { + return $this->downloadFileResource; + } + + /** @var resource $stream */ + $stream = fopen('php://temp', 'r+b'); + fwrite($stream, $url); + rewind($stream); + return $stream; } public function url(): ?string diff --git a/tests/TelegramBotApi/DownloadFileCopiesStream/DownloadFileCopiesStreamTest.php b/tests/TelegramBotApi/DownloadFileCopiesStream/DownloadFileCopiesStreamTest.php new file mode 100644 index 00000000..6b6a302a --- /dev/null +++ b/tests/TelegramBotApi/DownloadFileCopiesStream/DownloadFileCopiesStreamTest.php @@ -0,0 +1,33 @@ +downloadFile('test.jpg'); + + assertSame( + file_get_contents(self::FILE), + $result->getBody(), + ); + assertSame('php://temp', stream_get_meta_data($result->getStream())['uri']); + } +} diff --git a/tests/TelegramBotApi/DownloadFileCopiesStream/test.txt b/tests/TelegramBotApi/DownloadFileCopiesStream/test.txt new file mode 100644 index 00000000..ce013625 --- /dev/null +++ b/tests/TelegramBotApi/DownloadFileCopiesStream/test.txt @@ -0,0 +1 @@ +hello diff --git a/tests/TelegramBotApi/DownloadFileStreamCopyError/.gitignore b/tests/TelegramBotApi/DownloadFileStreamCopyError/.gitignore new file mode 100644 index 00000000..341707b0 --- /dev/null +++ b/tests/TelegramBotApi/DownloadFileStreamCopyError/.gitignore @@ -0,0 +1 @@ +/test.txt diff --git a/tests/TelegramBotApi/DownloadFileStreamCopyError/DownloadFileStreamCopyErrorTest.php b/tests/TelegramBotApi/DownloadFileStreamCopyError/DownloadFileStreamCopyErrorTest.php new file mode 100644 index 00000000..d6dc49c4 --- /dev/null +++ b/tests/TelegramBotApi/DownloadFileStreamCopyError/DownloadFileStreamCopyErrorTest.php @@ -0,0 +1,25 @@ +expectException(DownloadFileException::class); + $api->downloadFile('test.jpg'); + } +} diff --git a/tests/TelegramBotApiTest.php b/tests/TelegramBotApi/TelegramBotApiTest.php similarity index 99% rename from tests/TelegramBotApiTest.php rename to tests/TelegramBotApi/TelegramBotApiTest.php index 88f0cdaa..fd37367c 100644 --- a/tests/TelegramBotApiTest.php +++ b/tests/TelegramBotApi/TelegramBotApiTest.php @@ -2,23 +2,18 @@ declare(strict_types=1); -namespace Phptg\BotApi\Tests; +namespace Phptg\BotApi\Tests\TelegramBotApi; use HttpSoft\Message\StreamFactory; use LogicException; -use Phptg\BotApi\Type\MessageEntity; -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\TestWith; -use PHPUnit\Framework\TestCase; -use Throwable; use Phptg\BotApi\CustomMethod; -use Phptg\BotApi\Tests\Support\TestHelper; -use Phptg\BotApi\Transport\ApiResponse; use Phptg\BotApi\FailResult; use Phptg\BotApi\Method\GetMe; use Phptg\BotApi\ParseResult\TelegramParseResultException; use Phptg\BotApi\TelegramBotApi; +use Phptg\BotApi\Tests\Support\TestHelper; use Phptg\BotApi\Tests\Support\TransportMock; +use Phptg\BotApi\Transport\ApiResponse; use Phptg\BotApi\Type\AcceptedGiftTypes; use Phptg\BotApi\Type\BotCommand; use Phptg\BotApi\Type\BotDescription; @@ -43,6 +38,7 @@ use Phptg\BotApi\Type\InputStoryContentPhoto; use Phptg\BotApi\Type\MenuButtonDefault; use Phptg\BotApi\Type\Message; +use Phptg\BotApi\Type\MessageEntity; use Phptg\BotApi\Type\MessageId; use Phptg\BotApi\Type\OwnedGifts; use Phptg\BotApi\Type\Payment\StarTransactions; @@ -51,12 +47,16 @@ use Phptg\BotApi\Type\Sticker\InputSticker; use Phptg\BotApi\Type\Sticker\Sticker; use Phptg\BotApi\Type\Story; +use Phptg\BotApi\Type\Update\Update; +use Phptg\BotApi\Type\Update\WebhookInfo; use Phptg\BotApi\Type\User; use Phptg\BotApi\Type\UserChatBoosts; use Phptg\BotApi\Type\UserProfileAudios; use Phptg\BotApi\Type\UserProfilePhotos; -use Phptg\BotApi\Type\Update\Update; -use Phptg\BotApi\Type\Update\WebhookInfo; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\TestWith; +use PHPUnit\Framework\TestCase; +use Throwable; use Yiisoft\Test\Support\Log\SimpleLogger; use function count; @@ -455,22 +455,22 @@ public function testDownloadFile(): void $result = $api->downloadFile('test.jpg'); - assertSame('https://api.telegram.org/file/botxyz/test.jpg', $result); + assertSame('https://api.telegram.org/file/botxyz/test.jpg', $result->getBody()); } - public function testDownloadFileTo(): void + public function testDownloadFileReturnsSameStreamForPhpTemp(): void { - $transport = new TransportMock(); + /** @var resource $stream */ + $stream = fopen('php://temp', 'r+b'); + fwrite($stream, 'test-content'); + rewind($stream); + + $transport = new TransportMock(downloadFileResource: $stream); $api = new TelegramBotApi('xyz', transport: $transport); - $api->downloadFileTo('test.jpg', 'path/to/my_file.jpg'); + $result = $api->downloadFile('test.jpg'); - assertSame( - [ - ['https://api.telegram.org/file/botxyz/test.jpg', 'path/to/my_file.jpg'], - ], - $transport->savedFiles(), - ); + assertSame($stream, $result->getStream()); } public function testAddStickerToSet(): void diff --git a/tests/Transport/CurlTransport/CurlTransportDownloadFileTest.php b/tests/Transport/CurlTransport/CurlTransportDownloadFileTest.php index d5133ebb..2ac683d2 100644 --- a/tests/Transport/CurlTransport/CurlTransportDownloadFileTest.php +++ b/tests/Transport/CurlTransport/CurlTransportDownloadFileTest.php @@ -15,6 +15,7 @@ use function PHPUnit\Framework\assertCount; use function PHPUnit\Framework\assertInstanceOf; +use function PHPUnit\Framework\assertIsResource; use function PHPUnit\Framework\assertSame; use function PHPUnit\Framework\assertTrue; @@ -27,12 +28,12 @@ public function testBase(): void $result = $transport->downloadFile('https://example.test/hello.jpg'); - assertSame('hello-content', $result); + assertSame('hello-content', stream_get_contents($result)); $options = $curl->getOptions(); assertCount(4, $options); assertSame('https://example.test/hello.jpg', $options[CURLOPT_URL]); - assertTrue($options[CURLOPT_RETURNTRANSFER]); + assertIsResource($options[CURLOPT_FILE]); assertTrue($options[CURLOPT_FAILONERROR]); assertInstanceOf(CurlShareHandle::class, $options[CURLOPT_SHARE]); } diff --git a/tests/Transport/CurlTransport/DownloadFileTo/.gitignore b/tests/Transport/CurlTransport/DownloadFileTo/.gitignore deleted file mode 100644 index f78da738..00000000 --- a/tests/Transport/CurlTransport/DownloadFileTo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/runtime diff --git a/tests/Transport/CurlTransport/DownloadFileTo/CurlTransportDownloadFileToTest.php b/tests/Transport/CurlTransport/DownloadFileTo/CurlTransportDownloadFileToTest.php deleted file mode 100644 index e8d1322d..00000000 --- a/tests/Transport/CurlTransport/DownloadFileTo/CurlTransportDownloadFileToTest.php +++ /dev/null @@ -1,125 +0,0 @@ -downloadFileTo('https://example.test/test.txt', $filePath); - - $options = $curl->getOptions(); - assertCount(4, $options); - assertSame('https://example.test/test.txt', $options[CURLOPT_URL]); - assertIsResource($options[CURLOPT_FILE]); - assertTrue($options[CURLOPT_FAILONERROR]); - assertInstanceOf(CurlShareHandle::class, $options[CURLOPT_SHARE]); - - assertFileExists($filePath); - assertStringEqualsFile($filePath, 'hello-content'); - } - - public function testErrorOnFopen(): void - { - $filePath = self::RUNTIME_PATH . '/test.txt'; - touch($filePath); - chmod($filePath, 0444); - assertFileExists($filePath); - - $transport = new CurlTransport(curl: new CurlMock()); - - $this->expectException(SaveFileException::class); - $this->expectExceptionMessage('Failed to open stream: Permission denied'); - $transport->downloadFileTo('https://example.test/test.txt', $filePath); - } - - public function testInitException(): void - { - $initException = new CurlException('test'); - $curl = new CurlMock(initException: $initException); - $transport = new CurlTransport(curl: $curl); - - $exception = null; - try { - $transport->downloadFileTo( - 'https://example.test/hello.jpg', - self::RUNTIME_PATH . '/init-exception.txt', - ); - } catch (Throwable $exception) { - } - - assertInstanceOf(DownloadFileException::class, $exception); - assertSame('test', $exception->getMessage()); - assertSame($initException, $exception->getPrevious()); - } - - public function testExecException(): void - { - $execException = new CurlException('test'); - $curl = new CurlMock(execResult: $execException); - $transport = new CurlTransport(curl: $curl); - - $exception = null; - try { - $transport->downloadFileTo( - 'https://example.test/hello.jpg', - self::RUNTIME_PATH . '/exec-exception.txt', - ); - } catch (Throwable $exception) { - } - - assertInstanceOf(DownloadFileException::class, $exception); - assertSame('test', $exception->getMessage()); - assertSame($execException, $exception->getPrevious()); - } - - public function testCloseOnException(): void - { - $curl = new CurlMock(new RuntimeException()); - $transport = new CurlTransport(curl: $curl); - - try { - $transport->downloadFileTo( - 'https://example.test/hello.jpg', - self::RUNTIME_PATH . '/close-on-exception.txt', - ); - } catch (Throwable) { - } - - assertSame(1, $curl->getCountCallOfClose()); - } -} diff --git a/tests/Transport/NativeTransport/DownloadFileTo/.gitignore b/tests/Transport/NativeTransport/DownloadFileTo/.gitignore deleted file mode 100644 index f78da738..00000000 --- a/tests/Transport/NativeTransport/DownloadFileTo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/runtime diff --git a/tests/Transport/NativeTransport/DownloadFileTo/NativeTransportDownloadFileToTest.php b/tests/Transport/NativeTransport/DownloadFileTo/NativeTransportDownloadFileToTest.php deleted file mode 100644 index 156de340..00000000 --- a/tests/Transport/NativeTransport/DownloadFileTo/NativeTransportDownloadFileToTest.php +++ /dev/null @@ -1,69 +0,0 @@ -downloadFileTo('http://example.test/test.txt', $filePath); - $request = StreamMock::disable(); - - assertSame( - [ - 'path' => 'http://example.test/test.txt', - 'options' => [], - ], - $request, - ); - assertFileExists($filePath); - assertStringEqualsFile($filePath, 'hello-content'); - } - - public function testErrorOnSave(): void - { - $transport = new NativeTransport(); - $filePath = self::RUNTIME_PATH . '/non-exists/file.txt'; - - StreamMock::enable(responseBody: 'hello-content'); - - $exception = null; - try { - $transport->downloadFileTo('http://example.test/test.txt', $filePath); - } catch (Throwable $exception) { - } finally { - StreamMock::disable(); - } - - assertInstanceOf(RuntimeException::class, $exception); - assertStringContainsString('Failed to open stream: No such file or directory', $exception->getMessage()); - } -} diff --git a/tests/Transport/NativeTransport/NativeTransportDownloadFileTest.php b/tests/Transport/NativeTransport/NativeTransportDownloadFileTest.php index d17145a6..0d73ac30 100644 --- a/tests/Transport/NativeTransport/NativeTransportDownloadFileTest.php +++ b/tests/Transport/NativeTransport/NativeTransportDownloadFileTest.php @@ -18,7 +18,7 @@ public function testBase(): void $transport = new NativeTransport(); StreamMock::enable(responseBody: 'hello-content'); - $result = $transport->downloadFile('http://example.test/test.txt'); + $stream = $transport->downloadFile('http://example.test/test.txt'); $request = StreamMock::disable(); assertSame( @@ -28,7 +28,7 @@ public function testBase(): void ], $request, ); - assertSame('hello-content', $result); + assertSame('hello-content', stream_get_contents($stream)); } public function testError(): void @@ -36,7 +36,7 @@ public function testError(): void $transport = new NativeTransport(); $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('file_get_contents(): Unable to find the wrapper "example"'); + $this->expectExceptionMessage('fopen(): Unable to find the wrapper "example"'); $transport->downloadFile('example://example.test/test.txt'); } } diff --git a/tests/Transport/PsrTransport/DownloadFileTo/.gitignore b/tests/Transport/PsrTransport/DownloadFileTo/.gitignore deleted file mode 100644 index f78da738..00000000 --- a/tests/Transport/PsrTransport/DownloadFileTo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/runtime diff --git a/tests/Transport/PsrTransport/DownloadFileTo/PsrTransportDownloadFileToTest.php b/tests/Transport/PsrTransport/DownloadFileTo/PsrTransportDownloadFileToTest.php deleted file mode 100644 index 64f52686..00000000 --- a/tests/Transport/PsrTransport/DownloadFileTo/PsrTransportDownloadFileToTest.php +++ /dev/null @@ -1,83 +0,0 @@ -createMock(ClientInterface::class); - $client - ->expects($this->once()) - ->method('sendRequest') - ->with($httpRequest) - ->willReturn(new Response(200, body: $streamFactory->createStream('hello-content'))); - - $requestFactory = $this->createMock(RequestFactoryInterface::class); - $requestFactory - ->expects($this->once()) - ->method('createRequest') - ->with('GET', 'https://example.com/test.txt') - ->willReturn($httpRequest); - - $transport = new PsrTransport( - $client, - $requestFactory, - $streamFactory, - ); - - $filePath = self::RUNTIME_PATH . '/file.txt'; - - $transport->downloadFileTo('https://example.com/test.txt', $filePath); - - assertFileExists($filePath); - assertStringEqualsFile($filePath, 'hello-content'); - } - - public function testExceptionOnFilePutContents(): void - { - $filePath = self::RUNTIME_PATH . '/exception-file-put-contents.txt'; - touch($filePath); - chmod($filePath, 0444); - assertFileExists($filePath); - - $client = $this->createMock(ClientInterface::class); - $client->method('sendRequest')->willReturn(new Response(200)); - - $transport = new PsrTransport( - $client, - $this->createMock(RequestFactoryInterface::class), - new StreamFactory(), - ); - - $this->expectException(SaveFileException::class); - $this->expectExceptionMessage('Failed to open stream: Permission denied'); - $transport->downloadFileTo('https://example.com/test.txt', $filePath); - } -} diff --git a/tests/Transport/PsrTransport/PsrTransportDownloadFileTest.php b/tests/Transport/PsrTransport/PsrTransportDownloadFileTest.php index 0f875b2a..111f2472 100644 --- a/tests/Transport/PsrTransport/PsrTransportDownloadFileTest.php +++ b/tests/Transport/PsrTransport/PsrTransportDownloadFileTest.php @@ -8,14 +8,16 @@ use HttpSoft\Message\Response; use HttpSoft\Message\StreamFactory; use Phptg\BotApi\Tests\Support\RequestException; +use Phptg\BotApi\Transport\DownloadFileException; +use Phptg\BotApi\Transport\PsrTransport; use PHPUnit\Framework\TestCase; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\StreamInterface; use Throwable; -use Phptg\BotApi\Transport\DownloadFileException; -use Phptg\BotApi\Transport\PsrTransport; use function PHPUnit\Framework\assertInstanceOf; +use function PHPUnit\Framework\assertIsResource; use function PHPUnit\Framework\assertSame; final class PsrTransportDownloadFileTest extends TestCase @@ -45,9 +47,10 @@ public function testBase(): void $streamFactory, ); - $result = $transport->downloadFile('https://example.com/test.txt'); + $stream = $transport->downloadFile('https://example.com/test.txt'); - assertSame('hello-content', $result); + assertIsResource($stream); + assertSame('hello-content', stream_get_contents($stream)); } public function testSendRequestException(): void @@ -109,8 +112,79 @@ public function testRewind(): void $streamFactory, ); - $result = $transport->downloadFile('https://example.com/test.txt'); + $stream = $transport->downloadFile('https://example.com/test.txt'); + + assertIsResource($stream); + assertSame('hello-content', stream_get_contents($stream)); + } + + public function testFwriteErrorThrowsDownloadFileException(): void + { + $httpRequest = new Request(); + + $body = $this->createMock(StreamInterface::class); + $body->method('isSeekable')->willReturn(false); + $body->method('detach')->willReturn(null); + $body->method('__toString')->willReturnCallback(static function (): string { + trigger_error('test fwrite error', E_USER_WARNING); + return ''; + }); + + $response = new Response(body: $body); + + $client = $this->createMock(ClientInterface::class); + $client->method('sendRequest')->with($httpRequest)->willReturn($response); + + $requestFactory = $this->createMock(RequestFactoryInterface::class); + $requestFactory->method('createRequest') + ->with('GET', 'https://example.com/file') + ->willReturn($httpRequest); + + $transport = new PsrTransport( + $client, + $requestFactory, + new StreamFactory(), + ); + + $exception = null; + try { + $transport->downloadFile('https://example.com/file'); + } catch (Throwable $exception) { + } + + assertInstanceOf(DownloadFileException::class, $exception); + assertSame('test fwrite error', $exception->getMessage()); + } + + public function testWritesBodyContentWhenDetachReturnsNull(): void + { + $httpRequest = new Request(); + + $body = $this->createMock(StreamInterface::class); + $body->method('isSeekable')->willReturn(false); + $body->method('detach')->willReturn(null); + $body->method('__toString')->willReturn('file-content'); + + $response = new Response(body: $body); + + $client = $this->createMock(ClientInterface::class); + $client->method('sendRequest')->with($httpRequest)->willReturn($response); + + $requestFactory = $this->createMock(RequestFactoryInterface::class); + $requestFactory->method('createRequest') + ->with('GET', 'https://example.com/file') + ->willReturn($httpRequest); + + $transport = new PsrTransport( + $client, + $requestFactory, + new StreamFactory(), + ); + + $stream = $transport->downloadFile('https://example.com/file'); - assertSame('hello-content', $result); + assertIsResource($stream); + assertSame('file-content', stream_get_contents($stream)); + fclose($stream); } }