09b980f5ac
Защита денег для публичного чата (лендинг открыт всему интернету, каждый ответ бота ~0,44 руб): пауза 2 сек между репликами, лимит вопросов в час (гость 20 / вошедший 60), потолок реплик на разговор (40), лимит по IP в час (60) и общий суточный потолок ответов (1500) с одноразовым письмом-тревогой владельцу при пробитии. Отказ всегда вежливый — клиент получает текст, а не молчание.
91 lines
3.7 KiB
PHP
91 lines
3.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\Bot\ProcessChatMessageJob;
|
|
use App\Models\BotDialog;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Queue;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('принимает сообщение клиента, пишет его в журнал и ставит ответ в очередь', function () {
|
|
Queue::fake();
|
|
$chat = str_repeat('a', 32);
|
|
|
|
$response = $this->postJson('/api/chat/message', [
|
|
'chat_id' => $chat,
|
|
'text' => 'сколько стоит заявка?',
|
|
]);
|
|
|
|
$response->assertOk();
|
|
$id = $response->json('message_id');
|
|
expect($id)->toBeInt();
|
|
|
|
$row = BotDialog::query()->find($id);
|
|
expect($row->chat_id)->toBe($chat)
|
|
->and($row->direction)->toBe('in')
|
|
->and($row->message)->toBe('сколько стоит заявка?')
|
|
->and($row->source)->toBe('landing'); // гость → лендинг
|
|
|
|
Queue::assertPushed(ProcessChatMessageJob::class);
|
|
});
|
|
|
|
it('не принимает слишком длинное сообщение', function () {
|
|
$this->postJson('/api/chat/message', [
|
|
'chat_id' => str_repeat('b', 32),
|
|
'text' => str_repeat('я', 1001),
|
|
])->assertStatus(422);
|
|
});
|
|
|
|
it('не принимает кривой номер разговора', function () {
|
|
$this->postJson('/api/chat/message', [
|
|
'chat_id' => 'не-номер',
|
|
'text' => 'привет',
|
|
])->assertStatus(422);
|
|
});
|
|
|
|
it('отдаёт ответ бота, появившийся после сообщения клиента', function () {
|
|
$chat = str_repeat('c', 32);
|
|
$in = BotDialog::create(['chat_id' => $chat, 'direction' => 'in', 'message' => 'привет', 'created_at' => now()]);
|
|
$out = BotDialog::create(['chat_id' => $chat, 'direction' => 'out', 'message' => 'Здравствуйте!', 'created_at' => now()]);
|
|
|
|
$response = $this->getJson("/api/chat/{$chat}/messages?after={$in->id}");
|
|
|
|
$response->assertOk();
|
|
expect($response->json('messages'))->toHaveCount(1)
|
|
->and($response->json('messages.0.direction'))->toBe('out')
|
|
->and($response->json('messages.0.text'))->toBe('Здравствуйте!')
|
|
->and($response->json('messages.0.id'))->toBe((int) $out->id);
|
|
});
|
|
|
|
it('отдаёт всю переписку разговора, когда окошко открыли заново', function () {
|
|
$chat = str_repeat('d', 32);
|
|
BotDialog::create(['chat_id' => $chat, 'direction' => 'in', 'message' => 'вопрос', 'created_at' => now()]);
|
|
BotDialog::create(['chat_id' => $chat, 'direction' => 'out', 'message' => 'ответ', 'created_at' => now()]);
|
|
|
|
$response = $this->getJson("/api/chat/{$chat}/messages?after=0");
|
|
|
|
expect($response->json('messages'))->toHaveCount(2);
|
|
});
|
|
|
|
it('не отдаёт чужой разговор', function () {
|
|
$mine = str_repeat('e', 32);
|
|
BotDialog::create(['chat_id' => str_repeat('f', 32), 'direction' => 'out', 'message' => 'чужое', 'created_at' => now()]);
|
|
|
|
expect($this->getJson("/api/chat/{$mine}/messages?after=0")->json('messages'))->toBe([]);
|
|
});
|
|
|
|
it('вежливо отказывает, когда клиент строчит слишком быстро', function () {
|
|
Queue::fake();
|
|
$chat = str_repeat('7', 32);
|
|
|
|
$this->postJson('/api/chat/message', ['chat_id' => $chat, 'text' => 'раз'])->assertOk();
|
|
$second = $this->postJson('/api/chat/message', ['chat_id' => $chat, 'text' => 'два']);
|
|
|
|
$second->assertStatus(429);
|
|
expect($second->json('message'))->toContain('подождите');
|
|
// Деньги не тратим: второй вопрос в очередь не попал.
|
|
Queue::assertPushed(ProcessChatMessageJob::class, 1);
|
|
});
|