2026-05-26 11:27:53 +03:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
namespace Tests\Unit\Services\Project;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Http\Controllers\Api\ProjectController;
|
|
|
|
|
|
use App\Services\Project\ProjectService;
|
|
|
|
|
|
use ReflectionMethod;
|
|
|
|
|
|
use Tests\TestCase;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Гарантирует, что переключение `is_active` всегда сопровождается записью `paused_at`:
|
|
|
|
|
|
* - is_active = false → paused_at := NOW()
|
|
|
|
|
|
* - is_active = true → paused_at := null
|
|
|
|
|
|
*
|
|
|
|
|
|
* Без этого SupplierSnapshotGuard для bulk-paused проектов начнёт считать их
|
|
|
|
|
|
* "защищёнными навсегда" (paused_at NULL trait), и удаление никогда не разблокируется.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Тест читает исходник методов и проверяет наличие явной записи `paused_at` рядом
|
|
|
|
|
|
* с записью `is_active`. Это структурный smoke — поведенческие тесты (через БД)
|
|
|
|
|
|
* пишутся отдельно (Task 14 final regression).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Spec: docs/superpowers/plans/2026-05-26-supplier-snapshot-guard.md (Task 11).
|
|
|
|
|
|
*/
|
|
|
|
|
|
class PausedAtWriteSideTest extends TestCase
|
|
|
|
|
|
{
|
|
|
|
|
|
public function test_project_service_bulk_pause_resume_writes_paused_at(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$body = $this->methodBody(ProjectService::class, 'bulkPauseResume');
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('paused_at', $body,
|
|
|
|
|
|
'bulkPauseResume должен явно обновлять paused_at вместе с is_active');
|
|
|
|
|
|
$this->assertStringContainsString('is_active', $body);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function test_project_controller_toggle_active_writes_paused_at(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$body = $this->methodBody(ProjectController::class, 'toggleActive');
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('paused_at', $body,
|
|
|
|
|
|
'toggleActive должен явно обновлять paused_at вместе с is_active');
|
|
|
|
|
|
$this->assertStringContainsString('is_active', $body);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 11:28:57 +03:00
|
|
|
|
public function test_bulk_delete_distinguishes_supplier_snapshot_lock_from_has_deals(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$body = $this->methodBody(ProjectService::class, 'bulkDelete');
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('supplier_snapshot_locked', $body,
|
|
|
|
|
|
'bulkDelete должен помечать пропущенные проекты reason="supplier_snapshot_locked" при guard-блоке');
|
|
|
|
|
|
$this->assertStringContainsString('has_deals', $body);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 11:27:53 +03:00
|
|
|
|
private function methodBody(string $class, string $method): string
|
|
|
|
|
|
{
|
|
|
|
|
|
$rm = new ReflectionMethod($class, $method);
|
|
|
|
|
|
$lines = file($rm->getFileName());
|
|
|
|
|
|
$body = array_slice($lines, $rm->getStartLine() - 1, $rm->getEndLine() - $rm->getStartLine() + 1);
|
|
|
|
|
|
|
|
|
|
|
|
return implode('', $body);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|