185 lines
6.2 KiB
PHP
185 lines
6.2 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
use App\Services\Sms\Providers\MtsSmsProvider;
|
||
|
|
use App\Services\Sms\SmsOutgoing;
|
||
|
|
use App\Services\Sms\SmsSendException;
|
||
|
|
use Illuminate\Http\Client\ConnectionException;
|
||
|
|
use Illuminate\Support\Facades\Cache;
|
||
|
|
use Illuminate\Support\Facades\Http;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Как канал МТС ходит в сеть.
|
||
|
|
*
|
||
|
|
* 🔴 Зачем: защита МТС (Qrator) считает НОВЫЕ TCP-соединения, а не запросы.
|
||
|
|
* Прежний код открывал новое соединение на каждое сообщение — рассылка на тысячу
|
||
|
|
* номеров выглядела для их защиты как атака, и адрес уходил в чёрный список
|
||
|
|
* на часы (замеры 05-07.08.2026, заявка RUMAAS-73856). Здесь заперты три вещи:
|
||
|
|
* переиспользование соединения, короткие таймауты и предохранитель, который
|
||
|
|
* прекращает стучаться после серии отказов.
|
||
|
|
*/
|
||
|
|
function mts_conn_provider(): MtsSmsProvider
|
||
|
|
{
|
||
|
|
return new MtsSmsProvider(
|
||
|
|
token: 'token-xxx',
|
||
|
|
serves: ['mts'],
|
||
|
|
priceKopecks: ['*' => 400],
|
||
|
|
baseUrl: 'https://api.mts.ru/client-omni-adapter_production/1.0.2',
|
||
|
|
naming: 'MTSM_Test',
|
||
|
|
connectTo: null,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function mts_conn_message(): SmsOutgoing
|
||
|
|
{
|
||
|
|
return new SmsOutgoing(
|
||
|
|
phone: '79990000001',
|
||
|
|
body: 'Здравствуйте!',
|
||
|
|
senderName: 'liderra.ru',
|
||
|
|
operator: 'МТС',
|
||
|
|
segments: 1,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function mts_conn_ok(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
'code' => 0,
|
||
|
|
'description' => '',
|
||
|
|
'data' => ['submitResults' => [['msid' => '79990000001', 'messageID' => 1, 'code' => 'OK']]],
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(function () {
|
||
|
|
Cache::flush();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('переиспользует одно соединение вместо нового на каждое сообщение', function () {
|
||
|
|
$provider = mts_conn_provider();
|
||
|
|
|
||
|
|
// Соединения живут внутри обработчика curl: один и тот же обработчик —
|
||
|
|
// одно и то же соединение на много запросов.
|
||
|
|
expect($provider->httpHandler())->toBe($provider->httpHandler());
|
||
|
|
});
|
||
|
|
|
||
|
|
it('представляется своим именем — чтобы защита видела известного клиента', function () {
|
||
|
|
Http::fake(['api.mts.ru/*' => Http::response(mts_conn_ok(), 200)]);
|
||
|
|
|
||
|
|
mts_conn_provider()->send(mts_conn_message());
|
||
|
|
|
||
|
|
Http::assertSent(fn ($request) => str_contains(
|
||
|
|
(string) ($request->header('User-Agent')[0] ?? ''),
|
||
|
|
'Liderra',
|
||
|
|
));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('ждёт соединения недолго — чтобы не держать оборванные попытки', function () {
|
||
|
|
$opts = mts_conn_provider()->httpOptions();
|
||
|
|
|
||
|
|
expect($opts['connect_timeout'])->toBeLessThanOrEqual(5)
|
||
|
|
->and($opts['timeout'])->toBeLessThanOrEqual(30);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('после серии отказов перестаёт стучаться вовсе', function () {
|
||
|
|
// 🪤 Подделка сети, которая БРОСАЕТ обрыв, запрос в журнал Laravel не пишет —
|
||
|
|
// assertSentCount на ней всегда ноль. Поэтому считаем заходы сами.
|
||
|
|
$zahodov = 0;
|
||
|
|
|
||
|
|
Http::fake(function () use (&$zahodov) {
|
||
|
|
$zahodov++;
|
||
|
|
|
||
|
|
throw new ConnectionException('cURL error 28: Timeout was reached');
|
||
|
|
});
|
||
|
|
|
||
|
|
$provider = mts_conn_provider();
|
||
|
|
|
||
|
|
// Пять отказов подряд — предохранитель размыкается.
|
||
|
|
for ($i = 1; $i <= 5; $i++) {
|
||
|
|
try {
|
||
|
|
$provider->send(mts_conn_message());
|
||
|
|
} catch (SmsSendException) {
|
||
|
|
// ожидаемо
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
expect($zahodov)->toBe(5);
|
||
|
|
|
||
|
|
// Шестая попытка НЕ должна уйти в сеть вовсе.
|
||
|
|
$exception = null;
|
||
|
|
|
||
|
|
try {
|
||
|
|
$provider->send(mts_conn_message());
|
||
|
|
} catch (SmsSendException $e) {
|
||
|
|
$exception = $e;
|
||
|
|
}
|
||
|
|
|
||
|
|
expect($exception)->not->toBeNull()
|
||
|
|
->and($exception->terminal)->toBeFalse()
|
||
|
|
->and($zahodov)->toBe(5);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('удачный заход сбрасывает счётчик отказов', function () {
|
||
|
|
$zahodov = 0;
|
||
|
|
|
||
|
|
// 🪤 Режим держим в объекте, а не в простой переменной: статанализатор не видит
|
||
|
|
// присваивания по ссылке из замыкания и считает сравнение всегда истинным.
|
||
|
|
$set = new class
|
||
|
|
{
|
||
|
|
public string $rezhim = 'obryv';
|
||
|
|
};
|
||
|
|
|
||
|
|
Http::fake(function () use (&$zahodov, $set) {
|
||
|
|
$zahodov++;
|
||
|
|
|
||
|
|
if ($set->rezhim === 'obryv') {
|
||
|
|
throw new ConnectionException('cURL error 28: Timeout was reached');
|
||
|
|
}
|
||
|
|
|
||
|
|
return Http::response(mts_conn_ok(), 200);
|
||
|
|
});
|
||
|
|
|
||
|
|
$provider = mts_conn_provider();
|
||
|
|
|
||
|
|
for ($i = 1; $i <= 4; $i++) {
|
||
|
|
try {
|
||
|
|
$provider->send(mts_conn_message());
|
||
|
|
} catch (SmsSendException) {
|
||
|
|
// ожидаемо
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$set->rezhim = 'otvet';
|
||
|
|
$provider->send(mts_conn_message());
|
||
|
|
|
||
|
|
// Счётчик обнулён: следующие четыре отказа предохранитель ещё не размыкают.
|
||
|
|
$set->rezhim = 'obryv';
|
||
|
|
|
||
|
|
for ($i = 1; $i <= 4; $i++) {
|
||
|
|
try {
|
||
|
|
$provider->send(mts_conn_message());
|
||
|
|
} catch (SmsSendException) {
|
||
|
|
// ожидаемо
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
expect($zahodov)->toBe(9);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('отказ по существу дела предохранитель не трогает — сеть-то жива', function () {
|
||
|
|
Http::fake(['api.mts.ru/*' => Http::response(['code' => 3, 'description' => 'нет имени'], 200)]);
|
||
|
|
|
||
|
|
$provider = mts_conn_provider();
|
||
|
|
|
||
|
|
for ($i = 1; $i <= 6; $i++) {
|
||
|
|
try {
|
||
|
|
$provider->send(mts_conn_message());
|
||
|
|
} catch (SmsSendException) {
|
||
|
|
// ожидаемо: прикладной отказ
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Все шесть ушли в сеть — предохранитель не сработал.
|
||
|
|
Http::assertSentCount(6);
|
||
|
|
});
|