e41c8f5aef
Defense-in-depth: secret (≥32 chars system_setting) + IP allowlist (CIDR). Несовпадение → 404. UNIQUE vid → 200 OK на дубль (idempotency). Тесты пока FAIL (route регистрируется в Task 7 — пишем "красные" тесты заранее для TDD-цикла). Spec §5.1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
3.7 KiB
PHP
101 lines
3.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\RouteSupplierLeadJob;
|
|
use App\Models\SupplierLead;
|
|
use App\Models\SystemSetting;
|
|
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' => '[]']);
|
|
});
|
|
|
|
it('returns 404 for invalid secret', function () {
|
|
$response = $this->postJson('/api/webhook/supplier/wrong-secret', [
|
|
'vid' => 1, 'project' => 'B1_test.ru', 'phone' => '79991234567', 'time' => time(),
|
|
]);
|
|
$response->assertStatus(404);
|
|
});
|
|
|
|
it('returns 404 if IP not in allowlist (when allowlist non-empty)', function () {
|
|
SystemSetting::query()->where('key', 'supplier_ip_allowlist')
|
|
->update(['value' => '["1.2.3.4", "10.0.0.0/24"]']);
|
|
|
|
$response = $this->withServerVariables(['REMOTE_ADDR' => '5.6.7.8'])
|
|
->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [
|
|
'vid' => 1, 'project' => 'B1_test.ru', 'phone' => '79991234567', 'time' => time(),
|
|
]);
|
|
$response->assertStatus(404);
|
|
});
|
|
|
|
it('passes IP allowlist when IP matches CIDR', function () {
|
|
SystemSetting::query()->where('key', 'supplier_ip_allowlist')
|
|
->update(['value' => '["10.0.0.0/24"]']);
|
|
Bus::fake();
|
|
|
|
$response = $this->withServerVariables(['REMOTE_ADDR' => '10.0.0.50'])
|
|
->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [
|
|
'vid' => 1, 'project' => 'B1_test.ru', 'phone' => '79991234567', 'time' => time(),
|
|
]);
|
|
$response->assertStatus(202);
|
|
});
|
|
|
|
it('inserts supplier_lead row + dispatches RouteSupplierLeadJob', function () {
|
|
Bus::fake();
|
|
|
|
$response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [
|
|
'vid' => 432176649,
|
|
'project' => 'B1_vashinvestor.ru',
|
|
'tag' => 'Ваш инвестор',
|
|
'phone' => '79991234567',
|
|
'phones' => ['79991234567'],
|
|
'time' => 1703781939,
|
|
]);
|
|
|
|
$response->assertStatus(202);
|
|
expect(SupplierLead::where('vid', 432176649)->exists())->toBeTrue();
|
|
Bus::assertDispatched(RouteSupplierLeadJob::class);
|
|
});
|
|
|
|
it('returns 200 OK on duplicate vid (idempotency)', function () {
|
|
SupplierLead::factory()->create(['vid' => 12345]);
|
|
Bus::fake();
|
|
|
|
$response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [
|
|
'vid' => 12345,
|
|
'project' => 'B1_test.ru',
|
|
'phone' => '79991234567',
|
|
'time' => time(),
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
expect(SupplierLead::where('vid', 12345)->count())->toBe(1);
|
|
Bus::assertNotDispatched(RouteSupplierLeadJob::class);
|
|
});
|
|
|
|
it('rejects invalid payload (missing vid) with 422', function () {
|
|
$response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [
|
|
'project' => 'B1_test.ru', 'phone' => '79991234567', 'time' => time(),
|
|
]);
|
|
$response->assertStatus(422)->assertJsonValidationErrors('vid');
|
|
});
|
|
|
|
it('rejects invalid phone format (not 7XXXXXXXXXX) with 422', function () {
|
|
$response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [
|
|
'vid' => 1, 'project' => 'B1_test.ru', 'phone' => '89991234567', 'time' => time(),
|
|
]);
|
|
$response->assertStatus(422);
|
|
});
|
|
|
|
it('rejects invalid project format (no B[123]_ prefix) with 422', function () {
|
|
$response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [
|
|
'vid' => 1, 'project' => 'invalid_format', 'phone' => '79991234567', 'time' => time(),
|
|
]);
|
|
$response->assertStatus(422);
|
|
});
|