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(); // не удалено });