Files
portal/app/tests/Feature/Advertising/AdWalletUnderRealRoleTest.php
T
Дмитрий a5c278adaf test: рекламная папка и журнал вебхука перестали оставлять записи — остаток 44 → 0
Прогон рекламной папки оставлял в базе 44 записи. Теперь ноль, папка целиком
зелёная — 351 из 351. Полная tests/Feature: 3282 теста, 0 падений, и после неё
в базе не остаётся НИЧЕГО, кроме справочников, которые кладутся при сборке.

Два места прошлая смена считала неизлечимыми — оба вылечились, потому что
диагноз был не перепроверен:

  1. CampaignBannerEndpointsTest оставлял 24 записи, больше половины всей грязи.
     Он нарочно ловит отказ базы, а после отказа внутри транзакции Postgres
     глушит все следующие команды. Лечение — ловить отказ в ОТДЕЛЬНОЙ точке
     сохранения: DB::transaction внутри уже открытой ставит savepoint и
     откатывает только его. Приёмка вырезанием: убрал уникальный ключ в
     миграции — тест покраснел ровно там, где должен; вернул — зелёный.

  2. AdWalletUnderRealRoleTest. В промте: «откат противопоказан, он ходит
     настоящей ролью базы». Оказалось — не противопоказан, смена роли идёт по
     ТОМУ ЖЕ соединению и незакоммиченные записи видны. Тут была реальная
     опасность, что откат обезвредит самого сторожа, поэтому вырезание делал
     отдельно: убрал у службы установку контекста клиента — оба теста
     покраснели. Значит сторож сторожит по-прежнему.

Журнал вебхука оставлял 4 строки: он пишется ВТОРЫМ соединением к базе, куда
обычный откат не дотягивается. Двум файлам добавлена общая связка соединений
(SharesSupplierPdo) — папка вебхука теперь оставляет ноль.

Прежняя оценка исправлена. В промте стояло «~20 файлов оставляют по одной-две
записи». На деле после уборки восьми главных грязь оставляли ДВА файла из всей
папки: 2 и 3 записи, сумма сошлась с наблюдаемыми 5 ровно. «Нет отката» и
«гадит в базу» — разные вещи: папка Autopodbor, где без отката 16 файлов, не
оставляет ни одной записи. Мерить надо остатком, а не поиском по тексту.

Боевой код не тронут: git diff по app/app/ пуст после каждой правки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 01:15:44 +03:00

62 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\AdWallet;
use App\Models\Tenant;
use App\Services\Advertising\AdWalletService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
// Откат после теста. Смена ролей идёт по ТОМУ ЖЕ соединению, поэтому незакоммиченные
// записи боевой роли видны и проверка остаётся честной.
uses(DatabaseTransactions::class);
/**
* 🔴 КЛАСС «тихий ноль». Тесты ходят СУПЕРЮЗЕРОМ, который обходит защиту по клиентам,
* и потому физически не могут увидеть эту дыру. На бою очередь работает под обычной
* ролью, а защита на таблицах `ad_*` устроена СТРОГО:
*
* USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::bigint)
*
* Нет контекста — сравнение с NULL, и база молча отдаёт ноль строк. `release()` видит
* «кошелька нет», честно выходит, и заморозка клиента остаётся навсегда. Ни ошибки,
* ни строки в журнале.
*
* Этот сторож гоняет деньги ПОД БОЕВОЙ РОЛЬЮ и БЕЗ контекста — то есть ровно так, как
* их зовёт джоб проверки модерации.
*/
function podBoevoyRolyu(callable $chto): void
{
DB::statement('SET ROLE crm_app_user');
try {
$chto();
} finally {
DB::statement('RESET ROLE');
}
}
it('снимает заморозку под боевой ролью без контекста клиента — как её зовёт джоб модерации', function () {
$tenant = Tenant::factory()->create();
AdWallet::create(['tenant_id' => $tenant->id, 'balance_rub' => '3000.00', 'frozen_rub' => '0.00']);
$svc = app(AdWalletService::class);
$svc->freeze($tenant->id, 'yandex', 'campaign', 1, '1000.00');
// Яндекс отклонил весь набор объявлений: джоб возвращает клиенту заморозку целиком.
podBoevoyRolyu(fn () => $svc->release($tenant->id, 'yandex', 'campaign', 1));
// Читаем суперюзером — прибор не должен зависеть от той же защиты, что проверяем.
expect((string) AdWallet::where('tenant_id', $tenant->id)->first()->frozen_rub)->toBe('0.00');
});
it('замораживает под боевой ролью без контекста клиента', function () {
$tenant = Tenant::factory()->create();
AdWallet::create(['tenant_id' => $tenant->id, 'balance_rub' => '3000.00', 'frozen_rub' => '0.00']);
$svc = app(AdWalletService::class);
podBoevoyRolyu(fn () => $svc->freeze($tenant->id, 'yandex', 'campaign', 2, '700.00'));
expect((string) AdWallet::where('tenant_id', $tenant->id)->first()->frozen_rub)->toBe('700.00');
});