Files
portal/app/tests/Feature/Supplier/PruneSupplierExtraChannelsJobTest.php
T
2026-07-16 19:15:37 +03:00

117 lines
5.2 KiB
PHP

<?php
declare(strict_types=1);
use App\Exceptions\Supplier\SupplierClientException;
use App\Jobs\Supplier\PruneSupplierExtraChannelsJob;
use App\Mail\SupplierCriticalAlertMail;
use App\Services\Supplier\SupplierPortalClient;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
beforeEach(function (): void {
config(['services.supplier.prune_extra_channels' => true]);
config(['services.supplier.alert_email' => 'ops@liderra.test']);
});
test('удаляет B6/dop2, не трогает rt/bl/mt, пишет audit-строку', function (): void {
$mock = Mockery::mock(SupplierPortalClient::class);
$mock->shouldReceive('listProjects')->andReturn([
['id' => 1, 'src' => 'rt', 'name' => 'B1_a', 'tag' => 't'],
['id' => 2, 'src' => 'bl', 'name' => 'B2_b', 'tag' => 't'],
['id' => 3, 'src' => 'mt', 'name' => 'B3_c', 'tag' => 't'],
['id' => 6, 'src' => 'dop2', 'name' => 'B6_73919889006', 'tag' => 't'],
]);
// Ожидаем удаление РОВНО id=6 (Mockery упадёт, если позовут с другим id).
$mock->shouldReceive('deleteProject')->once()->with(6);
app()->instance(SupplierPortalClient::class, $mock);
(new PruneSupplierExtraChannelsJob)->handle(app(SupplierPortalClient::class));
$log = DB::connection('pgsql_supplier')->table('supplier_sync_log')
->where('action', 'delete')->orderByDesc('id')->first();
expect($log)->not->toBeNull();
expect($log->supplier_project_id)->toBeNull();
expect($log->http_status)->toBe(200);
$payload = json_decode((string) $log->request_payload, true);
expect($payload['external_id'])->toBe(6);
expect($payload['src'])->toBe('dop2');
});
test('рубильник false → в кабинет не ходит', function (): void {
config(['services.supplier.prune_extra_channels' => false]);
$mock = Mockery::mock(SupplierPortalClient::class);
$mock->shouldNotReceive('listProjects');
$mock->shouldNotReceive('deleteProject');
app()->instance(SupplierPortalClient::class, $mock);
(new PruneSupplierExtraChannelsJob)->handle(app(SupplierPortalClient::class));
})->throwsNoExceptions();
test('пустой список → ни одного удаления', function (): void {
$mock = Mockery::mock(SupplierPortalClient::class);
$mock->shouldReceive('listProjects')->andReturn([]);
$mock->shouldNotReceive('deleteProject');
app()->instance(SupplierPortalClient::class, $mock);
(new PruneSupplierExtraChannelsJob)->handle(app(SupplierPortalClient::class));
})->throwsNoExceptions();
test('стоп-кран: доля под удаление >= 50% → ничего не удаляем + тревога', function (): void {
Mail::fake();
$mock = Mockery::mock(SupplierPortalClient::class);
// 1 из 2 = 50% >= порога → стоп.
$mock->shouldReceive('listProjects')->andReturn([
['id' => 1, 'src' => 'rt', 'name' => 'B1_a'],
['id' => 6, 'src' => 'dop2', 'name' => 'B6_x'],
]);
$mock->shouldNotReceive('deleteProject');
app()->instance(SupplierPortalClient::class, $mock);
(new PruneSupplierExtraChannelsJob)->handle(app(SupplierPortalClient::class));
Mail::assertQueued(SupplierCriticalAlertMail::class);
});
test('404 на удалении трактуется как успех, джоба не падает', function (): void {
$mock = Mockery::mock(SupplierPortalClient::class);
// 1 из 3 = 33% < порога.
$mock->shouldReceive('listProjects')->andReturn([
['id' => 1, 'src' => 'rt', 'name' => 'B1_a'],
['id' => 2, 'src' => 'rt', 'name' => 'B1_b'],
['id' => 6, 'src' => 'dop2', 'name' => 'B6_x'],
]);
$mock->shouldReceive('deleteProject')->once()->with(6)
->andThrow(new SupplierClientException('gone', httpStatus: 404));
app()->instance(SupplierPortalClient::class, $mock);
(new PruneSupplierExtraChannelsJob)->handle(app(SupplierPortalClient::class));
$log = DB::connection('pgsql_supplier')->table('supplier_sync_log')
->where('action', 'delete')->orderByDesc('id')->first();
expect($log->http_status)->toBe(404);
})->throwsNoExceptions();
test('ошибка (не 404) на одном доп-канале не мешает удалить остальные', function (): void {
$mock = Mockery::mock(SupplierPortalClient::class);
// 2 из 5 = 40% < порога.
$mock->shouldReceive('listProjects')->andReturn([
['id' => 1, 'src' => 'rt', 'name' => 'B1_a'],
['id' => 2, 'src' => 'rt', 'name' => 'B1_b'],
['id' => 3, 'src' => 'rt', 'name' => 'B1_c'],
['id' => 6, 'src' => 'dop2', 'name' => 'B6_x'],
['id' => 8, 'src' => 'seg', 'name' => 'B8_y'],
]);
$mock->shouldReceive('deleteProject')->once()->with(6)
->andThrow(new SupplierClientException('boom', httpStatus: 400));
$mock->shouldReceive('deleteProject')->once()->with(8); // всё равно удаляется
app()->instance(SupplierPortalClient::class, $mock);
(new PruneSupplierExtraChannelsJob)->handle(app(SupplierPortalClient::class));
})->throwsNoExceptions();