UNPKG

@sync-in/server

Version:

The secure, open-source platform for file storage, sharing, collaboration, and sync

503 lines (502 loc) 18.8 kB
/* * 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