toBeTrue(); foreach ([ 'id', 'tenant_id', 'campaign_id', 'mode', 'payload', 'status', 'attempts', 'result', 'failure_reason', 'taken_at', 'finished_at', ] as $column) { expect(Schema::hasColumn('client_tg_robot_jobs', $column))->toBeTrue("нет столбца {$column}"); } }); it('на таблице включена построчная защита с политикой по тенанту', function () { $rls = DB::selectOne("SELECT relrowsecurity, relforcerowsecurity FROM pg_class WHERE relname = 'client_tg_robot_jobs'"); expect($rls->relrowsecurity)->toBeTrue(); expect($rls->relforcerowsecurity)->toBeTrue(); $policy = DB::selectOne("SELECT polname FROM pg_policy p JOIN pg_class c ON c.oid = p.polrelid WHERE c.relname = 'client_tg_robot_jobs'"); expect($policy->polname)->toBe('tenant_isolation'); }); /** * Мина, которую на dev не видно: там ходит суперпользователь, а на бою — роли. * * Задания ставят ТРИ места (запуск кампании, опросчик вердикта, пересдача), и все три * работают под подключением по умолчанию, то есть под ролью портала. Служебной роли * crm_supplier_worker права на эту таблицу НЕ нужны: она используется только для * кросс-тенантного ПЕРЕЧИСЛЕНИЯ кампаний в чужой таблице. */ it('роль портала имеет права ставить и вести задания, если роли вообще заведены', function () { $roleExists = DB::selectOne("SELECT 1 AS x FROM pg_roles WHERE rolname = 'crm_app_user'"); if ($roleExists === null) { expect(true)->toBeTrue(); // dev: ролей нет, ходит суперпользователь — проверять нечего return; } $grants = DB::select( "SELECT privilege_type FROM information_schema.role_table_grants WHERE table_name = 'client_tg_robot_jobs' AND grantee = 'crm_app_user'" ); $types = array_map(fn ($g) => $g->privilege_type, $grants); expect($types)->toContain('INSERT'); expect($types)->toContain('SELECT'); expect($types)->toContain('UPDATE'); });