@sync-in/server
Version:
The secure, open-source platform for file storage, sharing, collaboration, and sync
503 lines (502 loc) • 18.8 kB
JavaScript
/*
* Copyright (C) 2012-2025 Johan Legrand <johan.legrand@sync-in.com>
* This file is part of Sync-in | The open source file sync and share solution
* See the LICENSE file for licensing details
*/ "use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
const _testing = require("@nestjs/testing");
const _mailerservice = require("../../../infrastructure/mailer/mailer.service");
const _user = require("../../users/constants/user");
const _usersmanagerservice = require("../../users/services/users-manager.service");
const _avatar = require("../../users/utils/avatar");
const _notifications = require("../constants/notifications");
const _websocket = require("../constants/websocket");
const _models = /*#__PURE__*/ _interop_require_wildcard(require("../mails/models"));
const _notificationsgateway = require("../notifications.gateway");
const _notificationsmanagerservice = require("./notifications-manager.service");
const _notificationsqueriesservice = require("./notifications-queries.service");
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function(nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interop_require_wildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
return {
default: obj
};
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {
__proto__: null
};
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
for(var key in obj){
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
// Compact mock for mail generators
jest.mock('../mails/models', ()=>({
commentMail: jest.fn(()=>[
'comment title',
'comment html'
]),
spaceMail: jest.fn(()=>[
'space title',
'space html'
]),
spaceRootMail: jest.fn(()=>[
'spaceRoot title',
'spaceRoot html'
]),
shareMail: jest.fn(()=>[
'share title',
'share html'
]),
linkMail: jest.fn(()=>[
'link title',
'link html'
]),
syncMail: jest.fn(()=>[
'sync title',
'sync html'
])
}));
jest.mock('../../users/utils/avatar', ()=>({
getAvatarBase64: jest.fn()
}));
describe(_notificationsmanagerservice.NotificationsManager.name, ()=>{
let service;
const mailerMock = {
available: true,
sendMails: jest.fn()
};
const notificationsQueriesMock = {
list: jest.fn(),
usersNotifiedByEmail: jest.fn(),
create: jest.fn(),
wasRead: jest.fn(),
delete: jest.fn()
};
const webSocketNotificationsMock = {
sendMessageToUsers: jest.fn()
};
const flushPromises = ()=>new Promise((r)=>setImmediate(r));
const spyLogger = ()=>jest.spyOn(service.logger, 'error').mockImplementation(()=>undefined);
beforeEach(async ()=>{
jest.clearAllMocks();
mailerMock.available = true;
mailerMock.sendMails.mockResolvedValue(undefined);
notificationsQueriesMock.create.mockResolvedValue(undefined);
notificationsQueriesMock.wasRead.mockResolvedValue(undefined);
notificationsQueriesMock.delete.mockResolvedValue(undefined);
notificationsQueriesMock.list.mockResolvedValue([]);
notificationsQueriesMock.usersNotifiedByEmail.mockResolvedValue([]);
const module = await _testing.Test.createTestingModule({
providers: [
_notificationsmanagerservice.NotificationsManager,
{
provide: _usersmanagerservice.UsersManager,
useValue: {}
},
{
provide: _mailerservice.Mailer,
useValue: mailerMock
},
{
provide: _notificationsgateway.WebSocketNotifications,
useValue: webSocketNotificationsMock
},
{
provide: _notificationsqueriesservice.NotificationsQueries,
useValue: notificationsQueriesMock
}
]
}).compile();
service = module.get(_notificationsmanagerservice.NotificationsManager);
});
it('should be defined', ()=>{
expect(service).toBeDefined();
});
describe('list', ()=>{
it.each`
userId | onlyUnread | expected
${42} | ${true} | ${true}
${1} | ${undefined} | ${false}
`('should list notifications (userId=$userId, onlyUnread=$onlyUnread)', async ({ userId, onlyUnread, expected })=>{
const expectedRes = [
{
id: userId
}
];
notificationsQueriesMock.list.mockResolvedValueOnce(expectedRes);
const res = await service.list({
id: userId
}, onlyUnread);
expect(notificationsQueriesMock.list).toHaveBeenCalledWith(userId, expected);
expect(res).toBe(expectedRes);
});
});
describe('create', ()=>{
it('stores, sends WS and no email when filtered list empty (object input)', async ()=>{
const sendEmailSpy = jest.spyOn(service, 'sendEmailNotification').mockResolvedValue(undefined);
const toUsers = [
{
id: 10,
email: 'u1@test.tld',
language: 'en',
notification: _user.USER_NOTIFICATION.APPLICATION
},
{
id: 11,
email: 'u2@test.tld',
language: 'fr',
notification: _user.USER_NOTIFICATION.APPLICATION
}
];
await service.create(toUsers, {
app: _notifications.NOTIFICATION_APP.COMMENTS
}, {
author: {
id: 99,
login: 'john'
}
});
expect(notificationsQueriesMock.create).toHaveBeenCalledWith(99, [
10,
11
], {
app: _notifications.NOTIFICATION_APP.COMMENTS
});
expect(webSocketNotificationsMock.sendMessageToUsers).toHaveBeenCalledWith([
10,
11
], _websocket.NOTIFICATIONS_WS.EVENTS.NOTIFICATION, 'check');
expect(sendEmailSpy).not.toHaveBeenCalled();
expect(notificationsQueriesMock.usersNotifiedByEmail).not.toHaveBeenCalled();
});
it('stores, sends WS and email for ids input', async ()=>{
const sendEmailSpy = jest.spyOn(service, 'sendEmailNotification').mockResolvedValue(undefined);
const toUserIds = [
1,
2,
3
];
const content = {
app: _notifications.NOTIFICATION_APP.SHARES
};
const emailUsers = [
{
id: 1,
email: 'a@test',
language: 'en'
},
{
id: 3,
email: 'c@test',
language: 'fr'
}
];
notificationsQueriesMock.usersNotifiedByEmail.mockResolvedValueOnce(emailUsers);
await service.create(toUserIds, content);
expect(notificationsQueriesMock.create).toHaveBeenCalledWith(null, toUserIds, content);
expect(webSocketNotificationsMock.sendMessageToUsers).toHaveBeenCalledWith(toUserIds, _websocket.NOTIFICATIONS_WS.EVENTS.NOTIFICATION, 'check');
expect(notificationsQueriesMock.usersNotifiedByEmail).toHaveBeenCalledWith(toUserIds);
expect(sendEmailSpy).toHaveBeenCalledWith(emailUsers, content, undefined);
});
it('does not try email when mailer is unavailable', async ()=>{
mailerMock.available = false;
const sendEmailSpy = jest.spyOn(service, 'sendEmailNotification').mockResolvedValue(undefined);
await service.create([
7
], {
app: _notifications.NOTIFICATION_APP.SYNC
}, {
author: {
id: 12,
login: 'jane'
}
});
expect(notificationsQueriesMock.create).toHaveBeenCalledWith(12, [
7
], {
app: _notifications.NOTIFICATION_APP.SYNC
});
expect(webSocketNotificationsMock.sendMessageToUsers).toHaveBeenCalledWith([
7
], _websocket.NOTIFICATIONS_WS.EVENTS.NOTIFICATION, 'check');
expect(notificationsQueriesMock.usersNotifiedByEmail).not.toHaveBeenCalled();
expect(sendEmailSpy).not.toHaveBeenCalled();
});
it('logs error when storeNotification internal try/catch catches create error', async ()=>{
const loggerSpy = spyLogger();
notificationsQueriesMock.create.mockRejectedValueOnce(new Error('DB fail'));
await service.create([
1
], {
app: _notifications.NOTIFICATION_APP.LINKS
});
await flushPromises();
expect(loggerSpy).toHaveBeenCalled();
expect(loggerSpy.mock.calls[0]?.[0]).toMatch(/create/i);
});
it('logs error when storeNotification promise rejects (create catch)', async ()=>{
const loggerSpy = spyLogger();
jest.spyOn(service, 'storeNotification').mockRejectedValueOnce(new Error('store reject'));
await service.create([
1,
2
], {
app: _notifications.NOTIFICATION_APP.SYNC
}, {
author: {
id: 5,
login: 'xx'
}
});
await flushPromises();
expect(loggerSpy).toHaveBeenCalled();
expect(loggerSpy.mock.calls[0]?.[0]).toMatch(/create/i);
});
it('logs error when sendEmailNotification rejects (create catch)', async ()=>{
const loggerSpy = spyLogger();
notificationsQueriesMock.usersNotifiedByEmail.mockResolvedValueOnce([
{
id: 1,
email: 'a@test',
language: 'en'
}
]);
jest.spyOn(service, 'sendEmailNotification').mockRejectedValueOnce(new Error('email reject'));
await service.create([
1
], {
app: _notifications.NOTIFICATION_APP.COMMENTS
});
await flushPromises();
expect(loggerSpy).toHaveBeenCalled();
expect(loggerSpy.mock.calls[0]?.[0]).toMatch(/create/i);
});
});
describe('wasRead', ()=>{
it('calls queries.wasRead and logs on error', async ()=>{
service.wasRead({
id: 5
}, 123);
expect(notificationsQueriesMock.wasRead).toHaveBeenCalledWith(5, 123);
const loggerSpy = spyLogger();
notificationsQueriesMock.wasRead.mockRejectedValueOnce(new Error('fail'));
service.wasRead({
id: 8
}, undefined);
await flushPromises();
expect(loggerSpy).toHaveBeenCalled();
expect(loggerSpy.mock.calls[0]?.[0]).toMatch(/wasRead/i);
});
});
describe('delete', ()=>{
it('forwards to queries.delete', async ()=>{
await service.delete({
id: 77
}, 456);
expect(notificationsQueriesMock.delete).toHaveBeenCalledWith(77, 456);
});
});
describe('sendEmailNotification', ()=>{
it('returns early when mailer is not available', async ()=>{
mailerMock.available = false;
await service.sendEmailNotification([
{
id: 1,
email: 'a@test',
language: 'en'
}
], {
app: _notifications.NOTIFICATION_APP.COMMENTS
}, {
author: {
id: 1,
login: 'john'
}
});
expect(_avatar.getAvatarBase64).not.toHaveBeenCalled();
expect(mailerMock.sendMails).not.toHaveBeenCalled();
});
it('enriches author avatar and sends mapped mails', async ()=>{
;
_avatar.getAvatarBase64.mockResolvedValueOnce('base64-xxx');
const toUsers = [
{
id: 1,
email: 'a@test',
language: 'en'
},
{
id: 2,
email: 'b@test',
language: 'fr'
}
];
const options = {
author: {
id: 9,
login: 'jdoe'
},
content: 'hello',
currentUrl: 'https://app.test/path'
};
const content = {
app: _notifications.NOTIFICATION_APP.COMMENTS
};
await service.sendEmailNotification(toUsers, content, options);
expect(_avatar.getAvatarBase64).toHaveBeenCalledWith('jdoe');
expect(options.author.avatarBase64).toBe('base64-xxx');
expect(mailerMock.sendMails).toHaveBeenCalledTimes(1);
expect(mailerMock.sendMails.mock.calls[0][0]).toEqual([
{
to: 'a@test',
subject: 'comment title',
html: 'comment html'
},
{
to: 'b@test',
subject: 'comment title',
html: 'comment html'
}
]);
});
it('logs error when sendMails rejects', async ()=>{
mailerMock.sendMails.mockRejectedValueOnce(new Error('smtp down'));
const loggerSpy = spyLogger();
await service.sendEmailNotification([
{
id: 1,
email: 'a@test',
language: 'en'
}
], {
app: _notifications.NOTIFICATION_APP.SYNC
}, {});
expect(loggerSpy).toHaveBeenCalled();
expect(loggerSpy.mock.calls[0]?.[0]).toMatch(/sendEmailNotification/i);
});
});
describe('genMail (private) - switch coverage', ()=>{
const cases = [
{
name: 'COMMENTS',
app: _notifications.NOTIFICATION_APP.COMMENTS,
fn: 'commentMail',
options: {
content: 'c',
currentUrl: 'u',
author: {
id: 1,
login: 'x'
}
}
},
{
name: 'SPACES',
app: _notifications.NOTIFICATION_APP.SPACES,
fn: 'spaceMail',
options: {
currentUrl: 'u',
action: 'A'
}
},
{
name: 'SPACE_ROOTS',
app: _notifications.NOTIFICATION_APP.SPACE_ROOTS,
fn: 'spaceRootMail',
options: {
currentUrl: 'u',
author: {
id: 2,
login: 'y'
},
action: 'B'
}
},
{
name: 'SHARES',
app: _notifications.NOTIFICATION_APP.SHARES,
fn: 'shareMail',
options: {
currentUrl: 'u',
author: {
id: 3,
login: 'z'
},
action: 'C'
}
},
{
name: 'LINKS',
app: _notifications.NOTIFICATION_APP.LINKS,
fn: 'linkMail',
options: {
currentUrl: 'u',
author: {
id: 4,
login: 'w'
},
linkUUID: 'uuid',
action: 'D'
}
},
{
name: 'SYNC',
app: _notifications.NOTIFICATION_APP.SYNC,
fn: 'syncMail',
options: {
currentUrl: 'u',
action: 'E'
}
}
];
it.each(cases)('uses $fn for $name', ({ app, fn, options })=>{
const res = service.genMail('en', {
app
}, options);
expect(res).toEqual([
`${fn.replace('Mail', '')} title`.replace('spaceRoot', 'spaceRoot'),
`${fn.replace('Mail', '')} html`.replace('spaceRoot', 'spaceRoot')
]);
expect(_models[fn]).toHaveBeenCalled();
});
it('logs error for unhandled app', ()=>{
const loggerSpy = spyLogger();
const result = service.genMail('en', {
app: 99999
}, {});
expect(result).toBeUndefined();
expect(loggerSpy).toHaveBeenCalled();
expect(loggerSpy.mock.calls[0]?.[0]).toMatch(/case not handled/i);
});
});
});
//# sourceMappingURL=notifications-manager.service.spec.js.map