3be76152ed
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
117 lines
5.2 KiB
PHP
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();
|