369 أسطر
10 KiB
TypeScript
369 أسطر
10 KiB
TypeScript
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: '',
|
|
}),
|
|
);
|
|
});
|
|
});
|