Files
upstream-rocketchat/apps/meteor/tests/unit/server/lib/rooms/getRoomByNameOrIdWithOptionToJoin.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 1s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

519 lines
15 KiB
TypeScript

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' });
});
});
});