Skip to content

Commit 9b1f92d

Browse files
authored
Add input validation for file params, resolvePending, and postMultipart (#5)
1 parent 564527f commit 9b1f92d

9 files changed

Lines changed: 98 additions & 24 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ $bot = new \BelkaTech\VkTeamsBot\Bot(
3434
new \BelkaTech\VkTeamsBot\Http\HttpClient(
3535
baseUri: 'https://api.icq.net/bot',
3636
token: 'YOUR_BOT_TOKEN',
37-
client: new \GuzzleHttp\Client(),
37+
client: new \GuzzleHttp\Client(
38+
[
39+
'connect_timeout' => 4,
40+
'timeout' => 15,
41+
'http_errors' => false,
42+
],
43+
),
3844
requestFactory: new \GuzzleHttp\Psr7\HttpFactory(),
3945
streamFactory: new \GuzzleHttp\Psr7\HttpFactory(),
4046
),

src/Api/ChatsApi.php

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace BelkaTech\VkTeamsBot\Api;
66

77
use BelkaTech\VkTeamsBot\Http\HttpClient;
8-
use Exception;
98
use Psr\Http\Client\ClientExceptionInterface;
109

1110
final readonly class ChatsApi
@@ -233,33 +232,32 @@ public function unblockUser(
233232
* @return array{ok: bool}
234233
*
235234
* @throws ClientExceptionInterface
236-
* @throws Exception
237235
*/
238236
public function resolvePending(
239237
string $chatId,
240238
bool $approve,
241239
?string $userId = null,
242240
?bool $everyone = null,
243241
): array {
244-
if ($userId !== null) {
245-
/** @phpstan-ignore return.type */
246-
return $this->httpClient->get('/v1/chats/resolvePending', [
247-
'chatId' => $chatId,
248-
'approve' => $approve,
249-
'userId' => $userId,
250-
]);
242+
if (!($userId === null xor $everyone === null)) {
243+
throw new \InvalidArgumentException('Exactly one of userId or everyone must be provided');
251244
}
252245

253-
if ($everyone !== null) {
246+
if ($userId !== null) {
254247
/** @phpstan-ignore return.type */
255248
return $this->httpClient->get('/v1/chats/resolvePending', [
256249
'chatId' => $chatId,
257250
'approve' => $approve,
258-
'everyone' => $everyone,
251+
'userId' => $userId,
259252
]);
260253
}
261254

262-
throw new Exception('userId or everyone must be provided');
255+
/** @phpstan-ignore return.type */
256+
return $this->httpClient->get('/v1/chats/resolvePending', [
257+
'chatId' => $chatId,
258+
'approve' => $approve,
259+
'everyone' => $everyone,
260+
]);
263261
}
264262

265263
/**

src/Api/MessagesApi.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ public function sendVoice(
104104
?array $forwardMsgId = null,
105105
?Keyboard $inlineKeyboardMarkup = null,
106106
): array {
107+
if (!($fileId === null xor $filePath === null)) {
108+
throw new \InvalidArgumentException('Exactly one of fileId or filePath must be provided');
109+
}
110+
107111
$params = [
108112
'chatId' => $chatId,
109113
'fileId' => $fileId,
@@ -264,6 +268,10 @@ private function sendMedia(
264268
?object $format,
265269
?ParseModeEnum $parseMode,
266270
): array {
271+
if (!($fileId === null xor $filePath === null)) {
272+
throw new \InvalidArgumentException('Exactly one of fileId or filePath must be provided');
273+
}
274+
267275
$params = [
268276
'chatId' => $chatId,
269277
'caption' => $caption,

src/BotEventListener.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use BelkaTech\VkTeamsBot\Enum\EventTypeEnum;
88
use BelkaTech\VkTeamsBot\Event\EventDto;
9-
use InvalidArgumentException;
109
use Psr\Http\Client\NetworkExceptionInterface;
1110

1211
final class BotEventListener
@@ -32,7 +31,7 @@ public function onCommand(
3231
\Closure $handler,
3332
): void {
3433
if (isset($this->commandHandlers[$command])) {
35-
throw new InvalidArgumentException(
34+
throw new \InvalidArgumentException(
3635
"Command handler for '{$command}' is already registered",
3736
);
3837
}
@@ -126,7 +125,7 @@ public function listen(
126125
?\Closure $onException = null,
127126
): void {
128127
if ($pollTime < 1 || $pollTime > 60) {
129-
throw new InvalidArgumentException(
128+
throw new \InvalidArgumentException(
130129
"pollTime must be between 1 and 60 seconds, got {$pollTime}",
131130
);
132131
}

src/Http/HttpClient.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ public function postMultipart(
6262
$query = http_build_query($this->filterParams($params));
6363
$url = $this->baseUri . $path . '?' . $query;
6464

65+
if (!is_file($filePath)) {
66+
throw new \InvalidArgumentException(
67+
"File not found: {$filePath}",
68+
);
69+
}
70+
6571
$boundary = bin2hex(random_bytes(16));
6672

6773
$body = "--{$boundary}\r\n"

test/Unit/Api/ChatsApiTest.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use BelkaTech\VkTeamsBot\Api\ChatsApi;
88
use BelkaTech\VkTeamsBot\Test\Spy\HttpClientSpy;
9+
use PHPUnit\Framework\Attributes\DataProvider;
910
use PHPUnit\Framework\TestCase;
1011

1112
final class ChatsApiTest extends TestCase
@@ -166,12 +167,22 @@ public function testResolvePendingWithEveryone(): void
166167
$this->assertArrayNotHasKey('userId', $params);
167168
}
168169

169-
public function testResolvePendingThrowsWithoutUserIdOrEveryone(): void
170+
/**
171+
* @return iterable<string, array{?string, ?bool}>
172+
*/
173+
public static function invalidResolvePendingParamsProvider(): iterable
170174
{
171-
$this->expectException(\Exception::class);
172-
$this->expectExceptionMessage('userId or everyone must be provided');
175+
yield 'both null' => [null, null];
176+
yield 'both provided' => ['user1', true];
177+
}
178+
179+
#[DataProvider('invalidResolvePendingParamsProvider')]
180+
public function testResolvePendingThrowsOnInvalidParams(?string $userId, ?bool $everyone): void
181+
{
182+
$this->expectException(\InvalidArgumentException::class);
183+
$this->expectExceptionMessage('Exactly one of userId or everyone must be provided');
173184

174-
$this->api->resolvePending('chat1', approve: true);
185+
$this->api->resolvePending('chat1', approve: true, userId: $userId, everyone: $everyone);
175186
}
176187

177188
public function testSetTitle(): void

test/Unit/Api/MessagesApiTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,24 @@ public function testNullInlineKeyboardIsNotEncodedAsJsonNullString(): void
8585
$this->assertNotSame('null', $params['inlineKeyboardMarkup']);
8686
}
8787

88+
/**
89+
* @return iterable<string, array{?string, ?string}>
90+
*/
91+
public static function invalidFileParamsProvider(): iterable
92+
{
93+
yield 'both null' => [null, null];
94+
yield 'both provided' => ['abc', '/tmp/file.txt'];
95+
}
96+
97+
#[DataProvider('invalidFileParamsProvider')]
98+
public function testSendFileThrowsOnInvalidFileParams(?string $fileId, ?string $filePath): void
99+
{
100+
$this->expectException(\InvalidArgumentException::class);
101+
$this->expectExceptionMessage('Exactly one of fileId or filePath must be provided');
102+
103+
$this->api->sendFile(chatId: 'chat1', fileId: $fileId, filePath: $filePath);
104+
}
105+
88106
public function testSendFileWithFileIdUsesGet(): void
89107
{
90108
$this->api->sendFile(chatId: 'chat1', fileId: 'abc');
@@ -114,6 +132,15 @@ public function testSendFileWithCaption(): void
114132
$this->assertSame('my file', $params['caption']);
115133
}
116134

135+
#[DataProvider('invalidFileParamsProvider')]
136+
public function testSendVoiceThrowsOnInvalidFileParams(?string $fileId, ?string $filePath): void
137+
{
138+
$this->expectException(\InvalidArgumentException::class);
139+
$this->expectExceptionMessage('Exactly one of fileId or filePath must be provided');
140+
141+
$this->api->sendVoice(chatId: 'chat1', fileId: $fileId, filePath: $filePath);
142+
}
143+
117144
public function testSendVoiceWithFileIdUsesGet(): void
118145
{
119146
$this->api->sendVoice(chatId: 'chat1', fileId: 'voice1');

test/Unit/BotEventListenerTest.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use BelkaTech\VkTeamsBot\BotEventListener;
99
use BelkaTech\VkTeamsBot\Event\EventDto;
1010
use BelkaTech\VkTeamsBot\Http\HttpClient;
11-
use InvalidArgumentException;
1211
use PHPUnit\Framework\MockObject\MockObject;
1312
use PHPUnit\Framework\TestCase;
1413

@@ -19,7 +18,7 @@ public function testListenThrowsOnPollTimeTooLow(): void
1918
$bot = new Bot($this->createMock(HttpClient::class));
2019
$listener = new BotEventListener($bot);
2120

22-
$this->expectException(InvalidArgumentException::class);
21+
$this->expectException(\InvalidArgumentException::class);
2322
$this->expectExceptionMessage('pollTime must be between 1 and 60');
2423

2524
$listener->listen(0);
@@ -30,7 +29,7 @@ public function testListenThrowsOnPollTimeTooHigh(): void
3029
$bot = new Bot($this->createMock(HttpClient::class));
3130
$listener = new BotEventListener($bot);
3231

33-
$this->expectException(InvalidArgumentException::class);
32+
$this->expectException(\InvalidArgumentException::class);
3433
$this->expectExceptionMessage('pollTime must be between 1 and 60');
3534

3635
$listener->listen(61);
@@ -69,7 +68,7 @@ public function testDuplicateCommandRegistrationThrows(): void
6968

7069
$listener->onCommand('/start', function () {});
7170

72-
$this->expectException(InvalidArgumentException::class);
71+
$this->expectException(\InvalidArgumentException::class);
7372
$this->expectExceptionMessage("Command handler for '/start' is already registered");
7473

7574
$listener->onCommand('/start', function () {});

test/Unit/Http/HttpClientTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,26 @@ public function testFilterParamsRemovesNullButKeepsOtherFalsyValues(): void
232232
]);
233233
}
234234

235+
public function testPostMultipartThrowsWhenFileNotFound(): void
236+
{
237+
$streamFactory = $this->createMock(StreamFactoryInterface::class);
238+
$requestFactory = $this->createMock(RequestFactoryInterface::class);
239+
$psrClient = $this->createMock(ClientInterface::class);
240+
241+
$httpClient = new HttpClient(
242+
baseUri: 'https://api.example.com',
243+
token: 'test-token',
244+
client: $psrClient,
245+
requestFactory: $requestFactory,
246+
streamFactory: $streamFactory,
247+
);
248+
249+
$this->expectException(\InvalidArgumentException::class);
250+
$this->expectExceptionMessage('File not found: /nonexistent/file.txt');
251+
252+
$httpClient->postMultipart('/upload', [], '/nonexistent/file.txt');
253+
}
254+
235255
public function testGetThrowsOnClientError(): void
236256
{
237257
$stream = $this->createMock(StreamInterface::class);

0 commit comments

Comments
 (0)