Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/meteor/.mocharc.api.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
module.exports = /** @satisfies {import('mocha').MochaOptions} */ ({
...require('./.mocharc.base.json'), // see https://github.com/mochajs/mocha/issues/3916
timeout: 10000,
bail: true,
retries: 0,
file: 'tests/end-to-end/teardown.ts',
spec: ['tests/end-to-end/api/*.ts', 'tests/end-to-end/api/helpers/**/*', 'tests/end-to-end/api/methods/**/*', 'tests/end-to-end/apps/*'],
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/.mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ module.exports = {
'app/livechat/server/lib/**/*.spec.ts',
'app/push/server/**/*.spec.ts',
'app/utils/server/**/*.spec.ts',
'app/apps/**/**.spec.ts',
],
};
66 changes: 66 additions & 0 deletions apps/meteor/app/apps/server/bridges/listeners.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata';
import { expect } from 'chai';
import sinon from 'sinon';

import { AppListenerBridge } from './listeners';

describe('AppListenerBridge', () => {
let bridge: AppListenerBridge;
let listenerManager: { hasListeners: sinon.SinonStub; executeListener: sinon.SinonStub };
let messageConverter: { convertMessage: sinon.SinonStub; convertToApp: sinon.SinonStub; convertAppMessage: sinon.SinonStub };
let orch: any;

beforeEach(() => {
listenerManager = {
hasListeners: sinon.stub(),
executeListener: sinon.stub(),
};

messageConverter = {
convertMessage: sinon.stub().resolves({ id: 'converted-msg' }),
convertToApp: sinon.stub().returns({}),
convertAppMessage: sinon.stub().resolves({ _id: 'final-msg' }),
};

orch = {
getManager: sinon.stub().returns({
getListenerManager: sinon.stub().returns(listenerManager),
}),
getConverters: sinon.stub().returns({
get: sinon.stub().returns(messageConverter),
}),
};

bridge = new AppListenerBridge(orch);
});

afterEach(() => {
sinon.restore();
});

describe('handleEvent', () => {
it('should return undefined and skip conversion when there are no listeners', async () => {
listenerManager.hasListeners.returns(false);

const result = await bridge.handleEvent({
event: AppInterface.IPostMessageSent,
payload: [{ _id: 'msg-id', rid: 'room-id', msg: 'hello' } as any],
} as any);

expect(result).to.be.undefined;
expect(messageConverter.convertMessage.called).to.be.false;
});

it('should proceed to conversion and execution when there are listeners', async () => {
listenerManager.hasListeners.returns(true);
listenerManager.executeListener.resolves(undefined);

await bridge.handleEvent({
event: AppInterface.IPostMessageSent,
payload: [{ _id: 'msg-id', rid: 'room-id', msg: 'hello' } as any],
} as any);

expect(messageConverter.convertMessage.calledOnce).to.be.true;
});
});
});
125 changes: 69 additions & 56 deletions apps/meteor/app/apps/server/bridges/listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { UIKitIncomingInteraction } from '@rocket.chat/apps-engine/definiti
import type { IUIKitLivechatIncomingInteraction } from '@rocket.chat/apps-engine/definition/uikit/livechat';
import type { IUserContext, IUserUpdateContext } from '@rocket.chat/apps-engine/definition/users';
import type { IMessage, IRoom, IUser, ILivechatDepartment, IUpload } from '@rocket.chat/core-typings';
import { inspect } from 'util';

type LivechatTransferData = {
type: LivechatTransferEventType;
Expand Down Expand Up @@ -185,64 +186,76 @@ type HandleEvent =
export class AppListenerBridge {
constructor(private readonly orch: IAppServerOrchestrator) {}

// eslint-disable-next-line complexity
async handleEvent(args: HandleEvent): Promise<any> {
switch (args.event) {
case AppInterface.IPreFileUpload:
return this.uploadEvent(args);
case AppInterface.IPostMessageDeleted:
case AppInterface.IPostMessageReacted:
case AppInterface.IPostMessageFollowed:
case AppInterface.IPostMessagePinned:
case AppInterface.IPostMessageStarred:
case AppInterface.IPostMessageReported:
case AppInterface.IPostSystemMessageSent:
case AppInterface.IPreMessageSentPrevent:
case AppInterface.IPreMessageSentExtend:
case AppInterface.IPreMessageSentModify:
case AppInterface.IPostMessageSent:
case AppInterface.IPreMessageDeletePrevent:
case AppInterface.IPreMessageUpdatedPrevent:
case AppInterface.IPreMessageUpdatedExtend:
case AppInterface.IPreMessageUpdatedModify:
case AppInterface.IPostMessageUpdated:
return this.messageEvent(args);
case AppInterface.IPreRoomCreatePrevent:
case AppInterface.IPreRoomCreateExtend:
case AppInterface.IPreRoomCreateModify:
case AppInterface.IPostRoomCreate:
case AppInterface.IPreRoomDeletePrevent:
case AppInterface.IPostRoomDeleted:
case AppInterface.IPreRoomUserJoined:
case AppInterface.IPostRoomUserJoined:
case AppInterface.IPreRoomUserLeave:
case AppInterface.IPostRoomUserLeave:
return this.roomEvent(args);
/**
* @deprecated please prefer the AppInterface.IPostLivechatRoomClosed event
*/
case AppInterface.ILivechatRoomClosedHandler:
case AppInterface.IPreLivechatRoomCreatePrevent:
case AppInterface.IPostLivechatRoomStarted:
case AppInterface.IPostLivechatRoomClosed:
case AppInterface.IPostLivechatAgentAssigned:
case AppInterface.IPostLivechatAgentUnassigned:
case AppInterface.IPostLivechatRoomTransferred:
case AppInterface.IPostLivechatGuestSaved:
case AppInterface.IPostLivechatRoomSaved:
case AppInterface.IPostLivechatDepartmentRemoved:
case AppInterface.IPostLivechatDepartmentDisabled:
return this.livechatEvent(args);
case AppInterface.IPostUserCreated:
case AppInterface.IPostUserUpdated:
case AppInterface.IPostUserDeleted:
case AppInterface.IPostUserLoggedIn:
case AppInterface.IPostUserLoggedOut:
case AppInterface.IPostUserStatusChanged:
return this.userEvent(args);
default:
return this.defaultEvent(args);
const listenerManager = this.orch.getManager().getListenerManager();

const execute = () => {
switch (args.event) {
case AppInterface.IPreFileUpload:
return this.uploadEvent(args);
case AppInterface.IPostMessageDeleted:
case AppInterface.IPostMessageReacted:
case AppInterface.IPostMessageFollowed:
case AppInterface.IPostMessagePinned:
case AppInterface.IPostMessageStarred:
case AppInterface.IPostMessageReported:
case AppInterface.IPostSystemMessageSent:
case AppInterface.IPreMessageSentPrevent:
case AppInterface.IPreMessageSentExtend:
case AppInterface.IPreMessageSentModify:
case AppInterface.IPostMessageSent:
case AppInterface.IPreMessageDeletePrevent:
case AppInterface.IPreMessageUpdatedPrevent:
case AppInterface.IPreMessageUpdatedExtend:
case AppInterface.IPreMessageUpdatedModify:
case AppInterface.IPostMessageUpdated:
return this.messageEvent(args);
case AppInterface.IPreRoomCreatePrevent:
case AppInterface.IPreRoomCreateExtend:
case AppInterface.IPreRoomCreateModify:
case AppInterface.IPostRoomCreate:
case AppInterface.IPreRoomDeletePrevent:
case AppInterface.IPostRoomDeleted:
case AppInterface.IPreRoomUserJoined:
case AppInterface.IPostRoomUserJoined:
case AppInterface.IPreRoomUserLeave:
case AppInterface.IPostRoomUserLeave:
return this.roomEvent(args);
/**
* @deprecated please prefer the AppInterface.IPostLivechatRoomClosed event
*/
case AppInterface.ILivechatRoomClosedHandler:
case AppInterface.IPreLivechatRoomCreatePrevent:
case AppInterface.IPostLivechatRoomStarted:
case AppInterface.IPostLivechatRoomClosed:
case AppInterface.IPostLivechatAgentAssigned:
case AppInterface.IPostLivechatAgentUnassigned:
case AppInterface.IPostLivechatRoomTransferred:
case AppInterface.IPostLivechatGuestSaved:
case AppInterface.IPostLivechatRoomSaved:
case AppInterface.IPostLivechatDepartmentRemoved:
case AppInterface.IPostLivechatDepartmentDisabled:
return this.livechatEvent(args);
case AppInterface.IPostUserCreated:
case AppInterface.IPostUserUpdated:
case AppInterface.IPostUserDeleted:
case AppInterface.IPostUserLoggedIn:
case AppInterface.IPostUserLoggedOut:
case AppInterface.IPostUserStatusChanged:
return this.userEvent(args);
default:
return this.defaultEvent(args);
}
};

const result = await execute();

if (!listenerManager.hasListeners(args.event)) {
args.event.includes('Message') && console.debug(inspect({ args, result }, false, 10));
return undefined;
}

return result;
}

async defaultEvent(args: HandleDefaultEvent): Promise<unknown> {
Expand Down
9 changes: 5 additions & 4 deletions apps/meteor/app/apps/server/bridges/router.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expect } from 'chai';
import request from 'supertest';

import { apiServer } from './router';
Expand Down Expand Up @@ -31,7 +32,7 @@ describe('API Server Routes', () => {
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
expect(res.body).toEqual({
expect(res.body).to.deep.equal({
body: { key: 'value' },
params: { appId, hash },
query: {},
Expand All @@ -49,7 +50,7 @@ describe('API Server Routes', () => {
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
expect(res.body).toEqual({
expect(res.body).to.deep.equal({
body: { key: 'value' },
params: { appId, hash },
query: {},
Expand All @@ -68,7 +69,7 @@ describe('API Server Routes', () => {
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
expect(res.body).toEqual({
expect(res.body).to.deep.equal({
body: { key: 'value' },
params: { appId },
query: {},
Expand All @@ -85,7 +86,7 @@ describe('API Server Routes', () => {
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
expect(res.body).toEqual({
expect(res.body).to.deep.equal({
body: { key: 'value' },
params: { appId },
query: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,13 @@ export class AppListenerManager {
return !!lockedEventList?.size;
}

public hasListeners(event: AppInterface): boolean {
if (this.isEventBlocked(event)) {
return true;
}
return (this.listeners.get(event)?.length ?? 0) > 0;
}

/* eslint-disable-next-line complexity */
public async executeListener<I extends keyof IListenerExecutor>(
int: I,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,53 @@ export class AppListenerManagerTestFixture {
Expect(() => alm.registerListeners(this.mockApp)).not.toThrow();
Expect(alm.getListeners(AppInterface.IPostMessageSent).length).toBe(1);
}

@Test()
public hasListenersReturnsFalseWhenNoListeners() {
const alm = new AppListenerManager(this.mockManager);

Expect(alm.hasListeners(AppInterface.IPostMessageSent)).toBe(false);
}

@Test()
public hasListenersReturnsTrueAfterRegisterListeners() {
const alm = new AppListenerManager(this.mockManager);

alm.registerListeners(this.mockApp);

Expect(alm.hasListeners(AppInterface.IPostMessageSent)).toBe(true);
}

@Test()
public hasListenersReturnsFalseAfterUnregisterListeners() {
const alm = new AppListenerManager(this.mockManager);

alm.registerListeners(this.mockApp);
alm.unregisterListeners(this.mockApp);

Expect(alm.hasListeners(AppInterface.IPostMessageSent)).toBe(false);
}

@Test()
public hasListenersReturnsTrueForBlockedEvent() {
const alm = new AppListenerManager(this.mockManager);

const mockEssentialApp = {
getID() {
return 'essential-app';
},
getImplementationList() {
return {
[AppInterface.IPostMessageSent]: true,
} as { [inte: string]: boolean };
},
getEssentials() {
return [AppInterface.IPostMessageSent];
},
} as ProxiedApp;

alm.lockEssentialEvents(mockEssentialApp);

Expect(alm.hasListeners(AppInterface.IPostMessageSent)).toBe(true);
}
}
Loading