From a8d33ceeaf649b2dd757e76bca09699cada6a925 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Mon, 20 Jul 2026 11:54:02 +0330 Subject: [PATCH] feat: enhance purge commands with environment handling and testing improvements --- docs/api/doctor-import.md | 5 +++ src/Doctor/Command/PurgeDoctorsCommand.php | 16 +++++---- .../Command/PurgeUnclaimedDoctorsCommand.php | 12 +++++-- tests/Doctor/PurgeDoctorsCommandTest.php | 27 ++++++-------- .../PurgeUnclaimedDoctorsCommandTest.php | 35 +++++++++++++++++++ 5 files changed, 70 insertions(+), 25 deletions(-) diff --git a/docs/api/doctor-import.md b/docs/api/doctor-import.md index 5bff4c04..f4269656 100644 --- a/docs/api/doctor-import.md +++ b/docs/api/doctor-import.md @@ -280,6 +280,9 @@ php bin/console app:doctors:purge-unclaimed --all-sources # هر منبعِ غ | `--all-sources` | هر پزشک claim‌نشدهٔ غیرِ `manual` | | `--i-know-this-is-prod` | لازم برای اجرا روی `APP_ENV=prod` | +> محیط از kernel خوانده می‌شود (`%kernel.environment%`)، نه `$_ENV['APP_ENV']` — تا اگر +> APP_ENV از راهی غیر از dotenv ست شده باشد گارد دور زده نشود. + > حذف FK-safe است (همان ۱۶ جدول وابستهٔ `app:doctors:purge`). اگر نوبتی به پزشک > claim‌نشده وصل باشد در dry-run هشدار می‌دهد چون آن نوبت هم حذف می‌شود. کاربر جانشین > فقط وقتی حذف می‌شود که غیرفعال (`status=0`) و با موبایلِ `imp_` باشد — تا کاربر واقعی @@ -302,6 +305,8 @@ php bin/console app:doctors:purge --force # حذف واقعی + کاربرا > `dev`/`test` اجرا می‌شود**؛ روی هر `APP_ENV` دیگری (از جمله `prod`) بدون هیچ راه > فراری با خطا متوقف می‌شود. سوییچ `--i-know-this-is-prod` اینجا **وجود ندارد** — > تنها کامند این خانواده است که هیچ راهی برای اجرا روی prod ندارد. +> محیط از `%kernel.environment%` می‌آید، پس با دست‌کاری `$_ENV` هم دور نمی‌خورد. +> (تست‌ها: `PurgeDoctorsCommandTest`، `PurgeUnclaimedDoctorsCommandTest::testRefusesOnProdWithoutExplicitSwitch`) ### خلاصهٔ تفاوت دو کامند حذف diff --git a/src/Doctor/Command/PurgeDoctorsCommand.php b/src/Doctor/Command/PurgeDoctorsCommand.php index 756960b5..6fdcf74e 100644 --- a/src/Doctor/Command/PurgeDoctorsCommand.php +++ b/src/Doctor/Command/PurgeDoctorsCommand.php @@ -9,6 +9,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Attribute\Autowire; /** * پاک‌سازی کامل همهٔ پزشکان و داده‌های وابسته — برای ساختِ یک دیتابیس تمیزِ تست. @@ -34,8 +35,11 @@ class PurgeDoctorsCommand extends Command 'holidays', 'comments', 'rates', 'appointments', 'doctors', ]; - public function __construct(private readonly Connection $conn) - { + public function __construct( + private readonly Connection $conn, + #[Autowire('%kernel.environment%')] + private readonly string $env, + ) { parent::__construct(); } @@ -50,10 +54,10 @@ class PurgeDoctorsCommand extends Command $force = (bool) $input->getOption('force'); // فقط و فقط dev/test. این کامند همهٔ پزشکان + نوبت/نظر/پرداخت را پاک می‌کند و - // هیچ راه فراری برای prod ندارد. - $env = $_ENV['APP_ENV'] ?? 'dev'; - if (!in_array($env, ['dev', 'test'], true)) { - $io->error(sprintf('این کامند فقط در محیط dev اجرا می‌شود (APP_ENV فعلی: %s).', $env)); + // هیچ راه فراری برای prod ندارد. محیط از kernel می‌آید (نه $_ENV) تا اگر APP_ENV + // از راهی غیر از dotenv ست شده باشد، به اشتباه به dev fallback نشود. + if (!in_array($this->env, ['dev', 'test'], true)) { + $io->error(sprintf('این کامند فقط در محیط dev اجرا می‌شود (APP_ENV فعلی: %s).', $this->env)); return Command::FAILURE; } diff --git a/src/Doctor/Command/PurgeUnclaimedDoctorsCommand.php b/src/Doctor/Command/PurgeUnclaimedDoctorsCommand.php index 267d7f0a..1757528d 100644 --- a/src/Doctor/Command/PurgeUnclaimedDoctorsCommand.php +++ b/src/Doctor/Command/PurgeUnclaimedDoctorsCommand.php @@ -9,6 +9,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Attribute\Autowire; /** * حذف پزشکانِ ایمپورت‌شدهٔ خزنده که هنوز مالکیتشان گرفته نشده (`owner_status='unclaimed'`). @@ -35,8 +36,11 @@ class PurgeUnclaimedDoctorsCommand extends Command 'holidays', 'comments', 'rates', 'appointments', ]; - public function __construct(private readonly Connection $conn) - { + public function __construct( + private readonly Connection $conn, + #[Autowire('%kernel.environment%')] + private readonly string $env, + ) { parent::__construct(); } @@ -54,7 +58,9 @@ class PurgeUnclaimedDoctorsCommand extends Command $io = new SymfonyStyle($input, $output); $force = (bool) $input->getOption('force'); - if (($_ENV['APP_ENV'] ?? 'dev') === 'prod' && !$input->getOption('i-know-this-is-prod')) { + // محیط از kernel می‌آید (نه $_ENV) تا گاردِ prod با ست‌شدن APP_ENV از راهی + // غیر از dotenv دور زده نشود. + if ($this->env === 'prod' && !$input->getOption('i-know-this-is-prod')) { $io->error('روی prod بدون --i-know-this-is-prod اجرا نمی‌شود.'); return Command::FAILURE; } diff --git a/tests/Doctor/PurgeDoctorsCommandTest.php b/tests/Doctor/PurgeDoctorsCommandTest.php index fc37d018..07639566 100644 --- a/tests/Doctor/PurgeDoctorsCommandTest.php +++ b/tests/Doctor/PurgeDoctorsCommandTest.php @@ -2,6 +2,8 @@ namespace App\Tests\Doctor; +use App\Doctor\Command\PurgeDoctorsCommand; +use Doctrine\DBAL\Connection; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Console\Command\Command; @@ -30,26 +32,19 @@ class PurgeDoctorsCommandTest extends KernelTestCase $this->assertStringContainsString('dry-run', $tester->getDisplay()); } + /** + * محیط از kernel تزریق می‌شود، پس گارد با ساختِ مستقیم کامند در محیط prod تست + * می‌شود — نه با دست‌کاری $_ENV که دیگر خوانده نمی‌شود. + */ public function testRefusesOnProdWithNoEscapeHatch(): void { self::bootKernel(); - $original = $_ENV['APP_ENV'] ?? null; - $_ENV['APP_ENV'] = 'prod'; + $command = new PurgeDoctorsCommand(static::getContainer()->get(Connection::class), 'prod'); + $tester = new CommandTester($command); - try { - $tester = new CommandTester( - (new Application(self::$kernel))->find('app:doctors:purge') - ); - $tester->execute(['--force' => true]); + $tester->execute(['--force' => true]); - $this->assertSame(Command::FAILURE, $tester->getStatusCode()); - $this->assertStringContainsString('فقط در محیط dev', $tester->getDisplay()); - } finally { - if ($original === null) { - unset($_ENV['APP_ENV']); - } else { - $_ENV['APP_ENV'] = $original; - } - } + $this->assertSame(Command::FAILURE, $tester->getStatusCode()); + $this->assertStringContainsString('فقط در محیط dev', $tester->getDisplay()); } } diff --git a/tests/Doctor/PurgeUnclaimedDoctorsCommandTest.php b/tests/Doctor/PurgeUnclaimedDoctorsCommandTest.php index cbd6a663..2b9ef49e 100644 --- a/tests/Doctor/PurgeUnclaimedDoctorsCommandTest.php +++ b/tests/Doctor/PurgeUnclaimedDoctorsCommandTest.php @@ -3,10 +3,12 @@ namespace App\Tests\Doctor; use App\Auth\Entity\User; +use App\Doctor\Command\PurgeUnclaimedDoctorsCommand; use App\Doctor\Entity\Doctor; use Doctrine\DBAL\Connection; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; /** @@ -78,6 +80,39 @@ class PurgeUnclaimedDoctorsCommandTest extends KernelTestCase ); } + /** برخلاف app:doctors:purge، این کامند روی prod قابل اجراست — ولی فقط با سوییچ صریح. */ + private function prodTester(): CommandTester + { + self::bootKernel(); + + return new CommandTester( + new PurgeUnclaimedDoctorsCommand(static::getContainer()->get(Connection::class), 'prod') + ); + } + + public function testRefusesOnProdWithoutExplicitSwitch(): void + { + $doctor = $this->makeDoctor('irimc', 'unclaimed'); + + $tester = $this->prodTester(); + $tester->execute(['--force' => true]); + + $this->assertSame(Command::FAILURE, $tester->getStatusCode()); + $this->assertStringContainsString('i-know-this-is-prod', $tester->getDisplay()); + $this->assertTrue($this->doctorExists($doctor->getId()), 'گاردِ prod نباید چیزی حذف کند'); + } + + public function testRunsOnProdWithExplicitSwitch(): void + { + $doctor = $this->makeDoctor('irimc', 'unclaimed'); + + $tester = $this->prodTester(); + $tester->execute(['--force' => true, '--i-know-this-is-prod' => true]); + + $tester->assertCommandIsSuccessful(); + $this->assertFalse($this->doctorExists($doctor->getId()), 'با سوییچ صریح باید روی prod حذف کند'); + } + public function testClaimedAndOtherSourceDoctorsSurvive(): void { $claimed = $this->makeDoctor('irimc', 'claimed', userStatus: 1);