244 lines
10 KiB
PHP
244 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Services\Sms\Providers\BeelineSmsProvider;
|
|
use App\Services\Sms\SmsDeliveryState;
|
|
use App\Services\Sms\SmsOutgoing;
|
|
use App\Services\Sms\SmsSendException;
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
const BEE_URL = 'https://a2p-sms-https.beeline.ru/proto/http/rest';
|
|
|
|
function bee_provider(string $token = ''): BeelineSmsProvider
|
|
{
|
|
return new BeelineSmsProvider(
|
|
login: '1834321',
|
|
password: 'secret',
|
|
serves: ['beeline'],
|
|
priceKopecks: ['*' => 475],
|
|
baseUrl: BEE_URL,
|
|
naming: 'Liderra.ru',
|
|
token: $token,
|
|
);
|
|
}
|
|
|
|
function bee_message(string $sender = 'Liderra.ru', int $segments = 1): SmsOutgoing
|
|
{
|
|
return new SmsOutgoing(
|
|
phone: '79990000001',
|
|
body: 'Здравствуйте!',
|
|
senderName: $sender,
|
|
operator: 'beeline',
|
|
segments: $segments,
|
|
);
|
|
}
|
|
|
|
/** Успешный ответ платформы на отправку. */
|
|
function bee_ok(string $id = '508909732338141600334', string $count = '1'): array
|
|
{
|
|
return [
|
|
'actions' => [[
|
|
'sms_group_id' => '894323467546261497508434',
|
|
'id' => $id,
|
|
'sms_type' => 'SENDSMS',
|
|
'phone' => '+79990000001',
|
|
'sms_res_count' => $count,
|
|
'message' => 'Здравствуйте!',
|
|
'action' => 'post_sms_action',
|
|
]],
|
|
'agt_id' => '47309602',
|
|
'date_report' => '15.10.2022 18:42:59',
|
|
];
|
|
}
|
|
|
|
/** Отправить и вернуть пойманное исключение; прошло без ошибки — провалить тест. */
|
|
function bee_thrown(array $body, int $status = 200): SmsSendException
|
|
{
|
|
Http::fake(['a2p-sms-https.beeline.ru/*' => Http::response($body, $status)]);
|
|
|
|
try {
|
|
bee_provider()->send(bee_message());
|
|
} catch (SmsSendException $e) {
|
|
return $e;
|
|
}
|
|
|
|
throw new RuntimeException('ожидалось SmsSendException, но отправка прошла без ошибки');
|
|
}
|
|
|
|
it('называется beeline и обслуживает заявленные сети', function () {
|
|
expect(bee_provider()->key())->toBe('beeline')
|
|
->and(bee_provider()->servesOperators())->toBe(['beeline']);
|
|
});
|
|
|
|
it('успешная отправка возвращает номер сообщения и цену', function () {
|
|
Http::fake(['a2p-sms-https.beeline.ru/*' => Http::response(bee_ok(), 200)]);
|
|
|
|
$result = bee_provider()->send(bee_message());
|
|
|
|
expect($result->providerMessageId)->toBe('508909732338141600334')
|
|
->and($result->segments)->toBe(1)
|
|
->and($result->costKopecks)->toBe(475);
|
|
|
|
Http::assertSent(function ($request) {
|
|
return $request->url() === BEE_URL
|
|
&& $request->method() === 'POST'
|
|
&& $request['user'] === '1834321'
|
|
&& $request['pass'] === 'secret'
|
|
&& $request['action'] === 'post_sms'
|
|
&& $request['sender'] === 'Liderra.ru'
|
|
&& $request['message'] === 'Здравствуйте!'
|
|
// Номер уходит строкой в международном виде — так в примерах Билайна.
|
|
&& $request['target'] === '+79990000001';
|
|
});
|
|
});
|
|
|
|
it('за деньги отвечает счётчик частей, названный платформой, а не наша оценка', function () {
|
|
// Мы посчитали 1 часть, платформа насчитала 2 — платить придётся за 2.
|
|
Http::fake(['a2p-sms-https.beeline.ru/*' => Http::response(bee_ok(count: '2'), 200)]);
|
|
|
|
$result = bee_provider()->send(bee_message(segments: 1));
|
|
|
|
expect($result->segments)->toBe(2)
|
|
->and($result->costKopecks)->toBe(950);
|
|
});
|
|
|
|
it('по ключу ходит без логина и пароля, с заголовком X-ApiKey', function () {
|
|
Http::fake(['a2p-sms-https.beeline.ru/*' => Http::response(bee_ok(), 200)]);
|
|
|
|
bee_provider(token: 'GvEqzI1TSmw7bDn')->send(bee_message());
|
|
|
|
Http::assertSent(function ($request) {
|
|
return $request->hasHeader('X-ApiKey', 'ApiKey GvEqzI1TSmw7bDn')
|
|
// 🔴 Вместе с ключом логин с паролем слать НЕЛЬЗЯ — платформа откажет.
|
|
&& ! isset($request['user'])
|
|
&& ! isset($request['pass']);
|
|
});
|
|
});
|
|
|
|
it('чужое имя отправителя не подменяет, а своё приводит к согласованному написанию', function () {
|
|
Http::fake(['a2p-sms-https.beeline.ru/*' => Http::response(bee_ok(), 200)]);
|
|
|
|
bee_provider()->send(bee_message(sender: 'liderra.ru'));
|
|
bee_provider()->send(bee_message(sender: 'ChuzhoeImya'));
|
|
|
|
$otpravleno = [];
|
|
Http::assertSent(function ($request) use (&$otpravleno) {
|
|
$otpravleno[] = $request['sender'];
|
|
|
|
return true;
|
|
});
|
|
|
|
expect($otpravleno)->toBe(['Liderra.ru', 'ChuzhoeImya']);
|
|
});
|
|
|
|
it('отказ платформы без номера сообщения отправкой не считает', function () {
|
|
$e = bee_thrown(['error' => ['code' => '20200', 'message' => 'invalid request']]);
|
|
|
|
expect($e->getMessage())->toContain('20200');
|
|
});
|
|
|
|
it('неодобренное оператором имя отправителя — отказ окончательный', function () {
|
|
// 20230: «Отправитель X не одобрен на стороне оператора Y». Повтор не поможет,
|
|
// пока имя не согласуют — держать такое в очереди значит копить мусор.
|
|
$e = bee_thrown(['error' => ['code' => '20230', 'message' => 'Отправитель не одобрен']]);
|
|
|
|
expect($e->terminal)->toBeTrue();
|
|
});
|
|
|
|
it('упёрлись в суточный предел — отказ ВРЕМЕННЫЙ, завтра пройдёт', function () {
|
|
$e = bee_thrown(['error' => ['code' => '20280', 'message' => 'Достигнут суточный лимит']]);
|
|
|
|
expect($e->terminal)->toBeFalse();
|
|
});
|
|
|
|
// 🪤 Две подделки сети в ОДНОМ тесте ставить нельзя: второй вызов Http::fake не
|
|
// заменяет первую, а добавляется к ней, и срабатывает всё равно первая. Поэтому
|
|
// каждый код ответа проверяем отдельным тестом.
|
|
it('сбой на стороне Билайна — отказ временный', function () {
|
|
expect(bee_thrown([], 503)->terminal)->toBeFalse();
|
|
});
|
|
|
|
it('нас не пустили — отказ окончательный, повтор не поможет', function () {
|
|
expect(bee_thrown([], 401)->terminal)->toBeTrue();
|
|
});
|
|
|
|
it('не достучались — отказ временный', function () {
|
|
Http::fake(function () {
|
|
throw new ConnectionException('cURL error 28');
|
|
});
|
|
|
|
try {
|
|
bee_provider()->send(bee_message());
|
|
throw new RuntimeException('ожидалось SmsSendException');
|
|
} catch (SmsSendException $e) {
|
|
expect($e->terminal)->toBeFalse();
|
|
}
|
|
});
|
|
|
|
it('судьбу спрашивает пачкой и разбирает доставленное', function () {
|
|
Http::fake(['a2p-sms-https.beeline.ru/*' => Http::response([
|
|
'actions' => [[
|
|
'sms_id' => '508909732338141600334',
|
|
'target' => '79990000001',
|
|
'sms_count' => '2',
|
|
'stc_code' => 'delivered',
|
|
'close_time' => '15.10.2022 18:42:59 ',
|
|
'status' => 'Сообщение доставлено',
|
|
'action' => 'status_action',
|
|
]],
|
|
], 200)]);
|
|
|
|
$reports = bee_provider()->fetchDelivery(['508909732338141600334']);
|
|
|
|
expect($reports)->toHaveCount(1)
|
|
->and($reports[0]->providerMessageId)->toBe('508909732338141600334')
|
|
->and($reports[0]->state)->toBe(SmsDeliveryState::DELIVERED)
|
|
->and($reports[0]->phone)->toBe('79990000001')
|
|
->and($reports[0]->partCount)->toBe(2)
|
|
->and($reports[0]->deliveredAt)->not->toBeNull();
|
|
|
|
Http::assertSent(fn ($request) => $request['action'] === 'status'
|
|
&& $request['sms_id'] === '508909732338141600334');
|
|
});
|
|
|
|
it('незнакомое слово о судьбе не угадывает', function () {
|
|
Http::fake(['a2p-sms-https.beeline.ru/*' => Http::response([
|
|
'actions' => [[
|
|
'sms_id' => '1',
|
|
'stc_code' => 'nechto-novoe',
|
|
]],
|
|
], 200)]);
|
|
|
|
$reports = bee_provider()->fetchDelivery(['1']);
|
|
|
|
// 🔴 Ноль вместо догадки: за «не доставлено» клиенту возвращаются деньги.
|
|
expect($reports[0]->state)->toBeNull()
|
|
->and($reports[0]->rawState)->toBe('nechto-novoe');
|
|
});
|
|
|
|
it('принятое оператором и отклонённое им разводит по разным судьбам', function () {
|
|
$slova = ['accepted', 'rejected', 'undeliverable', 'aborted'];
|
|
|
|
// 🪤 Ровно ОДНА подделка на тест, ответы по очереди: повторный Http::fake
|
|
// не заменяет прежний, и все опросы получали бы первый ответ.
|
|
$ochered = Http::fakeSequence('a2p-sms-https.beeline.ru/*');
|
|
foreach ($slova as $slovo) {
|
|
$ochered->push(['actions' => [['sms_id' => '1', 'stc_code' => $slovo]]], 200);
|
|
}
|
|
|
|
$sudby = [];
|
|
foreach ($slova as $slovo) {
|
|
$sudby[$slovo] = bee_provider()->fetchDelivery(['1'])[0]->state;
|
|
}
|
|
|
|
expect($sudby['accepted'])->toBe(SmsDeliveryState::SENDING)
|
|
// Оператор отклонил и отменённое пользователем — сообщение НЕ ушло,
|
|
// платить за это клиент не должен.
|
|
->and($sudby['rejected'])->toBe(SmsDeliveryState::NOT_SENT)
|
|
->and($sudby['aborted'])->toBe(SmsDeliveryState::NOT_SENT)
|
|
// Абонент недоступен — ушло, но не дошло.
|
|
->and($sudby['undeliverable'])->toBe(SmsDeliveryState::NOT_DELIVERED);
|
|
});
|