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,264 @@
import { expect } from 'chai';
import { beforeEach, describe, it } from 'mocha';
import p from 'proxyquire';
import sinon from 'sinon';
const modelsMock = {
Rooms: {
findOneById: sinon.stub(),
incUsersCountById: sinon.stub(),
},
Subscriptions: {
findOneByRoomIdAndUserId: sinon.stub(),
banByRoomIdAndUserId: sinon.stub(),
},
Users: {
removeRoomByUserId: sinon.stub(),
},
};
const messageMock = {
saveSystemMessage: sinon.stub(),
};
const removeUserFromRolesAsyncMock = sinon.stub();
const notifyOnRoomChangedByIdMock = sinon.stub();
const notifyOnSubscriptionChangedMock = sinon.stub();
const afterBanFromRoomCallbackMock = { run: sinon.stub() };
const meteorErrorMock = class extends Error {
constructor(message: string) {
super(message);
}
};
const { performUserBan, banUserFromRoom } = p.noCallThru().load('../../../../../server/lib/rooms/banUserFromRoom.ts', {
'@rocket.chat/models': modelsMock,
'@rocket.chat/core-services': { Message: messageMock },
'meteor/meteor': { Meteor: { Error: meteorErrorMock } },
'../callbacks/afterBanFromRoomCallback': { afterBanFromRoomCallback: afterBanFromRoomCallbackMock },
'../roles/removeUserFromRoles': { removeUserFromRolesAsync: removeUserFromRolesAsyncMock },
'../notifyListener': {
notifyOnRoomChangedById: notifyOnRoomChangedByIdMock,
notifyOnSubscriptionChanged: notifyOnSubscriptionChangedMock,
},
});
describe('banUserFromRoom', () => {
const mockByUser = { _id: 'admin1', username: 'admin' };
beforeEach(() => {
modelsMock.Rooms.findOneById.reset();
modelsMock.Rooms.incUsersCountById.reset();
modelsMock.Subscriptions.findOneByRoomIdAndUserId.reset();
modelsMock.Subscriptions.banByRoomIdAndUserId.reset();
modelsMock.Users.removeRoomByUserId.reset();
messageMock.saveSystemMessage.reset();
removeUserFromRolesAsyncMock.reset();
notifyOnRoomChangedByIdMock.reset();
notifyOnSubscriptionChangedMock.reset();
afterBanFromRoomCallbackMock.run.reset();
});
describe('performUserBan', () => {
it('should return early if no subscription exists', async () => {
modelsMock.Subscriptions.findOneByRoomIdAndUserId.resolves(null);
const room = { _id: 'room1', t: 'c' };
const user = { _id: 'user1', username: 'testuser' };
await performUserBan(room, user, mockByUser);
expect(modelsMock.Subscriptions.banByRoomIdAndUserId.called).to.be.false;
expect(modelsMock.Users.removeRoomByUserId.called).to.be.false;
});
it('should throw if user has no username', async () => {
modelsMock.Subscriptions.findOneByRoomIdAndUserId.resolves({ _id: 'sub1' });
const room = { _id: 'room1', t: 'c' };
const user = { _id: 'user1' }; // no username
await expect(performUserBan(room, user, mockByUser)).to.be.rejectedWith('User must have a username to be banned from the room');
});
it('should return early if subscription is already BANNED', async () => {
modelsMock.Subscriptions.findOneByRoomIdAndUserId.resolves({ _id: 'sub1', status: 'BANNED' });
const room = { _id: 'room1', t: 'c' };
const user = { _id: 'user1', username: 'testuser' };
await performUserBan(room, user, mockByUser);
expect(modelsMock.Subscriptions.banByRoomIdAndUserId.called).to.be.false;
});
it('should ban the user subscription', async () => {
modelsMock.Subscriptions.findOneByRoomIdAndUserId.resolves({ _id: 'sub1' });
modelsMock.Subscriptions.banByRoomIdAndUserId.resolves();
modelsMock.Users.removeRoomByUserId.resolves();
modelsMock.Rooms.incUsersCountById.resolves();
removeUserFromRolesAsyncMock.resolves();
messageMock.saveSystemMessage.resolves();
const room = { _id: 'room1', t: 'c' };
const user = { _id: 'user1', username: 'testuser' };
await performUserBan(room, user, mockByUser);
expect(modelsMock.Subscriptions.banByRoomIdAndUserId.calledWith('room1', 'user1')).to.be.true;
});
it('should remove the room from user __rooms array', async () => {
modelsMock.Subscriptions.findOneByRoomIdAndUserId.resolves({ _id: 'sub1' });
modelsMock.Subscriptions.banByRoomIdAndUserId.resolves();
modelsMock.Users.removeRoomByUserId.resolves();
modelsMock.Rooms.incUsersCountById.resolves();
removeUserFromRolesAsyncMock.resolves();
messageMock.saveSystemMessage.resolves();
const room = { _id: 'room1', t: 'c' };
const user = { _id: 'user1', username: 'testuser' };
await performUserBan(room, user, mockByUser);
expect(modelsMock.Users.removeRoomByUserId.calledWith('user1', 'room1')).to.be.true;
});
it('should decrement the room user count', async () => {
modelsMock.Subscriptions.findOneByRoomIdAndUserId.resolves({ _id: 'sub1' });
modelsMock.Subscriptions.banByRoomIdAndUserId.resolves();
modelsMock.Users.removeRoomByUserId.resolves();
modelsMock.Rooms.incUsersCountById.resolves();
removeUserFromRolesAsyncMock.resolves();
messageMock.saveSystemMessage.resolves();
const room = { _id: 'room1', t: 'c' };
const user = { _id: 'user1', username: 'testuser' };
await performUserBan(room, user, mockByUser);
expect(modelsMock.Rooms.incUsersCountById.calledWith('room1', -1)).to.be.true;
});
it('should remove room-scoped roles for channels', async () => {
modelsMock.Subscriptions.findOneByRoomIdAndUserId.resolves({ _id: 'sub1' });
modelsMock.Subscriptions.banByRoomIdAndUserId.resolves();
modelsMock.Users.removeRoomByUserId.resolves();
modelsMock.Rooms.incUsersCountById.resolves();
removeUserFromRolesAsyncMock.resolves();
messageMock.saveSystemMessage.resolves();
const room = { _id: 'room1', t: 'c' };
const user = { _id: 'user1', username: 'testuser' };
await performUserBan(room, user, mockByUser);
expect(removeUserFromRolesAsyncMock.calledWith('user1', ['moderator', 'owner', 'leader'], 'room1')).to.be.true;
});
it('should remove room-scoped roles for private rooms', async () => {
modelsMock.Subscriptions.findOneByRoomIdAndUserId.resolves({ _id: 'sub1' });
modelsMock.Subscriptions.banByRoomIdAndUserId.resolves();
modelsMock.Users.removeRoomByUserId.resolves();
modelsMock.Rooms.incUsersCountById.resolves();
removeUserFromRolesAsyncMock.resolves();
messageMock.saveSystemMessage.resolves();
const room = { _id: 'room1', t: 'p' };
const user = { _id: 'user1', username: 'testuser' };
await performUserBan(room, user, mockByUser);
expect(removeUserFromRolesAsyncMock.calledWith('user1', ['moderator', 'owner', 'leader'], 'room1')).to.be.true;
});
it('should NOT remove room-scoped roles for DMs', async () => {
modelsMock.Subscriptions.findOneByRoomIdAndUserId.resolves({ _id: 'sub1' });
modelsMock.Subscriptions.banByRoomIdAndUserId.resolves();
modelsMock.Users.removeRoomByUserId.resolves();
modelsMock.Rooms.incUsersCountById.resolves();
messageMock.saveSystemMessage.resolves();
const room = { _id: 'room1', t: 'd' };
const user = { _id: 'user1', username: 'testuser' };
await performUserBan(room, user, mockByUser);
expect(removeUserFromRolesAsyncMock.called).to.be.false;
});
it('should save system message including who banned', async () => {
modelsMock.Subscriptions.findOneByRoomIdAndUserId.resolves({ _id: 'sub1' });
modelsMock.Subscriptions.banByRoomIdAndUserId.resolves();
modelsMock.Users.removeRoomByUserId.resolves();
modelsMock.Rooms.incUsersCountById.resolves();
removeUserFromRolesAsyncMock.resolves();
messageMock.saveSystemMessage.resolves();
const room = { _id: 'room1', t: 'c' };
const user = { _id: 'user1', username: 'testuser' };
const byUser = { _id: 'moderator2', username: 'mod' };
await performUserBan(room, user, byUser);
expect(messageMock.saveSystemMessage.calledOnce).to.be.true;
const { args } = messageMock.saveSystemMessage.firstCall;
expect(args[0]).to.equal('user-banned');
expect(args[1]).to.equal('room1');
expect(args[2]).to.equal('testuser');
expect(args[4]).to.deep.include({ u: byUser });
});
it('should notify subscription as removed so client drops the stream', async () => {
const subscription = { _id: 'sub1', rid: 'room1', u: { _id: 'user1' } };
modelsMock.Subscriptions.findOneByRoomIdAndUserId.resolves(subscription);
modelsMock.Subscriptions.banByRoomIdAndUserId.resolves();
modelsMock.Users.removeRoomByUserId.resolves();
modelsMock.Rooms.incUsersCountById.resolves();
removeUserFromRolesAsyncMock.resolves();
messageMock.saveSystemMessage.resolves();
const room = { _id: 'room1', t: 'c' };
const user = { _id: 'user1', username: 'testuser' };
await performUserBan(room, user, mockByUser);
expect(notifyOnSubscriptionChangedMock.calledWith(subscription, 'removed')).to.be.true;
expect(notifyOnRoomChangedByIdMock.calledWith('room1')).to.be.true;
});
});
describe('banUserFromRoom', () => {
it('should throw if room does not exist', async () => {
modelsMock.Rooms.findOneById.resolves(null);
const user = { _id: 'user1', username: 'testuser' };
await expect(banUserFromRoom('room1', user, mockByUser)).to.be.rejectedWith('error-invalid-room');
});
it('should call performUserBan with correct arguments', async () => {
const room = { _id: 'room1', t: 'c' };
modelsMock.Rooms.findOneById.resolves(room);
modelsMock.Subscriptions.findOneByRoomIdAndUserId.resolves({ _id: 'sub1' });
modelsMock.Subscriptions.banByRoomIdAndUserId.resolves();
modelsMock.Users.removeRoomByUserId.resolves();
modelsMock.Rooms.incUsersCountById.resolves();
removeUserFromRolesAsyncMock.resolves();
messageMock.saveSystemMessage.resolves();
const user = { _id: 'user1', username: 'testuser' };
await banUserFromRoom('room1', user, mockByUser);
expect(modelsMock.Subscriptions.banByRoomIdAndUserId.calledWith('room1', 'user1')).to.be.true;
expect(afterBanFromRoomCallbackMock.run.calledOnce).to.be.true;
expect(afterBanFromRoomCallbackMock.run.firstCall.args[0]).to.deep.include({
bannedUser: user,
userWhoBanned: mockByUser,
});
expect(afterBanFromRoomCallbackMock.run.firstCall.args[1]).to.equal(room);
});
});
});

View File

@@ -0,0 +1,107 @@
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import proxyquire from 'proxyquire';
import Sinon from 'sinon';
const RoomsStub = {
findByTypeAndNameOrId: Sinon.stub(),
findOneDirectRoomContainingAllUserIDs: Sinon.stub(),
};
const UsersStub = {
findOneById: Sinon.stub(),
findUsersByUsernames: Sinon.stub(),
};
const { findDirectRoomByIdentifier } = proxyquire.noCallThru().load('../../../../../server/lib/rooms/findDirectRoomByIdentifier.ts', {
'@rocket.chat/models': { Rooms: RoomsStub, Users: UsersStub },
});
const cursorOf = (docs: unknown[]) => ({ toArray: async () => docs });
describe('findDirectRoomByIdentifier', () => {
beforeEach(() => {
RoomsStub.findByTypeAndNameOrId.reset();
RoomsStub.findOneDirectRoomContainingAllUserIDs.reset();
UsersStub.findOneById.reset();
UsersStub.findUsersByUsernames.reset();
RoomsStub.findByTypeAndNameOrId.resolves(null);
UsersStub.findOneById.resolves({ _id: 'me', username: 'me' });
});
it('should return the room when the identifier is a room id', async () => {
const room = { _id: 'rid1', t: 'd' };
RoomsStub.findByTypeAndNameOrId.resolves(room);
expect(await findDirectRoomByIdentifier('rid1', 'me')).to.equal(room);
expect(UsersStub.findUsersByUsernames.called).to.be.false;
});
it('should resolve a two-person DM by the other username, over sorted uids', async () => {
const room = { _id: 'rid1', t: 'd' };
UsersStub.findUsersByUsernames.returns(cursorOf([{ _id: 'zeta' }, { _id: 'alpha' }]));
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves(room);
expect(await findDirectRoomByIdentifier('alice', 'me')).to.equal(room);
expect(RoomsStub.findOneDirectRoomContainingAllUserIDs.calledWith(['alpha', 'zeta'])).to.be.true;
});
it('should include the caller in the member set exactly once for a self-DM', async () => {
UsersStub.findUsersByUsernames.returns(cursorOf([{ _id: 'me' }]));
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves({ _id: 'self', t: 'd' });
await findDirectRoomByIdentifier('me', 'me');
expect(UsersStub.findUsersByUsernames.firstCall.args[0]).to.deep.equal(['me']);
expect(RoomsStub.findOneDirectRoomContainingAllUserIDs.calledWith(['me'])).to.be.true;
});
it('should resolve a group DM from a comma-separated identifier', async () => {
UsersStub.findUsersByUsernames.returns(cursorOf([{ _id: 'me' }, { _id: 'a' }, { _id: 'b' }]));
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves({ _id: 'group', t: 'd' });
await findDirectRoomByIdentifier('a,b', 'me');
expect(UsersStub.findUsersByUsernames.firstCall.args[0]).to.deep.equal(['me', 'a', 'b']);
expect(RoomsStub.findOneDirectRoomContainingAllUserIDs.calledWith(['a', 'b', 'me'])).to.be.true;
});
it('should not look the identifier up as a room id when it is a username list', async () => {
UsersStub.findUsersByUsernames.returns(cursorOf([{ _id: 'me' }, { _id: 'a' }, { _id: 'b' }]));
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves({ _id: 'group', t: 'd' });
await findDirectRoomByIdentifier('a,b', 'me');
expect(RoomsStub.findByTypeAndNameOrId.called).to.be.false;
});
it('should trim whitespace around each username in the list', async () => {
UsersStub.findUsersByUsernames.returns(cursorOf([{ _id: 'me' }, { _id: 'a' }, { _id: 'b' }]));
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves({ _id: 'group', t: 'd' });
await findDirectRoomByIdentifier('a, b', 'me');
expect(UsersStub.findUsersByUsernames.firstCall.args[0]).to.deep.equal(['me', 'a', 'b']);
});
it('should return null when any username does not resolve to a user', async () => {
UsersStub.findUsersByUsernames.returns(cursorOf([{ _id: 'me' }]));
expect(await findDirectRoomByIdentifier('ghost', 'me')).to.be.null;
expect(RoomsStub.findOneDirectRoomContainingAllUserIDs.called).to.be.false;
});
it('should return null when no direct room contains that member set', async () => {
UsersStub.findUsersByUsernames.returns(cursorOf([{ _id: 'me' }, { _id: 'alice' }]));
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves(null);
expect(await findDirectRoomByIdentifier('alice', 'me')).to.be.null;
});
it('should return null when the caller has no username', async () => {
UsersStub.findOneById.resolves({ _id: 'me' });
expect(await findDirectRoomByIdentifier('alice', 'me')).to.be.null;
});
});

View File

@@ -0,0 +1,518 @@
import { expect } from 'chai';
import { describe, it, beforeEach, afterEach } from 'mocha';
import proxyquire from 'proxyquire';
import Sinon from 'sinon';
type RoomType = 'c' | 'p' | 'd' | 'l';
interface IUser {
_id: string;
username: string;
federated?: boolean;
federation?: any;
}
interface IRoom {
_id: string;
t: RoomType;
name?: string;
}
type GetRoomByNameOrIdWithOptionToJoinFn = (params: {
user: Pick<IUser, '_id' | 'username' | 'federated' | 'federation'>;
nameOrId: string;
type?: RoomType;
tryDirectByUserIdOnly?: boolean;
joinChannel?: boolean;
errorOnEmpty?: boolean;
}) => Promise<IRoom | null>;
const RoomsStub = {
findOneByIdOrName: Sinon.stub(),
findOneDirectRoomContainingAllUserIDs: Sinon.stub(),
findOneById: Sinon.stub(),
};
const SubscriptionsStub = {
findOneByRoomIdAndUserId: Sinon.stub(),
};
const UsersStub = {
findOneById: Sinon.stub(),
findOne: Sinon.stub(),
};
const RoomServiceStub = {
join: Sinon.stub().resolves(),
};
const MeteorStub = {
Meteor: {
Error: Sinon.stub().callsFake(function (this: any, code: string) {
this.error = code;
this.errorType = 'Meteor.Error';
} as any),
},
};
const createDirectMessageStub = Sinon.stub().resolves({ rid: 'newDirectRoomId' });
const isObjectMock = {
isObject(obj: unknown) {
return obj !== null && typeof obj === 'object';
},
};
const {
getRoomByNameOrIdWithOptionToJoin,
}: {
getRoomByNameOrIdWithOptionToJoin: GetRoomByNameOrIdWithOptionToJoinFn;
} = proxyquire.noCallThru().load('../../../../../server/lib/rooms/getRoomByNameOrIdWithOptionToJoin.ts', {
'@rocket.chat/models': {
Rooms: RoomsStub,
Subscriptions: SubscriptionsStub,
Users: UsersStub,
},
'@rocket.chat/core-services': {
Room: RoomServiceStub,
},
'../../../lib/utils/isObject': isObjectMock,
'../../meteor-methods/messages/createDirectMessage': {
createDirectMessage: createDirectMessageStub,
},
'meteor/meteor': MeteorStub,
}) as any;
describe('getRoomByNameOrIdWithOptionToJoin', () => {
const baseUser: IUser = {
_id: 'userId',
username: 'user',
};
beforeEach(() => {
RoomsStub.findOneByIdOrName.reset();
RoomsStub.findOneDirectRoomContainingAllUserIDs.reset();
RoomsStub.findOneById.reset();
SubscriptionsStub.findOneByRoomIdAndUserId.reset();
UsersStub.findOneById.reset();
UsersStub.findOne.reset();
RoomServiceStub.join.reset();
RoomServiceStub.join.resolves();
MeteorStub.Meteor.Error.resetBehavior();
MeteorStub.Meteor.Error.resetHistory();
MeteorStub.Meteor.Error.callsFake(function (this: any, code: string) {
this.error = code;
this.errorType = 'Meteor.Error';
} as any);
createDirectMessageStub.reset();
createDirectMessageStub.resolves({ rid: 'newDirectRoomId' });
});
afterEach(() => {
Sinon.restore();
});
describe('channel / group resolution', () => {
it('should strip leading # and lookup by id or name', async () => {
const room: IRoom = { _id: 'room1', t: 'c', name: 'general' };
RoomsStub.findOneByIdOrName.resolves(room);
SubscriptionsStub.findOneByRoomIdAndUserId.resolves(null);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: '#general',
type: undefined,
tryDirectByUserIdOnly: false,
joinChannel: false,
errorOnEmpty: true,
});
expect(RoomsStub.findOneByIdOrName.calledOnceWithExactly('general')).to.equal(true);
expect(result).to.equal(room);
});
it('should treat name without # as channel/group and call findOneByIdOrName', async () => {
const room: IRoom = { _id: 'room2', t: 'c', name: 'random' };
RoomsStub.findOneByIdOrName.resolves(room);
SubscriptionsStub.findOneByRoomIdAndUserId.resolves(null);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'random',
type: undefined,
tryDirectByUserIdOnly: false,
joinChannel: false,
errorOnEmpty: true,
});
expect(RoomsStub.findOneByIdOrName.calledOnceWithExactly('random')).to.equal(true);
expect(result).to.equal(room);
});
it('should throw Meteor.Error("invalid-channel") when channel not found and errorOnEmpty=true', async () => {
RoomsStub.findOneByIdOrName.resolves(null);
let caught: any;
try {
await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'nonexistent',
type: undefined,
tryDirectByUserIdOnly: false,
joinChannel: false,
errorOnEmpty: true,
});
} catch (e) {
caught = e;
}
expect(caught).to.be.an('object');
expect(caught.error).to.equal('invalid-channel');
});
it('should return null when channel not found and errorOnEmpty=false', async () => {
RoomsStub.findOneByIdOrName.resolves(null);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'nonexistent',
type: undefined,
tryDirectByUserIdOnly: false,
joinChannel: false,
errorOnEmpty: false,
});
expect(result).to.equal(null);
});
});
describe('type filtering', () => {
it('should return room when type matches', async () => {
const room: IRoom = { _id: 'room3', t: 'c', name: 'dev' };
RoomsStub.findOneByIdOrName.resolves(room);
SubscriptionsStub.findOneByRoomIdAndUserId.resolves(null);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'dev',
type: 'c',
tryDirectByUserIdOnly: false,
joinChannel: false,
errorOnEmpty: true,
});
expect(result).to.equal(room);
});
it('should throw Meteor.Error when type does not match and errorOnEmpty=true', async () => {
const room: IRoom = { _id: 'room4', t: 'p', name: 'private' };
RoomsStub.findOneByIdOrName.resolves(room);
SubscriptionsStub.findOneByRoomIdAndUserId.resolves(null);
let caught: any;
try {
await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'private',
type: 'c',
tryDirectByUserIdOnly: false,
joinChannel: false,
errorOnEmpty: true,
});
} catch (e) {
caught = e;
}
expect(caught).to.be.an('object');
expect(caught.error).to.equal('invalid-channel');
});
it('should return null when type does not match and errorOnEmpty=false', async () => {
const room: IRoom = { _id: 'room5', t: 'p', name: 'private' };
RoomsStub.findOneByIdOrName.resolves(room);
SubscriptionsStub.findOneByRoomIdAndUserId.resolves(null);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'private',
type: 'c',
tryDirectByUserIdOnly: false,
joinChannel: false,
errorOnEmpty: false,
});
expect(result).to.equal(null);
});
});
describe('joining channel behaviour', () => {
it('should call Room.join when room is channel, joinChannel=true, and user not subscribed', async () => {
const room: IRoom = { _id: 'room6', t: 'c', name: 'joinable' };
RoomsStub.findOneByIdOrName.resolves(room);
SubscriptionsStub.findOneByRoomIdAndUserId.resolves(null);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'joinable',
type: 'c',
tryDirectByUserIdOnly: false,
joinChannel: true,
errorOnEmpty: true,
});
expect(SubscriptionsStub.findOneByRoomIdAndUserId.calledOnceWithExactly('room6', baseUser._id, { projection: { _id: 1 } })).to.equal(
true,
);
expect(
RoomServiceStub.join.calledOnceWithExactly({
room,
user: baseUser,
}),
).to.equal(true);
expect(result).to.equal(room);
});
it('should not call Room.join when joinChannel=false', async () => {
const room: IRoom = { _id: 'room7', t: 'c', name: 'nojoin' };
RoomsStub.findOneByIdOrName.resolves(room);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'nojoin',
type: 'c',
tryDirectByUserIdOnly: false,
joinChannel: false,
errorOnEmpty: true,
});
expect(SubscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.equal(true);
expect(RoomServiceStub.join.notCalled).to.equal(true);
expect(result).to.equal(room);
});
it('should not call Room.join when room type is not channel', async () => {
const room: IRoom = { _id: 'room8', t: 'p', name: 'private' };
RoomsStub.findOneByIdOrName.resolves(room);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'private',
type: 'p',
tryDirectByUserIdOnly: false,
joinChannel: true,
errorOnEmpty: true,
});
expect(SubscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.equal(true);
expect(RoomServiceStub.join.notCalled).to.equal(true);
expect(result).to.equal(room);
});
it('should not call Room.join when user already subscribed', async () => {
const room: IRoom = { _id: 'room9', t: 'c', name: 'joined' };
RoomsStub.findOneByIdOrName.resolves(room);
SubscriptionsStub.findOneByRoomIdAndUserId.resolves({ _id: 'subId' });
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'joined',
type: 'c',
tryDirectByUserIdOnly: false,
joinChannel: true,
errorOnEmpty: true,
});
expect(SubscriptionsStub.findOneByRoomIdAndUserId.calledOnceWithExactly('room9', baseUser._id, { projection: { _id: 1 } })).to.equal(
true,
);
expect(RoomServiceStub.join.notCalled).to.equal(true);
expect(result).to.equal(room);
});
});
describe('direct message resolution by username', () => {
it('should find direct room with existing roomUser and existing DM room', async () => {
const otherUser: IUser = { _id: 'otherId', username: 'other' };
UsersStub.findOne.resolves(otherUser);
const dmRoom: IRoom = { _id: 'dm1', t: 'd' };
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves(dmRoom);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: '@other',
type: 'd',
tryDirectByUserIdOnly: false,
joinChannel: true,
errorOnEmpty: true,
});
expect(
UsersStub.findOne.calledOnceWithExactly({
$or: [{ _id: 'other' }, { username: 'other' }],
}),
).to.equal(true);
expect(RoomsStub.findOneDirectRoomContainingAllUserIDs.calledOnce).to.equal(true);
const callArgs = RoomsStub.findOneDirectRoomContainingAllUserIDs.getCall(0).args[0];
expect(callArgs).to.be.an('array');
expect(callArgs.sort()).to.deep.equal(['otherId', 'userId'].sort());
expect(result).to.equal(dmRoom);
});
it('should strip leading @ when resolving DM', async () => {
const otherUser: IUser = { _id: 'otherId2', username: 'other2' };
UsersStub.findOne.resolves(otherUser);
const dmRoom: IRoom = { _id: 'dm2', t: 'd' };
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves(dmRoom);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: '@other2',
type: undefined,
tryDirectByUserIdOnly: false,
joinChannel: true,
errorOnEmpty: true,
});
expect(
UsersStub.findOne.calledOnceWithExactly({
$or: [{ _id: 'other2' }, { username: 'other2' }],
}),
).to.equal(true);
expect(result).to.equal(dmRoom);
});
it('should treat nameOrId as DM when type is d even without @', async () => {
const otherUser: IUser = { _id: 'otherId3', username: 'plain' };
UsersStub.findOne.resolves(otherUser);
const dmRoom: IRoom = { _id: 'dm3', t: 'd' };
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves(dmRoom);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'plain',
type: 'd',
tryDirectByUserIdOnly: false,
joinChannel: true,
errorOnEmpty: true,
});
expect(
UsersStub.findOne.calledOnceWithExactly({
$or: [{ _id: 'plain' }, { username: 'plain' }],
}),
).to.equal(true);
expect(result).to.equal(dmRoom);
});
});
describe('direct message resolution with tryDirectByUserIdOnly', () => {
it('should lookup user by id only when tryDirectByUserIdOnly=true', async () => {
const otherUser: IUser = { _id: 'idOnly', username: 'ignored' };
UsersStub.findOneById.resolves(otherUser);
const dmRoom: IRoom = { _id: 'dm4', t: 'd' };
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves(dmRoom);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'idOnly',
type: 'd',
tryDirectByUserIdOnly: true,
joinChannel: true,
errorOnEmpty: true,
});
expect(UsersStub.findOneById.calledOnceWithExactly('idOnly')).to.equal(true);
expect(UsersStub.findOne.notCalled).to.equal(true);
expect(result).to.equal(dmRoom);
});
it('should fallback to Rooms.findOneById when roomUser is falsy but room exists by id', async () => {
UsersStub.findOneById.resolves(null);
const dmRoom: IRoom = { _id: 'idRoom', t: 'd' };
RoomsStub.findOneById.resolves(dmRoom);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: 'idRoom',
type: 'd',
tryDirectByUserIdOnly: true,
joinChannel: true,
errorOnEmpty: true,
});
expect(RoomsStub.findOneById.calledOnceWithExactly('idRoom')).to.equal(true);
expect(result).to.equal(dmRoom);
});
});
describe('direct message creation when roomUser exists but room does not', () => {
it('should throw Meteor.Error when roomUser not found and errorOnEmpty=true', async () => {
UsersStub.findOne.resolves(null);
RoomsStub.findOneById.resolves(null);
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves(null);
let caught: any;
try {
await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: '@missing',
type: 'd',
tryDirectByUserIdOnly: false,
joinChannel: true,
errorOnEmpty: true,
});
} catch (e) {
caught = e;
}
expect(caught).to.be.an('object');
expect(caught.error).to.equal('invalid-channel');
expect(createDirectMessageStub.notCalled).to.equal(true);
});
it('should return null when roomUser not found and errorOnEmpty=false', async () => {
UsersStub.findOne.resolves(null);
RoomsStub.findOneById.resolves(null);
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves(null);
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: '@missing',
type: 'd',
tryDirectByUserIdOnly: false,
joinChannel: true,
errorOnEmpty: false,
});
expect(result).to.equal(null);
expect(createDirectMessageStub.notCalled).to.equal(true);
});
it('should create a new direct message when roomUser exists and room is not found', async () => {
const otherUser: IUser = { _id: 'otherCreate', username: 'createMe' };
UsersStub.findOne.resolves(otherUser);
RoomsStub.findOneDirectRoomContainingAllUserIDs.resolves(null);
RoomsStub.findOneById.resolves({ _id: 'newDirectRoomId', t: 'd' });
const result = await getRoomByNameOrIdWithOptionToJoin({
user: baseUser,
nameOrId: '@createMe',
type: 'd',
tryDirectByUserIdOnly: false,
joinChannel: true,
errorOnEmpty: true,
});
expect(createDirectMessageStub.calledOnceWithExactly(['createMe'], baseUser._id)).to.equal(true);
expect(RoomsStub.findOneById.calledWith('newDirectRoomId')).to.equal(true);
expect(result).to.deep.equal({ _id: 'newDirectRoomId', t: 'd' });
});
});
});