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); });