From 43986a0e8e7b9ed35ad3222c854606f6fdbe865c Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 31 Oct 2025 17:55:44 +0300 Subject: [PATCH 1/8] get --- src/Api.php | 33 +++++++++++++++++---- src/Transport/CurlTransport.php | 35 ++++++++++++++++++++++ src/Transport/NativeTransport.php | 44 ++++++++++++++++++++++++++++ src/Transport/PsrTransport.php | 22 ++++++++++++++ src/Transport/TransportInterface.php | 2 ++ 5 files changed, 131 insertions(+), 5 deletions(-) diff --git a/src/Api.php b/src/Api.php index 242cece9..1b53b581 100644 --- a/src/Api.php +++ b/src/Api.php @@ -10,6 +10,7 @@ use Vjik\TelegramBot\Api\ParseResult\ResultFactory; use Vjik\TelegramBot\Api\ParseResult\TelegramParseResultException; use Vjik\TelegramBot\Api\Transport\ApiResponse; +use Vjik\TelegramBot\Api\Transport\HttpMethod; use Vjik\TelegramBot\Api\Transport\TransportInterface; use Vjik\TelegramBot\Api\Type\ResponseParameters; @@ -50,11 +51,15 @@ public function call(MethodInterface $method, ?LoggerInterface $logger): mixed LogContextFactory::sendRequest($method), ); - $response = $this->transport->send( - $this->makeUrlPath($method->getApiMethod()), - $method->getData(), - $method->getHttpMethod(), - ); + if ($method->getHttpMethod() === HttpMethod::GET) { + $response = $this->sendGetRequest($method->getApiMethod(), $method->getData()); + } else { + $response = $this->transport->send( + $this->makeUrlPath($method->getApiMethod()), + $method->getData(), + $method->getHttpMethod(), + ); + } try { $decodedBody = json_decode($response->body, true, flags: JSON_THROW_ON_ERROR); @@ -106,6 +111,24 @@ public function call(MethodInterface $method, ?LoggerInterface $logger): mixed return $result; } + /** + * @param array $data + */ + private function sendGetRequest(string $apiMethod, array $data): ApiResponse + { + $queryParameters = array_map( + static fn($value) => is_scalar($value) ? $value : json_encode($value, JSON_THROW_ON_ERROR), + $data, + ); + + $url = $this->makeUrlPath($apiMethod); + if (!empty($queryParameters)) { + $url .= '?' . http_build_query($queryParameters); + } + + return $this->transport->get($url); + } + private function makeUrlPath(string $apiMethod): string { return $this->baseUrl . '/bot' . $this->token . '/' . $apiMethod; diff --git a/src/Transport/CurlTransport.php b/src/Transport/CurlTransport.php index fe3269e7..1a5bd646 100644 --- a/src/Transport/CurlTransport.php +++ b/src/Transport/CurlTransport.php @@ -28,6 +28,41 @@ public function __construct( $this->curlShareHandle = $this->createCurlShareHandle(); } + public function get(string $url): ApiResponse + { + $options = [ + CURLOPT_HTTPGET => true, + CURLOPT_URL => $url, + ]; + return $this->sendRequest($options); + } + + private function sendRequest(array $options): ApiResponse + { + $options[CURLOPT_RETURNTRANSFER] = true; + $options[CURLOPT_SHARE] = $this->curlShareHandle; + + $curl = $this->curl->init(); + + try { + $this->curl->setopt_array($curl, $options); + + /** + * @var string $body `curl_exec` returns string because `CURLOPT_RETURNTRANSFER` is set to `true`. + */ + $body = $this->curl->exec($curl); + + $statusCode = $this->curl->getinfo($curl, CURLINFO_HTTP_CODE); + if (!is_int($statusCode)) { + $statusCode = 0; + } + } finally { + $this->curl->close($curl); + } + + return new ApiResponse($statusCode, $body); + } + /** * @psalm-param array $data */ diff --git a/src/Transport/NativeTransport.php b/src/Transport/NativeTransport.php index 0cb1005a..4a4486a8 100644 --- a/src/Transport/NativeTransport.php +++ b/src/Transport/NativeTransport.php @@ -29,6 +29,50 @@ public function __construct( $this->mimeTypeResolver = $mimeTypeResolver ?? new ApacheMimeTypeResolver(); } + public function get(string $url): ApiResponse + { + return $this->sendRequest([ + $url, + ['method' => 'GET'], + ]); + } + + /** + * @psalm-param array{0: string, ...} $options + */ + private function sendRequest(array $options): ApiResponse + { + global $http_response_header; + + $options['ignore_errors'] = true; + + $context = stream_context_create(['http' => $options]); + + set_error_handler( + static function (int $errorNumber, string $errorString): bool { + throw new RuntimeException($errorString); + }, + ); + try { + /** + * @var string $body We throw an exception on error, so `file_get_contents()` returns the string. + */ + $body = file_get_contents($options[0], context: $context); + } finally { + restore_error_handler(); + } + + /** + * @psalm-var non-empty-list $http_response_header + * @see https://www.php.net/manual/reserved.variables.httpresponseheader.php + */ + + return new ApiResponse( + $this->parseStatusCode($http_response_header), + $body, + ); + } + public function send(string $urlPath, array $data = [], HttpMethod $httpMethod = HttpMethod::POST): ApiResponse { global $http_response_header; diff --git a/src/Transport/PsrTransport.php b/src/Transport/PsrTransport.php index 2b1841fc..d6a5192b 100644 --- a/src/Transport/PsrTransport.php +++ b/src/Transport/PsrTransport.php @@ -28,6 +28,28 @@ public function __construct( private StreamFactoryInterface $streamFactory, ) {} + public function get(string $url): ApiResponse + { + return $this->sendRequest( + $this->requestFactory->createRequest('GET', $url), + ); + } + + private function sendRequest(RequestInterface $request): ApiResponse + { + $response = $this->client->sendRequest($request); + + $body = $response->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + + return new ApiResponse( + $response->getStatusCode(), + $body->getContents(), + ); + } + public function send( string $urlPath, array $data = [], diff --git a/src/Transport/TransportInterface.php b/src/Transport/TransportInterface.php index 8978c592..4f3dec2e 100644 --- a/src/Transport/TransportInterface.php +++ b/src/Transport/TransportInterface.php @@ -9,6 +9,8 @@ */ interface TransportInterface { + public function get(string $url): ApiResponse; + /** * @psalm-param array $data */ From 03458631b9ca96c83ad1c5743f83df0b844ba2b7 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 1 Nov 2025 12:14:56 +0300 Subject: [PATCH 2/8] implement --- src/Api.php | 54 ++++-- src/Transport/CurlTransport.php | 136 +++++---------- src/Transport/NativeTransport.php | 163 ++++++------------ src/Transport/PsrTransport.php | 144 +++++----------- src/Transport/TransportInterface.php | 16 +- tests/Support/TransportMock.php | 32 ++-- tests/TelegramBotApiTest.php | 2 +- .../CurlTransport/CurlTransportTest.php | 113 ++++++------ .../NativeTransport/NativeTransportTest.php | 132 ++++++-------- .../PsrTransport/PsrTransportTest.php | 52 ++++-- .../StrictTypeRequestTest.php | 2 +- 11 files changed, 351 insertions(+), 495 deletions(-) diff --git a/src/Api.php b/src/Api.php index 1b53b581..a61cda6e 100644 --- a/src/Api.php +++ b/src/Api.php @@ -12,14 +12,17 @@ use Vjik\TelegramBot\Api\Transport\ApiResponse; use Vjik\TelegramBot\Api\Transport\HttpMethod; use Vjik\TelegramBot\Api\Transport\TransportInterface; +use Vjik\TelegramBot\Api\Type\InputFile; use Vjik\TelegramBot\Api\Type\ResponseParameters; use function array_key_exists; use function is_array; use function is_bool; use function is_int; +use function is_scalar; use function is_string; use function json_decode; +use function strlen; /** * @internal @@ -51,15 +54,11 @@ public function call(MethodInterface $method, ?LoggerInterface $logger): mixed LogContextFactory::sendRequest($method), ); - if ($method->getHttpMethod() === HttpMethod::GET) { - $response = $this->sendGetRequest($method->getApiMethod(), $method->getData()); - } else { - $response = $this->transport->send( - $this->makeUrlPath($method->getApiMethod()), - $method->getData(), - $method->getHttpMethod(), - ); - } + $url = $this->baseUrl . '/bot' . $this->token . '/' . $method->getApiMethod(); + $response = match ($method->getHttpMethod()) { + HttpMethod::GET => $this->sendGetRequest($url, $method->getData()), + HttpMethod::POST => $this->sendPostRequest($url, $method->getData()), + }; try { $decodedBody = json_decode($response->body, true, flags: JSON_THROW_ON_ERROR); @@ -112,16 +111,15 @@ public function call(MethodInterface $method, ?LoggerInterface $logger): mixed } /** - * @param array $data + * @psalm-param array $data */ - private function sendGetRequest(string $apiMethod, array $data): ApiResponse + private function sendGetRequest(string $url, array $data): ApiResponse { $queryParameters = array_map( static fn($value) => is_scalar($value) ? $value : json_encode($value, JSON_THROW_ON_ERROR), $data, ); - $url = $this->makeUrlPath($apiMethod); if (!empty($queryParameters)) { $url .= '?' . http_build_query($queryParameters); } @@ -129,9 +127,37 @@ private function sendGetRequest(string $apiMethod, array $data): ApiResponse return $this->transport->get($url); } - private function makeUrlPath(string $apiMethod): string + /** + * @psalm-param array $data + */ + private function sendPostRequest(string $url, array $data): ApiResponse { - return $this->baseUrl . '/bot' . $this->token . '/' . $apiMethod; + $files = []; + foreach ($data as $key => $value) { + if ($value instanceof InputFile) { + $files[$key] = $value; + unset($data[$key]); + } + } + + if (empty($files)) { + $content = json_encode($data, JSON_THROW_ON_ERROR); + return $this->transport->post( + $url, + $content, + [ + 'Content-Length' => (string) strlen($content), + 'Content-Type' => 'application/json; charset=utf-8', + ], + ); + } + + $data = array_map( + static fn(mixed $value) => is_scalar($value) ? $value : json_encode($value, JSON_THROW_ON_ERROR), + $data, + ); + + return $this->transport->postWithFiles($url, $data, $files); } /** diff --git a/src/Transport/CurlTransport.php b/src/Transport/CurlTransport.php index 1a5bd646..89f7e3b2 100644 --- a/src/Transport/CurlTransport.php +++ b/src/Transport/CurlTransport.php @@ -9,11 +9,8 @@ use Vjik\TelegramBot\Api\Curl\Curl; use Vjik\TelegramBot\Api\Curl\CurlException; use Vjik\TelegramBot\Api\Curl\CurlInterface; -use Vjik\TelegramBot\Api\Type\InputFile; use function is_int; -use function is_scalar; -use function json_encode; /** * @api @@ -34,66 +31,40 @@ public function get(string $url): ApiResponse CURLOPT_HTTPGET => true, CURLOPT_URL => $url, ]; - return $this->sendRequest($options); + return $this->send($options); } - private function sendRequest(array $options): ApiResponse + public function post(string $url, string $body, array $headers): ApiResponse { - $options[CURLOPT_RETURNTRANSFER] = true; - $options[CURLOPT_SHARE] = $this->curlShareHandle; - - $curl = $this->curl->init(); - - try { - $this->curl->setopt_array($curl, $options); - - /** - * @var string $body `curl_exec` returns string because `CURLOPT_RETURNTRANSFER` is set to `true`. - */ - $body = $this->curl->exec($curl); - - $statusCode = $this->curl->getinfo($curl, CURLINFO_HTTP_CODE); - if (!is_int($statusCode)) { - $statusCode = 0; - } - } finally { - $this->curl->close($curl); + $header = []; + foreach ($headers as $name => $value) { + $header[] = $name . ': ' . $value; } - return new ApiResponse($statusCode, $body); + $options = [ + CURLOPT_POST => true, + CURLOPT_URL => $url, + CURLOPT_POSTFIELDS => $body, + CURLOPT_HTTPHEADER => $header, + ]; + return $this->send($options); } - /** - * @psalm-param array $data - */ - public function send(string $urlPath, array $data = [], HttpMethod $httpMethod = HttpMethod::POST): ApiResponse + public function postWithFiles(string $url, array $data, array $files): ApiResponse { - $options = match ($httpMethod) { - HttpMethod::GET => $this->createGetOptions($urlPath, $data), - HttpMethod::POST => $this->createPostOptions($urlPath, $data), - }; - $options[CURLOPT_RETURNTRANSFER] = true; - $options[CURLOPT_SHARE] = $this->curlShareHandle; - - $curl = $this->curl->init(); - - try { - $this->curl->setopt_array($curl, $options); - - /** - * @var string $body `curl_exec` returns string because `CURLOPT_RETURNTRANSFER` is set to `true`. - */ - $body = $this->curl->exec($curl); - - $statusCode = $this->curl->getinfo($curl, CURLINFO_HTTP_CODE); - if (!is_int($statusCode)) { - $statusCode = 0; - } - } finally { - $this->curl->close($curl); + foreach ($files as $key => $file) { + $data[$key] = new CURLStringFile( + FileHelper::read($file), + $file->filename ?? '', + ); } - return new ApiResponse($statusCode, $body); + $options = [ + CURLOPT_POST => true, + CURLOPT_URL => $url, + CURLOPT_POSTFIELDS => $data, + ]; + return $this->send($options); } public function downloadFile(string $url): string @@ -163,55 +134,30 @@ static function (int $errorNumber, string $errorString): bool { } } - /** - * @psalm-param array $data - */ - private function createPostOptions(string $urlPath, array $data): array + private function send(array $options): ApiResponse { - $postFields = []; - foreach ($data as $key => $value) { - if (is_scalar($value)) { - $postFields[$key] = $value; - continue; - } - - if ($value instanceof InputFile) { - $postFields[$key] = new CURLStringFile( - FileHelper::read($value), - $value->filename ?? '', - ); - continue; - } + $options[CURLOPT_RETURNTRANSFER] = true; + $options[CURLOPT_SHARE] = $this->curlShareHandle; - $postFields[$key] = json_encode($value, JSON_THROW_ON_ERROR); - } + $curl = $this->curl->init(); - return [ - CURLOPT_POST => true, - CURLOPT_URL => $urlPath, - CURLOPT_POSTFIELDS => $postFields, - ]; - } + try { + $this->curl->setopt_array($curl, $options); - /** - * @psalm-param array $data - */ - private function createGetOptions(string $urlPath, array $data): array - { - $queryParameters = []; - foreach ($data as $key => $value) { - $queryParameters[$key] = is_scalar($value) ? $value : json_encode($value, JSON_THROW_ON_ERROR); - } + /** + * @var string $body `curl_exec` returns string because `CURLOPT_RETURNTRANSFER` is set to `true`. + */ + $body = $this->curl->exec($curl); - $url = $urlPath; - if (!empty($queryParameters)) { - $url .= '?' . http_build_query($queryParameters); + $statusCode = $this->curl->getinfo($curl, CURLINFO_HTTP_CODE); + if (!is_int($statusCode)) { + $statusCode = 0; + } + } finally { + $this->curl->close($curl); } - return [ - CURLOPT_HTTPGET => true, - CURLOPT_URL => $url, - ]; + return new ApiResponse($statusCode, $body); } private function createCurlShareHandle(): CurlShareHandle diff --git a/src/Transport/NativeTransport.php b/src/Transport/NativeTransport.php index 4a4486a8..3e0ff90a 100644 --- a/src/Transport/NativeTransport.php +++ b/src/Transport/NativeTransport.php @@ -9,7 +9,6 @@ use Vjik\TelegramBot\Api\Transport\MimeTypeResolver\MimeTypeResolverInterface; use Vjik\TelegramBot\Api\Type\InputFile; -use function is_scalar; use function is_string; use function json_encode; @@ -31,82 +30,42 @@ public function __construct( public function get(string $url): ApiResponse { - return $this->sendRequest([ + return $this->send( $url, ['method' => 'GET'], - ]); + ); } - /** - * @psalm-param array{0: string, ...} $options - */ - private function sendRequest(array $options): ApiResponse + public function post(string $url, string $body, array $headers): ApiResponse { - global $http_response_header; - - $options['ignore_errors'] = true; - - $context = stream_context_create(['http' => $options]); - - set_error_handler( - static function (int $errorNumber, string $errorString): bool { - throw new RuntimeException($errorString); - }, - ); - try { - /** - * @var string $body We throw an exception on error, so `file_get_contents()` returns the string. - */ - $body = file_get_contents($options[0], context: $context); - } finally { - restore_error_handler(); + $header = []; + foreach ($headers as $name => $value) { + $header[] = $name . ': ' . $value; } - /** - * @psalm-var non-empty-list $http_response_header - * @see https://www.php.net/manual/reserved.variables.httpresponseheader.php - */ - - return new ApiResponse( - $this->parseStatusCode($http_response_header), - $body, + return $this->send( + $url, + [ + 'method' => 'POST', + 'header' => $header, + 'content' => $body, + ], ); } - public function send(string $urlPath, array $data = [], HttpMethod $httpMethod = HttpMethod::POST): ApiResponse + public function postWithFiles(string $url, array $data, array $files): ApiResponse { - global $http_response_header; - - [$url, $options] = match ($httpMethod) { - HttpMethod::GET => $this->createGetRequest($urlPath, $data), - HttpMethod::POST => $this->createPostRequest($urlPath, $data), - }; - $options['ignore_errors'] = true; - - $context = stream_context_create(['http' => $options]); + $boundary = uniqid('', true); + $content = $this->buildMultipartFormData($data, $files, $boundary); + $contentType = 'multipart/form-data; boundary=' . $boundary . '; charset=utf-8'; - set_error_handler( - static function (int $errorNumber, string $errorString): bool { - throw new RuntimeException($errorString); - }, - ); - try { - /** - * @var string $body We throw exception on error, so `file_get_contents()` returns string. - */ - $body = file_get_contents($url, context: $context); - } finally { - restore_error_handler(); - } - - /** - * @psalm-var non-empty-list $http_response_header - * @see https://www.php.net/manual/reserved.variables.httpresponseheader.php - */ - - return new ApiResponse( - $this->parseStatusCode($http_response_header), - $body, + return $this->send( + $url, + [ + 'method' => 'POST', + 'header' => 'Content-type: ' . $contentType, + 'content' => $content, + ], ); } @@ -143,63 +102,37 @@ static function (int $errorNumber, string $errorString): bool { } } - /** - * @psalm-param array $data - * @psalm-return list{string, array} - */ - private function createGetRequest(string $urlPath, array $data): array + private function send(string $url, array $options): ApiResponse { - $queryParameters = array_map( - static fn($value) => is_scalar($value) ? $value : json_encode($value, JSON_THROW_ON_ERROR), - $data, - ); + global $http_response_header; - $url = $urlPath; - if (!empty($queryParameters)) { - $url .= '?' . http_build_query($queryParameters); - } + $options['ignore_errors'] = true; - return [ - $url, - ['method' => 'GET'], - ]; - } + $context = stream_context_create(['http' => $options]); - /** - * @psalm-param array $data - * @psalm-return list{string, array} - */ - private function createPostRequest(string $urlPath, array $data): array - { - $files = []; - foreach ($data as $key => $value) { - if ($value instanceof InputFile) { - $files[$key] = $value; - unset($data[$key]); - } + set_error_handler( + static function (int $errorNumber, string $errorString): bool { + throw new RuntimeException($errorString); + }, + ); + try { + /** + * @var string $body We throw an exception on error, so `file_get_contents()` returns the string. + */ + $body = file_get_contents($url, context: $context); + } finally { + restore_error_handler(); } - if (empty($files)) { - $fields = array_map( - static fn($value) => is_scalar($value) ? $value : json_encode($value, JSON_THROW_ON_ERROR), - $data, - ); - $content = http_build_query($fields); - $contentType = 'application/x-www-form-urlencoded'; - } else { - $boundary = uniqid('', true); - $content = $this->buildMultipartFormData($data, $files, $boundary); - $contentType = 'multipart/form-data; boundary=' . $boundary . '; charset=utf-8'; - } + /** + * @psalm-var non-empty-list $http_response_header + * @see https://www.php.net/manual/reserved.variables.httpresponseheader.php + */ - return [ - $urlPath, - [ - 'method' => 'POST', - 'header' => 'Content-type: ' . $contentType, - 'content' => $content, - ], - ]; + return new ApiResponse( + $this->parseStatusCode($http_response_header), + $body, + ); } /** diff --git a/src/Transport/PsrTransport.php b/src/Transport/PsrTransport.php index d6a5192b..2fd3fc0b 100644 --- a/src/Transport/PsrTransport.php +++ b/src/Transport/PsrTransport.php @@ -11,11 +11,6 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\StreamInterface; -use Vjik\TelegramBot\Api\Type\InputFile; - -use function is_scalar; -use function is_string; -use function json_encode; /** * @api @@ -30,47 +25,48 @@ public function __construct( public function get(string $url): ApiResponse { - return $this->sendRequest( + return $this->send( $this->requestFactory->createRequest('GET', $url), ); } - private function sendRequest(RequestInterface $request): ApiResponse + public function post(string $url, string $body, array $headers): ApiResponse { - $response = $this->client->sendRequest($request); + $request = $this->requestFactory->createRequest('POST', $url); - $body = $response->getBody(); - if ($body->isSeekable()) { - $body->rewind(); + $stream = $this->streamFactory->createStream($body); + $request = $request->withBody($stream); + + foreach ($headers as $name => $value) { + $request = $request->withHeader($name, $value); } - return new ApiResponse( - $response->getStatusCode(), - $body->getContents(), - ); + return $this->send($request); } - public function send( - string $urlPath, - array $data = [], - HttpMethod $httpMethod = HttpMethod::POST, - ): ApiResponse { - $response = $this->client->sendRequest( - match ($httpMethod) { - HttpMethod::GET => $this->createGetRequest($urlPath, $data), - HttpMethod::POST => $this->createPostRequest($urlPath, $data), - }, - ); - - $body = $response->getBody(); - if ($body->isSeekable()) { - $body->rewind(); + public function postWithFiles(string $url, array $data, array $files): ApiResponse + { + $streamBuilder = new MultipartStreamBuilder($this->streamFactory); + foreach ($data as $key => $value) { + $streamBuilder->addResource($key, (string) $value); + } + foreach ($files as $key => $file) { + $streamBuilder->addResource( + $key, + $file->resource, + $file->filename === null ? [] : ['filename' => $file->filename], + ); } + $body = $streamBuilder->build(); + $contentType = 'multipart/form-data; boundary=' . $streamBuilder->getBoundary() . '; charset=utf-8'; - return new ApiResponse( - $response->getStatusCode(), - $body->getContents(), - ); + $request = $this->requestFactory + ->createRequest('POST', $url) + ->withHeader('Content-Length', (string) $body->getSize()) + ->withHeader('Content-Type', $contentType) + ->withBody($body); + + return $this->send($request); } public function downloadFile(string $url): string @@ -97,6 +93,21 @@ static function (int $errorNumber, string $errorString): bool { } } + private function send(RequestInterface $request): ApiResponse + { + $response = $this->client->sendRequest($request); + + $body = $response->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + + return new ApiResponse( + $response->getStatusCode(), + $body->getContents(), + ); + } + /** * @throws DownloadFileException */ @@ -117,69 +128,4 @@ private function internalDownload(string $url): StreamInterface return $body; } - - /** - * @psalm-param array $data - */ - private function createPostRequest(string $urlPath, array $data): RequestInterface - { - $request = $this->requestFactory->createRequest('POST', $urlPath); - - $files = []; - foreach ($data as $key => $value) { - if ($value instanceof InputFile) { - $files[$key] = $value; - unset($data[$key]); - } - } - - if (empty($data) && empty($files)) { - return $request; - } - if (empty($files)) { - $content = json_encode($data, JSON_THROW_ON_ERROR); - $body = $this->streamFactory->createStream($content); - $contentType = 'application/json; charset=utf-8'; - } else { - $streamBuilder = new MultipartStreamBuilder($this->streamFactory); - foreach ($data as $key => $value) { - $streamBuilder->addResource( - $key, - is_string($value) ? $value : json_encode($value, JSON_THROW_ON_ERROR), - ); - } - foreach ($files as $key => $file) { - $streamBuilder->addResource( - $key, - $file->resource, - $file->filename === null ? [] : ['filename' => $file->filename], - ); - } - $body = $streamBuilder->build(); - $contentType = 'multipart/form-data; boundary=' . $streamBuilder->getBoundary() . '; charset=utf-8'; - } - - return $request - ->withHeader('Content-Length', (string) $body->getSize()) - ->withHeader('Content-Type', $contentType) - ->withBody($body); - } - - /** - * @psalm-param array $data - */ - private function createGetRequest(string $urlPath, array $data): RequestInterface - { - $queryParameters = []; - foreach ($data as $key => $value) { - $queryParameters[$key] = is_scalar($value) ? $value : json_encode($value, JSON_THROW_ON_ERROR); - } - - $url = $urlPath; - if (!empty($queryParameters)) { - $url .= '?' . http_build_query($queryParameters); - } - - return $this->requestFactory->createRequest('GET', $url); - } } diff --git a/src/Transport/TransportInterface.php b/src/Transport/TransportInterface.php index 4f3dec2e..43cc387c 100644 --- a/src/Transport/TransportInterface.php +++ b/src/Transport/TransportInterface.php @@ -4,6 +4,8 @@ namespace Vjik\TelegramBot\Api\Transport; +use Vjik\TelegramBot\Api\Type\InputFile; + /** * @api */ @@ -12,13 +14,15 @@ interface TransportInterface public function get(string $url): ApiResponse; /** - * @psalm-param array $data + * @psalm-param array $headers + */ + public function post(string $url, string $body, array $headers): ApiResponse; + + /** + * @psalm-param array $data + * @psalm-param array $files */ - public function send( - string $urlPath, - array $data = [], - HttpMethod $httpMethod = HttpMethod::POST, - ): ApiResponse; + public function postWithFiles(string $url, array $data, array $files): ApiResponse; /** * Downloads a file by URL. diff --git a/tests/Support/TransportMock.php b/tests/Support/TransportMock.php index 31342035..1cbbf163 100644 --- a/tests/Support/TransportMock.php +++ b/tests/Support/TransportMock.php @@ -4,16 +4,15 @@ namespace Vjik\TelegramBot\Api\Tests\Support; -use Vjik\TelegramBot\Api\Transport\HttpMethod; use Vjik\TelegramBot\Api\Transport\TransportInterface; use Vjik\TelegramBot\Api\Transport\ApiResponse; final class TransportMock implements TransportInterface { - private ?string $urlPath = null; + private ?string $url = null; /** - * @var list + * @psalm-var list */ private array $savedFiles = []; @@ -21,12 +20,21 @@ public function __construct( private readonly ?ApiResponse $response = null, ) {} - public function send( - string $urlPath, - array $data = [], - HttpMethod $httpMethod = HttpMethod::POST, - ): ApiResponse { - $this->urlPath = $urlPath; + public function get(string $url): ApiResponse + { + $this->url = $url; + return $this->response ?? new ApiResponse(200, '{"ok":true,"result":true}'); + } + + public function post(string $url, string $body, array $headers): ApiResponse + { + $this->url = $url; + return $this->response ?? new ApiResponse(200, '{"ok":true,"result":true}'); + } + + public function postWithFiles(string $url, array $data, array $files): ApiResponse + { + $this->url = $url; return $this->response ?? new ApiResponse(200, '{"ok":true,"result":true}'); } @@ -41,15 +49,15 @@ public function downloadFileTo(string $url, string $savePath): void } /** - * @return list + * @psalm-return list */ public function savedFiles(): array { return $this->savedFiles; } - public function urlPath(): ?string + public function url(): ?string { - return $this->urlPath; + return $this->url; } } diff --git a/tests/TelegramBotApiTest.php b/tests/TelegramBotApiTest.php index d9bb199e..760a866b 100644 --- a/tests/TelegramBotApiTest.php +++ b/tests/TelegramBotApiTest.php @@ -346,7 +346,7 @@ public function testMakeUrlPath(): void $api->logout(); - assertSame('https://api.telegram.org/botstub-token/logOut', $transport->urlPath()); + assertSame('https://api.telegram.org/botstub-token/logOut', $transport->url()); } public static function dataMakeFileUrl(): iterable diff --git a/tests/Transport/CurlTransport/CurlTransportTest.php b/tests/Transport/CurlTransport/CurlTransportTest.php index c4265ded..1d2c1e53 100644 --- a/tests/Transport/CurlTransport/CurlTransportTest.php +++ b/tests/Transport/CurlTransport/CurlTransportTest.php @@ -9,11 +9,9 @@ use HttpSoft\Message\StreamFactory; use PHPUnit\Framework\TestCase; use RuntimeException; -use stdClass; use Throwable; use Vjik\TelegramBot\Api\Tests\Curl\CurlMock; use Vjik\TelegramBot\Api\Transport\CurlTransport; -use Vjik\TelegramBot\Api\Transport\HttpMethod; use Vjik\TelegramBot\Api\Type\InputFile; use function PHPUnit\Framework\assertCount; @@ -32,14 +30,7 @@ public function testGet(): void ); $transport = new CurlTransport($curl); - $response = $transport->send( - '//url/getMe', - [ - 'key' => 'value', - 'array' => [1, 'test'], - ], - HttpMethod::GET, - ); + $response = $transport->get('//url/getMe?key=value&array=%5B1%2C%22test%22%5D'); assertSame(200, $response->statusCode); assertSame('{"ok":true,"result":[]}', $response->body); @@ -60,45 +51,29 @@ public function testPost(): void ); $transport = new CurlTransport($curl); - $response = $transport->send('//url/logOut'); + $response = $transport->post( + '//url/logOut', + '', + [ + 'Content-Length' => '0', + 'Content-Type' => 'application/json; charset=utf-8', + ], + ); assertSame(200, $response->statusCode); assertSame('{"ok":true,"result":[]}', $response->body); $options = $curl->getOptions(); - assertCount(5, $options); + assertCount(6, $options); assertTrue($options[CURLOPT_POST]); assertSame('//url/logOut', $options[CURLOPT_URL]); - assertSame([], $options[CURLOPT_POSTFIELDS]); - assertTrue($options[CURLOPT_RETURNTRANSFER]); - assertInstanceOf(CurlShareHandle::class, $options[CURLOPT_SHARE]); - } - - public function testPostWithParams(): void - { - $curl = new CurlMock( - execResult: '{"ok":true,"result":[]}', - getinfoResult: [CURLINFO_HTTP_CODE => 200], - ); - $transport = new CurlTransport($curl); - - $transport->send('//url/setChatTitle', [ - 'chat_id' => 123, - 'title' => 'test', - 'object' => new stdClass(), - ]); - - $options = $curl->getOptions(); - assertCount(5, $options); - assertTrue($options[CURLOPT_POST]); - assertSame('//url/setChatTitle', $options[CURLOPT_URL]); + assertSame('', $options[CURLOPT_POSTFIELDS]); assertSame( [ - 'chat_id' => 123, - 'title' => 'test', - 'object' => '{}', + 'Content-Length: 0', + 'Content-Type: application/json; charset=utf-8', ], - $options[CURLOPT_POSTFIELDS], + $options[CURLOPT_HTTPHEADER], ); assertTrue($options[CURLOPT_RETURNTRANSFER]); assertInstanceOf(CurlShareHandle::class, $options[CURLOPT_SHARE]); @@ -112,7 +87,7 @@ public function testWithoutCode(): void ), ); - $response = $transport->send('logOut'); + $response = $transport->get('getMe'); assertSame(0, $response->statusCode); } @@ -125,10 +100,14 @@ public function testPostWithLocalFiles(): void ); $transport = new CurlTransport($curl); - $response = $transport->send('//url/sendPhoto', [ - 'photo1' => InputFile::fromLocalFile(__DIR__ . '/photo.png'), - 'photo2' => InputFile::fromLocalFile(__DIR__ . '/photo.png', 'photo.png'), - ]); + $response = $transport->postWithFiles( + '//url/sendPhoto', + [], + [ + 'photo1' => InputFile::fromLocalFile(__DIR__ . '/photo.png'), + 'photo2' => InputFile::fromLocalFile(__DIR__ . '/photo.png', 'photo.png'), + ], + ); assertSame(200, $response->statusCode); assertSame('{"ok":true,"result":[]}', $response->body); @@ -162,15 +141,19 @@ public function testPostWithStreamFile(): void ); $transport = new CurlTransport($curl); - $transport->send('sendPhoto', [ - 'photo1' => new InputFile( - (new StreamFactory())->createStream('test1'), - ), - 'photo2' => new InputFile( - (new StreamFactory())->createStream('test2'), - 'test.jpg', - ), - ]); + $transport->postWithFiles( + 'sendPhoto', + [], + [ + 'photo1' => new InputFile( + (new StreamFactory())->createStream('test1'), + ), + 'photo2' => new InputFile( + (new StreamFactory())->createStream('test2'), + 'test.jpg', + ), + ], + ); assertEquals( [ @@ -188,9 +171,13 @@ public function testSeekableStream(): void $stream = (new StreamFactory())->createStream('test1'); $stream->getContents(); - $transport->send('sendPhoto', [ - 'photo' => new InputFile($stream), - ]); + $transport->postWithFiles( + 'sendPhoto', + [], + [ + 'photo' => new InputFile($stream), + ], + ); assertEquals( [ @@ -207,9 +194,13 @@ public function testSeekableResource(): void $resource = fopen(__DIR__ . '/photo.png', 'r'); stream_get_contents($resource); - $transport->send('sendPhoto', [ - 'photo' => new InputFile($resource), - ]); + $transport->postWithFiles( + 'sendPhoto', + [], + [ + 'photo' => new InputFile($resource), + ], + ); assertEquals( [ @@ -228,7 +219,7 @@ public function testCloseOnException(): void $transport = new CurlTransport($curl); try { - $transport->send('getMe'); + $transport->get('getMe'); } catch (Throwable) { } @@ -242,7 +233,7 @@ public function testShareOptions(): void getinfoResult: [CURLINFO_HTTP_CODE => 200], ); - (new CurlTransport($curl))->send('getMe'); + (new CurlTransport($curl))->get('getMe'); assertSame( [ diff --git a/tests/Transport/NativeTransport/NativeTransportTest.php b/tests/Transport/NativeTransport/NativeTransportTest.php index a7da1707..e210ab4a 100644 --- a/tests/Transport/NativeTransport/NativeTransportTest.php +++ b/tests/Transport/NativeTransport/NativeTransportTest.php @@ -7,9 +7,7 @@ use HttpSoft\Message\StreamFactory; use PHPUnit\Framework\TestCase; use RuntimeException; -use stdClass; use Vjik\TelegramBot\Api\Tests\Transport\NativeTransport\StreamMock\StreamMock; -use Vjik\TelegramBot\Api\Transport\HttpMethod; use Vjik\TelegramBot\Api\Transport\MimeTypeResolver\MimeTypeResolverInterface; use Vjik\TelegramBot\Api\Transport\NativeTransport; use Vjik\TelegramBot\Api\Type\InputFile; @@ -35,14 +33,7 @@ public function testGet(): void responseBody: '{"ok":true,"result":[]}', ); - $response = $transport->send( - 'http://url/getMe', - [ - 'key' => 'value', - 'array' => [1, 'test'], - ], - HttpMethod::GET, - ); + $response = $transport->get('http://url/getMe?key=value&array=%5B1%2C%22test%22%5D'); $request = StreamMock::disable(); @@ -74,7 +65,14 @@ public function testPost(): void responseBody: '{"ok":true,"result":[]}', ); - $response = $transport->send('http://url/logOut'); + $response = $transport->post( + 'http://url/logOut', + '', + [ + 'Content-Length' => '0', + 'Content-Type' => 'application/json; charset=utf-8', + ], + ); $request = StreamMock::disable(); @@ -86,7 +84,10 @@ public function testPost(): void 'options' => [ 'http' => [ 'method' => 'POST', - 'header' => 'Content-type: application/x-www-form-urlencoded', + 'header' => [ + 'Content-Length: 0', + 'Content-Type: application/json; charset=utf-8', + ], 'content' => '', 'ignore_errors' => true, ], @@ -96,7 +97,7 @@ public function testPost(): void ); } - public function testPostWithParams(): void + public function testPostWithLocalFiles(): void { $transport = new NativeTransport(); @@ -108,50 +109,18 @@ public function testPostWithParams(): void responseBody: '{"ok":true,"result":[]}', ); - $response = $transport->send('http://url/setChatTitle', [ - 'chat_id' => 123, - 'title' => 'test', - 'object' => new stdClass(), - ]); - - $request = StreamMock::disable(); - - assertSame(200, $response->statusCode); - assertSame('{"ok":true,"result":[]}', $response->body); - assertSame( + $response = $transport->postWithFiles( + 'http://url/sendPhoto', [ - 'path' => 'http://url/setChatTitle', - 'options' => [ - 'http' => [ - 'method' => 'POST', - 'header' => 'Content-type: application/x-www-form-urlencoded', - 'content' => 'chat_id=123&title=test&object=%7B%7D', - 'ignore_errors' => true, - ], - ], - ], - $request, - ); - } - public function testPostWithLocalFiles(): void - { - $transport = new NativeTransport(); - - StreamMock::enable( - responseHeaders: [ - 'HTTP/1.1 200 OK', - 'Content-Type: text/json', + 'age' => 19, + ], + [ + 'photo1' => InputFile::fromLocalFile(__DIR__ . '/photo.png'), + 'photo2' => InputFile::fromLocalFile(__DIR__ . '/photo.png', 'face.png'), ], - responseBody: '{"ok":true,"result":[]}', ); - $response = $transport->send('http://url/sendPhoto', [ - 'age' => 19, - 'photo1' => InputFile::fromLocalFile(__DIR__ . '/photo.png'), - 'photo2' => InputFile::fromLocalFile(__DIR__ . '/photo.png', 'face.png'), - ]); - $request = StreamMock::disable(); assertSame(200, $response->statusCode); @@ -188,15 +157,19 @@ public function testPostWithStreamFile(): void responseBody: '{"ok":true,"result":[]}', ); - $response = $transport->send('http://url/sendPhoto', [ - 'file1' => new InputFile( - (new StreamFactory())->createStream('test1'), - ), - 'file2' => new InputFile( - (new StreamFactory())->createStream('test2'), - 'test.txt', - ), - ]); + $response = $transport->postWithFiles( + 'http://url/sendPhoto', + [], + [ + 'file1' => new InputFile( + (new StreamFactory())->createStream('test1'), + ), + 'file2' => new InputFile( + (new StreamFactory())->createStream('test2'), + 'test.txt', + ), + ], + ); $request = StreamMock::disable(); @@ -225,7 +198,7 @@ public function testPostWithStreamFile(): void assertTrue($request['options']['http']['ignore_errors']); } - public function testPostWithFileAndArray(): void + public function testPostWithFiles(): void { $transport = new NativeTransport(); @@ -237,12 +210,17 @@ public function testPostWithFileAndArray(): void responseBody: '{"ok":true,"result":[]}', ); - $transport->send('http://url/method', [ - 'file1' => new InputFile( - (new StreamFactory())->createStream('test1'), - ), - 'ages' => [23, 45], - ]); + $transport->postWithFiles( + 'http://url/method', + [ + 'ages' => [23, 45], + ], + [ + 'file1' => new InputFile( + (new StreamFactory())->createStream('test1'), + ), + ], + ); $request = StreamMock::disable(); @@ -280,11 +258,15 @@ public function resolve(InputFile $file): ?string responseBody: '{"ok":true,"result":[]}', ); - $transport->send('http://url/method', [ - 'file1' => new InputFile( - (new StreamFactory())->createStream('test1'), - ), - ]); + $transport->postWithFiles( + 'http://url/method', + [], + [ + 'file1' => new InputFile( + (new StreamFactory())->createStream('test1'), + ), + ], + ); $request = StreamMock::disable(); @@ -304,7 +286,7 @@ public function testErrorOnSend(): void $this->expectException(RuntimeException::class); $this->expectExceptionMessage('file_get_contents(): Unable to find the wrapper "example"'); - $transport->send('example://url/logOut'); + $transport->get('example://url/getMe'); } public function testWithoutCode(): void @@ -318,7 +300,7 @@ public function testWithoutCode(): void responseBody: '{"ok":true,"result":[]}', ); - $response = $transport->send('http://url/logOut'); + $response = $transport->get('http://url/getMe'); StreamMock::disable(); diff --git a/tests/Transport/PsrTransport/PsrTransportTest.php b/tests/Transport/PsrTransport/PsrTransportTest.php index 66028c4c..b41909b4 100644 --- a/tests/Transport/PsrTransport/PsrTransportTest.php +++ b/tests/Transport/PsrTransport/PsrTransportTest.php @@ -12,7 +12,6 @@ use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Vjik\TelegramBot\Api\Transport\PsrTransport; -use Vjik\TelegramBot\Api\Transport\HttpMethod; use Vjik\TelegramBot\Api\Type\InputFile; use function PHPUnit\Framework\assertInstanceOf; @@ -45,14 +44,7 @@ public function testGet(): void new StreamFactory(), ); - $response = $transport->send( - '//url/getMe', - [ - 'key' => 'value', - 'array' => [1, 'test'], - ], - HttpMethod::GET, - ); + $response = $transport->get('//url/getMe?key=value&array=%5B1%2C%22test%22%5D'); assertSame(201, $response->statusCode); } @@ -65,7 +57,20 @@ public function testPost(): void $client ->expects($this->once()) ->method('sendRequest') - ->with($httpRequest) + ->with( + new Callback(function ($request): bool { + assertInstanceOf(Request::class, $request); + assertSame( + [ + 'Content-Length' => ['0'], + 'Content-Type' => ['application/json; charset=utf-8'], + ], + $request->getHeaders(), + ); + assertSame('', $request->getBody()->getContents()); + return true; + }), + ) ->willReturn(new Response(201)); $requestFactory = $this->createMock(RequestFactoryInterface::class); @@ -81,7 +86,14 @@ public function testPost(): void new StreamFactory(), ); - $response = $transport->send('//url/logOut'); + $response = $transport->post( + '//url/logOut', + '', + [ + 'Content-Length' => '0', + 'Content-Type' => 'application/json; charset=utf-8', + ], + ); assertSame(201, $response->statusCode); } @@ -97,7 +109,6 @@ public function testPostWithData(): void ->with( new Callback(function ($request): bool { assertInstanceOf(Request::class, $request); - /** @var Request $request */ assertSame( [ 'Content-Length' => ['29'], @@ -124,12 +135,19 @@ public function testPostWithData(): void new StreamFactory(), ); - $response = $transport->send('//url/sendMessage', ['chat_id' => 123, 'text' => 'test']); + $response = $transport->post( + '//url/sendMessage', + '{"chat_id":123,"text":"test"}', + [ + 'Content-Length' => '29', + 'Content-Type' => 'application/json; charset=utf-8', + ], + ); assertSame(201, $response->statusCode); } - public function testPostWithDataAndFiles(): void + public function testPostWithFiles(): void { $httpRequest = new Request(); @@ -190,11 +208,13 @@ public function testPostWithDataAndFiles(): void new StreamFactory(), ); - $response = $transport->send( + $response = $transport->postWithFiles( '//url/sendPhoto', [ 'chat_id' => 123, 'caption' => 'hello', + ], + [ 'photo' => new InputFile( (new StreamFactory())->createStream('test-file-body'), 'face.png', @@ -223,7 +243,7 @@ public function testRewind(): void $streamFactory, ); - $response = $transport->send('getMe'); + $response = $transport->get('getMe'); assertSame(201, $response->statusCode); assertSame('hello', $response->body); diff --git a/tests/Transport/PsrTransport/StrictTypeRequest/StrictTypeRequestTest.php b/tests/Transport/PsrTransport/StrictTypeRequest/StrictTypeRequestTest.php index 374d09ea..5eeaddca 100644 --- a/tests/Transport/PsrTransport/StrictTypeRequest/StrictTypeRequestTest.php +++ b/tests/Transport/PsrTransport/StrictTypeRequest/StrictTypeRequestTest.php @@ -29,7 +29,7 @@ public function testWithHeader(): void $streamFactory, ); - $response = $transport->send('getMyName', ['language_code' => 'ru']); + $response = $transport->get('getMyName'); assertSame(201, $response->statusCode); assertSame('hello', $response->body); From 80e80d2cee72e3a235543c1bfbf39e4926aa2779 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Wed, 5 Nov 2025 17:43:15 +0300 Subject: [PATCH 3/8] fix --- src/Transport/TransportInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transport/TransportInterface.php b/src/Transport/TransportInterface.php index f60ec5e1..bd4b5fd7 100644 --- a/src/Transport/TransportInterface.php +++ b/src/Transport/TransportInterface.php @@ -4,7 +4,7 @@ namespace Phptg\BotApi\Transport; -use Vjik\TelegramBot\Api\Type\InputFile; +use Phptg\BotApi\Type\InputFile; /** * @api From b9d9ec85511b90de8e197a30711a6ce8883c2866 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Wed, 5 Nov 2025 18:05:27 +0300 Subject: [PATCH 4/8] mutant 1 --- tests/Support/TransportMock.php | 18 ++++++++++++++---- tests/TelegramBotApiTest.php | 13 +++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/tests/Support/TransportMock.php b/tests/Support/TransportMock.php index 55e9b8b0..c0b79399 100644 --- a/tests/Support/TransportMock.php +++ b/tests/Support/TransportMock.php @@ -17,25 +17,35 @@ final class TransportMock implements TransportInterface private array $savedFiles = []; public function __construct( - private readonly ?ApiResponse $response = null, + private readonly ApiResponse $response = new ApiResponse(200, '{"ok":true,"result":true}'), ) {} + public static function successResult(mixed $result): self + { + return new self( + new ApiResponse( + 200, + json_encode(['ok' => true, 'result' => $result], JSON_THROW_ON_ERROR), + ), + ); + } + public function get(string $url): ApiResponse { $this->url = $url; - return $this->response ?? new ApiResponse(200, '{"ok":true,"result":true}'); + return $this->response; } public function post(string $url, string $body, array $headers): ApiResponse { $this->url = $url; - return $this->response ?? new ApiResponse(200, '{"ok":true,"result":true}'); + return $this->response; } public function postWithFiles(string $url, array $data, array $files): ApiResponse { $this->url = $url; - return $this->response ?? new ApiResponse(200, '{"ok":true,"result":true}'); + return $this->response; } public function downloadFile(string $url): string diff --git a/tests/TelegramBotApiTest.php b/tests/TelegramBotApiTest.php index d47e6104..6fbea6ec 100644 --- a/tests/TelegramBotApiTest.php +++ b/tests/TelegramBotApiTest.php @@ -1328,6 +1328,19 @@ public function testGetUpdates(): void assertSame(2, $result[1]->updateId); } + public function testGetMethodWithParams(): void + { + $transport = TransportMock::successResult([]); + $api = new TelegramBotApi('stub-token', transport: $transport); + + $api->getUpdates(offset: 5, allowedUpdates: ['message', 'edited_message', 'channel_post']); + + assertSame( + 'https://api.telegram.org/botstub-token/getUpdates?offset=5&allowed_updates=%5B%22message%22%2C%22edited_message%22%2C%22channel_post%22%5D', + $transport->url(), + ); + } + public function testGetUserChatBoosts(): void { $api = TestHelper::createSuccessStubApi([ From 5fe6e68b995a587930989311701d2871bbec03b6 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 7 Nov 2025 14:24:16 +0300 Subject: [PATCH 5/8] mutant 2 --- tests/Support/TransportMock.php | 21 ++++++++++++++------- tests/TelegramBotApiTest.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/tests/Support/TransportMock.php b/tests/Support/TransportMock.php index c0b79399..30c478bd 100644 --- a/tests/Support/TransportMock.php +++ b/tests/Support/TransportMock.php @@ -10,11 +10,9 @@ final class TransportMock implements TransportInterface { private ?string $url = null; - - /** - * @psalm-var list - */ private array $savedFiles = []; + private array $sentData = []; + private array $sentFiles = []; public function __construct( private readonly ApiResponse $response = new ApiResponse(200, '{"ok":true,"result":true}'), @@ -45,6 +43,8 @@ public function post(string $url, string $body, array $headers): ApiResponse public function postWithFiles(string $url, array $data, array $files): ApiResponse { $this->url = $url; + $this->sentData = $data; + $this->sentFiles = $files; return $this->response; } @@ -58,9 +58,6 @@ public function downloadFileTo(string $url, string $savePath): void $this->savedFiles[] = [$url, $savePath]; } - /** - * @psalm-return list - */ public function savedFiles(): array { return $this->savedFiles; @@ -70,4 +67,14 @@ public function url(): ?string { return $this->url; } + + public function sentData(): array + { + return $this->sentData; + } + + public function sentFiles(): array + { + return $this->sentFiles; + } } diff --git a/tests/TelegramBotApiTest.php b/tests/TelegramBotApiTest.php index 6fbea6ec..ee436444 100644 --- a/tests/TelegramBotApiTest.php +++ b/tests/TelegramBotApiTest.php @@ -1341,6 +1341,37 @@ public function testGetMethodWithParams(): void ); } + public function testPostMethodWithFiles(): void + { + $transport = TransportMock::successResult([ + 'message_id' => 7, + 'date' => 1620000000, + 'chat' => [ + 'id' => 1, + 'type' => 'private', + ], + ]); + $api = new TelegramBotApi('stub-token', transport: $transport); + + $file = new InputFile( + (new StreamFactory())->createStream('test1'), + ); + $api->sendDocument('id1', $file); + + assertSame( + 'https://api.telegram.org/botstub-token/sendDocument', + $transport->url(), + ); + assertSame( + ['chat_id' => 'id1'], + $transport->sentData(), + ); + assertSame( + ['document' => $file], + $transport->sentFiles(), + ); + } + public function testGetUserChatBoosts(): void { $api = TestHelper::createSuccessStubApi([ From 1bd84b4486c028d62c7c2f1acf86d385e738f8ed Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 7 Nov 2025 14:30:37 +0300 Subject: [PATCH 6/8] mutant 3 --- tests/Support/TransportMock.php | 24 ++++++++++++++++++++---- tests/TelegramBotApiTest.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/tests/Support/TransportMock.php b/tests/Support/TransportMock.php index 30c478bd..512c76a1 100644 --- a/tests/Support/TransportMock.php +++ b/tests/Support/TransportMock.php @@ -11,8 +11,12 @@ final class TransportMock implements TransportInterface { private ?string $url = null; private array $savedFiles = []; - private array $sentData = []; - private array $sentFiles = []; + + private ?string $sentBody = null; + private ?array $sentHeaders = null; + + private ?array $sentData = null; + private ?array $sentFiles = null; public function __construct( private readonly ApiResponse $response = new ApiResponse(200, '{"ok":true,"result":true}'), @@ -37,6 +41,8 @@ public function get(string $url): ApiResponse public function post(string $url, string $body, array $headers): ApiResponse { $this->url = $url; + $this->sentBody = $body; + $this->sentHeaders = $headers; return $this->response; } @@ -68,12 +74,22 @@ public function url(): ?string return $this->url; } - public function sentData(): array + public function sentHeaders(): ?array + { + return $this->sentHeaders; + } + + public function sentBody(): ?string + { + return $this->sentBody; + } + + public function sentData(): ?array { return $this->sentData; } - public function sentFiles(): array + public function sentFiles(): ?array { return $this->sentFiles; } diff --git a/tests/TelegramBotApiTest.php b/tests/TelegramBotApiTest.php index ee436444..6a44eced 100644 --- a/tests/TelegramBotApiTest.php +++ b/tests/TelegramBotApiTest.php @@ -1341,6 +1341,37 @@ public function testGetMethodWithParams(): void ); } + public function testPostMethodWithParams(): void + { + $transport = TransportMock::successResult([ + 'message_id' => 7, + 'date' => 1620000000, + 'chat' => [ + 'id' => 1, + 'type' => 'private', + ], + ]); + $api = new TelegramBotApi('stub-token', transport: $transport); + + $api->sendMessage('id1', 'hello'); + + assertSame( + 'https://api.telegram.org/botstub-token/sendMessage', + $transport->url(), + ); + assertSame( + '{"chat_id":"id1","text":"hello"}', + $transport->sentBody(), + ); + assertSame( + [ + 'Content-Length' => '32', + 'Content-Type' => 'application/json; charset=utf-8', + ], + $transport->sentHeaders(), + ); + } + public function testPostMethodWithFiles(): void { $transport = TransportMock::successResult([ From 9dc93300fd10fdf67be9e8911ab1fbf59011ba90 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 7 Nov 2025 14:39:03 +0300 Subject: [PATCH 7/8] mutant 4 --- tests/TelegramBotApiTest.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/TelegramBotApiTest.php b/tests/TelegramBotApiTest.php index 6a44eced..66445ca7 100644 --- a/tests/TelegramBotApiTest.php +++ b/tests/TelegramBotApiTest.php @@ -6,6 +6,7 @@ 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; @@ -1387,14 +1388,21 @@ public function testPostMethodWithFiles(): void $file = new InputFile( (new StreamFactory())->createStream('test1'), ); - $api->sendDocument('id1', $file); + $api->sendDocument( + 'id1', + $file, + captionEntities: [new MessageEntity('bold', 0, 4)], + ); assertSame( 'https://api.telegram.org/botstub-token/sendDocument', $transport->url(), ); assertSame( - ['chat_id' => 'id1'], + [ + 'chat_id' => 'id1', + 'caption_entities' => '[{"type":"bold","offset":0,"length":4}]', + ], $transport->sentData(), ); assertSame( From 24fcbddb30ce0bbd4b709fad070e0428307d7371 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 7 Nov 2025 15:05:18 +0300 Subject: [PATCH 8/8] mutant 5 --- .../StrictTypeRequest/StrictTypeRequestTest.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/Transport/PsrTransport/StrictTypeRequest/StrictTypeRequestTest.php b/tests/Transport/PsrTransport/StrictTypeRequest/StrictTypeRequestTest.php index 1fc20256..3311cf9b 100644 --- a/tests/Transport/PsrTransport/StrictTypeRequest/StrictTypeRequestTest.php +++ b/tests/Transport/PsrTransport/StrictTypeRequest/StrictTypeRequestTest.php @@ -6,6 +6,7 @@ use HttpSoft\Message\Response; use HttpSoft\Message\StreamFactory; +use Phptg\BotApi\Type\InputFile; use PHPUnit\Framework\TestCase; use Psr\Http\Client\ClientInterface; use Phptg\BotApi\Transport\PsrTransport; @@ -29,7 +30,14 @@ public function testWithHeader(): void $streamFactory, ); - $response = $transport->get('getMyName'); + $file = new InputFile( + $streamFactory->createStream('file content'), + ); + $response = $transport->postWithFiles( + 'https://api.example.com/test', + ['key1' => 'value1'], + ['file1' => $file], + ); assertSame(201, $response->statusCode); assertSame('hello', $response->body);