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://93.184.216.34/hook', ]); $response = $this->getJson('/api/tenants/me/webhook-settings'); $response->assertOk(); expect($response->json('data.target_url'))->toBe('https://93.184.216.34/hook'); expect($response->json('data'))->toHaveKeys(['target_url', 'secret_prefix', 'events', 'is_active']); expect($response->json('data'))->not->toHaveKey('secret_hash'); }); test('GET webhook-settings изолирован по тенанту', function () { $otherTenant = Tenant::factory()->create(); $otherUser = User::factory()->create(['tenant_id' => $otherTenant->id]); OutboundWebhookSubscription::factory()->create([ 'tenant_id' => $otherTenant->id, 'user_id' => $otherUser->id, 'target_url' => 'https://other.example.ru/hook', ]); $response = $this->getJson('/api/tenants/me/webhook-settings'); $response->assertOk(); expect($response->json('data'))->toBeNull(); }); test('PUT webhook-settings создаёт подписку и возвращает secret один раз', function () { $response = $this->putJson('/api/tenants/me/webhook-settings', [ 'target_url' => 'https://93.184.216.34/hook', ]); $response->assertOk(); expect($response->json('data.target_url'))->toBe('https://93.184.216.34/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://8.8.8.8/hook', ]); $response = $this->putJson('/api/tenants/me/webhook-settings', [ 'target_url' => 'https://1.1.1.1/hook', ]); $response->assertOk(); expect($response->json('data.target_url'))->toBe('https://1.1.1.1/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('PUT webhook-settings: 422 для приватного/служебного IP в target_url (SSRF), не сохраняет', function () { $this->putJson('/api/tenants/me/webhook-settings', [ 'target_url' => 'https://169.254.169.254/hook', ])->assertStatus(422)->assertJsonValidationErrorFor('target_url'); expect(OutboundWebhookSubscription::query()->where('tenant_id', $this->tenant->id)->count())->toBe(0); }); 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://93.184.216.34/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://93.184.216.34/hook'); }); test('POST webhooks/test возвращает ok=false при ошибке endpoint', function () { Http::fake(['*' => Http::response([], 500)]); OutboundWebhookSubscription::factory()->create([ 'tenant_id' => $this->tenant->id, 'user_id' => $this->user->id, 'target_url' => 'https://93.184.216.34/hook', ]); $response = $this->postJson('/api/webhooks/test'); $response->assertOk(); expect($response->json('ok'))->toBeFalse(); expect($response->json('status'))->toBe(500); }); 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); });