snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
import type { AgendaCronJobs } from '@rocket.chat/cron';
// #TODO: Move this to a package and write unit tests there ensuring that the behavior of the mock and the real class match 1:1
export class MockedCronJobs {
public jobNames = new Set<string>();
private _started = false;
public get started(): boolean {
return this._started;
}
start: AgendaCronJobs['start'] = async () => {
this._started = true;
};
add: AgendaCronJobs['add'] = async (name) => {
this.jobNames.add(name);
};
addAtTimestamp: AgendaCronJobs['addAtTimestamp'] = async (name) => {
this.jobNames.add(name);
};
remove: AgendaCronJobs['remove'] = async (name) => {
this.jobNames.delete(name);
};
has: AgendaCronJobs['has'] = async (jobName) => {
return this.jobNames.has(jobName);
};
}

View File

@@ -0,0 +1,636 @@
import { api } from '@rocket.chat/core-services';
import { expect } from 'chai';
import { describe, it, beforeEach, afterEach } from 'mocha';
import type { DeleteResult, UpdateResult } from 'mongodb';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
import { testPrivateMethod, createFreshServiceInstance } from '../utils';
import { MockedCronJobs } from './mocks/cronJobs';
const settingsMock = new Map<string, any>();
const cronJobsMock = new MockedCronJobs();
const CalendarEventMock = {
insertOne: sinon.stub(),
findOne: sinon.stub(),
findByUserIdAndDate: sinon.stub(),
updateEvent: sinon.stub(),
deleteOne: sinon.stub(),
findNextNotificationDate: sinon.stub(),
findEventsToNotify: sinon.stub(),
flagNotificationSent: sinon.stub(),
findOneByExternalIdAndUserId: sinon.stub(),
findNextFutureEvent: sinon.stub(),
findOverlappingEvents: sinon.stub(),
};
const PresenceMock = {
setActiveState: sinon.stub().resolves(),
endActiveState: sinon.stub().resolves(),
};
const UsersMock = {
findOneById: sinon.stub().resolves({ language: 'en' }),
};
const getUserPreferenceMock = sinon.stub();
const serviceMocks = {
'../../settings': { settings: settingsMock },
'@rocket.chat/core-services': {
api,
ServiceClassInternal: class {},
Presence: PresenceMock,
},
'@rocket.chat/cron': { cronJobs: cronJobsMock },
'@rocket.chat/models': { CalendarEvent: CalendarEventMock, Users: UsersMock },
'../../lib/utils/lib/getUserPreference': { getUserPreference: getUserPreferenceMock },
'../../lib/i18n': { i18n: { t: sinon.stub().returns('Outlook: In a meeting') } },
};
const { CalendarService } = proxyquire.noCallThru().load('../../../../../server/services/calendar/service', serviceMocks);
describe('CalendarService', () => {
let sandbox: sinon.SinonSandbox;
let service: InstanceType<typeof CalendarService>;
const fakeUserId = 'user123';
const fakeEventId = 'event456';
const fakeExternalId = 'external789';
const fakeStartTime = new Date('2025-01-01T10:00:00Z');
const fakeEndTime = new Date('2025-01-01T11:00:00Z');
const fakeSubject = 'Test Meeting';
const fakeDescription = 'This is a test meeting';
const fakeMeetingUrl = 'https://meet.test/123';
beforeEach(() => {
sandbox = sinon.createSandbox();
service = new CalendarService();
stubServiceMethods();
setupCalendarEventMocks();
setupPresenceMocks();
setupOtherMocks();
});
function stubServiceMethods() {
const proto = Object.getPrototypeOf(service);
sandbox.stub(proto, 'parseDescriptionForMeetingUrl').resolves(fakeMeetingUrl);
sandbox.stub(proto, 'findImportedEvent').callsFake(async (externalId, uid) => {
return CalendarEventMock.findOneByExternalIdAndUserId(externalId, uid);
});
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() {
const freshMocks = {
insertOne: sinon.stub().resolves({ insertedId: fakeEventId }),
findOne: sinon.stub().resolves(null),
findByUserIdAndDate: sinon.stub().returns({
toArray: sinon.stub().resolves([]),
}),
updateEvent: sinon.stub().resolves({ modifiedCount: 1, matchedCount: 1 } as UpdateResult),
deleteOne: sinon.stub().resolves({ deletedCount: 1 } as DeleteResult),
findNextNotificationDate: sinon.stub().resolves(null),
findEventsToNotify: sinon.stub().returns({
toArray: sinon.stub().resolves([]),
}),
flagNotificationSent: sinon.stub().resolves(),
findOneByExternalIdAndUserId: sinon.stub().resolves(null),
findNextFutureEvent: sinon.stub().resolves(null),
findOverlappingEvents: sinon.stub().returns({
next: sinon.stub().resolves(null),
toArray: sinon.stub().resolves([]),
}),
};
Object.assign(CalendarEventMock, freshMocks);
}
function setupPresenceMocks() {
PresenceMock.setActiveState.resetHistory();
PresenceMock.endActiveState.resetHistory();
UsersMock.findOneById.resetHistory();
UsersMock.findOneById.resolves({ language: 'en' });
}
function setupOtherMocks() {
sandbox.stub(api, 'broadcast').resolves();
settingsMock.clear();
settingsMock.set(
'Calendar_MeetingUrl_Regex',
'(?:[?&]callUrl=([^\n&<]+))|(?:(?:%3F)|(?:%26))callUrl(?:%3D)((?:(?:[^\n&<](?!%26)))+[^\n&<]?)',
);
settingsMock.set('Calendar_BusyStatus_Enabled', true);
cronJobsMock.jobNames.clear();
getUserPreferenceMock.reset();
getUserPreferenceMock.resolves(true);
}
afterEach(() => {
sandbox.restore();
});
describe('#create', () => {
it('should create a new calendar event', async () => {
const eventData = {
uid: fakeUserId,
startTime: fakeStartTime,
endTime: fakeEndTime,
subject: fakeSubject,
description: fakeDescription,
meetingUrl: fakeMeetingUrl,
reminderMinutesBeforeStart: 5,
};
const result = await service.create(eventData);
expect(result).to.equal(fakeEventId);
expect(CalendarEventMock.insertOne.callCount).to.equal(1);
expect(CalendarEventMock.insertOne.firstCall.args[0]).to.include({
uid: fakeUserId,
startTime: fakeStartTime,
subject: fakeSubject,
description: fakeDescription,
meetingUrl: fakeMeetingUrl,
reminderMinutesBeforeStart: 5,
notificationSent: false,
});
sinon.assert.calledOnce(service.setupNextStatusChange);
});
});
describe('#import', () => {
it('should create a new event if externalId is not provided', async () => {
const eventData = {
uid: fakeUserId,
startTime: fakeStartTime,
subject: fakeSubject,
description: fakeDescription,
};
await service.import(eventData);
sinon.assert.calledOnce(CalendarEventMock.insertOne);
sinon.assert.calledOnce(service.setupNextStatusChange);
});
it('should update existing event if found by externalId', async () => {
const eventData = {
uid: fakeUserId,
startTime: fakeStartTime,
subject: fakeSubject,
description: fakeDescription,
externalId: fakeExternalId,
};
CalendarEventMock.findOneByExternalIdAndUserId.resolves({
_id: fakeEventId,
uid: fakeUserId,
externalId: fakeExternalId,
});
await service.import(eventData);
sinon.assert.calledWith(CalendarEventMock.findOneByExternalIdAndUserId, fakeExternalId, fakeUserId);
sinon.assert.calledOnce(CalendarEventMock.updateEvent);
sinon.assert.notCalled(CalendarEventMock.insertOne);
});
});
describe('#update', () => {
it('should update an existing event', async () => {
const fakeEvent = {
_id: fakeEventId,
uid: fakeUserId,
startTime: fakeStartTime,
endTime: fakeEndTime,
subject: fakeSubject,
};
CalendarEventMock.findOne.resolves(fakeEvent);
const updateData = {
subject: 'Updated Subject',
description: 'Updated Description',
};
await service.update(fakeEventId, updateData);
sinon.assert.calledWith(CalendarEventMock.updateEvent, fakeEventId, sinon.match.has('subject', 'Updated Subject'));
});
it('should reschedule the status change when start/end times change', async () => {
const fakeEvent = {
_id: fakeEventId,
uid: fakeUserId,
startTime: fakeStartTime,
endTime: fakeEndTime,
subject: fakeSubject,
};
CalendarEventMock.findOne.resolves(fakeEvent);
const newStartTime = new Date('2025-01-02T10:00:00Z');
const newEndTime = new Date('2025-01-02T11:00:00Z');
await service.update(fakeEventId, {
startTime: newStartTime,
endTime: newEndTime,
});
sinon.assert.calledOnce(service.setupNextStatusChange);
});
});
describe('#delete', () => {
it('should delete an event', async () => {
const fakeEvent = {
_id: fakeEventId,
uid: fakeUserId,
startTime: fakeStartTime,
subject: fakeSubject,
};
CalendarEventMock.findOne.resolves(fakeEvent);
await service.delete(fakeEventId);
sinon.assert.calledWith(CalendarEventMock.deleteOne, { _id: fakeEventId });
});
it('should lower the busy expiry to the remaining active event when deleting an overlapping in-progress event', async () => {
const t = Date.now();
CalendarEventMock.findOne.resolves({
_id: fakeEventId,
uid: fakeUserId,
startTime: new Date(t - 60 * 1000),
endTime: new Date(t + 120 * 60 * 1000),
});
CalendarEventMock.findOverlappingEvents.returns({
next: sinon.stub().resolves(null),
toArray: sinon.stub().resolves([{ _id: 'remaining', endTime: new Date(t + 30 * 60 * 1000) }]),
});
await service.delete(fakeEventId);
sinon.assert.calledOnce(PresenceMock.setActiveState);
sinon.assert.notCalled(PresenceMock.endActiveState);
const [uid, state] = PresenceMock.setActiveState.firstCall.args;
expect(uid).to.equal(fakeUserId);
expect(state.statusExpiresAt.getTime()).to.equal(t + 30 * 60 * 1000);
});
it('should end the busy state when deleting the only active in-progress event', async () => {
const t = Date.now();
CalendarEventMock.findOne.resolves({
_id: fakeEventId,
uid: fakeUserId,
startTime: new Date(t - 60 * 1000),
endTime: new Date(t + 60 * 60 * 1000),
});
CalendarEventMock.findOverlappingEvents.returns({
next: sinon.stub().resolves(null),
toArray: sinon.stub().resolves([]),
});
await service.delete(fakeEventId);
sinon.assert.calledOnceWithExactly(PresenceMock.endActiveState, fakeUserId, 'calendar');
sinon.assert.notCalled(PresenceMock.setActiveState);
});
it('should not touch presence when deleting an event that is not in progress', async () => {
const t = Date.now();
CalendarEventMock.findOne.resolves({
_id: fakeEventId,
uid: fakeUserId,
startTime: new Date(t + 60 * 60 * 1000),
endTime: new Date(t + 120 * 60 * 1000),
});
await service.delete(fakeEventId);
sinon.assert.notCalled(PresenceMock.endActiveState);
sinon.assert.notCalled(PresenceMock.setActiveState);
});
});
describe('#setupNextNotification', () => {
it('should call doSetupNextNotification internally', async () => {
const serviceExports = proxyquire.noCallThru().load('../../../../../server/services/calendar/service', serviceMocks);
const testService = createFreshServiceInstance<InstanceType<typeof CalendarService>>(serviceExports);
const localSandbox = sinon.createSandbox();
try {
const doSetupStub = localSandbox.stub(Object.getPrototypeOf(testService), 'doSetupNextNotification').resolves();
await testService.setupNextNotification();
sinon.assert.calledOnceWithExactly(doSetupStub, false);
} finally {
localSandbox.restore();
}
});
});
describe('Private: parseDescriptionForMeetingUrl', () => {
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';
const result = await method(testDescription);
expect(result).to.equal('https://special-meeting.com/123');
});
});
});
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);
await method(false);
expect(cronJobsMock.jobNames.has('calendar-reminders')).to.true;
});
});
});
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-status-scheduler');
sinon.assert.calledWith(removeStub, 'calendar-status-scheduler');
sinon.assert.notCalled(addAtTimestampStub);
} finally {
cronJobsMock.has = originalHas;
cronJobsMock.remove = originalRemove;
cronJobsMock.addAtTimestamp = originalAddAtTimestamp;
}
});
});
it('should schedule a job at the next event start time', async () => {
await testPrivateMethod(service, 'doSetupNextStatusChange', async (method) => {
settingsMock.set('Calendar_BusyStatus_Enabled', true);
const futureEvent = {
_id: 'future123',
uid: fakeUserId,
startTime: new Date(Date.now() + 60000),
endTime: new Date(Date.now() + 3660000),
};
CalendarEventMock.findNextFutureEvent.resolves(futureEvent);
const originalHas = cronJobsMock.has;
const originalRemove = cronJobsMock.remove;
const originalAddAtTimestamp = cronJobsMock.addAtTimestamp;
const hasStub = sinon.stub().resolves(false);
const removeStub = sinon.stub().resolves();
const addAtTimestampStub = sinon.stub().resolves();
cronJobsMock.has = hasStub;
cronJobsMock.remove = removeStub;
cronJobsMock.addAtTimestamp = addAtTimestampStub;
try {
await method();
sinon.assert.calledOnce(addAtTimestampStub);
sinon.assert.calledWith(addAtTimestampStub, 'calendar-status-scheduler', futureEvent.startTime, sinon.match.func);
} finally {
cronJobsMock.has = originalHas;
cronJobsMock.remove = originalRemove;
cronJobsMock.addAtTimestamp = originalAddAtTimestamp;
}
});
});
it('should not schedule a job when there are no future events', async () => {
await testPrivateMethod(service, 'doSetupNextStatusChange', async (method) => {
settingsMock.set('Calendar_BusyStatus_Enabled', true);
CalendarEventMock.findNextFutureEvent.resolves(null);
const originalHas = cronJobsMock.has;
const originalRemove = cronJobsMock.remove;
const originalAddAtTimestamp = cronJobsMock.addAtTimestamp;
const hasStub = sinon.stub().resolves(false);
const removeStub = sinon.stub().resolves();
const addAtTimestampStub = sinon.stub().resolves();
cronJobsMock.has = hasStub;
cronJobsMock.remove = removeStub;
cronJobsMock.addAtTimestamp = addAtTimestampStub;
try {
await method();
sinon.assert.notCalled(addAtTimestampStub);
} finally {
cronJobsMock.has = originalHas;
cronJobsMock.remove = originalRemove;
cronJobsMock.addAtTimestamp = originalAddAtTimestamp;
}
});
});
});
describe('Private: processEventStart', () => {
// await directly: testPrivateMethod doesn't await its callback, so async assertions wouldn't fail
const callProcessEventStart = (event: unknown) => (service as any).processEventStart(event);
it('should set statusExpiresAt to the latest endTime among overlapping active events', async () => {
const now = Date.now();
const event = {
_id: fakeEventId,
uid: fakeUserId,
startTime: new Date(now - 1000),
endTime: new Date(now + 60 * 60 * 1000),
};
CalendarEventMock.findOverlappingEvents.returns({
next: sinon.stub().resolves(null),
toArray: sinon.stub().resolves([
{ _id: 'later', endTime: new Date(now + 120 * 60 * 1000) },
{ _id: 'earlier', endTime: new Date(now + 30 * 60 * 1000) },
]),
});
await callProcessEventStart(event);
sinon.assert.calledOnce(PresenceMock.setActiveState);
const [uid, state] = PresenceMock.setActiveState.firstCall.args;
expect(uid).to.equal(fakeUserId);
expect(state.statusSource).to.equal('external');
expect(state.statusExpiresAt.getTime()).to.equal(now + 120 * 60 * 1000);
expect(state.statusId).to.equal('calendar');
expect(state.statusText).to.equal('Outlook: In a meeting');
});
it('should use the event own endTime when there are no overlapping events', async () => {
const now = Date.now();
const endTime = new Date(now + 60 * 60 * 1000);
const event = {
_id: fakeEventId,
uid: fakeUserId,
startTime: new Date(now - 1000),
endTime,
};
await callProcessEventStart(event);
sinon.assert.calledOnce(PresenceMock.setActiveState);
const [, state] = PresenceMock.setActiveState.firstCall.args;
expect(state.statusExpiresAt.getTime()).to.equal(endTime.getTime());
});
it('should not apply busy status when the event has no endTime', async () => {
const event = {
_id: fakeEventId,
uid: fakeUserId,
startTime: new Date(),
};
await callProcessEventStart(event);
sinon.assert.notCalled(PresenceMock.setActiveState);
});
});
describe('Private: reconcileInProgressEvent', () => {
const callReconcile = (eventId: string) => (service as any).reconcileInProgressEvent(eventId);
const now = () => Date.now();
it('applies the busy claim for an in-progress event using its endTime', async () => {
const t = now();
CalendarEventMock.findOne.resolves({
_id: fakeEventId,
uid: fakeUserId,
startTime: new Date(t - 60 * 1000),
endTime: new Date(t + 60 * 60 * 1000),
});
await callReconcile(fakeEventId);
sinon.assert.calledOnce(PresenceMock.setActiveState);
sinon.assert.notCalled(PresenceMock.endActiveState);
const [uid, state] = PresenceMock.setActiveState.firstCall.args;
expect(uid).to.equal(fakeUserId);
expect(state.statusSource).to.equal('external');
expect(state.statusExpiresAt.getTime()).to.equal(t + 60 * 60 * 1000);
});
it('extends the busy claim to the latest end among overlapping in-progress events', async () => {
const t = now();
CalendarEventMock.findOne.resolves({
_id: fakeEventId,
uid: fakeUserId,
startTime: new Date(t - 60 * 1000),
endTime: new Date(t + 30 * 60 * 1000),
});
CalendarEventMock.findOverlappingEvents.returns({
next: sinon.stub().resolves(null),
toArray: sinon.stub().resolves([{ _id: 'longer', endTime: new Date(t + 90 * 60 * 1000) }]),
});
await callReconcile(fakeEventId);
sinon.assert.calledOnce(PresenceMock.setActiveState);
const [, state] = PresenceMock.setActiveState.firstCall.args;
expect(state.statusExpiresAt.getTime()).to.equal(t + 90 * 60 * 1000);
});
it('does nothing for an event that has not started yet', async () => {
const t = now();
CalendarEventMock.findOne.resolves({
_id: fakeEventId,
uid: fakeUserId,
startTime: new Date(t + 60 * 60 * 1000),
endTime: new Date(t + 120 * 60 * 1000),
});
await callReconcile(fakeEventId);
sinon.assert.notCalled(PresenceMock.setActiveState);
sinon.assert.notCalled(PresenceMock.endActiveState);
});
it('does nothing for an event that has already ended', async () => {
const t = now();
CalendarEventMock.findOne.resolves({
_id: fakeEventId,
uid: fakeUserId,
startTime: new Date(t - 120 * 60 * 1000),
endTime: new Date(t - 60 * 60 * 1000),
});
await callReconcile(fakeEventId);
sinon.assert.notCalled(PresenceMock.setActiveState);
sinon.assert.notCalled(PresenceMock.endActiveState);
});
it('does nothing for a non-busy event', async () => {
const t = now();
CalendarEventMock.findOne.resolves({
_id: fakeEventId,
uid: fakeUserId,
startTime: new Date(t - 60 * 1000),
endTime: new Date(t + 60 * 60 * 1000),
busy: false,
});
await callReconcile(fakeEventId);
sinon.assert.notCalled(PresenceMock.setActiveState);
sinon.assert.notCalled(PresenceMock.endActiveState);
});
it('does nothing when busy status is disabled', async () => {
settingsMock.set('Calendar_BusyStatus_Enabled', false);
const t = now();
CalendarEventMock.findOne.resolves({
_id: fakeEventId,
uid: fakeUserId,
startTime: new Date(t - 60 * 1000),
endTime: new Date(t + 60 * 60 * 1000),
});
await callReconcile(fakeEventId);
sinon.assert.notCalled(PresenceMock.setActiveState);
sinon.assert.notCalled(PresenceMock.endActiveState);
});
});
});

View File

@@ -0,0 +1,22 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import proxyquire from 'proxyquire';
const { getShiftedTime } = proxyquire.noCallThru().load('../../../../../../server/services/calendar/utils/getShiftedTime', {});
describe('#getShiftedTime', () => {
it('should shift time forward by minutes', () => {
const date = new Date('2025-01-01T10:00:00Z');
const result = getShiftedTime(date, 30);
expect(result.getTime()).to.equal(new Date('2025-01-01T10:30:00Z').getTime());
expect(date.getTime()).to.equal(new Date('2025-01-01T10:00:00Z').getTime());
});
it('should shift time backward by negative minutes', () => {
const date = new Date('2025-01-01T10:00:00Z');
const result = getShiftedTime(date, -15);
expect(result.getTime()).to.equal(new Date('2025-01-01T09:45:00Z').getTime());
});
});