3266909346
Audit J5/D4/D5: the outbound_webhook_subscriptions table existed in schema but had zero code. Adds the OutboundWebhookSubscription model + factory and WebhookSettingsController with GET/PUT /api/tenants/me/webhook-settings (one subscription per tenant; secret generated + returned once on creation, bcrypt-hashed) and POST /api/webhooks/test (unsigned connectivity check — HMAC-signed event delivery is a separate post-MVP epic). Tenant-scoped via auth:sanctum + tenant middleware. phpstan-baseline.neon: additive-only entries for new test file (Pest\PendingCalls\TestCall false-positives — documented project pattern) and OutboundWebhookSubscriptionFactory method.childReturnType (same pattern as ProjectFactory/TenantFactory/UserFactory already in baseline). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
4.0 KiB
PHP
103 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\OutboundWebhookSubscription;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
beforeEach(function () {
|
|
$this->tenant = Tenant::factory()->create();
|
|
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
|
|
$this->actingAs($this->user);
|
|
});
|
|
|
|
test('GET webhook-settings: null когда подписки нет', function () {
|
|
$response = $this->getJson('/api/tenants/me/webhook-settings');
|
|
$response->assertOk();
|
|
expect($response->json('data'))->toBeNull();
|
|
});
|
|
|
|
test('GET webhook-settings возвращает подписку тенанта', function () {
|
|
OutboundWebhookSubscription::factory()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'user_id' => $this->user->id,
|
|
'target_url' => 'https://crm.example.ru/hook',
|
|
]);
|
|
|
|
$response = $this->getJson('/api/tenants/me/webhook-settings');
|
|
|
|
$response->assertOk();
|
|
expect($response->json('data.target_url'))->toBe('https://crm.example.ru/hook');
|
|
expect($response->json('data'))->toHaveKeys(['target_url', 'secret_prefix', 'events', 'is_active']);
|
|
expect($response->json('data'))->not->toHaveKey('secret_hash');
|
|
});
|
|
|
|
test('PUT webhook-settings создаёт подписку и возвращает secret один раз', function () {
|
|
$response = $this->putJson('/api/tenants/me/webhook-settings', [
|
|
'target_url' => 'https://crm.example.ru/hook',
|
|
]);
|
|
|
|
$response->assertOk();
|
|
expect($response->json('data.target_url'))->toBe('https://crm.example.ru/hook');
|
|
expect($response->json('data.secret'))->toStartWith('whsec_');
|
|
expect($response->json('data.events'))->toBeArray()->not->toBeEmpty();
|
|
|
|
$row = OutboundWebhookSubscription::query()->where('tenant_id', $this->tenant->id)->first();
|
|
expect($row)->not->toBeNull();
|
|
expect(Hash::check($response->json('data.secret'), $row->secret_hash))->toBeTrue();
|
|
});
|
|
|
|
test('PUT webhook-settings обновляет URL существующей подписки без нового secret', function () {
|
|
OutboundWebhookSubscription::factory()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'user_id' => $this->user->id,
|
|
'target_url' => 'https://old.example.ru/hook',
|
|
]);
|
|
|
|
$response = $this->putJson('/api/tenants/me/webhook-settings', [
|
|
'target_url' => 'https://new.example.ru/hook',
|
|
]);
|
|
|
|
$response->assertOk();
|
|
expect($response->json('data.target_url'))->toBe('https://new.example.ru/hook');
|
|
expect($response->json('data'))->not->toHaveKey('secret');
|
|
expect(OutboundWebhookSubscription::query()->where('tenant_id', $this->tenant->id)->count())->toBe(1);
|
|
});
|
|
|
|
test('PUT webhook-settings: 422 при не-https URL', function () {
|
|
$this->putJson('/api/tenants/me/webhook-settings', [
|
|
'target_url' => 'http://insecure.example.ru/hook',
|
|
])->assertStatus(422)->assertJsonValidationErrorFor('target_url');
|
|
});
|
|
|
|
test('POST webhooks/test отправляет запрос и возвращает результат', function () {
|
|
Http::fake(['*' => Http::response(['ok' => true], 200)]);
|
|
OutboundWebhookSubscription::factory()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'user_id' => $this->user->id,
|
|
'target_url' => 'https://crm.example.ru/hook',
|
|
]);
|
|
|
|
$response = $this->postJson('/api/webhooks/test');
|
|
|
|
$response->assertOk();
|
|
expect($response->json('ok'))->toBeTrue();
|
|
expect($response->json('status'))->toBe(200);
|
|
Http::assertSent(fn ($req) => $req->url() === 'https://crm.example.ru/hook');
|
|
});
|
|
|
|
test('POST webhooks/test: 422 когда подписки нет', function () {
|
|
$this->postJson('/api/webhooks/test')->assertStatus(422);
|
|
});
|
|
|
|
test('GET webhook-settings без auth: 401', function () {
|
|
auth()->logout();
|
|
$this->getJson('/api/tenants/me/webhook-settings')->assertStatus(401);
|
|
});
|