Files
portal/app/tests/Feature/Notifications/InAppNotificationApiTest.php
T

179 lines
6.9 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\InAppNotification;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* Тесты API для in-app уведомлений: GET /api/notifications + PATCH /{id}/read +
* POST /mark-all-read + DELETE /{id}. Все endpoint'ы под Sanctum SPA auth.
*
* RLS-проверка через защиту от кражи чужого id (where user_id) — постgres
* superuser BYPASSRLS в тестах, но controller-level фильтр работает.
*/
uses(DatabaseTransactions::class);
beforeEach(function () {
$this->tenant = Tenant::factory()->create();
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
$this->actingAs($this->user);
});
function makeNotif(int $userId, int $tenantId, ?string $readAt = null, string $event = 'new_lead'): InAppNotification
{
return InAppNotification::create([
'tenant_id' => $tenantId,
'user_id' => $userId,
'event' => $event,
'title' => 'Новый лид — Caranga',
'body' => '79001234567',
'deal_id' => 42,
'payload' => ['deal_id' => 42, 'project_name' => 'Caranga'],
'read_at' => $readAt,
]);
}
test('GET /api/notifications: 401 без auth', function () {
auth()->logout();
$this->getJson('/api/notifications')->assertStatus(401);
});
test('GET /api/notifications: пустой', function () {
$response = $this->getJson('/api/notifications');
$response->assertOk();
expect($response->json('items'))->toBe([]);
expect($response->json('unread_count'))->toBe(0);
expect($response->json('total'))->toBe(0);
});
test('GET /api/notifications: возвращает только свои + сортировка по created_at DESC', function () {
$other = User::factory()->create(['tenant_id' => $this->tenant->id]);
makeNotif($this->user->id, $this->tenant->id);
sleep(1);
$newer = makeNotif($this->user->id, $this->tenant->id);
makeNotif($other->id, $this->tenant->id); // чужое
$response = $this->getJson('/api/notifications');
$response->assertOk();
expect($response->json('total'))->toBe(2);
expect($response->json('items.0.id'))->toBe($newer->id); // newer first
expect($response->json('unread_count'))->toBe(2);
});
test('GET /api/notifications?unread_only=1: только непрочитанные', function () {
makeNotif($this->user->id, $this->tenant->id, readAt: now()->toIso8601String());
makeNotif($this->user->id, $this->tenant->id);
$response = $this->getJson('/api/notifications?unread_only=1');
expect($response->json('items'))->toHaveCount(1);
expect($response->json('items.0.read_at'))->toBeNull();
expect($response->json('unread_count'))->toBe(1);
expect($response->json('total'))->toBe(2);
});
test('GET /api/notifications?limit=2: лимитирует выдачу', function () {
for ($i = 0; $i < 5; $i++) {
makeNotif($this->user->id, $this->tenant->id);
}
$response = $this->getJson('/api/notifications?limit=2');
expect($response->json('items'))->toHaveCount(2);
expect($response->json('total'))->toBe(5);
});
test('GET /api/notifications: 422 на limit > 100', function () {
$this->getJson('/api/notifications?limit=101')->assertStatus(422);
});
test('GET /api/notifications: возвращает поля title/body/event/payload/deal_id', function () {
$notif = makeNotif($this->user->id, $this->tenant->id);
$response = $this->getJson('/api/notifications');
$item = $response->json('items.0');
expect($item['id'])->toBe($notif->id);
expect($item['event'])->toBe('new_lead');
expect($item['title'])->toBe('Новый лид — Caranga');
expect($item['body'])->toBe('79001234567');
expect($item['deal_id'])->toBe(42);
expect($item['payload']['project_name'])->toBe('Caranga');
expect($item['read_at'])->toBeNull();
});
test('PATCH /api/notifications/{id}/read: ставит read_at + idempotent', function () {
$notif = makeNotif($this->user->id, $this->tenant->id);
$response = $this->patchJson("/api/notifications/{$notif->id}/read");
$response->assertOk();
expect($response->json('read_at'))->not->toBeNull();
$notif->refresh();
$firstReadAt = $notif->read_at?->toIso8601String();
expect($firstReadAt)->not->toBeNull();
// Повторный — не меняет read_at.
$this->patchJson("/api/notifications/{$notif->id}/read")->assertOk();
$notif->refresh();
expect($notif->read_at?->toIso8601String())->toBe($firstReadAt);
});
test('PATCH /api/notifications/{id}/read: 404 для чужого', function () {
$other = User::factory()->create(['tenant_id' => $this->tenant->id]);
$notif = makeNotif($other->id, $this->tenant->id);
$this->patchJson("/api/notifications/{$notif->id}/read")->assertStatus(404);
});
test('PATCH /api/notifications/{id}/read: 404 на несуществующий id', function () {
$this->patchJson('/api/notifications/999999/read')->assertStatus(404);
});
test('POST /api/notifications/mark-all-read: bulk-update + count', function () {
makeNotif($this->user->id, $this->tenant->id);
makeNotif($this->user->id, $this->tenant->id);
makeNotif($this->user->id, $this->tenant->id, readAt: now()->toIso8601String()); // уже прочитано
$response = $this->postJson('/api/notifications/mark-all-read');
$response->assertOk();
expect($response->json('updated'))->toBe(2);
$unreadCount = InAppNotification::query()
->where('user_id', $this->user->id)
->whereNull('read_at')
->count();
expect($unreadCount)->toBe(0);
});
test('POST /api/notifications/mark-all-read: только свои', function () {
$other = User::factory()->create(['tenant_id' => $this->tenant->id]);
makeNotif($other->id, $this->tenant->id); // чужое
makeNotif($this->user->id, $this->tenant->id);
$response = $this->postJson('/api/notifications/mark-all-read');
expect($response->json('updated'))->toBe(1); // только своё
$otherUnread = InAppNotification::query()
->where('user_id', $other->id)
->whereNull('read_at')
->count();
expect($otherUnread)->toBe(1); // чужое осталось непрочитанным
});
test('DELETE /api/notifications/{id}: удаляет своё', function () {
$notif = makeNotif($this->user->id, $this->tenant->id);
$this->deleteJson("/api/notifications/{$notif->id}")->assertOk();
expect(InAppNotification::query()->find($notif->id))->toBeNull();
});
test('DELETE /api/notifications/{id}: 404 для чужого', function () {
$other = User::factory()->create(['tenant_id' => $this->tenant->id]);
$notif = makeNotif($other->id, $this->tenant->id);
$this->deleteJson("/api/notifications/{$notif->id}")->assertStatus(404);
expect(InAppNotification::query()->find($notif->id))->not->toBeNull(); // не удалено
});