From e71a02e498a31bcdf860846a96db47136a4f18ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sun, 10 May 2026 23:04:32 +0300 Subject: [PATCH] feat(commands): supplier:check-webhook-secret deploy validator (Plan 2.6 #i) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Закрывает CV.11 audit WARN #4 (placeholder secret '__SET_ON_DEPLOY__' = silent 404 на production через verifySecret в SupplierWebhookController). Console command для deploy-script: SELECT system_settings.supplier_webhook_secret → exit 1 если placeholder OR len < 32 OR row отсутствует. Иначе exit 0. Использование: deploy-script вызывает `php artisan supplier:check-webhook-secret` перед запуском приложения; non-zero exit прерывает deploy fail-fast. TDD: 4 теста (placeholder rejected / short rejected / missing rejected / valid accepted). phpstan-baseline +1 entry: Pest TestCall::artisan() PhpDoc-quirk (как ResetDeliveredTodayCommandTest). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../CheckSupplierWebhookSecretCommand.php | 59 +++++++++++++++++++ app/phpstan-baseline.neon | 6 ++ .../CheckSupplierWebhookSecretCommandTest.php | 44 ++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 app/app/Console/Commands/CheckSupplierWebhookSecretCommand.php create mode 100644 app/tests/Feature/Console/CheckSupplierWebhookSecretCommandTest.php diff --git a/app/app/Console/Commands/CheckSupplierWebhookSecretCommand.php b/app/app/Console/Commands/CheckSupplierWebhookSecretCommand.php new file mode 100644 index 00000000..6c671219 --- /dev/null +++ b/app/app/Console/Commands/CheckSupplierWebhookSecretCommand.php @@ -0,0 +1,59 @@ += 32 chars (требование verifySecret в SupplierWebhookController). + */ +class CheckSupplierWebhookSecretCommand extends Command +{ + protected $signature = 'supplier:check-webhook-secret'; + + protected $description = 'Deploy-time validator: проверка supplier_webhook_secret seed (Plan 2.6 fix #i)'; + + public function handle(): int + { + $row = DB::table('system_settings')->where('key', 'supplier_webhook_secret')->first(); + + if ($row === null) { + $this->error('FAIL: system_settings row для key=supplier_webhook_secret не найдена. Schema seed повреждён или БД не мигрирована.'); + + return self::FAILURE; + } + + $value = (string) $row->value; + + if ($value === '__SET_ON_DEPLOY__') { + $this->error('FAIL: supplier_webhook_secret = "__SET_ON_DEPLOY__" (placeholder из schema seed). Override через UPDATE system_settings перед deploy.'); + + return self::FAILURE; + } + + if (strlen($value) < 32) { + $this->error('FAIL: supplier_webhook_secret слишком короткий (length='.strlen($value).', нужно >=32 chars для совместимости с verifySecret в SupplierWebhookController).'); + + return self::FAILURE; + } + + $this->info('OK: supplier_webhook_secret valid (length='.strlen($value).' chars, не placeholder).'); + + return self::SUCCESS; + } +} diff --git a/app/phpstan-baseline.neon b/app/phpstan-baseline.neon index 20f7e7d1..86cae2ca 100644 --- a/app/phpstan-baseline.neon +++ b/app/phpstan-baseline.neon @@ -911,3 +911,9 @@ parameters: identifier: method.notFound count: 2 path: tests/Feature/Console/ResetDeliveredTodayCommandTest.php + + - + message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:artisan\(\)\.$#' + identifier: method.notFound + count: 4 + path: tests/Feature/Console/CheckSupplierWebhookSecretCommandTest.php diff --git a/app/tests/Feature/Console/CheckSupplierWebhookSecretCommandTest.php b/app/tests/Feature/Console/CheckSupplierWebhookSecretCommandTest.php new file mode 100644 index 00000000..b12b0dd4 --- /dev/null +++ b/app/tests/Feature/Console/CheckSupplierWebhookSecretCommandTest.php @@ -0,0 +1,44 @@ +updateOrInsert( + ['key' => 'supplier_webhook_secret'], + ['value' => '__SET_ON_DEPLOY__', 'type' => 'string', 'description' => 'test seed'] + ); + + $this->artisan('supplier:check-webhook-secret')->assertExitCode(1); +}); + +test('rejects too-short secret (< 32 chars)', function () { + DB::table('system_settings') + ->updateOrInsert( + ['key' => 'supplier_webhook_secret'], + ['value' => 'short-secret-only-20-chars', 'type' => 'string', 'description' => 'test'] + ); + + $this->artisan('supplier:check-webhook-secret')->assertExitCode(1); +}); + +test('rejects missing seed row', function () { + DB::table('system_settings')->where('key', 'supplier_webhook_secret')->delete(); + + $this->artisan('supplier:check-webhook-secret')->assertExitCode(1); +}); + +test('accepts valid secret (>=32 chars and not placeholder)', function () { + DB::table('system_settings') + ->updateOrInsert( + ['key' => 'supplier_webhook_secret'], + ['value' => str_repeat('a', 64), 'type' => 'string', 'description' => 'test seed'] + ); + + $this->artisan('supplier:check-webhook-secret')->assertExitCode(0); +});