Files
portal/app/tests/Feature/Http/Webhook/WebhookHardeningTest.php
T

93 lines
3.6 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Jobs\RouteSupplierLeadJob;
use App\Models\SupplierLead;
use App\Models\SystemSetting;
use App\Support\WebhookUrlGuard;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Bus;
uses(DatabaseTransactions::class);
beforeEach(function () {
SystemSetting::query()->where('key', 'supplier_webhook_secret')
->update(['value' => 'test-secret-32chars-aaaaaaaaaaaaaa']);
SystemSetting::query()->where('key', 'supplier_ip_allowlist')->update(['value' => '[]']);
});
// --- Часть А: DNS-rebind пиннинг (юнит на WebhookUrlGuard::safeDeliveryIp) ---
test('safeDeliveryIp блокирует приватный/служебный адрес и не отдаёт ip для пиннинга', function () {
foreach ([
'https://10.0.0.1/hook',
'https://169.254.169.254/hook',
'https://127.0.0.1/hook',
'https://192.168.1.1/hook',
] as $url) {
$result = WebhookUrlGuard::safeDeliveryIp($url);
expect($result['blockReason'])->not->toBeNull()
->and($result['ip'])->toBeNull();
}
});
test('safeDeliveryIp пропускает публичный IP и отдаёт его для пиннинга', function () {
$result = WebhookUrlGuard::safeDeliveryIp('https://1.1.1.1/hook');
expect($result['blockReason'])->toBeNull()
->and($result['ip'])->toBe('1.1.1.1');
});
// --- Часть Б: аддитивный HMAC для supplier-webhook ---
test('secretless /api/webhook/supplier принимает валидную HMAC-подпись → 202', function () {
Bus::fake();
$secret = 'test-secret-32chars-aaaaaaaaaaaaaa';
$body = json_encode(['vid' => 55501, 'project' => 'B1_hmac.ru', 'phone' => '79991234567', 'time' => time()]);
$sig = hash_hmac('sha256', $body, $secret);
$response = $this->call('POST', '/api/webhook/supplier', [], [], [], [
'CONTENT_TYPE' => 'application/json',
'HTTP_ACCEPT' => 'application/json',
'HTTP_X_WEBHOOK_SIGNATURE' => $sig,
], $body);
expect($response->getStatusCode())->toBe(202);
expect(SupplierLead::where('vid', 55501)->exists())->toBeTrue();
Bus::assertDispatched(RouteSupplierLeadJob::class);
});
test('secretless /api/webhook/supplier без подписи → 404', function () {
$body = json_encode(['vid' => 55502, 'project' => 'B1_hmac.ru', 'phone' => '79991234567', 'time' => time()]);
$response = $this->call('POST', '/api/webhook/supplier', [], [], [], [
'CONTENT_TYPE' => 'application/json',
'HTTP_ACCEPT' => 'application/json',
], $body);
expect($response->getStatusCode())->toBe(404);
});
test('secretless /api/webhook/supplier с неверной подписью → 404', function () {
$body = json_encode(['vid' => 55503, 'project' => 'B1_hmac.ru', 'phone' => '79991234567', 'time' => time()]);
$response = $this->call('POST', '/api/webhook/supplier', [], [], [], [
'CONTENT_TYPE' => 'application/json',
'HTTP_ACCEPT' => 'application/json',
'HTTP_X_WEBHOOK_SIGNATURE' => 'deadbeefdeadbeefdeadbeefdeadbeef',
], $body);
expect($response->getStatusCode())->toBe(404);
});
test('существующий {secret}-маршрут продолжает принимать по URL-секрету → 202', function () {
Bus::fake();
$response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [
'vid' => 55504, 'project' => 'B1_hmac.ru', 'phone' => '79991234567', 'time' => time(),
]);
expect($response->getStatusCode())->toBe(202);
});