import { NotFoundException } from '@nestjs/common'; import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; import { NotificationUnreadCountQueryDto } from './dto/notification-query.dto'; import { NotificationsService } from './notifications.service'; describe('NotificationsService', () => { it('rejects invalid unread-count category through DTO validation', async () => { const dto = plainToInstance(NotificationUnreadCountQueryDto, { category: 'badges' }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); expect(errors[0].property).toBe('category'); }); it('creates mention notifications with mention type', async () => { const notificationsRepository = { create: jest.fn().mockResolvedValue({ toJSON: () => ({ _id: 'notification-1' }) }), countUnread: jest.fn().mockResolvedValue(5), countUnreadByFilter: jest.fn().mockResolvedValue(0), }; const notificationsGateway = { emitCreated: jest.fn(), }; const service = new NotificationsService( notificationsRepository as any, notificationsGateway as any, ); await service.createMentionNotification( '507f1f77bcf86cd799439011', '507f191e810c19729de860ea', '507f1f77bcf86cd799439012', { previewText: 'Hello @rami' }, ); expect(notificationsRepository.create).toHaveBeenCalledWith( expect.objectContaining({ type: 'mention', previewText: 'Hello @rami', }), ); }); it('recalculates unread count after markAllRead', async () => { const notificationsRepository = { markAllRead: jest.fn().mockResolvedValue(4), countUnread: jest.fn().mockResolvedValue(2), countUnreadByFilter: jest.fn().mockResolvedValue(0), }; const notificationsGateway = { emitUnreadCount: jest.fn(), }; const service = new NotificationsService( notificationsRepository as any, notificationsGateway as any, ); await expect(service.markAllRead('user-1')).resolves.toEqual({ message: 'All notifications marked as read', modifiedCount: 4, updatedCount: 4, unreadCount: 2, }); expect(notificationsGateway.emitUnreadCount).toHaveBeenCalledWith( 'user-1', 2, expect.objectContaining({ total: 2, interactions: 0, messages: 0, }), ); }); it('throws not found for invalid notification id in markRead', async () => { const notificationsRepository = { markRead: jest.fn(), countUnread: jest.fn(), }; const notificationsGateway = { emitUnreadCount: jest.fn(), }; const service = new NotificationsService( notificationsRepository as any, notificationsGateway as any, ); await expect(service.markRead('user-1', 'invalid-id')).rejects.toBeInstanceOf(NotFoundException); expect(notificationsRepository.markRead).not.toHaveBeenCalled(); }); it('returns total unread count for the current user in the notifications list', async () => { const notificationsRepository = { findMine: jest.fn().mockResolvedValue([{ id: 'notification-1' }]), countMine: jest.fn().mockResolvedValue(30), countUnread: jest.fn().mockResolvedValue(7), }; const notificationsGateway = { emitUnreadCount: jest.fn(), }; const service = new NotificationsService( notificationsRepository as any, notificationsGateway as any, ); await expect( service.getMine('507f1f77bcf86cd799439011', { page: 1, limit: 20, sortOrder: 'desc' as any, }), ).resolves.toMatchObject({ count: 1, page: 1, limit: 20, total: 30, unreadCount: 7, pagination: { mode: 'offset', hasNextPage: true, }, }); expect(notificationsRepository.countUnread).toHaveBeenCalledWith('507f1f77bcf86cd799439011'); }); it('returns total unread count without category for backward compatibility', async () => { const notificationsRepository = { countUnread: jest.fn().mockResolvedValue(10), }; const notificationsGateway = {}; const service = new NotificationsService( notificationsRepository as any, notificationsGateway as any, ); await expect(service.getUnreadCountByCategory('507f1f77bcf86cd799439011')).resolves.toEqual({ unreadCount: 10, }); expect(notificationsRepository.countUnread).toHaveBeenCalledWith('507f1f77bcf86cd799439011'); }); it('returns unread count for interactions category only', async () => { const notificationsRepository = { countUnreadByFilter: jest.fn().mockResolvedValue(4), }; const notificationsGateway = {}; const service = new NotificationsService( notificationsRepository as any, notificationsGateway as any, ); await expect( service.getUnreadCountByCategory('507f1f77bcf86cd799439011', 'interactions'), ).resolves.toEqual({ unreadCount: 4, category: 'interactions', }); expect(notificationsRepository.countUnreadByFilter).toHaveBeenCalledWith( '507f1f77bcf86cd799439011', { type: { $in: [ 'like', 'comment', 'reply', 'mention', 'save', 'share', 'collaboration_request', 'system', ], }, }, ); }); it('returns unread count for messages category only', async () => { const notificationsRepository = { countUnreadByFilter: jest.fn().mockResolvedValue(3), }; const notificationsGateway = {}; const service = new NotificationsService( notificationsRepository as any, notificationsGateway as any, ); await expect( service.getUnreadCountByCategory('507f1f77bcf86cd799439011', 'messages'), ).resolves.toEqual({ unreadCount: 3, category: 'messages', }); expect(notificationsRepository.countUnreadByFilter).toHaveBeenCalledWith( '507f1f77bcf86cd799439011', { type: { $in: ['message'] } }, ); }); it('returns unread count for follow requests category only', async () => { const notificationsRepository = { countUnreadByFilter: jest.fn().mockResolvedValue(2), }; const notificationsGateway = {}; const service = new NotificationsService( notificationsRepository as any, notificationsGateway as any, ); await expect( service.getUnreadCountByCategory('507f1f77bcf86cd799439011', 'follow_requests'), ).resolves.toEqual({ unreadCount: 2, category: 'follow_requests', }); expect(notificationsRepository.countUnreadByFilter).toHaveBeenCalledWith( '507f1f77bcf86cd799439011', { type: { $in: ['follow_request', 'follow_request_approved', 'follow_request_rejected'], }, }, ); }); it('returns grouped unread counts for badge hydration', async () => { const notificationsRepository = { countUnread: jest.fn().mockResolvedValue(10), countUnreadByFilter: jest .fn() .mockResolvedValueOnce(4) .mockResolvedValueOnce(3) .mockResolvedValueOnce(1) .mockResolvedValueOnce(2) .mockResolvedValueOnce(1) .mockResolvedValueOnce(0), }; const notificationsGateway = {}; const service = new NotificationsService( notificationsRepository as any, notificationsGateway as any, ); await expect(service.getUnreadCounts('507f1f77bcf86cd799439011')).resolves.toEqual({ total: 10, interactions: 4, messages: 3, follows: 1, followRequests: 2, collaboration: 1, system: 0, }); }); it('filters notifications by interactions category', async () => { const notificationsRepository = { findMine: jest.fn().mockResolvedValue([]), countMine: jest.fn().mockResolvedValue(0), countUnread: jest.fn().mockResolvedValue(0), }; const notificationsGateway = { emitUnreadCount: jest.fn(), }; const service = new NotificationsService( notificationsRepository as any, notificationsGateway as any, ); await service.getMine('507f1f77bcf86cd799439011', { page: 1, limit: 20, category: 'interactions', }); expect(notificationsRepository.findMine).toHaveBeenCalledWith( '507f1f77bcf86cd799439011', { type: { $in: [ 'like', 'comment', 'reply', 'mention', 'save', 'share', 'collaboration_request', 'system', ], }, }, 0, 20, { createdAt: -1 }, ); }); it('keeps type filter priority over category for backward compatibility', async () => { const notificationsRepository = { findMine: jest.fn().mockResolvedValue([]), countMine: jest.fn().mockResolvedValue(0), countUnread: jest.fn().mockResolvedValue(0), }; const notificationsGateway = { emitUnreadCount: jest.fn(), }; const service = new NotificationsService( notificationsRepository as any, notificationsGateway as any, ); await service.getMine('507f1f77bcf86cd799439011', { page: 1, limit: 20, type: 'message', category: 'interactions', }); expect(notificationsRepository.findMine).toHaveBeenCalledWith( '507f1f77bcf86cd799439011', { type: 'message' }, 0, 20, { createdAt: -1 }, ); }); it('creates system notifications with system resource mapping when requested', async () => { const notificationsRepository = { create: jest.fn().mockResolvedValue({ toJSON: () => ({ _id: 'notification-1' }) }), countUnread: jest.fn().mockResolvedValue(1), countUnreadByFilter: jest.fn().mockResolvedValue(0), }; const notificationsGateway = { emitCreated: jest.fn(), }; const service = new NotificationsService( notificationsRepository as any, notificationsGateway as any, ); await service.create({ actorId: '507f1f77bcf86cd799439011', recipientId: '507f191e810c19729de860ea', type: 'system', }); expect(notificationsRepository.create).toHaveBeenCalledWith( expect.objectContaining({ type: 'system', resourceType: 'system', deepLink: '', }), ); }); });