Files
portal/app/tests/Feature/Supplier/FailoverProjectChannelLiveSmokeTest.php
T
Дмитрий 316bc2d852 fix(supplier): FailoverProjectChannel не шлёт per-project CRITICAL письма
Убраны письма manual_required и failover_to_form (по проекту, без троттла) —
источник потопа ~116 писем на ops@ 07.07. Строка в ручной очереди остаётся
(запись видна в админке). Зависимость Mailer удалена из канала и DI-привязки.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 72818f5a80)
2026-07-07 19:19:09 +03:00

75 lines
3.2 KiB
PHP

<?php
declare(strict_types=1);
use App\Exceptions\Supplier\SupplierClientException;
use App\Exceptions\Supplier\SupplierTransientException;
use App\Models\Project;
use App\Models\SupplierManualSyncQueue;
use App\Models\Tenant;
use App\Services\Supplier\Channel\Exceptions\TierEscalatedException;
use App\Services\Supplier\Channel\FailoverProjectChannel;
use App\Services\Supplier\Channel\SupplierProjectChannel;
use App\Services\Supplier\Dto\SupplierProjectDto;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
uses(RefreshDatabase::class);
test('Tier-1 fail + Tier-2 fail → Tier-3 escalation creates manual queue row, no per-project alert mail', function (): void {
Mail::fake();
config(['services.supplier.alert_email' => 'ops@liderra.local']);
$tenant = Tenant::factory()->create();
$project = Project::factory()->for($tenant)->create();
$tier1 = mock(SupplierProjectChannel::class);
$tier1->shouldReceive('listProjects')->andReturn([]); // dedup-сверка: нет совпадений
$tier1->shouldReceive('createProject')->andThrow(new SupplierClientException('Tier-1 mock fail'));
$tier2 = mock(SupplierProjectChannel::class);
$tier2->shouldReceive('createProject')->andThrow(new RuntimeException('Tier-2 manage-project.js selector break'));
$channel = new FailoverProjectChannel($tier1, $tier2);
$dto = new SupplierProjectDto(
platform: 'B1', signalType: 'site', uniqueKey: 'failover-smoke.example',
limit: 1, workdays: [1, 2, 3, 4, 5], regions: [], regionsReverse: false, status: 'active',
);
expect(fn () => $channel->createProjectForLiderra($project, $dto))
->toThrow(TierEscalatedException::class);
expect(SupplierManualSyncQueue::where('project_id', $project->id)->count())->toBe(1);
Mail::assertNothingQueued(); // per-project тревога убрана — запись в ручной очереди достаточна
});
test('Tier-1 transient fail (portal unreachable) bypasses Tier-2 and goes straight to Tier-3', function (): void {
Mail::fake();
config(['services.supplier.alert_email' => 'ops@liderra.local']);
$tenant = Tenant::factory()->create();
$project = Project::factory()->for($tenant)->create();
$tier1 = mock(SupplierProjectChannel::class);
$tier1->shouldReceive('listProjects')->andReturn([]);
$tier1->shouldReceive('createProject')->andThrow(new SupplierTransientException('Connection refused'));
$tier2 = mock(SupplierProjectChannel::class);
$tier2->shouldNotReceive('createProject'); // КЛЮЧЕВОЕ — transient НЕ должен попасть в tier-2
$channel = new FailoverProjectChannel($tier1, $tier2);
$dto = new SupplierProjectDto(
platform: 'B1', signalType: 'site', uniqueKey: 'transient-smoke.example',
limit: 1, workdays: [1, 2, 3, 4, 5], regions: [], regionsReverse: false, status: 'active',
);
expect(fn () => $channel->createProjectForLiderra($project, $dto))
->toThrow(TierEscalatedException::class);
$row = SupplierManualSyncQueue::where('project_id', $project->id)->first();
expect($row->failure_reason)->toBe('portal_unreachable');
});