feat(commands): supplier:check-webhook-secret deploy validator (Plan 2.6 #i)
Закрывает 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) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Deploy-time validator для supplier_webhook_secret.
|
||||
*
|
||||
* Spec: docs/superpowers/specs/2026-05-10-supplier-integration-design.md §5.1.
|
||||
* Plan 2.6 fix #i: closes CV.11 audit WARN #4 (placeholder seed на production
|
||||
* → endpoint silent 404 на ВСЕ запросы из-за verifySecret в SupplierWebhookController).
|
||||
*
|
||||
* Использование в deploy-script: `php artisan supplier:check-webhook-secret`
|
||||
* exit code 0 = OK, exit code 1 = блокирующая ошибка (deploy не должен продолжаться).
|
||||
*
|
||||
* Проверки:
|
||||
* 1. system_settings row существует с key='supplier_webhook_secret'.
|
||||
* 2. value !== '__SET_ON_DEPLOY__' (placeholder из schema seed).
|
||||
* 3. strlen(value) >= 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
|
||||
test('rejects placeholder seed __SET_ON_DEPLOY__', function () {
|
||||
DB::table('system_settings')
|
||||
->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);
|
||||
});
|
||||
Reference in New Issue
Block a user