Files
upstream-rocketchat/apps/meteor/tests/unit/server/modules/notifications/notifications.module.spec.ts
upstream-mirror 30810eecd7
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 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

154 lines
5.4 KiB
TypeScript

import { expect } from 'chai';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
import { Streamer, StreamerCentral } from '../../../../../server/modules/streamer/streamer.module';
class TestStreamer extends Streamer<any> {
registerPublication(): void {
// noop
}
registerMethod(): void {
// noop
}
changedPayload(): string {
return 'payload';
}
}
const validateActionStub = sinon.stub();
const processSerializedSignalStub = sinon.stub();
const countByRoomIdAndUserIdStub = sinon.stub();
const findSubscriptionsExcludingUserStub = sinon.stub();
const { NotificationsModule } = proxyquire.noCallThru().load('../../../../../server/modules/notifications/notifications.module', {
'@rocket.chat/core-services': {
VideoConf: { validateAction: validateActionStub },
MediaCall: { processSerializedSignal: processSerializedSignalStub },
},
'@rocket.chat/models': {
Subscriptions: {
countByRoomIdAndUserId: countByRoomIdAndUserIdStub,
findByRoomIdAndNotUserId: findSubscriptionsExcludingUserStub,
},
Rooms: {},
Users: {},
},
});
describe('NotificationsModule', () => {
let notifications: any;
beforeEach(() => {
notifications = new NotificationsModule(TestStreamer as any);
notifications.configure();
});
describe('notify-user allowWrite', () => {
// `isWriteAllowed` is public on the concrete Streamer class but not on the IStreamer
// interface that `streamUser` is typed as, so cast to reach it.
const writeAllowed = (eventName: string, ...args: unknown[]) =>
(notifications.streamUser as unknown as Streamer<'notify-user'>).isWriteAllowed({ userId: 'userId' } as any, eventName, args);
beforeEach(() => {
validateActionStub.reset();
validateActionStub.resolves(true);
processSerializedSignalStub.reset();
processSerializedSignalStub.resolves(undefined);
countByRoomIdAndUserIdStub.reset();
findSubscriptionsExcludingUserStub.reset();
});
afterEach(() => {
Object.keys(StreamerCentral.instances).forEach((name) => delete StreamerCentral.instances[name]);
});
['force_logout', 'notification', 'message', 'uiInteraction', 'subscriptions-changed', 'webdav', 'banners'].forEach((event) => {
it(`should deny a logged-in client writing "${event}" to another user's stream`, async () => {
expect(await writeAllowed(`victim/${event}`, { foo: 'bar' })).to.equal(false);
});
});
it("should deny writes even to the client's own stream", async () => {
expect(await writeAllowed(`userId/force_logout`, undefined)).to.equal(false);
});
it('should accept "video-conference" and delegate authorization to VideoConf.validateAction', async () => {
const result = await writeAllowed(`userId/video-conference`, {
action: 'call-start',
params: { callId: '123', uid: '456', rid: '789' },
});
expect(result).to.be.true;
expect(validateActionStub.calledOnceWith('call-start', 'userId', { callId: '123', uid: '456', rid: '789' })).to.be.true;
});
it('should process "media-calls" signals server-side and never broadcast them', async () => {
const signal = '{"type":"offer"}';
const result = await writeAllowed(`userId/media-calls`, signal);
expect(result).to.equal(false);
expect(processSerializedSignalStub.calledOnceWith('userId', signal)).to.be.true;
});
});
describe('notify-room-users allowWrite', () => {
const writeAllowed = (eventName: string, ...args: unknown[]) =>
(notifications.streamRoomUsers as unknown as Streamer<'notify-room-users'>).isWriteAllowed(
{ userId: 'attacker' } as any,
eventName,
args,
);
beforeEach(() => {
countByRoomIdAndUserIdStub.reset();
countByRoomIdAndUserIdStub.resolves(1); // attacker is subscribed to the room
findSubscriptionsExcludingUserStub.reset();
findSubscriptionsExcludingUserStub.returns({ toArray: async () => [{ u: { _id: 'victim' } }] });
});
afterEach(() => {
Object.keys(StreamerCentral.instances).forEach((name) => delete StreamerCentral.instances[name]);
});
['force_logout', 'notification', 'message', 'uiInteraction', 'subscriptions-changed', 'webdav', 'banners'].forEach((event) => {
it(`should deny and not relay an arbitrary "${event}" event to other room members`, async () => {
const emitSpy = sinon.spy(notifications.streamUser, 'emit');
const result = await writeAllowed(`room1/${event}`, { foo: 'bar' });
expect(result).to.equal(false);
expect(findSubscriptionsExcludingUserStub.called).to.equal(false);
expect(emitSpy.called).to.equal(false);
});
});
it('should not relay anything for a user not subscribed to the room', async () => {
countByRoomIdAndUserIdStub.resolves(0);
const emitSpy = sinon.spy(notifications.streamUser, 'emit');
const result = await writeAllowed('room1/video-conference', {
action: 'call-start',
params: { callId: '123', uid: 'victim', rid: 'room1' },
});
expect(result).to.equal(false);
expect(findSubscriptionsExcludingUserStub.called).to.equal(false);
expect(emitSpy.called).to.equal(false);
});
['video-conference', 'userData'].forEach((event) => {
it(`should relay "${event}" event to other room members`, async () => {
const emitSpy = sinon.spy(notifications.streamUser, 'emit');
const result = await writeAllowed(`room1/${event}`, { foo: 'bar' });
expect(result).to.equal(false);
expect(findSubscriptionsExcludingUserStub.called).to.equal(true);
expect(emitSpy.called).to.equal(true);
});
});
});
});