test: дозакрытие оздоровления — protekateli pd-аудита, видимость supplier, новый флоу регистрации
Accessibility (Pa11y live) / a11y (push) Has been cancelled
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:
@@ -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' => 'ООО Альфа']);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user