Files
portal/app/tests/Feature/ClientSms/MigrationGrantsTest.php
T
Дмитрий 51b9c20d0b fix(смс-клиент): повторный накат доделывает индекс ключа заказа + сторож прав по трём ролям
Две находки обязательного проверяющего доступа (приёмка Этапа 2, журнал В-80).
Обе — про молчаливые поломки: ошибок нет, тесты зелёные, защиты нет.

1. Индекс ключа заказа мог тихо не создаться. Миграция делает два шага —
   колонку и уникальный индекс, — а защита от повторного запуска стояла общим
   выходом в начале: «колонка есть, значит всё сделано». На бою SQL подаётся
   руками; прервалась подача между шагами — колонка легла, индекс нет, повторный
   накат прошёл мимо. Дальше два одновременных запроса с одним ключом создали бы
   две рассылки и списали деньги дважды.

   Проверка стала пошаговой. Доказано вырезанием: до правки новый тест краснеет
   («защита от двойного заказа потеряна молча»), после — зелёный. Живьём на
   локальной dev-базе: индекс уронен руками, повторный накат его вернул.

2. Сторож прав спрашивал базу только про рабочую роль. А гардов с именами
   служебных ролей в миграциях семь, и опечатка внутри такого гарда ошибки НЕ
   даёт — права просто молча не выдаются. Ровно тот блокер выката, ради которого
   сторож и заводился (В-36).

   Теперь спрашиваем и crm_admin_user, и crm_supplier_worker — по матрице,
   сверенной со всеми GRANT'ами миграций, — и счётчики для всех трёх ролей.
   Доказано вырезанием: опечатка в имени роли внутри гарда → сторож краснеет с
   именем роли, таблицы и права.

Прогоны: СМС 171/171 (было 169, два новых теста), приём лидов 17/17, pint чисто.
Структура таблиц не менялась — запись в журнале схемы v9.11.
2026-07-28 13:56:14 +03:00

148 lines
6.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
/**
* Сторож прав рабочей роли на таблицы модуля.
*
* Зачем он появился (Task 8). В миграциях модуля права выдавались строкой
* `GRANT … TO crm_app_user` без проверки, есть ли такая роль: на чистой базе это
* валит `migrate` целиком. Проверку я добавил — но у неё есть своя опасность:
* опечатка в имени роли внутри проверки НЕ даёт ошибки, права просто молча не
* выдаются. А это ровно тот блокер выката, что стоил нам дня 27.07 (журнал В-36):
* на бою первая же запись падает с «нет доступа», а тесты и локальная база этого
* не видят — там суперпользователь.
*
* Поэтому здесь проверяется не текст миграции, а результат: реально ли у роли есть
* право писать в каждую таблицу модуля и пользоваться её счётчиком.
*
* ⚠️ Честное ограничение: тест доказывает это только там, где роли заведены
* (`db/00_create_roles.sql`). Где ролей нет — он пропускается и не доказывает ничего.
*/
uses(RefreshDatabase::class);
/** Таблица → права, которые модуль обещает рабочей роли. */
const CLIENT_SMS_TABLE_GRANTS = [
'client_sms_campaigns' => ['SELECT', 'INSERT', 'UPDATE'],
'client_sms_campaign_phones' => ['SELECT', 'INSERT', 'DELETE'],
'client_sms_messages' => ['SELECT', 'INSERT', 'UPDATE'],
'client_sms_optouts' => ['SELECT', 'INSERT', 'DELETE'],
'client_sms_contacts' => ['SELECT', 'INSERT', 'DELETE'],
'client_sms_templates' => ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
'client_sms_tariffs' => ['SELECT'],
'client_sms_settings' => ['SELECT'],
'client_sms_senders' => ['SELECT', 'INSERT', 'UPDATE'],
'client_sms_auto_rule' => ['SELECT', 'INSERT', 'UPDATE'],
'sms_global_optouts' => ['SELECT'],
];
/**
* Служебные роли: что модуль обещает АДМИН-зоне и служебному работнику.
*
* Появилось при приёмке Этапа 2 (журнал В-80, замечание проверяющего З-2): сторож
* выше спрашивал базу только про рабочую роль, а гардов с именами двух других ролей
* в миграциях семь. Опечатка внутри такого гарда ошибки НЕ даёт — права молча не
* выдаются, и это ровно тот блокер выката, ради которого сторож и заводился.
*
* Список сверен с миграциями (все `GRANT … TO …` семейства), а не переписан на глаз.
*/
const CLIENT_SMS_SERVICE_GRANTS = [
'crm_admin_user' => [
'client_sms_tariffs' => ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
'client_sms_settings' => ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
'client_sms_senders' => ['SELECT', 'UPDATE'],
'sms_global_optouts' => ['SELECT', 'INSERT', 'DELETE'],
],
'crm_supplier_worker' => [
'client_sms_senders' => ['SELECT'],
'client_sms_auto_rule' => ['SELECT'],
'sms_global_optouts' => ['SELECT'],
],
];
function clientSmsRoleExists(string $role): bool
{
return DB::selectOne('SELECT 1 AS ok FROM pg_roles WHERE rolname = ?', [$role]) !== null;
}
it('у рабочей роли есть обещанные права на таблицы модуля', function () {
if (! clientSmsRoleExists('crm_app_user')) {
$this->markTestSkipped('Роли crm_app_user на этой базе нет — проверять нечего.');
}
foreach (CLIENT_SMS_TABLE_GRANTS as $table => $privileges) {
foreach ($privileges as $privilege) {
$granted = DB::selectOne(
'SELECT has_table_privilege(?, ?, ?) AS ok',
['crm_app_user', $table, $privilege],
)->ok;
expect($granted)->toBeTrue("Роли crm_app_user не выдано право {$privilege} на {$table}");
}
}
});
it('у служебных ролей есть обещанные права на таблицы модуля', function () {
$checked = 0;
foreach (CLIENT_SMS_SERVICE_GRANTS as $role => $tables) {
if (! clientSmsRoleExists($role)) {
continue;
}
foreach ($tables as $table => $privileges) {
foreach ($privileges as $privilege) {
$granted = DB::selectOne(
'SELECT has_table_privilege(?, ?, ?) AS ok',
[$role, $table, $privilege],
)->ok;
expect($granted)->toBeTrue("Роли {$role} не выдано право {$privilege} на {$table}");
$checked++;
}
}
}
if ($checked === 0) {
$this->markTestSkipped('Служебных ролей на этой базе нет — проверять нечего.');
}
});
/**
* Счётчики (sequence) — отдельная история: право на таблицу их НЕ покрывает, и
* именно этого не хватало всем 14 миграциям модуля до фикса В-36.
*
* Спрашиваем про все три роли: миграция прав (`100600`) выдаёт счётчики каждой из них.
*/
it('у ролей есть право пользоваться счётчиками таблиц модуля', function () {
$roles = array_filter(
['crm_app_user', 'crm_admin_user', 'crm_supplier_worker'],
fn (string $role) => clientSmsRoleExists($role),
);
if ($roles === []) {
$this->markTestSkipped('Ролей модуля на этой базе нет — проверять нечего.');
}
$sequences = DB::select(<<<'SQL'
SELECT sequencename FROM pg_sequences
WHERE schemaname = 'public' AND sequencename LIKE 'client\_sms\_%'
SQL);
expect($sequences)->not->toBeEmpty();
foreach ($roles as $role) {
foreach ($sequences as $row) {
$granted = DB::selectOne(
'SELECT has_sequence_privilege(?, ?, ?) AS ok',
[$role, $row->sequencename, 'USAGE'],
)->ok;
expect($granted)->toBeTrue("Роли {$role} не выдано право на счётчик {$row->sequencename}");
}
}
});