|
|
|
|
@ -21,12 +21,19 @@ const CalendarEventMock = { |
|
|
|
|
findEventsToNotify: sinon.stub(), |
|
|
|
|
flagNotificationSent: sinon.stub(), |
|
|
|
|
findOneByExternalIdAndUserId: sinon.stub(), |
|
|
|
|
findEventsToScheduleNow: sinon.stub(), |
|
|
|
|
findNextFutureEvent: sinon.stub(), |
|
|
|
|
findInProgressEvents: sinon.stub(), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const UsersMock = { |
|
|
|
|
findOne: sinon.stub(), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const statusEventManagerMock = { |
|
|
|
|
setupAppointmentStatusChange: sinon.stub().resolves(), |
|
|
|
|
removeCronJobs: sinon.stub().resolves(), |
|
|
|
|
cancelUpcomingStatusChanges: sinon.stub().resolves(), |
|
|
|
|
applyStatusChange: sinon.stub().resolves(), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const getUserPreferenceMock = sinon.stub(); |
|
|
|
|
@ -34,11 +41,11 @@ const getUserPreferenceMock = sinon.stub(); |
|
|
|
|
const serviceMocks = { |
|
|
|
|
'./statusEvents/cancelUpcomingStatusChanges': { cancelUpcomingStatusChanges: statusEventManagerMock.cancelUpcomingStatusChanges }, |
|
|
|
|
'./statusEvents/removeCronJobs': { removeCronJobs: statusEventManagerMock.removeCronJobs }, |
|
|
|
|
'./statusEvents/setupAppointmentStatusChange': { setupAppointmentStatusChange: statusEventManagerMock.setupAppointmentStatusChange }, |
|
|
|
|
'./statusEvents/applyStatusChange': { applyStatusChange: statusEventManagerMock.applyStatusChange }, |
|
|
|
|
'../../../app/settings/server': { settings: settingsMock }, |
|
|
|
|
'@rocket.chat/core-services': { api, ServiceClassInternal: class {} }, |
|
|
|
|
'@rocket.chat/cron': { cronJobs: cronJobsMock }, |
|
|
|
|
'@rocket.chat/models': { CalendarEvent: CalendarEventMock }, |
|
|
|
|
'@rocket.chat/models': { CalendarEvent: CalendarEventMock, Users: UsersMock }, |
|
|
|
|
'../../../app/utils/server/lib/getUserPreference': { getUserPreference: getUserPreferenceMock }, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
@ -74,8 +81,10 @@ describe('CalendarService', () => { |
|
|
|
|
sandbox.stub(proto, 'sendEventNotification').resolves(); |
|
|
|
|
sandbox.stub(proto, 'sendCurrentNotifications').resolves(); |
|
|
|
|
sandbox.stub(proto, 'doSetupNextNotification').resolves(); |
|
|
|
|
sandbox.stub(proto, 'doSetupNextStatusChange').resolves(); |
|
|
|
|
|
|
|
|
|
sandbox.stub(service, 'setupNextNotification').resolves(); |
|
|
|
|
sandbox.stub(service, 'setupNextStatusChange').resolves(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function setupCalendarEventMocks() { |
|
|
|
|
@ -93,6 +102,13 @@ describe('CalendarService', () => { |
|
|
|
|
}), |
|
|
|
|
flagNotificationSent: sinon.stub().resolves(), |
|
|
|
|
findOneByExternalIdAndUserId: sinon.stub().resolves(null), |
|
|
|
|
findEventsToScheduleNow: sinon.stub().returns({ |
|
|
|
|
toArray: sinon.stub().resolves([]), |
|
|
|
|
}), |
|
|
|
|
findNextFutureEvent: sinon.stub().resolves(null), |
|
|
|
|
findInProgressEvents: sinon.stub().returns({ |
|
|
|
|
toArray: sinon.stub().resolves([]), |
|
|
|
|
}), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
Object.assign(CalendarEventMock, freshMocks); |
|
|
|
|
@ -147,34 +163,7 @@ describe('CalendarService', () => { |
|
|
|
|
reminderMinutesBeforeStart: 5, |
|
|
|
|
notificationSent: false, |
|
|
|
|
}); |
|
|
|
|
sinon.assert.calledOnce(statusEventManagerMock.setupAppointmentStatusChange); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should create event without end time if not provided', async () => { |
|
|
|
|
const eventData = { |
|
|
|
|
uid: fakeUserId, |
|
|
|
|
startTime: fakeStartTime, |
|
|
|
|
subject: fakeSubject, |
|
|
|
|
description: fakeDescription, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
await service.create(eventData); |
|
|
|
|
|
|
|
|
|
expect(CalendarEventMock.insertOne.firstCall.args[0]).to.not.have.property('endTime'); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should use default reminder minutes if not provided', async () => { |
|
|
|
|
const eventData = { |
|
|
|
|
uid: fakeUserId, |
|
|
|
|
startTime: fakeStartTime, |
|
|
|
|
subject: fakeSubject, |
|
|
|
|
description: fakeDescription, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
await service.create(eventData); |
|
|
|
|
|
|
|
|
|
const insertedData = CalendarEventMock.insertOne.firstCall.args[0]; |
|
|
|
|
expect(insertedData).to.have.property('reminderMinutesBeforeStart', 5); |
|
|
|
|
sinon.assert.calledOnce(service.setupNextStatusChange); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
@ -190,24 +179,7 @@ describe('CalendarService', () => { |
|
|
|
|
await service.import(eventData); |
|
|
|
|
|
|
|
|
|
sinon.assert.calledOnce(CalendarEventMock.insertOne); |
|
|
|
|
sinon.assert.calledOnce(statusEventManagerMock.setupAppointmentStatusChange); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should create a new event if event with externalId not found', async () => { |
|
|
|
|
const eventData = { |
|
|
|
|
uid: fakeUserId, |
|
|
|
|
startTime: fakeStartTime, |
|
|
|
|
subject: fakeSubject, |
|
|
|
|
description: fakeDescription, |
|
|
|
|
externalId: fakeExternalId, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
CalendarEventMock.findOneByExternalIdAndUserId.resolves(null); |
|
|
|
|
|
|
|
|
|
await service.import(eventData); |
|
|
|
|
|
|
|
|
|
sinon.assert.calledWith(CalendarEventMock.findOneByExternalIdAndUserId, fakeExternalId, fakeUserId); |
|
|
|
|
sinon.assert.calledOnce(CalendarEventMock.insertOne); |
|
|
|
|
sinon.assert.calledOnce(service.setupNextStatusChange); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should update existing event if found by externalId', async () => { |
|
|
|
|
@ -231,58 +203,6 @@ describe('CalendarService', () => { |
|
|
|
|
sinon.assert.calledOnce(CalendarEventMock.updateEvent); |
|
|
|
|
sinon.assert.notCalled(CalendarEventMock.insertOne); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should extract meeting URL from description if not provided', async () => { |
|
|
|
|
const eventData = { |
|
|
|
|
uid: fakeUserId, |
|
|
|
|
startTime: fakeStartTime, |
|
|
|
|
subject: fakeSubject, |
|
|
|
|
description: 'Description with callUrl=https://meet.test/123', |
|
|
|
|
externalId: fakeExternalId, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const proto = Object.getPrototypeOf(service); |
|
|
|
|
await service.import(eventData); |
|
|
|
|
|
|
|
|
|
sinon.assert.calledWith(proto.parseDescriptionForMeetingUrl as sinon.SinonStub, eventData.description); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('#get', () => { |
|
|
|
|
it('should retrieve a single event by ID', async () => { |
|
|
|
|
const fakeEvent = { |
|
|
|
|
_id: fakeEventId, |
|
|
|
|
uid: fakeUserId, |
|
|
|
|
startTime: fakeStartTime, |
|
|
|
|
subject: fakeSubject, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
CalendarEventMock.findOne.resolves(fakeEvent); |
|
|
|
|
|
|
|
|
|
const result = await service.get(fakeEventId); |
|
|
|
|
|
|
|
|
|
sinon.assert.calledWith(CalendarEventMock.findOne, { _id: fakeEventId }); |
|
|
|
|
expect(result).to.equal(fakeEvent); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('#list', () => { |
|
|
|
|
it('should retrieve events for a user on a specific date', async () => { |
|
|
|
|
const fakeEvents = [ |
|
|
|
|
{ _id: 'event1', uid: fakeUserId, startTime: fakeStartTime }, |
|
|
|
|
{ _id: 'event2', uid: fakeUserId, startTime: fakeStartTime }, |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
CalendarEventMock.findByUserIdAndDate.returns({ |
|
|
|
|
toArray: sinon.stub().resolves(fakeEvents), |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const fakeDate = new Date('2025-01-01'); |
|
|
|
|
const result = await service.list(fakeUserId, fakeDate); |
|
|
|
|
|
|
|
|
|
sinon.assert.calledWith(CalendarEventMock.findByUserIdAndDate, fakeUserId, fakeDate); |
|
|
|
|
expect(result).to.equal(fakeEvents); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('#update', () => { |
|
|
|
|
@ -307,14 +227,6 @@ describe('CalendarService', () => { |
|
|
|
|
sinon.assert.calledWith(CalendarEventMock.updateEvent, fakeEventId, sinon.match.has('subject', 'Updated Subject')); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should do nothing if event not found', async () => { |
|
|
|
|
CalendarEventMock.findOne.resolves(null); |
|
|
|
|
|
|
|
|
|
await service.update(fakeEventId, { subject: 'New Subject' }); |
|
|
|
|
|
|
|
|
|
sinon.assert.notCalled(CalendarEventMock.updateEvent); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should update cron jobs when start/end times change', async () => { |
|
|
|
|
const fakeEvent = { |
|
|
|
|
_id: fakeEventId, |
|
|
|
|
@ -335,42 +247,7 @@ describe('CalendarService', () => { |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
sinon.assert.calledOnce(statusEventManagerMock.removeCronJobs); |
|
|
|
|
sinon.assert.calledOnce(statusEventManagerMock.setupAppointmentStatusChange); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should extract meeting URL from description if not provided', async () => { |
|
|
|
|
const fakeEvent = { |
|
|
|
|
_id: fakeEventId, |
|
|
|
|
uid: fakeUserId, |
|
|
|
|
startTime: fakeStartTime, |
|
|
|
|
subject: fakeSubject, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
CalendarEventMock.findOne.resolves(fakeEvent); |
|
|
|
|
|
|
|
|
|
const proto = Object.getPrototypeOf(service); |
|
|
|
|
|
|
|
|
|
await service.update(fakeEventId, { |
|
|
|
|
description: 'Description with callUrl=https://meet.test/123', |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
sinon.assert.called(proto.parseDescriptionForMeetingUrl as sinon.SinonStub); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should setup next notification if event was modified', async () => { |
|
|
|
|
const fakeEvent = { |
|
|
|
|
_id: fakeEventId, |
|
|
|
|
uid: fakeUserId, |
|
|
|
|
startTime: fakeStartTime, |
|
|
|
|
subject: fakeSubject, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
CalendarEventMock.findOne.resolves(fakeEvent); |
|
|
|
|
CalendarEventMock.updateEvent.resolves({ modifiedCount: 1 } as UpdateResult); |
|
|
|
|
|
|
|
|
|
await service.update(fakeEventId, { subject: 'New Subject' }); |
|
|
|
|
|
|
|
|
|
sinon.assert.calledOnce(service.setupNextNotification as sinon.SinonStub); |
|
|
|
|
sinon.assert.calledOnce(service.setupNextStatusChange); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
@ -390,15 +267,6 @@ describe('CalendarService', () => { |
|
|
|
|
sinon.assert.calledOnce(statusEventManagerMock.removeCronJobs); |
|
|
|
|
sinon.assert.calledWith(CalendarEventMock.deleteOne, { _id: fakeEventId }); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should only delete the event if not found', async () => { |
|
|
|
|
CalendarEventMock.findOne.resolves(null); |
|
|
|
|
|
|
|
|
|
await service.delete(fakeEventId); |
|
|
|
|
|
|
|
|
|
sinon.assert.notCalled(statusEventManagerMock.removeCronJobs); |
|
|
|
|
sinon.assert.calledOnce(CalendarEventMock.deleteOne); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('#setupNextNotification', () => { |
|
|
|
|
@ -421,30 +289,7 @@ describe('CalendarService', () => { |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('#cancelUpcomingStatusChanges', () => { |
|
|
|
|
it('should delegate to statusEventManager', async () => { |
|
|
|
|
await service.cancelUpcomingStatusChanges(fakeUserId); |
|
|
|
|
|
|
|
|
|
sinon.assert.calledWith(statusEventManagerMock.cancelUpcomingStatusChanges, fakeUserId); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should pass custom end time if provided', async () => { |
|
|
|
|
const customDate = new Date('2025-02-01'); |
|
|
|
|
|
|
|
|
|
await service.cancelUpcomingStatusChanges(fakeUserId, customDate); |
|
|
|
|
|
|
|
|
|
sinon.assert.calledWith(statusEventManagerMock.cancelUpcomingStatusChanges, fakeUserId, customDate); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('Private: parseDescriptionForMeetingUrl', () => { |
|
|
|
|
it('should return undefined for empty description', async () => { |
|
|
|
|
await testPrivateMethod(service, 'parseDescriptionForMeetingUrl', async (method) => { |
|
|
|
|
const result = await method(''); |
|
|
|
|
expect(result).to.be.undefined; |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should extract URL from description with default pattern', async () => { |
|
|
|
|
await testPrivateMethod(service, 'parseDescriptionForMeetingUrl', async (method) => { |
|
|
|
|
const testDescription = 'Join at https://meet.example.com?callUrl=https://special-meeting.com/123'; |
|
|
|
|
@ -452,199 +297,162 @@ describe('CalendarService', () => { |
|
|
|
|
expect(result).to.equal('https://special-meeting.com/123'); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should return undefined if regex pattern is empty', async () => { |
|
|
|
|
await testPrivateMethod(service, 'parseDescriptionForMeetingUrl', async (method) => { |
|
|
|
|
settingsMock.set('Calendar_MeetingUrl_Regex', ''); |
|
|
|
|
describe('Private: doSetupNextNotification', () => { |
|
|
|
|
it('should schedule notifications at the next date', async () => { |
|
|
|
|
await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { |
|
|
|
|
const nextDate = new Date('2025-01-01T10:00:00Z'); |
|
|
|
|
CalendarEventMock.findNextNotificationDate.resolves(nextDate); |
|
|
|
|
|
|
|
|
|
const result = await method('Test description with no pattern match'); |
|
|
|
|
expect(result).to.be.undefined; |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
await method(false); |
|
|
|
|
|
|
|
|
|
it('should handle URL decoding', async () => { |
|
|
|
|
await testPrivateMethod(service, 'parseDescriptionForMeetingUrl', async (method) => { |
|
|
|
|
const encodedUrl = 'Join meeting at link with callUrl%3Dhttps%3A%2F%2Fmeeting.example.com%2F123'; |
|
|
|
|
const result = await method(encodedUrl); |
|
|
|
|
expect(result).to.include('https://meeting.example.com/123'); |
|
|
|
|
expect(cronJobsMock.jobNames.has('calendar-reminders')).to.true; |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('Private: findImportedEvent', () => { |
|
|
|
|
it('should call the model method with correct parameters', async () => { |
|
|
|
|
await testPrivateMethod(service, 'findImportedEvent', async (method) => { |
|
|
|
|
await method(fakeExternalId, fakeUserId); |
|
|
|
|
sinon.assert.calledWith(CalendarEventMock.findOneByExternalIdAndUserId, fakeExternalId, fakeUserId); |
|
|
|
|
describe('Private: doSetupNextStatusChange', () => { |
|
|
|
|
it('should not run when busy status setting is disabled', async () => { |
|
|
|
|
await testPrivateMethod(service, 'doSetupNextStatusChange', async (method) => { |
|
|
|
|
settingsMock.set('Calendar_BusyStatus_Enabled', false); |
|
|
|
|
|
|
|
|
|
const originalHas = cronJobsMock.has; |
|
|
|
|
const originalRemove = cronJobsMock.remove; |
|
|
|
|
const originalAddAtTimestamp = cronJobsMock.addAtTimestamp; |
|
|
|
|
|
|
|
|
|
const hasStub = sinon.stub().resolves(true); |
|
|
|
|
const removeStub = sinon.stub().resolves(); |
|
|
|
|
const addAtTimestampStub = sinon.stub().resolves(); |
|
|
|
|
|
|
|
|
|
cronJobsMock.has = hasStub; |
|
|
|
|
cronJobsMock.remove = removeStub; |
|
|
|
|
cronJobsMock.addAtTimestamp = addAtTimestampStub; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
await method(); |
|
|
|
|
sinon.assert.calledWith(hasStub, 'calendar-next-status-change'); |
|
|
|
|
sinon.assert.calledWith(removeStub, 'calendar-next-status-change'); |
|
|
|
|
sinon.assert.notCalled(addAtTimestampStub); |
|
|
|
|
} finally { |
|
|
|
|
cronJobsMock.has = originalHas; |
|
|
|
|
cronJobsMock.remove = originalRemove; |
|
|
|
|
cronJobsMock.addAtTimestamp = originalAddAtTimestamp; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should return the event when found', async () => { |
|
|
|
|
await testPrivateMethod(service, 'findImportedEvent', async (method) => { |
|
|
|
|
const fakeEvent = { _id: fakeEventId, externalId: fakeExternalId, uid: fakeUserId }; |
|
|
|
|
CalendarEventMock.findOneByExternalIdAndUserId.resolves(fakeEvent); |
|
|
|
|
it('should schedule a single chain job to handle all events when busy status setting is enabled', async () => { |
|
|
|
|
await testPrivateMethod(service, 'doSetupNextStatusChange', async (method) => { |
|
|
|
|
settingsMock.set('Calendar_BusyStatus_Enabled', true); |
|
|
|
|
|
|
|
|
|
const result = await method(fakeExternalId, fakeUserId); |
|
|
|
|
expect(result).to.equal(fakeEvent); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should return null when event not found', async () => { |
|
|
|
|
await testPrivateMethod(service, 'findImportedEvent', async (method) => { |
|
|
|
|
CalendarEventMock.findOneByExternalIdAndUserId.resolves(null); |
|
|
|
|
|
|
|
|
|
const result = await method(fakeExternalId, fakeUserId); |
|
|
|
|
expect(result).to.be.null; |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
const startOfNextMinute = new Date(); |
|
|
|
|
startOfNextMinute.setSeconds(0, 0); |
|
|
|
|
startOfNextMinute.setMinutes(startOfNextMinute.getMinutes() + 1); |
|
|
|
|
|
|
|
|
|
describe('Private: sendEventNotification', () => { |
|
|
|
|
it('should not send notification if user preference is disabled', async () => { |
|
|
|
|
await testPrivateMethod(service, 'sendEventNotification', async (method) => { |
|
|
|
|
getUserPreferenceMock.resolves(false); |
|
|
|
|
const endOfNextMinute = new Date(startOfNextMinute); |
|
|
|
|
endOfNextMinute.setMinutes(startOfNextMinute.getMinutes() + 1); |
|
|
|
|
|
|
|
|
|
const fakeEvent = { |
|
|
|
|
_id: fakeEventId, |
|
|
|
|
const eventStartingSoon = { |
|
|
|
|
_id: 'soon123', |
|
|
|
|
uid: fakeUserId, |
|
|
|
|
startTime: fakeStartTime, |
|
|
|
|
subject: fakeSubject, |
|
|
|
|
startTime: startOfNextMinute, |
|
|
|
|
endTime: new Date(startOfNextMinute.getTime() + 3600000), // 1 hour later
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
await method(fakeEvent); |
|
|
|
|
|
|
|
|
|
sinon.assert.calledWith(getUserPreferenceMock, fakeUserId, 'notifyCalendarEvents'); |
|
|
|
|
sinon.assert.notCalled(api.broadcast as sinon.SinonStub); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should send notification with correct event data', async () => { |
|
|
|
|
await testPrivateMethod(service, 'sendEventNotification', async (method) => { |
|
|
|
|
getUserPreferenceMock.resolves(true); |
|
|
|
|
|
|
|
|
|
const fakeEvent = { |
|
|
|
|
_id: fakeEventId, |
|
|
|
|
const futureEvent = { |
|
|
|
|
_id: 'future123', |
|
|
|
|
uid: fakeUserId, |
|
|
|
|
startTime: fakeStartTime, |
|
|
|
|
subject: fakeSubject, |
|
|
|
|
startTime: endOfNextMinute, |
|
|
|
|
endTime: new Date(endOfNextMinute.getTime() + 3600000), // 1 hour later
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
await method(fakeEvent); |
|
|
|
|
|
|
|
|
|
sinon.assert.calledWith( |
|
|
|
|
api.broadcast as sinon.SinonStub, |
|
|
|
|
'notify.calendar', |
|
|
|
|
fakeUserId, |
|
|
|
|
sinon.match({ |
|
|
|
|
title: fakeSubject, |
|
|
|
|
payload: { _id: fakeEventId }, |
|
|
|
|
}), |
|
|
|
|
); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('Private: sendCurrentNotifications', () => { |
|
|
|
|
it('should send notification for all events and flag them as sent', async () => { |
|
|
|
|
await testPrivateMethod(service, 'sendCurrentNotifications', async (method) => { |
|
|
|
|
const proto = Object.getPrototypeOf(service); |
|
|
|
|
(proto.sendEventNotification as sinon.SinonStub).restore(); |
|
|
|
|
sandbox.stub(proto, 'sendEventNotification').resolves(); |
|
|
|
|
|
|
|
|
|
const fakeDate = new Date('2025-01-01T10:00:00Z'); |
|
|
|
|
const fakeEvents = [ |
|
|
|
|
{ _id: 'event1', uid: fakeUserId, startTime: fakeStartTime }, |
|
|
|
|
{ _id: 'event2', uid: fakeUserId, startTime: fakeStartTime }, |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
CalendarEventMock.findEventsToNotify.returns({ |
|
|
|
|
toArray: sinon.stub().resolves(fakeEvents), |
|
|
|
|
CalendarEventMock.findEventsToScheduleNow.returns({ |
|
|
|
|
toArray: sinon.stub().resolves([eventStartingSoon]), |
|
|
|
|
}); |
|
|
|
|
CalendarEventMock.findNextFutureEvent.resolves(futureEvent); |
|
|
|
|
|
|
|
|
|
await method(fakeDate); |
|
|
|
|
const originalHas = cronJobsMock.has; |
|
|
|
|
const originalRemove = cronJobsMock.remove; |
|
|
|
|
const originalAddAtTimestamp = cronJobsMock.addAtTimestamp; |
|
|
|
|
|
|
|
|
|
sinon.assert.calledWith(CalendarEventMock.findEventsToNotify, fakeDate, 1); |
|
|
|
|
sinon.assert.calledTwice(proto.sendEventNotification as sinon.SinonStub); |
|
|
|
|
sinon.assert.calledTwice(CalendarEventMock.flagNotificationSent); |
|
|
|
|
sinon.assert.calledWith(CalendarEventMock.flagNotificationSent, 'event1'); |
|
|
|
|
sinon.assert.calledWith(CalendarEventMock.flagNotificationSent, 'event2'); |
|
|
|
|
sinon.assert.calledOnceWithExactly(proto.doSetupNextNotification as sinon.SinonStub, true); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
const hasStub = sinon.stub().resolves(false); |
|
|
|
|
const removeStub = sinon.stub().resolves(); |
|
|
|
|
const addAtTimestampStub = sinon.stub().resolves(); |
|
|
|
|
|
|
|
|
|
describe('Private: doSetupNextNotification', () => { |
|
|
|
|
it('should remove calendar-reminders cron job if no events found', async () => { |
|
|
|
|
await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { |
|
|
|
|
CalendarEventMock.findNextNotificationDate.resolves(null); |
|
|
|
|
cronJobsMock.jobNames.add('calendar-reminders'); |
|
|
|
|
cronJobsMock.has = hasStub; |
|
|
|
|
cronJobsMock.remove = removeStub; |
|
|
|
|
cronJobsMock.addAtTimestamp = addAtTimestampStub; |
|
|
|
|
|
|
|
|
|
await method(false); |
|
|
|
|
try { |
|
|
|
|
await method(); |
|
|
|
|
|
|
|
|
|
expect(cronJobsMock.jobNames.has('calendar-reminders')).to.false; |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
sinon.assert.calledWith(hasStub, 'calendar-next-status-change'); |
|
|
|
|
sinon.assert.notCalled(removeStub); |
|
|
|
|
|
|
|
|
|
it('should schedule notifications at the next date', async () => { |
|
|
|
|
await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { |
|
|
|
|
const nextDate = new Date('2025-01-01T10:00:00Z'); |
|
|
|
|
CalendarEventMock.findNextNotificationDate.resolves(nextDate); |
|
|
|
|
sinon.assert.calledOnce(addAtTimestampStub); |
|
|
|
|
|
|
|
|
|
await method(false); |
|
|
|
|
sinon.assert.calledWith(addAtTimestampStub, 'calendar-next-status-change', futureEvent.startTime, sinon.match.func); |
|
|
|
|
|
|
|
|
|
expect(cronJobsMock.jobNames.has('calendar-reminders')).to.true; |
|
|
|
|
sinon.assert.neverCalledWith(addAtTimestampStub, sinon.match(/^calendar-status-/), sinon.match.any, sinon.match.any); |
|
|
|
|
} finally { |
|
|
|
|
cronJobsMock.has = originalHas; |
|
|
|
|
cronJobsMock.remove = originalRemove; |
|
|
|
|
cronJobsMock.addAtTimestamp = originalAddAtTimestamp; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should send current notifications if date is in the past', async () => { |
|
|
|
|
await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { |
|
|
|
|
const proto = Object.getPrototypeOf(service); |
|
|
|
|
(proto.sendCurrentNotifications as sinon.SinonStub).restore(); |
|
|
|
|
sandbox.stub(proto, 'sendCurrentNotifications').resolves(); |
|
|
|
|
it('should fetch events at execution time rather than scheduling them individually', async () => { |
|
|
|
|
await testPrivateMethod(service, 'doSetupNextStatusChange', async (method) => { |
|
|
|
|
settingsMock.set('Calendar_BusyStatus_Enabled', true); |
|
|
|
|
|
|
|
|
|
const pastDate = new Date(); |
|
|
|
|
pastDate.setMinutes(pastDate.getMinutes() - 10); |
|
|
|
|
CalendarEventMock.findNextNotificationDate.resolves(pastDate); |
|
|
|
|
const now = new Date(); |
|
|
|
|
const startOfNextMinute = new Date(now); |
|
|
|
|
startOfNextMinute.setSeconds(0, 0); |
|
|
|
|
startOfNextMinute.setMinutes(startOfNextMinute.getMinutes() + 1); |
|
|
|
|
|
|
|
|
|
await method(false); |
|
|
|
|
const endOfNextMinute = new Date(startOfNextMinute); |
|
|
|
|
endOfNextMinute.setMinutes(startOfNextMinute.getMinutes() + 1); |
|
|
|
|
|
|
|
|
|
sinon.assert.calledWith(proto.sendCurrentNotifications as sinon.SinonStub, pastDate); |
|
|
|
|
expect(cronJobsMock.jobNames.size).to.equal(0); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
CalendarEventMock.findEventsToScheduleNow.returns({ |
|
|
|
|
toArray: sinon.stub().resolves([]), |
|
|
|
|
}); |
|
|
|
|
CalendarEventMock.findNextFutureEvent.resolves(null); |
|
|
|
|
|
|
|
|
|
it('should schedule future notifications even if date is in the past when recursive', async () => { |
|
|
|
|
await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { |
|
|
|
|
const pastDate = new Date(); |
|
|
|
|
pastDate.setMinutes(pastDate.getMinutes() - 10); |
|
|
|
|
CalendarEventMock.findNextNotificationDate.resolves(pastDate); |
|
|
|
|
const originalHas = cronJobsMock.has; |
|
|
|
|
const originalRemove = cronJobsMock.remove; |
|
|
|
|
const originalAddAtTimestamp = cronJobsMock.addAtTimestamp; |
|
|
|
|
|
|
|
|
|
await method(true); |
|
|
|
|
const hasStub = sinon.stub().resolves(false); |
|
|
|
|
const removeStub = sinon.stub().resolves(); |
|
|
|
|
const addAtTimestampStub = sinon.stub().resolves(); |
|
|
|
|
|
|
|
|
|
sinon.assert.notCalled(service.sendCurrentNotifications as sinon.SinonStub); |
|
|
|
|
expect(cronJobsMock.jobNames.size).to.equal(1); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
cronJobsMock.has = hasStub; |
|
|
|
|
cronJobsMock.remove = removeStub; |
|
|
|
|
cronJobsMock.addAtTimestamp = addAtTimestampStub; |
|
|
|
|
|
|
|
|
|
describe('Overlapping events', () => { |
|
|
|
|
it('should not set up status change if no endTime is provided when updating', async () => { |
|
|
|
|
const fakeEvent = { |
|
|
|
|
_id: fakeEventId, |
|
|
|
|
uid: fakeUserId, |
|
|
|
|
startTime: fakeStartTime, |
|
|
|
|
subject: fakeSubject, |
|
|
|
|
}; |
|
|
|
|
try { |
|
|
|
|
await method(); |
|
|
|
|
|
|
|
|
|
CalendarEventMock.findOne.resolves(fakeEvent); |
|
|
|
|
sinon.assert.calledWith(addAtTimestampStub, 'calendar-next-status-change', endOfNextMinute, sinon.match.func); |
|
|
|
|
|
|
|
|
|
await service.update(fakeEventId, { |
|
|
|
|
subject: 'New Subject', |
|
|
|
|
}); |
|
|
|
|
const callback = addAtTimestampStub.firstCall.args[2]; |
|
|
|
|
const doSetupNextStatusChangeStub = sinon.stub(service, 'doSetupNextStatusChange').resolves(); |
|
|
|
|
await callback(); |
|
|
|
|
|
|
|
|
|
sinon.assert.notCalled(statusEventManagerMock.setupAppointmentStatusChange); |
|
|
|
|
sinon.assert.calledOnce(doSetupNextStatusChangeStub); |
|
|
|
|
doSetupNextStatusChangeStub.restore(); |
|
|
|
|
} finally { |
|
|
|
|
cronJobsMock.has = originalHas; |
|
|
|
|
cronJobsMock.remove = originalRemove; |
|
|
|
|
cronJobsMock.addAtTimestamp = originalAddAtTimestamp; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('Overlapping events', () => { |
|
|
|
|
it('should cancel upcoming status changes for a user', async () => { |
|
|
|
|
const customDate = new Date('2025-02-01'); |
|
|
|
|
|
|
|
|
|
|