Files
portal/app/tests/Feature/ClientSms/GlobalOptoutTest.php
T
Дмитрий efcb8034a0 chore(смс-клиент): убрана врущая графа «кто внёс» + миграции модуля переживают повторный запуск
Хвосты Этапа 1 (журнал В-37). Строк приёмочного листа не закрывают — уборка.

1. Графа «кто внёс» в общем стоп-листе портала снесена. Она была не пустой, а
врущей: при открытом в том же браузере обычном кабинете туда записывался id
КЛИЕНТСКОГО пользователя — число, неотличимое от id администратора. Проверено
пробой: пользователь 1 → в графе 1. Админ-зона закрыта паролем nginx, своего
входа Laravel у неё нет, а сессия кабинета видна и там — то же эхо коллизии
24.07. Заполнить правдой нечем: настоящий вход админа ждёт Б-1, соседние экраны
пишут id служебной заглушки, то есть одно число во всех строках. Остались номер,
причина словами и дата — этого хватает и для разбора жалобы, и для договора с
МТС. Возврат — down() миграции.

2. Все 16 миграций модуля начинаются с «уже сделано — выходим», уникальный
индекс ключа заказа создаётся с IF NOT EXISTS. Причина не теоретическая: на бою
SQL миграций подаётся в базу руками, памяти «этот файл уже применён» там нет. У
тарифов и настроек это особенно важно — они засевают строки, и повторный запуск
завёл бы ВТОРУЮ строку настроек молча, без ошибки.

3. Восемь GRANT … TO crm_app_user стояли без проверки существования роли — на
чистой базе migrate падал бы целиком. Теперь все в гарде, как в v9.00 и v9.07.

Своя ловушка гарда: опечатка в имени роли внутри IF EXISTS ошибки НЕ даёт, права
просто не выдаются — а это ровно блокер выката В-36. Поэтому заведён сторож:
тест спрашивает у самой базы has_table_privilege / has_sequence_privilege по
каждой таблице и счётчику модуля. Заодно закрыта дыра — у фикса В-36 теста не
было вовсе.

Хвост «7 замечаний squawk» проверить его же инструментом нельзя: squawk читает
SQL, а миграции у нас PHP. Чужой отчёт не пересказываю — проверка своя и
воспроизводимая: тест прогоняет up() каждой миграции второй раз.

Тесты: +3 (повторный запуск, права ролей, «чужого следа не остаётся»), один
переписан. ClientSms 169/169, приём лидов 17/17, phpstan 0, pint чисто.
Три выреза, все покраснели: испорченное имя роли, снятая защита от повторного
запуска, возвращённая графа.

Живой прогон: вошёл в обычный кабинет (та самая опасная обстановка), внёс номер
через админ-раздел — в базе ровно три поля, чужого следа нет; убрал кнопкой.
Пять миграций запущены по второму разу прямо на dev-базе — прошли, тарифов 5,
строка настроек одна. Контроль: голый CREATE TABLE та же база отвергает.

Запись схемы — v9.10.
2026-07-28 11:42:37 +03:00

70 lines
2.9 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\SmsGlobalOptout;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\RefreshDatabase;
/**
* Общий стоп-лист портала (строка листа 1.4): номер, закрытый владельцем,
* перестаёт получать СМС у ВСЕХ клиентов сразу. Это защита договора с МТС,
* а не удобство одного клиента, — поэтому таблица общая, без изоляции по тенанту.
*/
uses(RefreshDatabase::class);
it('хранит номер в общем стоп-листе и находит его', function () {
SmsGlobalOptout::create([
'phone' => '79001112233',
'reason' => 'жалоба оператору',
]);
expect(SmsGlobalOptout::where('phone', '79001112233')->exists())->toBeTrue();
});
it('не допускает дубля номера', function () {
SmsGlobalOptout::create(['phone' => '79001112233']);
expect(fn () => SmsGlobalOptout::create(['phone' => '79001112233']))
->toThrow(QueryException::class);
});
it('хранит причину словами и дату', function () {
SmsGlobalOptout::create([
'phone' => '79001112244',
'reason' => 'жалоба абонента',
]);
$row = SmsGlobalOptout::find('79001112244');
expect($row->reason)->toBe('жалоба абонента')
->and($row->created_at)->not->toBeNull();
});
/**
* Графы «кто внёс» в общем стоп-листе нет намеренно (журнал В-72). Раньше она была,
* и в неё попадал ЧУЖОЙ след: админ-зона не под замком Laravel, поэтому при открытом
* в том же браузере обычном кабинете туда записывался id КЛИЕНТСКОГО пользователя —
* значение, неотличимое от id администратора. Настоящего входа для админа нет (ждёт Б-1),
* заполнять графу нечем, и пустая графа хуже отсутствующей.
*/
it('не записывает «кто внёс»: чужого следа в общем стоп-листе не остаётся', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create(['tenant_id' => $tenant->id]);
$this->actingAs($user);
$this->postJson('/api/admin/sms/global-optouts', [
'phone' => '79995550777',
'reason' => 'жалоба через МТС',
])->assertStatus(201);
$row = SmsGlobalOptout::find('79995550777');
expect($row->getAttributes())->not->toHaveKey('created_by')
->and($row->reason)->toBe('жалоба через МТС')
->and($row->created_at)->not->toBeNull();
});