Files
portal/app/tests/Feature/Supplier/CleanupInactiveSupplierProjectsJobTest.php
T
Дмитрий 88ace4e3d9
Accessibility (Pa11y live) / a11y (push) Has been cancelled
test: дозакрытие оздоровления — protekateli pd-аудита, видимость supplier, новый флоу регистрации
Снижение остатка 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>
2026-06-25 08:19:53 +03:00

168 lines
5.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Jobs\Supplier\CleanupInactiveSupplierProjectsJob;
use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\SupplierSyncLog;
use App\Models\Tenant;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
beforeEach(function (): void {
Cache::store('redis')->put('supplier:session', [
'phpsessid' => 'sess',
'csrf' => 'csrf',
'refreshed_at' => now()->toIso8601String(),
], now()->addHours(6));
config(['services.supplier.portal_url' => 'https://crm.bp-gr.ru']);
});
afterEach(function (): void {
Cache::store('redis')->forget('supplier:session');
Carbon::setTestNow();
});
test('phase A re-activates supplier_project when active liderra project still references it', function (): void {
$tenant = Tenant::factory()->create();
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'reactivate.example.com',
'inactive_since' => now()->subDays(30),
]);
$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();
expect($sp->fresh()->inactive_since)->toBeNull();
});
test('phase B marks inactive_since=NOW for newly orphaned supplier_project', function (): void {
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'orphan.example.com',
'inactive_since' => null,
]);
Carbon::setTestNow(Carbon::parse('2026-05-12 02:00:00'));
(new CleanupInactiveSupplierProjectsJob)->handle();
expect($sp->fresh()->inactive_since)->not->toBeNull();
});
test('phase C deletes supplier_project after 180 days inactive and writes audit row', function (): void {
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'old.example.com',
'supplier_external_id' => '999',
'inactive_since' => now()->subDays(181),
]);
Http::fake([
'crm.bp-gr.ru/admin/visit/rt-project-delete' => Http::response(
['status' => 'OK', 'message' => '', 'result' => null],
200,
),
]);
(new CleanupInactiveSupplierProjectsJob)->handle();
expect(SupplierProject::on('pgsql_supplier')->find($sp->id))->toBeNull();
// FK ON DELETE SET NULL зануляет supplier_project_id у лога после delete'а
// supplier_project — трассировка через request_payload (JSONB snapshot).
expect(
SupplierSyncLog::on('pgsql_supplier')
->where('action', 'delete')
->where('http_status', 200)
->whereRaw("request_payload->>'supplier_project_id' = ?", [(string) $sp->id])
->exists()
)->toBeTrue();
});
test('phase A runs before phase C (safety ordering): returned-active is reactivated, not deleted', function (): void {
$tenant = Tenant::factory()->create();
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'edge.example.com',
'supplier_external_id' => '888',
'inactive_since' => now()->subDays(185),
]);
$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();
expect($sp->fresh()?->inactive_since)->toBeNull();
expect(SupplierProject::on('pgsql_supplier')->find($sp->id))->not->toBeNull();
Http::assertNothingSent();
});
test('handles 404 from supplier as already-deleted: local delete + audit row with 404 status', function (): void {
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'ghost.example.com',
'supplier_external_id' => '777',
'inactive_since' => now()->subDays(181),
]);
Http::fake([
'crm.bp-gr.ru/admin/visit/rt-project-delete' => Http::response('not found', 404),
]);
(new CleanupInactiveSupplierProjectsJob)->handle();
expect(SupplierProject::on('pgsql_supplier')->find($sp->id))->toBeNull();
expect(
SupplierSyncLog::on('pgsql_supplier')
->where('action', 'delete')
->where('http_status', 404)
->whereRaw("request_payload->>'supplier_project_id' = ?", [(string) $sp->id])
->exists()
)->toBeTrue();
});
test('does not delete supplier_project marked inactive less than 180 days ago', function (): void {
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'recent.example.com',
'supplier_external_id' => '555',
'inactive_since' => now()->subDays(100),
]);
Http::fake();
(new CleanupInactiveSupplierProjectsJob)->handle();
expect(SupplierProject::on('pgsql_supplier')->find($sp->id))->not->toBeNull();
Http::assertNothingSent();
});