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); });