316bc2d852
Убраны письма 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)
75 lines
3.2 KiB
PHP
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');
|
|
});
|