test: дозакрытие оздоровления — protekateli pd-аудита, видимость supplier, новый флоу регистрации
Accessibility (Pa11y live) / a11y (push) Has been cancelled

Снижение остатка 19 to 5. Всё тест-сторона:
- PdErasureServiceTest + AdminPdSubjectRequestsControllerTest: SharesSupplierPdo —
  перестали коммитить pd_processing_log через pgsql_supplier, что ломало
  глобальный audit:verify-chains (6 падений) и амплифицировало PhoneRegionSmoke.
- ReportFileDeletePdLogTest: SharesSupplierPdo — cron reports:cleanup-expired
  теперь видит незакоммиченные job'ы теста.
- AdminSuppliersControllerTest: устойчивый ассерт (с фазы 3 в suppliers есть direct).
- AuthLogCoverageTest/AuthFlowIntegrationTest: новый флоу самозаписи G1/SP1 —
  register_success пишется после confirm-email; добавлен шаг подтверждения.
- ImpersonationTest end: verify (G7-B) ставит маркер impersonation → admin-зона
  закрыта by design; помечаем токен used напрямую вместо session-takeover.
- CleanupInactiveSupplierProjectsJobTest: phase A читает pivot project_supplier_links —
  добавлена привязка linkProjectToSupplier (раньше был только legacy FK).
- Pint-нормализация uses() FQN to import в ранее тронутых файлах.

Остаток 5 (НЕ слепой патч): webhook B-префикс ×2 (решение владельца), advisory-lock
audit-цепочки (возможный дрейф схемы, флажок), SupplierConnection WARN#2 (cap-3,
поведенческое), SupplierPortalClientTest (пре-существующий, не от этих правок).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-25 08:19:53 +03:00
parent 2ec70b338f
commit 88ace4e3d9
26 changed files with 95 additions and 37 deletions
+2 -1
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Tests\Concerns;
use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB;
/**
@@ -44,7 +45,7 @@ trait SharesSupplierPdo
// запись видна последующим чтениям и откатывается общим rollback'ом в teardown.
$level = $defaultConnection->transactionLevel();
if ($level > 0) {
$prop = new \ReflectionProperty(\Illuminate\Database\Connection::class, 'transactions');
$prop = new \ReflectionProperty(Connection::class, 'transactions');
$prop->setAccessible(true);
$prop->setValue($supplierConnection, $level);
}
@@ -8,8 +8,12 @@ use App\Services\Pd\PdErasureService;
use Carbon\CarbonImmutable;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
// /erase эндпоинт пишет pd_processing_log через pgsql_supplier; без SharesSupplierPdo
// строки коммитятся (откатывается только default pgsql) и ломают глобальный
// audit:verify-chains в других тестах. Также шарит committed admin/tenant/user строки.
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
// ---------------------------------------------------------------------------
// Helpers
@@ -8,12 +8,15 @@ use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
it('GET /api/admin/suppliers returns 3 suppliers B1/B2/B3', function () {
it('GET /api/admin/suppliers returns B1/B2/B3 suppliers', function () {
// NB: с фазы 3 в suppliers есть и псевдо-поставщик 'direct' (DIRECT-лиды,
// миграция 2026_05_25_120100), поэтому проверяем наличие закупочных b1/b2/b3,
// а не строгий счёт. Если 'direct' не должен быть в этом списке — это правка
// контроллера (фильтр), решение владельца, не тест.
$response = $this->getJson('/api/admin/suppliers');
$response->assertOk();
$data = $response->json('data');
expect($data)->toHaveCount(3);
expect(collect($data)->pluck('code')->all())->toContain('b1', 'b2', 'b3');
$codes = collect($response->json('data'))->pluck('code')->all();
expect($codes)->toContain('b1', 'b2', 'b3');
});
it('PATCH updates cost_rub for supplier', function () {
@@ -14,8 +14,12 @@ declare(strict_types=1);
use App\Services\Pd\PdErasureService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
// PdErasureService пишет pd_processing_log + sees данные через pgsql_supplier.
// Без SharesSupplierPdo эти записи коммитятся (DatabaseTransactions откатывает
// только default pgsql) и отравляют глобальный audit:verify-chains в других тестах.
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
// ---------------------------------------------------------------------------
// Helpers — данные вставляются через pgsql_supplier (committed), т.к. сервис
@@ -5,8 +5,9 @@ declare(strict_types=1);
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
it('accepts update_regions action with subject-code arrays', function () {
$tenant = Tenant::factory()->create();
@@ -40,18 +40,30 @@ it('full auth-flow writes all expected auth_log events', function () {
$tenant = Tenant::factory()->create();
// ── Step 1: Register ─────────────────────────────────────────────────────
$this->postJson('/api/auth/register', [
$reg = $this->postJson('/api/auth/register', [
'email' => 'flow-test@example.ru',
'password' => 'secure-pass-1234',
'accept_offer' => true,
'accept_pdn' => true,
'captcha_token' => 'tok-123', // RegisterRequest требует captcha_token (CAPTCHA_FAKE_PASSES в phpunit.xml пропускает)
])->assertStatus(201);
// ── Step 1b: Confirm email (новый флоу G1/SP1) ───────────────────────────
// register создаёт pending-пользователя (is_active=false); до подтверждения
// почты login невозможен. Подтверждаем dev-кодом → активация + register_success.
$this->postJson('/api/auth/confirm-email', [
'email' => 'flow-test@example.ru',
'code' => $reg->json('_dev_plain_code'),
])->assertOk();
// logs: register_success
$user = User::where('email', 'flow-test@example.ru')->first();
expect($user)->not->toBeNull();
// confirm-email уже выполнил Auth::login; выходим, чтобы Step 2 проверил чистый login.
$this->postJson('/api/auth/logout');
resetAuthToWebGuard();
// ── Step 2: Login (no 2FA yet) — establish session auth ──────────────────
// No prior auth:sanctum request, so no reset needed here.
$this->postJson('/api/auth/login', [
@@ -42,6 +42,8 @@ it('logout writes auth_log event=logout', function () {
it('register writes auth_log event=register_success', function () {
Tenant::factory()->create();
// Новый флоу самозаписи (G1/SP1): register создаёт pending-пользователя и шлёт код;
// событие register_success пишется ПОСЛЕ подтверждения почты (confirmEmail), а не на register.
$response = $this->postJson('/api/auth/register', [
'email' => 'reg-log-test@example.ru',
'password' => 'fresh-pass-123',
@@ -51,6 +53,12 @@ it('register writes auth_log event=register_success', function () {
]);
$response->assertStatus(201);
$code = $response->json('_dev_plain_code');
$this->postJson('/api/auth/confirm-email', [
'email' => 'reg-log-test@example.ru',
'code' => $code,
])->assertOk();
$user = User::where('email', 'reg-log-test@example.ru')->first();
@@ -6,12 +6,14 @@ use App\Models\EmailVerification;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Auth\RegistrationService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Tests\Concerns\SharesSupplierPdo;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(\Tests\Concerns\SharesSupplierPdo::class);
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
// FN-3 (приёмка 22.06.2026): онбординг-confirm активировал владельца, но НЕ ставил
// users.email_verified_at — поле оставалось NULL, хотя email фактически подтверждён
@@ -6,8 +6,9 @@ use App\Mail\BalanceFrozenMail;
use App\Mail\BalanceUnfrozenMail;
use App\Models\Tenant;
use App\Services\Billing\PreflightResult;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
it('renders frozen mail with deficit', function () {
$tenant = Tenant::factory()->create(['organization_name' => 'ООО Альфа']);
+5 -6
View File
@@ -258,13 +258,12 @@ test('POST /api/admin/impersonation/end ставит session_ended_at', function
])->assertOk();
$tokenId = $init->json('token_id');
$code = $init->json('_dev_plain_code');
// Сначала verify, чтобы токен стал used.
$this->postJson('/api/admin/impersonation/verify', [
'token_id' => $tokenId,
'code' => $code,
])->assertOk();
// end() требует used_at (иначе 422). Раньше тут звали verify, но с G7-B verify
// делает session-takeover и ставит маркер 'impersonation' в сессии → EnsureSaasAdmin
// блокирует admin-only end с 403 (во время impersonation админка запрещена, by design).
// Помечаем токен использованным напрямую, чтобы проверить именно поведение end.
ImpersonationToken::on('pgsql_supplier')->where('id', $tokenId)->update(['used_at' => now()]);
$r = $this->postJson('/api/admin/impersonation/end', [
'token_id' => $tokenId,
@@ -10,9 +10,9 @@ use App\Models\Tenant;
use App\Services\Billing\LedgerService;
use App\Services\LeadDistributor;
use App\Services\LeadRouter;
use App\Services\MonthlyPartitionManager;
use App\Services\NotificationService;
use App\Services\RegionTagResolver;
use App\Services\MonthlyPartitionManager;
use App\Services\SupplierProjects\SupplierProjectResolver;
use Carbon\Carbon;
use Database\Seeders\PricingTierSeeder;
@@ -7,9 +7,11 @@ use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Services\LeadRouter;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\Concerns\SharesSupplierPdo;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(\Tests\Concerns\SharesSupplierPdo::class);
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
it('uses snapshot before 21:00 MSK, snapshot_date = today', function () {
Carbon::setTestNow('2026-05-28 12:00:00', 'Europe/Moscow');
@@ -8,8 +8,12 @@ use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
// Cron reports:cleanup-expired читает ReportJob и пишет pd_processing_log через
// pgsql_supplier; без SharesSupplierPdo отдельный PDO не видит незакоммиченные
// job'ы теста → 0 удалений. Трейт шарит PDO/транзакцию.
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
beforeEach(function () {
Storage::fake('local');
@@ -6,10 +6,11 @@ use App\Jobs\SyncSupplierProjectJob;
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
beforeEach(fn () => Queue::fake());
@@ -5,8 +5,9 @@ declare(strict_types=1);
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
it('returns paginated list of active projects for current tenant', function () {
$tenant = Tenant::factory()->create();
@@ -6,9 +6,10 @@ use App\Jobs\SyncSupplierProjectJob;
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Queue;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
beforeEach(fn () => Queue::fake());
@@ -6,9 +6,10 @@ use App\Jobs\SyncSupplierProjectJob;
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Queue;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
beforeEach(fn () => Queue::fake());
@@ -5,10 +5,11 @@ declare(strict_types=1);
use App\Models\Project;
use App\Models\Tenant;
use App\Services\Project\ProjectService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\Queue;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
beforeEach(function () {
Queue::fake();
@@ -5,11 +5,12 @@ declare(strict_types=1);
use App\Models\Project;
use App\Models\Tenant;
use App\Services\Project\ProjectService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
beforeEach(fn () => Queue::fake());
@@ -5,8 +5,9 @@ declare(strict_types=1);
use App\Models\Tenant;
use App\Models\User;
use App\Services\Requisites\RequisitesService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
/** @return array<string,mixed> */
function validProjectPayload(): array
@@ -4,8 +4,9 @@ declare(strict_types=1);
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
it('GET requisites returns null when none', function () {
$tenant = Tenant::factory()->create();
@@ -4,8 +4,9 @@ declare(strict_types=1);
use App\Models\Tenant;
use App\Services\Requisites\RequisitesService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
it('upsert normalizes phone and is idempotent on tenant', function () {
$tenant = Tenant::factory()->create();
@@ -6,8 +6,9 @@ use App\Models\Tenant;
use App\Models\User;
use App\Services\DaData\Dto\PartyLookupResult;
use App\Services\DaData\PartyLookup;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
it('lookup-inn returns found:false when party lookup is null (default Null driver)', function () {
$tenant = Tenant::factory()->create();
@@ -7,9 +7,11 @@ use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Services\Supplier\SupplierPortalClient;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\Concerns\SharesSupplierPdo;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(\Tests\Concerns\SharesSupplierPdo::class);
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
it('does not mark inactive supplier_project that has pivot link to active project', function () {
$tenant = Tenant::factory()->create();
@@ -39,13 +39,15 @@ test('phase A re-activates supplier_project when active liderra project still re
'unique_key' => 'reactivate.example.com',
'inactive_since' => now()->subDays(30),
]);
Project::factory()->create([
$project = Project::factory()->create([
'tenant_id' => $tenant->id,
'is_active' => true,
'signal_type' => 'site',
'signal_identifier' => 'reactivate.example.com',
'supplier_b1_project_id' => $sp->id,
]);
// Phase A смотрит pivot project_supplier_links (Plan 3+), а не legacy FK → нужна привязка.
linkProjectToSupplier($project, $sp);
(new CleanupInactiveSupplierProjectsJob)->handle();
@@ -105,13 +107,15 @@ test('phase A runs before phase C (safety ordering): returned-active is reactiva
'supplier_external_id' => '888',
'inactive_since' => now()->subDays(185),
]);
Project::factory()->create([
$project = Project::factory()->create([
'tenant_id' => $tenant->id,
'is_active' => true,
'signal_type' => 'site',
'signal_identifier' => 'edge.example.com',
'supplier_b1_project_id' => $sp->id,
]);
// Phase A смотрит pivot project_supplier_links (Plan 3+), а не legacy FK → нужна привязка.
linkProjectToSupplier($project, $sp);
Http::fake();
(new CleanupInactiveSupplierProjectsJob)->handle();
@@ -6,9 +6,10 @@ use App\Mail\SupportRequestMail;
use App\Models\SupportRequest;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Mail;
uses(\Illuminate\Foundation\Testing\DatabaseTransactions::class);
uses(DatabaseTransactions::class);
use function Pest\Laravel\actingAs;
use function Pest\Laravel\postJson;