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
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (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,277 @@
import * as assert from 'node:assert';
import { describe, it, afterEach, mock } from 'node:test';
import { AppManager } from '../../src/server/AppManager';
import { AppBridges } from '../../src/server/bridges';
import { AppCompiler, AppPackageParser } from '../../src/server/compiler';
import {
AppApiManager,
AppExternalComponentManager,
AppListenerManager,
AppSettingsManager,
AppSlashCommandManager,
AppVideoConfProviderManager,
AppOutboundCommunicationProviderManager,
} from '../../src/server/managers';
import type { AppLogStorage, AppMetadataStorage, AppSourceStorage } from '../../src/server/storage';
import { SimpleClass, TestData, TestInfastructureSetup } from '../test-data/utilities';
describe('AppManager', () => {
const testingInfastructure = new TestInfastructureSetup();
afterEach(() => {
AppManager.Instance = undefined;
mock.restoreAll();
});
it('Setup of the AppManager', () => {
const manager = new AppManager({
metadataStorage: testingInfastructure.getAppStorage(),
logStorage: testingInfastructure.getLogStorage(),
bridges: testingInfastructure.getAppBridges(),
sourceStorage: testingInfastructure.getSourceStorage(),
tempFilePath: testingInfastructure.getTempFilePath(),
});
assert.strictEqual(manager.getStorage(), testingInfastructure.getAppStorage());
assert.strictEqual(manager.getLogStorage(), testingInfastructure.getLogStorage());
// NOTE: manager.getBridges() returns a proxy, so they are vlaue equality instead of reference equality
assert.deepStrictEqual(manager.getBridges(), testingInfastructure.getAppBridges());
assert.strictEqual(manager.areAppsLoaded(), false);
assert.throws(
() =>
new AppManager({
metadataStorage: {} as AppMetadataStorage,
logStorage: {} as AppLogStorage,
bridges: {} as AppBridges,
sourceStorage: {} as AppSourceStorage,
tempFilePath: 'temp-file-path',
}),
{
name: 'Error',
message: 'There is already a valid AppManager instance',
},
);
});
it('Invalid Storage and Bridge', () => {
const invalid = new SimpleClass();
assert.throws(
() =>
new AppManager({
metadataStorage: invalid as any,
logStorage: invalid as any,
bridges: invalid as any,
sourceStorage: invalid as any,
tempFilePath: 'temp-file-path',
}),
{
name: 'Error',
message: 'Invalid instance of the AppMetadataStorage',
},
);
assert.throws(
() =>
new AppManager({
metadataStorage: testingInfastructure.getAppStorage(),
logStorage: invalid as any,
bridges: invalid as any,
sourceStorage: invalid as any,
tempFilePath: 'temp-file-path',
}),
{
name: 'Error',
message: 'Invalid instance of the AppLogStorage',
},
);
assert.throws(
() =>
new AppManager({
metadataStorage: testingInfastructure.getAppStorage(),
logStorage: testingInfastructure.getLogStorage(),
bridges: invalid as any,
sourceStorage: invalid as any,
tempFilePath: 'temp-file-path',
}),
{
name: 'Error',
message: 'Invalid instance of the AppBridges',
},
);
assert.throws(
() =>
new AppManager({
metadataStorage: testingInfastructure.getAppStorage(),
logStorage: testingInfastructure.getLogStorage(),
bridges: testingInfastructure.getAppBridges(),
sourceStorage: invalid as any,
tempFilePath: testingInfastructure.getTempFilePath(),
}),
{
name: 'Error',
message: 'Invalid instance of the AppSourceStorage',
},
);
});
it('Ensure Managers are Valid Types', () => {
const manager = new AppManager({
metadataStorage: testingInfastructure.getAppStorage(),
logStorage: testingInfastructure.getLogStorage(),
bridges: testingInfastructure.getAppBridges(),
sourceStorage: testingInfastructure.getSourceStorage(),
tempFilePath: testingInfastructure.getTempFilePath(),
});
assert.ok(manager.getParser() instanceof AppPackageParser);
assert.ok(manager.getCompiler() instanceof AppCompiler);
assert.ok(manager.getBridges() instanceof AppBridges);
assert.ok(manager.getListenerManager() instanceof AppListenerManager);
assert.ok(manager.getCommandManager() instanceof AppSlashCommandManager);
assert.ok(manager.getExternalComponentManager() instanceof AppExternalComponentManager);
assert.ok(manager.getApiManager() instanceof AppApiManager);
assert.ok(manager.getSettingsManager() instanceof AppSettingsManager);
assert.ok(manager.getVideoConfProviderManager() instanceof AppVideoConfProviderManager);
assert.ok(manager.getOutboundCommunicationProviderManager() instanceof AppOutboundCommunicationProviderManager);
});
it('Update Apps Marketplace Info - Apps without subscription info are skipped', async () => {
const manager = new AppManager({
metadataStorage: testingInfastructure.getAppStorage(),
logStorage: testingInfastructure.getLogStorage(),
bridges: testingInfastructure.getAppBridges(),
sourceStorage: testingInfastructure.getSourceStorage(),
tempFilePath: testingInfastructure.getTempFilePath(),
});
const appsOverview = TestData.getAppsOverview();
appsOverview[0].latest.subscriptionInfo = undefined; // No subscription info
// Mock the apps Map to return our mock app
(manager as any).apps = new Map([['test-app', TestData.getMockApp(TestData.getAppStorageItem(), manager)]]);
const updatePartialAndReturnDocumentSpy = mock.method(manager.getStorage(), 'updatePartialAndReturnDocument', () => Promise.resolve());
// Should not throw and complete successfully
await manager.updateAppsMarketplaceInfo(appsOverview);
assert.strictEqual(updatePartialAndReturnDocumentSpy.mock.calls.length, 0);
});
it('Update Apps Marketplace Info - Apps not found in manager are skipped', async () => {
const manager = new AppManager({
metadataStorage: testingInfastructure.getAppStorage(),
logStorage: testingInfastructure.getLogStorage(),
bridges: testingInfastructure.getAppBridges(),
sourceStorage: testingInfastructure.getSourceStorage(),
tempFilePath: testingInfastructure.getTempFilePath(),
});
const appsOverview = TestData.getAppsOverview();
appsOverview[0].latest.id = 'nonexistent-app'; // App not in manager
// Mock the apps Map to return our mock app
(manager as any).apps = new Map([['test-app', TestData.getMockApp(TestData.getAppStorageItem(), manager)]]);
const updatePartialAndReturnDocumentSpy = mock.method(manager.getStorage(), 'updatePartialAndReturnDocument', () => Promise.resolve());
// Should not throw and complete successfully
await manager.updateAppsMarketplaceInfo(appsOverview);
assert.strictEqual(updatePartialAndReturnDocumentSpy.mock.calls.length, 0);
});
it('Update Apps Marketplace Info - Apps with same license are skipped', async () => {
const manager = new AppManager({
metadataStorage: testingInfastructure.getAppStorage(),
logStorage: testingInfastructure.getLogStorage(),
bridges: testingInfastructure.getAppBridges(),
sourceStorage: testingInfastructure.getSourceStorage(),
tempFilePath: testingInfastructure.getTempFilePath(),
});
const sameLicenseData = 'same-license-data';
const existingSubscriptionInfo = TestData.getMarketplaceSubscriptionInfo({
license: { license: sameLicenseData, version: 1, expireDate: new Date('2023-01-01') },
});
const mockStorageItem = TestData.getAppStorageItem({
marketplaceInfo: [TestData.getMarketplaceInfo({ subscriptionInfo: existingSubscriptionInfo })],
});
const mockApp = TestData.getMockApp(mockStorageItem, manager);
// Mock the apps Map to return our mock app
(manager as any).apps = new Map([['test-app', mockApp]]);
const appsOverview = TestData.getAppsOverview(
TestData.getMarketplaceSubscriptionInfo({
license: { license: sameLicenseData, version: 1, expireDate: new Date('2023-01-01') },
}),
);
const updatePartialAndReturnDocumentSpy = mock.method(manager.getStorage(), 'updatePartialAndReturnDocument', () => Promise.resolve());
// Should not throw and complete successfully
await manager.updateAppsMarketplaceInfo(appsOverview);
// Verify the subscription info was not updated (should remain the same)
assert.strictEqual(mockStorageItem.marketplaceInfo[0].subscriptionInfo.seats, 10); // Original value
assert.strictEqual(updatePartialAndReturnDocumentSpy.mock.calls.length, 0);
});
it('Update Apps Marketplace Info - Subscription info is updated and app is signed', async () => {
const manager = new AppManager({
metadataStorage: testingInfastructure.getAppStorage(),
logStorage: testingInfastructure.getLogStorage(),
bridges: testingInfastructure.getAppBridges(),
sourceStorage: testingInfastructure.getSourceStorage(),
tempFilePath: testingInfastructure.getTempFilePath(),
});
const existingSubscriptionInfo = TestData.getMarketplaceSubscriptionInfo({
license: { license: 'old-license-data', version: 1, expireDate: new Date('2023-01-01') },
});
const newSubscriptionInfo = TestData.getMarketplaceSubscriptionInfo({
seats: 20,
maxSeats: 200,
startDate: '2023-02-01',
periodEnd: '2024-01-31',
license: { license: 'new-license-data', version: 1, expireDate: new Date('2026-01-01') },
});
const mockStorageItem = TestData.getAppStorageItem({
marketplaceInfo: [TestData.getMarketplaceInfo({ subscriptionInfo: existingSubscriptionInfo })],
});
const mockApp = TestData.getMockApp(mockStorageItem, manager);
mock.method(manager.getSignatureManager(), 'signApp', () => Promise.resolve('signed-app-data'));
mock.method(mockApp, 'validateLicense', () => Promise.resolve());
const updatePartialAndReturnDocumentSpy = mock.method(manager.getStorage(), 'updatePartialAndReturnDocument', () =>
Promise.resolve(mockStorageItem),
);
// Mock the apps Map and dependencies
(manager as any).apps = new Map([['test-app', mockApp]]);
const appsOverview = TestData.getAppsOverview(newSubscriptionInfo);
await manager.updateAppsMarketplaceInfo(appsOverview);
const expectedStorageItem = mockApp.getStorageItem();
// Verify the subscription info was updated
assert.strictEqual(expectedStorageItem.marketplaceInfo[0].subscriptionInfo.seats, 20);
assert.strictEqual(expectedStorageItem.marketplaceInfo[0].subscriptionInfo.license.license, 'new-license-data');
assert.strictEqual(expectedStorageItem.signature, 'signed-app-data');
assert.strictEqual(updatePartialAndReturnDocumentSpy.mock.calls.length, 1);
});
});

View File

@@ -0,0 +1,24 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import { AppCompiler } from '../../../src/server/compiler';
describe('AppCompiler', () => {
it('verifyStorageFileToCompiler', () => {
const compiler = new AppCompiler();
assert.doesNotThrow(() => compiler.normalizeStorageFiles({}));
const files: { [key: string]: string } = {
TestingApp$ts: 'act-like-this-is-real',
TestingAppCommand$ts: 'something-here-as well, yay',
};
const expected: { [key: string]: string } = {
'TestingApp.ts': files.TestingApp$ts,
'TestingAppCommand.ts': files.TestingAppCommand$ts,
};
assert.deepStrictEqual(compiler.normalizeStorageFiles(files), expected);
});
});

View File

@@ -0,0 +1,129 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata';
import type { AppManager } from '../../../src/server/AppManager';
import { ProxiedApp } from '../../../src/server/ProxiedApp';
import { AppFabricationFulfillment } from '../../../src/server/compiler';
import { AppPermissions } from '../../../src/server/permissions/AppPermissions';
import type { IAppStorageItem } from '../../../src/server/storage';
import { TestData } from '../../test-data/utilities';
describe('AppFabricationFulfillment', () => {
it('appFabricationDefinement', () => {
const expctedInfo: IAppInfo = {
id: '614055e2-3dba-41fb-be48-c1ff146f5932',
name: 'Testing App',
nameSlug: 'testing-app',
description: 'A Rocket.Chat Application used to test out the various features.',
version: '0.0.8',
requiredApiVersion: '>=0.9.6',
author: {
name: 'Bradley Hilton',
homepage: 'https://github.com/RocketChat/Rocket.Chat.Apps-ts-definitions',
support: 'https://github.com/RocketChat/Rocket.Chat.Apps-ts-definitions/issues',
},
classFile: 'TestingApp.ts',
iconFile: 'testing.jpg',
implements: [],
permissions: [],
};
assert.doesNotThrow(() => new AppFabricationFulfillment());
const aff = new AppFabricationFulfillment();
assert.doesNotThrow(() => aff.setAppInfo(expctedInfo));
assert.deepStrictEqual(aff.getAppInfo(), expctedInfo);
const expectedInter: { [key: string]: boolean } = {};
expectedInter[AppInterface.IPreMessageSentPrevent] = true;
assert.doesNotThrow(() => aff.setImplementedInterfaces(expectedInter));
assert.deepStrictEqual(aff.getImplementedInferfaces(), expectedInter);
const fakeApp = new ProxiedApp(
{} as AppManager,
{ status: AppStatus.UNKNOWN } as IAppStorageItem,
TestData.getMockRuntimeController('unknown'),
);
assert.doesNotThrow(() => aff.setApp(fakeApp));
assert.deepStrictEqual(aff.getApp(), fakeApp);
});
it('setAppInfoCreatesDeepClone', () => {
const originalInfo: IAppInfo = {
id: 'test-app-id',
name: 'Test App',
nameSlug: 'test-app',
description: 'A test application',
version: '1.0.0',
requiredApiVersion: '>=1.0.0',
author: {
name: 'Test Author',
homepage: 'https://example.com',
support: 'https://example.com/support',
},
classFile: 'TestApp.ts',
iconFile: 'test.jpg',
implements: [AppInterface.IPreMessageSentPrevent],
permissions: [AppPermissions.user.read, AppPermissions.user.write],
};
const aff = new AppFabricationFulfillment();
aff.setAppInfo(originalInfo);
assert.deepStrictEqual(aff.getAppInfo(), originalInfo);
originalInfo.name = 'Modified Name';
originalInfo.author.name = 'Modified Author';
originalInfo.implements.push(AppInterface.IPostMessageSent);
originalInfo.permissions.push(AppPermissions.message.write);
assert.notStrictEqual(aff.getAppInfo().name, 'Modified Name');
assert.notStrictEqual(aff.getAppInfo().author.name, 'Modified Author');
assert.ok(!aff.getAppInfo().implements.includes(AppInterface.IPostMessageSent));
assert.ok(!aff.getAppInfo().permissions.includes(AppPermissions.message.write));
assert.strictEqual(aff.getAppInfo().name, 'Test App');
assert.strictEqual(aff.getAppInfo().author.name, 'Test Author');
assert.deepStrictEqual(aff.getAppInfo().implements, [AppInterface.IPreMessageSentPrevent]);
assert.deepStrictEqual(aff.getAppInfo().permissions, [AppPermissions.user.read, AppPermissions.user.write]);
});
it('setImplementedInterfacesCreatesDeepClone', () => {
const originalInterfaces: { [int: string]: boolean } = {
[AppInterface.IPreMessageSentPrevent]: true,
[AppInterface.IPostMessageSent]: false,
[AppInterface.IPreMessageSentExtend]: true,
};
const aff = new AppFabricationFulfillment();
aff.setImplementedInterfaces(originalInterfaces);
assert.deepStrictEqual(aff.getImplementedInferfaces(), originalInterfaces);
originalInterfaces[AppInterface.IPreMessageSentPrevent] = false;
originalInterfaces[AppInterface.IPostMessageSent] = true;
originalInterfaces[AppInterface.IPreMessageSentModify] = true;
assert.notStrictEqual(aff.getImplementedInferfaces()[AppInterface.IPreMessageSentPrevent], false);
assert.notStrictEqual(aff.getImplementedInferfaces()[AppInterface.IPostMessageSent], true);
assert.ok(aff.getImplementedInferfaces()[AppInterface.IPreMessageSentModify] === undefined);
assert.strictEqual(aff.getImplementedInferfaces()[AppInterface.IPreMessageSentPrevent], true);
assert.strictEqual(aff.getImplementedInferfaces()[AppInterface.IPostMessageSent], false);
assert.strictEqual(aff.getImplementedInferfaces()[AppInterface.IPreMessageSentExtend], true);
});
it('setImplementedInterfacesHandlesEmptyObject', () => {
const emptyInterfaces: { [int: string]: boolean } = {};
const aff = new AppFabricationFulfillment();
assert.doesNotThrow(() => aff.setImplementedInterfaces(emptyInterfaces));
assert.strictEqual(Object.keys(aff.getImplementedInferfaces()).length, 0);
assert.notStrictEqual(aff.getImplementedInferfaces(), emptyInterfaces);
});
});

View File

@@ -0,0 +1,21 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata';
import { AppImplements } from '../../../src/server/compiler';
describe('AppImplements', () => {
it('appImplements', () => {
assert.doesNotThrow(() => new AppImplements());
const impls = new AppImplements();
assert.ok(impls.getValues() !== undefined);
assert.doesNotThrow(() => impls.setImplements(AppInterface.IPreMessageSentPrevent));
assert.strictEqual(impls.doesImplement(AppInterface.IPreMessageSentPrevent), true);
assert.strictEqual(impls.doesImplement(AppInterface.IPostMessageDeleted), false);
assert.strictEqual(impls.getValues()[AppInterface.IPreMessageSentPrevent], true);
assert.strictEqual(impls.getValues()[AppInterface.IPostMessageDeleted], false);
});
});

View File

@@ -0,0 +1,13 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import { CommandAlreadyExistsError } from '../../../src/server/errors';
describe('CommandAlreadyExistsError', () => {
it('verifyCommandAlreadyExistsError', () => {
const er = new CommandAlreadyExistsError('testing');
assert.strictEqual(er.name, 'CommandAlreadyExists');
assert.strictEqual(er.message, 'The command "testing" already exists in the system.');
});
});

View File

@@ -0,0 +1,13 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import { CommandHasAlreadyBeenTouchedError } from '../../../src/server/errors';
describe('CommandHasAlreadyBeenTouchedError', () => {
it('verifyCommandHasAlreadyBeenTouched', () => {
const er = new CommandHasAlreadyBeenTouchedError('testing');
assert.strictEqual(er.name, 'CommandHasAlreadyBeenTouched');
assert.strictEqual(er.message, 'The command "testing" has already been touched by another App.');
});
});

View File

@@ -0,0 +1,13 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import { CompilerError } from '../../../src/server/errors';
describe('CompilerError', () => {
it('verifyCompilerError', () => {
const er = new CompilerError('syntax');
assert.strictEqual(er.name, 'CompilerError');
assert.strictEqual(er.message, 'An error occured while compiling an App: syntax');
});
});

View File

@@ -0,0 +1,13 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import { MustContainFunctionError } from '../../../src/server/errors';
describe('MustContainFunctionError', () => {
it('verifyCompilerError', () => {
const er = new MustContainFunctionError('App.ts', 'getVersion');
assert.strictEqual(er.name, 'MustContainFunction');
assert.strictEqual(er.message, 'The App (App.ts) doesn\'t have a "getVersion" function which is required.');
});
});

View File

@@ -0,0 +1,13 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import { MustExtendAppError } from '../../../src/server/errors';
describe('MustExtendAppError', () => {
it('verifyCompilerError', () => {
const er = new MustExtendAppError();
assert.strictEqual(er.name, 'MustExtendApp');
assert.strictEqual(er.message, 'App must extend the "App" abstract class.');
});
});

View File

@@ -0,0 +1,13 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import { NotEnoughMethodArgumentsError } from '../../../src/server/errors';
describe('NotEnoughMethodArgumentsError', () => {
it('verifyCompilerError', () => {
const er = new NotEnoughMethodArgumentsError('enable', 3, 1);
assert.strictEqual(er.name, 'NotEnoughMethodArgumentsError');
assert.strictEqual(er.message, 'The method "enable" requires 3 parameters but was only passed 1.');
});
});

View File

@@ -0,0 +1,31 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
import { RequiredApiVersionError } from '../../../src/server/errors';
describe('RequiredApiVersionError', () => {
it('verifyCompilerError', () => {
const info = {
requiredApiVersion: '1.0.1',
name: 'Testing',
id: 'fake-id',
} as IAppInfo;
const er = new RequiredApiVersionError(info, '1.0.0');
assert.strictEqual(er.name, 'RequiredApiVersion');
assert.strictEqual(
er.message,
'Failed to load the App "Testing" (fake-id) as it requires v1.0.1 of the App API however your server comes with v1.0.0.',
);
const er2 = new RequiredApiVersionError(info, '2.0.0');
assert.strictEqual(er2.name, 'RequiredApiVersion');
assert.strictEqual(
er2.message,
'Failed to load the App "Testing" (fake-id) as it requires v1.0.1 of the App API however your server comes with v2.0.0. Please tell the author to update their App as it is out of date.',
);
});
});

View File

@@ -0,0 +1,92 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import type { TestContext } from 'node:test';
import { LogMessageSeverity } from '@rocket.chat/apps-engine/definition/accessors';
import { AppMethod } from '@rocket.chat/apps-engine/definition/metadata';
import type * as stackTrace from 'stack-trace';
import { AppConsole } from '../../../src/server/logging';
describe('AppConsole', () => {
it('basicConsoleMethods', (t: TestContext) => {
t.mock.timers.enable({ apis: ['Date'] });
assert.doesNotThrow(() => new AppConsole(AppMethod._CONSTRUCTOR));
const logger = new AppConsole(AppMethod._CONSTRUCTOR);
const { entries } = logger as any;
assert.doesNotThrow(() => logger.debug('this is a debug'));
assert.strictEqual(entries.length, 1);
assert.strictEqual(entries[0].severity, LogMessageSeverity.DEBUG);
assert.strictEqual(entries[0].args[0], 'this is a debug');
assert.doesNotThrow(() => logger.info('this is an info log'));
assert.strictEqual(entries.length, 2);
assert.strictEqual(entries[1].severity, LogMessageSeverity.INFORMATION);
assert.strictEqual(entries[1].args[0], 'this is an info log');
assert.doesNotThrow(() => logger.log('this is a log'));
assert.strictEqual(entries.length, 3);
assert.strictEqual(entries[2].severity, LogMessageSeverity.LOG);
assert.strictEqual(entries[2].args[0], 'this is a log');
assert.doesNotThrow(() => logger.warn('this is a warn'));
assert.strictEqual(entries.length, 4);
assert.strictEqual(entries[3].severity, LogMessageSeverity.WARNING);
assert.strictEqual(entries[3].args[0], 'this is a warn');
const e = new Error('just a test');
assert.doesNotThrow(() => logger.error(e));
assert.strictEqual(entries.length, 5);
assert.strictEqual(entries[4].severity, LogMessageSeverity.ERROR);
assert.strictEqual(entries[4].args[0], JSON.stringify(e, Object.getOwnPropertyNames(e)));
assert.doesNotThrow(() => logger.success('this is a success'));
assert.strictEqual(entries.length, 6);
assert.strictEqual(entries[5].severity, LogMessageSeverity.SUCCESS);
assert.strictEqual(entries[5].args[0], 'this is a success');
assert.doesNotThrow(() => {
class Item {
constructor() {
logger.debug('inside');
}
}
return new Item();
});
assert.deepStrictEqual(logger.getEntries(), entries);
assert.strictEqual(logger.getMethod(), AppMethod._CONSTRUCTOR);
assert.ok(logger.getStartTime() !== undefined);
assert.ok(logger.getEndTime() !== undefined);
t.mock.timers.tick(1000);
assert.strictEqual(logger.getTotalTime(), 1000);
const getFunc = (logger as any).getFunc.bind(logger);
assert.strictEqual(getFunc([{} as stackTrace.StackFrame]), 'anonymous');
const mockFrames = [];
mockFrames.push({} as stackTrace.StackFrame);
mockFrames.push({
getMethodName() {
return 'testing';
},
getFunctionName() {
return null;
},
} as stackTrace.StackFrame);
assert.strictEqual(getFunc(mockFrames), 'testing');
const entry = AppConsole.toStorageEntry('testing-app', logger);
assert.strictEqual(entry.appId, 'testing-app');
assert.strictEqual(entry.method, AppMethod._CONSTRUCTOR);
assert.deepStrictEqual(entry.entries, logger.getEntries());
assert.ok(entry.startTime instanceof Date);
assert.ok(entry.endTime instanceof Date);
assert.strictEqual(entry.totalTime, 1000);
assert.ok(entry._createdAt instanceof Date);
});
});

View File

@@ -0,0 +1,24 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import type { IApi } from '@rocket.chat/apps-engine/definition/api';
import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint';
import type { ProxiedApp } from '../../../src/server/ProxiedApp';
import { AppApi } from '../../../src/server/managers/AppApi';
describe('AppApi', () => {
it('ensureAppApi', () => {
const mockApp = {
getID() {
return 'id';
},
} as ProxiedApp;
assert.doesNotThrow(() => new AppApi(mockApp, {} as IApi, {} as IApiEndpoint));
const ascr = new AppApi(mockApp, {} as IApi, {} as IApiEndpoint);
assert.ok(ascr.app !== undefined);
assert.ok(ascr.api !== undefined);
});
});

View File

@@ -0,0 +1,219 @@
import * as assert from 'node:assert';
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
import { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors';
import type { IApi, IApiRequest } from '@rocket.chat/apps-engine/definition/api';
import type { AppManager } from '../../../src/server/AppManager';
import type { ProxiedApp } from '../../../src/server/ProxiedApp';
import type { AppBridges } from '../../../src/server/bridges';
import type {
AppExternalComponentManager,
AppSchedulerManager,
AppSlashCommandManager,
AppVideoConfProviderManager,
} from '../../../src/server/managers';
import { AppApiManager } from '../../../src/server/managers';
import { AppApi } from '../../../src/server/managers/AppApi';
import type { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager';
import type { AppLogStorage, IAppStorageItem } from '../../../src/server/storage';
import { TestsAppBridges } from '../../test-data/bridges/appBridges';
import { TestsAppLogStorage } from '../../test-data/storage/logStorage';
import { TestData } from '../../test-data/utilities';
describe('AppApiManager', () => {
let mockBridges: TestsAppBridges;
let mockApp: ProxiedApp;
let mockManager: AppManager;
beforeEach(() => {
mockBridges = new TestsAppBridges();
mockApp = TestData.getMockApp({ info: { id: 'testing', name: 'TestApp' } } as IAppStorageItem, {} as AppManager);
const bri = mockBridges;
const app = mockApp;
mockManager = {
getBridges(): AppBridges {
return bri;
},
getCommandManager() {
return {} as AppSlashCommandManager;
},
getExternalComponentManager() {
return {} as AppExternalComponentManager;
},
getApiManager() {
return {} as AppApiManager;
},
getOneById(appId: string): ProxiedApp {
return appId === 'failMePlease' ? undefined : app;
},
getLogStorage(): AppLogStorage {
return new TestsAppLogStorage();
},
getSchedulerManager() {
return {} as AppSchedulerManager;
},
getUIActionButtonManager() {
return {} as UIActionButtonManager;
},
getVideoConfProviderManager() {
return {} as AppVideoConfProviderManager;
},
} as AppManager;
});
afterEach(() => {
mock.restoreAll();
});
it('basicAppApiManager', () => {
assert.throws(() => new AppApiManager({} as AppManager));
assert.doesNotThrow(() => new AppApiManager(mockManager));
const ascm = new AppApiManager(mockManager);
assert.strictEqual((ascm as any).manager, mockManager);
assert.strictEqual((ascm as any).bridge, mockBridges.getApiBridge());
assert.ok((ascm as any).providedApis !== undefined);
assert.strictEqual((ascm as any).providedApis.size, 0);
});
it('registerApi', async () => {
const doRegisterApiSpy = mock.method(mockBridges.getApiBridge(), 'doRegisterApi');
const ascm = new AppApiManager(mockManager);
const api: IApi = TestData.getApi('path');
const regInfo = new AppApi(mockApp, api, api.endpoints[0]);
await assert.doesNotReject(() => (ascm as any).registerApi('testing', regInfo));
assert.strictEqual(doRegisterApiSpy.mock.calls.length, 1);
assert.deepStrictEqual(doRegisterApiSpy.mock.calls[0].arguments, [regInfo, 'testing']);
});
it('addApi', () => {
mockBridges = new TestsAppBridges();
const bri = mockBridges;
mockManager.getBridges = function _refreshedGetBridges(): AppBridges {
return bri;
};
const doRegisterApiSpy = mock.method(mockBridges.getApiBridge(), 'doRegisterApi');
void doRegisterApiSpy;
const api = TestData.getApi('apipath');
const ascm = new AppApiManager(mockManager);
assert.doesNotThrow(() => ascm.addApi('testing', api));
assert.strictEqual(mockBridges.getApiBridge().apis.size, 1);
assert.strictEqual((ascm as any).providedApis.size, 1);
assert.strictEqual((ascm as any).providedApis.get('testing').get('apipath').api, api);
assert.throws(() => ascm.addApi('testing', api), {
name: 'PathAlreadyExists',
message: 'The api path "apipath" already exists in the system.',
});
assert.throws(() => ascm.addApi('failMePlease', TestData.getApi('yet-another')), {
name: 'Error',
message: 'App must exist in order for an api to be added.',
});
assert.doesNotThrow(() => ascm.addApi('testing', TestData.getApi('another-api')));
assert.strictEqual((ascm as any).providedApis.size, 1);
assert.strictEqual((ascm as any).providedApis.get('testing').size, 2);
});
it('registerApis', async () => {
mockBridges = new TestsAppBridges();
const bri = mockBridges;
mockManager.getBridges = function _refreshedGetBridges(): AppBridges {
return bri;
};
const doRegisterApiSpy = mock.method(mockBridges.getApiBridge(), 'doRegisterApi');
const ascm = new AppApiManager(mockManager);
const registerApiSpy = mock.method(ascm as any, 'registerApi');
ascm.addApi('testing', TestData.getApi('apipath'));
const regInfo = (ascm as any).providedApis.get('testing').get('apipath') as AppApi;
await assert.doesNotReject(() => ascm.registerApis('non-existant'));
await assert.doesNotReject(() => ascm.registerApis('testing'));
assert.strictEqual(registerApiSpy.mock.calls.filter((c: any) => c.arguments[0] === 'testing' && c.arguments[1] === regInfo).length, 1);
assert.strictEqual(
doRegisterApiSpy.mock.calls.filter((c: any) => c.arguments[0] === regInfo && c.arguments[1] === 'testing').length,
1,
);
});
it('unregisterApis', async () => {
mockBridges = new TestsAppBridges();
const bri = mockBridges;
mockManager.getBridges = function _refreshedGetBridges(): AppBridges {
return bri;
};
const doUnregisterApisSpy = mock.method(mockBridges.getApiBridge(), 'doUnregisterApis');
const ascm = new AppApiManager(mockManager);
ascm.addApi('testing', TestData.getApi('apipath'));
await assert.doesNotReject(() => ascm.unregisterApis('non-existant'));
await assert.doesNotReject(() => ascm.unregisterApis('testing'));
assert.strictEqual(doUnregisterApisSpy.mock.calls.length, 1);
});
it('executeApis', async () => {
mockBridges = new TestsAppBridges();
const bri = mockBridges;
mockManager.getBridges = function _refreshedGetBridges(): AppBridges {
return bri;
};
const ascm = new AppApiManager(mockManager);
ascm.addApi('testing', TestData.getApi('api1'));
ascm.addApi('testing', TestData.getApi('api2'));
ascm.addApi('testing', TestData.getApi('api3'));
await ascm.registerApis('testing');
const request: IApiRequest = {
method: RequestMethod.GET,
headers: {},
query: {},
params: {},
content: '',
};
await assert.doesNotReject(() => ascm.executeApi('testing', 'nope', request));
await assert.doesNotReject(() => ascm.executeApi('testing', 'not-exists', request));
await assert.doesNotReject(() => ascm.executeApi('testing', 'api1', request));
await assert.doesNotReject(() => ascm.executeApi('testing', 'api2', request));
await assert.doesNotReject(() => ascm.executeApi('testing', 'api3', request));
});
it('listApis', async () => {
mockBridges = new TestsAppBridges();
const bri = mockBridges;
mockManager.getBridges = function _refreshedGetBridges(): AppBridges {
return bri;
};
const ascm = new AppApiManager(mockManager);
assert.deepStrictEqual(ascm.listApis('testing'), []);
ascm.addApi('testing', TestData.getApi('api1'));
await ascm.registerApis('testing');
assert.doesNotThrow(() => ascm.listApis('testing'));
assert.notDeepStrictEqual(ascm.listApis('testing'), []);
assert.deepStrictEqual(ascm.listApis('testing'), [
{
path: 'api1',
computedPath: '/api/apps/public/testing/api1',
methods: ['get'],
examples: {},
},
]);
});
});

View File

@@ -0,0 +1,137 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent/IExternalComponent';
import { ExternalComponentLocation } from '@rocket.chat/apps-engine/definition/externalComponent/IExternalComponent';
import { AppExternalComponentManager } from '../../../src/server/managers';
describe('AppExternalComponentManager', () => {
const mockExternalComponent1: IExternalComponent = {
appId: '1eb382c0-3679-44a6-8af0-18802e342fb1',
name: 'TestExternalComponent1',
description: 'TestExternalComponent1',
url: '',
icon: '',
location: ExternalComponentLocation.CONTEXTUAL_BAR,
};
const mockExternalComponent2: IExternalComponent = {
appId: '125a944b-9747-4e6e-b029-6e9b26bb3481',
name: 'TestExternalComponent2',
description: 'TestExternalComponent2',
url: '',
icon: '',
location: ExternalComponentLocation.CONTEXTUAL_BAR,
};
const mockExternalComponent3: IExternalComponent = {
...mockExternalComponent2,
appId: mockExternalComponent1.appId,
name: mockExternalComponent2.name,
description: 'TestExternalComponent3',
};
function register(aecm: AppExternalComponentManager, externalComponent: IExternalComponent): void {
const { appId } = externalComponent;
aecm.addExternalComponent(appId, externalComponent);
aecm.registerExternalComponents(appId);
}
it('basicAppExternalComponentManager', () => {
const aecm = new AppExternalComponentManager();
assert.strictEqual((aecm as any).registeredExternalComponents.size, 0);
assert.strictEqual((aecm as any).appTouchedExternalComponents.size, 0);
});
it('verifyGetRegisteredExternalComponents', () => {
const aecm = new AppExternalComponentManager();
const component = mockExternalComponent1;
assert.strictEqual(aecm.getRegisteredExternalComponents().size, 0);
register(aecm, component);
assert.strictEqual(aecm.getRegisteredExternalComponents().size, 1);
});
it('verifyGetAppTouchedExternalComponents', () => {
const aecm = new AppExternalComponentManager();
const component = mockExternalComponent1;
assert.strictEqual(aecm.getAppTouchedExternalComponents().size, 0);
aecm.addExternalComponent(component.appId, component);
assert.strictEqual(aecm.getAppTouchedExternalComponents().size, 1);
});
it('verifyGetExternalComponents', () => {
const aecm = new AppExternalComponentManager();
const component = mockExternalComponent1;
assert.strictEqual(aecm.getExternalComponents(component.appId), null);
aecm.addExternalComponent(component.appId, component);
const components = aecm.getExternalComponents(component.appId);
assert.notStrictEqual(components, null);
assert.strictEqual(components!.size, 1);
});
it('verifyGetProvidedComponents', () => {
const aecm = new AppExternalComponentManager();
const component1 = mockExternalComponent1;
const component2 = mockExternalComponent2;
assert.strictEqual(Array.isArray(aecm.getProvidedComponents()), true);
register(aecm, component1);
register(aecm, component2);
assert.strictEqual(aecm.getProvidedComponents().length, 2);
});
it('verifyAddExternalComponent', () => {
const aecm1 = new AppExternalComponentManager();
const component1 = mockExternalComponent1;
const component3 = mockExternalComponent3;
aecm1.addExternalComponent(component1.appId, component1);
assert.strictEqual(aecm1.getAppTouchedExternalComponents().size, 1);
assert.strictEqual(aecm1.getExternalComponents(component1.appId).size, 1);
aecm1.addExternalComponent(component1.appId, component3);
assert.strictEqual(aecm1.getExternalComponents(component1.appId).size, 2);
});
it('verifyRegisterExternalComponents', () => {
const aecm = new AppExternalComponentManager();
const component = mockExternalComponent1;
assert.strictEqual(aecm.getRegisteredExternalComponents().size, 0);
register(aecm, component);
assert.strictEqual(aecm.getRegisteredExternalComponents().size, 1);
});
it('verifyUnregisterExternalComponents', () => {
const aecm = new AppExternalComponentManager();
const component = mockExternalComponent1;
register(aecm, component);
assert.strictEqual(aecm.getAppTouchedExternalComponents().size, 1);
assert.strictEqual(aecm.getRegisteredExternalComponents().size, 1);
aecm.unregisterExternalComponents(component.appId);
assert.strictEqual(aecm.getAppTouchedExternalComponents().size, 1);
assert.strictEqual(aecm.getRegisteredExternalComponents().size, 0);
});
it('verifyPurgeExternalComponents', () => {
const aecm = new AppExternalComponentManager();
const component = mockExternalComponent1;
register(aecm, component);
assert.strictEqual(aecm.getAppTouchedExternalComponents().size, 1);
assert.strictEqual(aecm.getRegisteredExternalComponents().size, 1);
aecm.purgeExternalComponents(component.appId);
assert.strictEqual(aecm.getAppTouchedExternalComponents().size, 0);
assert.strictEqual(aecm.getRegisteredExternalComponents().size, 0);
});
});

View File

@@ -0,0 +1,37 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata';
import type { AppManager } from '../../../src/server/AppManager';
import type { ProxiedApp } from '../../../src/server/ProxiedApp';
import { AppListenerManager } from '../../../src/server/managers';
describe('AppListenerManager', () => {
const mockApp = {
getID() {
return 'testing';
},
getImplementationList() {
return {
[AppInterface.IPostMessageSent]: true,
} as { [inte: string]: boolean };
},
} as ProxiedApp;
const mockManager = {
getOneById(appId: string) {
return mockApp;
},
} as AppManager;
it('basicAppListenerManager', () => {
assert.doesNotThrow(() => new AppListenerManager(mockManager));
const alm = new AppListenerManager(mockManager);
assert.strictEqual(alm.getListeners(AppInterface.IPostMessageSent).length, 0);
assert.doesNotThrow(() => alm.registerListeners(mockApp));
assert.strictEqual(alm.getListeners(AppInterface.IPostMessageSent).length, 1);
});
});

View File

@@ -0,0 +1,23 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import type { IOutboundMessageProviders } from '@rocket.chat/apps-engine/definition/outboundCommunication';
import type { ProxiedApp } from '../../../src/server/ProxiedApp';
import { OutboundMessageProvider } from '../../../src/server/managers/AppOutboundCommunicationProvider';
describe('OutboundMessageProvider', () => {
it('ensureAppOutboundCommunicationProviderManager', () => {
const mockApp = {} as ProxiedApp;
assert.doesNotThrow(() => new OutboundMessageProvider(mockApp, {} as IOutboundMessageProviders));
const aocp = new OutboundMessageProvider(mockApp, {} as IOutboundMessageProviders);
assert.strictEqual(aocp.isRegistered, false);
aocp.setRegistered(true);
assert.strictEqual(aocp.isRegistered, true);
});
});

View File

@@ -0,0 +1,276 @@
import * as assert from 'node:assert';
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
import type { AppManager } from '../../../src/server/AppManager';
import type { ProxiedApp } from '../../../src/server/ProxiedApp';
import type { AppBridges } from '../../../src/server/bridges';
import type { AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSlashCommandManager } from '../../../src/server/managers';
import { AppOutboundCommunicationProviderManager } from '../../../src/server/managers';
import { OutboundMessageProvider } from '../../../src/server/managers/AppOutboundCommunicationProvider';
import { AppPermissionManager } from '../../../src/server/managers/AppPermissionManager';
import type { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager';
import type { AppLogStorage, IAppStorageItem } from '../../../src/server/storage';
import { TestsAppBridges } from '../../test-data/bridges/appBridges';
import { TestsAppLogStorage } from '../../test-data/storage/logStorage';
import { TestData } from '../../test-data/utilities';
describe('AppOutboundCommunicationProviderManager', () => {
let mockBridges: TestsAppBridges;
let mockApp: ProxiedApp;
let mockManager: AppManager;
let hasPermissionSpy: ReturnType<typeof mock.method>;
beforeEach(() => {
mockBridges = new TestsAppBridges();
mockApp = TestData.getMockApp({ info: { id: 'testing', name: 'testing' } } as IAppStorageItem, {} as AppManager);
const bri = mockBridges;
const app = mockApp;
mockManager = {
getBridges(): AppBridges {
return bri;
},
getCommandManager() {
return {} as AppSlashCommandManager;
},
getExternalComponentManager(): AppExternalComponentManager {
return {} as AppExternalComponentManager;
},
getApiManager() {
return {} as AppApiManager;
},
getOneById(appId: string): ProxiedApp {
return appId === 'failMePlease' ? undefined : app;
},
getLogStorage(): AppLogStorage {
return new TestsAppLogStorage();
},
getSchedulerManager() {
return {} as AppSchedulerManager;
},
getUIActionButtonManager() {
return {} as UIActionButtonManager;
},
getOutboundCommunicationProviderManager() {
return {} as AppOutboundCommunicationProviderManager;
},
} as AppManager;
hasPermissionSpy = mock.method(AppPermissionManager, 'hasPermission', () => true);
});
afterEach(() => {
mock.restoreAll();
});
it('basicAppOutboundCommunicationProviderManager', () => {
assert.throws(() => new AppOutboundCommunicationProviderManager({} as AppManager));
assert.doesNotThrow(() => new AppOutboundCommunicationProviderManager(mockManager));
const manager = new AppOutboundCommunicationProviderManager(mockManager);
assert.strictEqual((manager as any).manager, mockManager);
assert.ok((manager as any).outboundMessageProviders !== undefined);
assert.strictEqual((manager as any).outboundMessageProviders.size, 0);
});
it('addProvider', () => {
const provider = TestData.getOutboundPhoneMessageProvider();
const manager = new AppOutboundCommunicationProviderManager(mockManager);
assert.doesNotThrow(() => manager.addProvider('testing', provider));
assert.strictEqual((manager as any).outboundMessageProviders.size, 1);
assert.throws(() => manager.addProvider('failMePlease', provider), {
name: 'Error',
message: 'App must exist in order for an outbound provider to be added.',
});
assert.strictEqual((manager as any).outboundMessageProviders.size, 1);
});
it('isAlreadyDefined', () => {
const provider = TestData.getOutboundPhoneMessageProvider();
const manager = new AppOutboundCommunicationProviderManager(mockManager);
assert.strictEqual(manager.isAlreadyDefined('testing', 'phone'), false);
manager.addProvider('testing', provider);
assert.strictEqual(manager.isAlreadyDefined('testing', 'phone'), true);
assert.strictEqual(manager.isAlreadyDefined('testing', 'email'), false);
assert.strictEqual(manager.isAlreadyDefined('another-app', 'phone'), false);
});
it('addProviderTwiceShouldOverwrite', () => {
const provider1 = TestData.getOutboundPhoneMessageProvider('provider1');
const provider2 = TestData.getOutboundPhoneMessageProvider('provider2');
const manager = new AppOutboundCommunicationProviderManager(mockManager);
manager.addProvider('testing', provider1);
const firstProviderInfo = (manager as any).outboundMessageProviders.get('testing').get('phone');
assert.strictEqual(firstProviderInfo.provider.name, 'provider1');
// Adding a provider of the same type should overwrite the previous one
manager.addProvider('testing', provider2);
const secondProviderInfo = (manager as any).outboundMessageProviders.get('testing').get('phone');
assert.strictEqual(secondProviderInfo.provider.name, 'provider2');
assert.strictEqual((manager as any).outboundMessageProviders.get('testing').size, 1);
});
it('addProviderWithoutPermission', () => {
const provider = TestData.getOutboundPhoneMessageProvider();
const manager = new AppOutboundCommunicationProviderManager(mockManager);
hasPermissionSpy.mock.mockImplementation(() => false);
assert.throws(() => manager.addProvider('testing', provider));
});
it('ignoreAppsWithoutProviders', async () => {
const manager = new AppOutboundCommunicationProviderManager(mockManager);
await assert.doesNotReject(() => manager.registerProviders('non-existant'));
});
it('registerProviders', async () => {
const manager = new AppOutboundCommunicationProviderManager(mockManager);
manager.addProvider('firstApp', TestData.getOutboundPhoneMessageProvider());
const appInfo = (manager as any).outboundMessageProviders.get('firstApp');
assert.ok(appInfo !== undefined);
const regInfo = appInfo.get('phone');
assert.ok(regInfo !== undefined);
assert.strictEqual(regInfo.isRegistered, false);
await assert.doesNotReject(async () => manager.registerProviders('firstApp'));
assert.strictEqual(regInfo.isRegistered, true);
});
it('registerTwoProviders', async () => {
const manager = new AppOutboundCommunicationProviderManager(mockManager);
manager.addProvider('firstApp', TestData.getOutboundPhoneMessageProvider());
manager.addProvider('firstApp', TestData.getOutboundEmailMessageProvider());
const firstApp = (manager as any).outboundMessageProviders.get('firstApp');
assert.ok(firstApp !== undefined);
const firstRegInfo = firstApp.get('phone');
assert.ok(firstRegInfo !== undefined);
const secondRegInfo = firstApp.get('email');
assert.ok(secondRegInfo !== undefined);
assert.strictEqual(firstRegInfo.isRegistered, false);
assert.strictEqual(secondRegInfo.isRegistered, false);
await assert.doesNotReject(async () => manager.registerProviders('firstApp'));
assert.strictEqual(firstRegInfo.isRegistered, true);
assert.strictEqual(secondRegInfo.isRegistered, true);
});
it('registerProvidersFromMultipleApps', async () => {
const manager = new AppOutboundCommunicationProviderManager(mockManager);
manager.addProvider('firstApp', TestData.getOutboundPhoneMessageProvider());
manager.addProvider('firstApp', TestData.getOutboundEmailMessageProvider());
manager.addProvider('secondApp', TestData.getOutboundPhoneMessageProvider('another-phone-provider'));
const firstApp = (manager as any).outboundMessageProviders.get('firstApp');
assert.ok(firstApp !== undefined);
const firstRegInfo = firstApp.get('phone');
const secondRegInfo = firstApp.get('email');
assert.ok(firstRegInfo !== undefined);
assert.ok(secondRegInfo !== undefined);
const secondApp = (manager as any).outboundMessageProviders.get('secondApp');
assert.ok(secondApp !== undefined);
const thirdRegInfo = secondApp.get('phone');
assert.ok(thirdRegInfo !== undefined);
assert.strictEqual(firstRegInfo.isRegistered, false);
assert.strictEqual(secondRegInfo.isRegistered, false);
await assert.doesNotReject(async () => manager.registerProviders('firstApp'));
assert.strictEqual(firstRegInfo.isRegistered, true);
assert.strictEqual(secondRegInfo.isRegistered, true);
assert.strictEqual(thirdRegInfo.isRegistered, false);
await assert.doesNotReject(async () => manager.registerProviders('secondApp'));
assert.strictEqual(thirdRegInfo.isRegistered, true);
});
it('unregisterProviders', async () => {
const manager = new AppOutboundCommunicationProviderManager(mockManager);
manager.addProvider('testing', TestData.getOutboundPhoneMessageProvider());
const regInfo = (manager as any).outboundMessageProviders.get('testing').get('phone');
await assert.doesNotReject(async () => manager.registerProviders('testing'));
await assert.doesNotReject(async () => manager.unregisterProviders('non-existant'));
assert.strictEqual(regInfo.isRegistered, true);
await assert.doesNotReject(async () => manager.unregisterProviders('testing'));
assert.strictEqual(regInfo.isRegistered, false);
// It should be removed from the map
assert.strictEqual((manager as any).outboundMessageProviders.has('testing'), false);
});
it('unregisterProvidersWithKeepReferences', async () => {
const manager = new AppOutboundCommunicationProviderManager(mockManager);
manager.addProvider('testing', TestData.getOutboundPhoneMessageProvider());
const appInfo = (manager as any).outboundMessageProviders.get('testing');
const regInfo = appInfo.get('phone');
await assert.doesNotReject(async () => manager.registerProviders('testing'));
assert.strictEqual(regInfo.isRegistered, true);
await assert.doesNotReject(async () => manager.unregisterProviders('testing', { keepReferences: true }));
assert.strictEqual(regInfo.isRegistered, false);
// It should not be removed from the map
assert.strictEqual((manager as any).outboundMessageProviders.has('testing'), true);
});
it('failToGetMetadataWithoutProvider', () => {
const manager = new AppOutboundCommunicationProviderManager(mockManager);
assert.throws(() => manager.getProviderMetadata('testing', 'phone'), { name: 'Error', message: 'provider-not-registered' });
manager.addProvider('testing', TestData.getOutboundPhoneMessageProvider());
assert.throws(() => manager.getProviderMetadata('testing', 'email'), { name: 'Error', message: 'provider-not-registered' });
});
it('getProviderMetadata', async () => {
const manager = new AppOutboundCommunicationProviderManager(mockManager);
manager.addProvider('testing', TestData.getOutboundPhoneMessageProvider());
mock.method(OutboundMessageProvider.prototype, 'runGetProviderMetadata', () =>
Promise.resolve({
name: 'test-provider',
capabilities: ['sms'],
}),
);
const metadata = await manager.getProviderMetadata('testing', 'phone');
assert.deepStrictEqual(metadata, {
name: 'test-provider',
capabilities: ['sms'],
});
});
it('failToSendOutboundMessageWithoutProvider', () => {
const manager = new AppOutboundCommunicationProviderManager(mockManager);
const message = TestData.getOutboundMessage();
assert.throws(() => manager.sendOutboundMessage('testing', 'phone', message), { name: 'Error', message: 'provider-not-registered' });
manager.addProvider('testing', TestData.getOutboundPhoneMessageProvider());
assert.throws(() => manager.sendOutboundMessage('testing', 'email', message), { name: 'Error', message: 'provider-not-registered' });
});
it('sendOutboundMessage', async () => {
const manager = new AppOutboundCommunicationProviderManager(mockManager);
manager.addProvider('testing', TestData.getOutboundPhoneMessageProvider());
const message = TestData.getOutboundMessage();
mock.method(OutboundMessageProvider.prototype, 'runSendOutboundMessage', () => Promise.resolve('message-id'));
const result = await manager.sendOutboundMessage('testing', 'phone', message);
assert.strictEqual(result, 'message-id');
});
});

View File

@@ -0,0 +1,140 @@
import * as assert from 'node:assert';
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import type { AppManager } from '../../../src/server/AppManager';
import type { IParseAppPackageResult } from '../../../src/server/compiler';
import { AppRuntimeManager } from '../../../src/server/managers/AppRuntimeManager';
import type { IRuntimeController } from '../../../src/server/runtime/IRuntimeController';
import type { DenoRuntimeSubprocessController } from '../../../src/server/runtime/deno/AppsEngineDenoRuntime';
import type { IAppStorageItem } from '../../../src/server/storage';
import { TestInfastructureSetup } from '../../test-data/utilities';
describe('AppRuntimeManager', () => {
let mockManager: AppManager;
let runtimeManager: AppRuntimeManager;
let mockAppPackage: IParseAppPackageResult;
let mockStorageItem: IAppStorageItem;
let mockSubprocessController: IRuntimeController;
beforeEach(() => {
const testInfrastructure = new TestInfastructureSetup();
mockManager = testInfrastructure.getMockManager();
mockAppPackage = {
info: {
id: 'test-app',
name: 'Test App',
nameSlug: 'test-app',
version: '1.0.0',
description: 'Test app for unit testing',
author: {
name: 'Test Author',
homepage: 'https://test.com',
support: 'https://test.com/support',
},
permissions: [],
requiredApiVersion: '1.0.0',
classFile: 'main.js',
iconFile: 'icon.png',
implements: [],
},
files: {
'main.js': 'console.log("Hello World");',
},
languageContent: {} as unknown as IParseAppPackageResult['languageContent'],
implemented: {} as unknown as IParseAppPackageResult['implemented'],
} as IParseAppPackageResult;
mockStorageItem = {
id: 'test-app',
status: AppStatus.MANUALLY_ENABLED,
info: mockAppPackage.info,
createdAt: new Date(),
updatedAt: new Date(),
} as IAppStorageItem;
mockSubprocessController = {
async setupApp() {
return Promise.resolve();
},
async stopApp() {
return Promise.resolve();
},
getAppId() {
return 'test-app';
},
async getStatus() {
return Promise.resolve(AppStatus.MANUALLY_ENABLED);
},
async sendRequest() {
return Promise.resolve(true);
},
on: () => mockSubprocessController,
once: () => mockSubprocessController,
off: () => mockSubprocessController,
emit: () => true,
addListener: () => mockSubprocessController,
removeListener: () => mockSubprocessController,
removeAllListeners: () => mockSubprocessController,
setMaxListeners: () => mockSubprocessController,
getMaxListeners: () => 10,
listeners: () => [],
rawListeners: () => [],
listenerCount: () => 0,
prependListener: () => mockSubprocessController,
prependOnceListener: () => mockSubprocessController,
eventNames: () => [],
} as IRuntimeController;
runtimeManager = new AppRuntimeManager(mockManager, () => mockSubprocessController as unknown as DenoRuntimeSubprocessController);
});
afterEach(() => {
mock.restoreAll();
});
it('Starts runtime for app successfully', async () => {
await assert.doesNotReject(() => runtimeManager.startRuntimeForApp(mockAppPackage, mockStorageItem));
/* eslint-disable-next-line dot-notation -- We need to access the property like this for the compile not to complain */
assert.strictEqual(runtimeManager['subprocesses'][mockAppPackage.info.id], mockSubprocessController);
});
it('Fails to start runtime for app that already has a runtime', async () => {
await assert.doesNotReject(() => runtimeManager.startRuntimeForApp(mockAppPackage, mockStorageItem));
await assert.rejects(() => runtimeManager.startRuntimeForApp(mockAppPackage, mockStorageItem), {
name: 'Error',
message: 'App already has an associated runtime',
});
});
it('Starts multiple runtimes for app successfully with force option', async () => {
await assert.doesNotReject(() => runtimeManager.startRuntimeForApp(mockAppPackage, mockStorageItem));
await assert.doesNotReject(() => runtimeManager.startRuntimeForApp(mockAppPackage, mockStorageItem, { force: true }));
/* eslint-disable-next-line dot-notation -- We need to access the property like this for the compile not to complain */
assert.strictEqual(runtimeManager['subprocesses'][mockAppPackage.info.id], mockSubprocessController);
});
it('startRuntimeThatFailsToSetup', async () => {
mock.method(mockSubprocessController, 'setupApp', () => Promise.reject(new Error('Nope')));
await assert.rejects(() => runtimeManager.startRuntimeForApp(mockAppPackage, mockStorageItem), {
name: 'Error',
message: 'Nope',
});
/* eslint-disable-next-line dot-notation -- We need to access the property like this for the compile not to complain */
assert.strictEqual(runtimeManager['subprocesses'][mockAppPackage.info.id], undefined);
});
});

View File

@@ -0,0 +1,152 @@
import * as assert from 'node:assert';
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
import { AppMethod } from '@rocket.chat/apps-engine/definition/metadata';
import type { ISetting } from '@rocket.chat/apps-engine/definition/settings';
import type { AppManager } from '../../../src/server/AppManager';
import type { ProxiedApp } from '../../../src/server/ProxiedApp';
import type { AppBridges } from '../../../src/server/bridges';
import type {
AppApiManager,
AppExternalComponentManager,
AppSchedulerManager,
AppSlashCommandManager,
AppVideoConfProviderManager,
} from '../../../src/server/managers';
import { AppSettingsManager } from '../../../src/server/managers';
import type { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager';
import type { AppMetadataStorage, IAppStorageItem } from '../../../src/server/storage';
import { TestsAppBridges } from '../../test-data/bridges/appBridges';
import { TestData } from '../../test-data/utilities';
describe('AppSettingsManager', () => {
let mockStorageItem: IAppStorageItem;
let mockApp: ProxiedApp;
let mockBridges: AppBridges;
let mockStorage: AppMetadataStorage;
let mockManager: AppManager;
beforeEach(() => {
mockStorageItem = {
_id: 'test_underscore_id',
settings: {},
} as IAppStorageItem;
mockStorageItem.settings.testing = TestData.getSetting('testing');
const si = mockStorageItem;
mockApp = {
getID() {
return 'testing';
},
getStorageItem() {
return si;
},
setStorageItem(item: IAppStorageItem) {},
call(method: AppMethod, ...args: Array<any>): Promise<any> {
return Promise.resolve();
},
} as ProxiedApp;
mockBridges = new TestsAppBridges();
mockStorage = {
updateSetting(appId: string, setting: ISetting): Promise<boolean> {
return Promise.resolve(true);
},
} as AppMetadataStorage;
const st = mockStorage;
const bri = mockBridges;
const app = mockApp;
mockManager = {
getOneById(appId: string) {
return appId === 'testing' ? app : undefined;
},
getBridges(): AppBridges {
return bri;
},
getStorage(): AppMetadataStorage {
return st;
},
getCommandManager(): AppSlashCommandManager {
return {} as AppSlashCommandManager;
},
getExternalComponentManager(): AppExternalComponentManager {
return {} as AppExternalComponentManager;
},
getApiManager(): AppApiManager {
return {} as AppApiManager;
},
getSchedulerManager() {
return {} as AppSchedulerManager;
},
getUIActionButtonManager() {
return {} as UIActionButtonManager;
},
getVideoConfProviderManager() {
return {} as AppVideoConfProviderManager;
},
} as AppManager;
});
afterEach(() => {
mock.restoreAll();
});
it('basicAppSettingsManager', () => {
assert.doesNotThrow(() => new AppSettingsManager(mockManager));
const asm = new AppSettingsManager(mockManager);
assert.notStrictEqual(asm.getAppSettings('testing'), mockStorageItem.settings);
assert.deepStrictEqual(asm.getAppSettings('testing'), mockStorageItem.settings);
assert.throws(() => asm.getAppSettings('fake'), { name: 'Error', message: 'No App found by the provided id.' });
assert.throws(() => {
asm.getAppSettings('testing').testing.value = 'testing';
});
assert.notStrictEqual(asm.getAppSetting('testing', 'testing'), mockStorageItem.settings.testing);
assert.deepStrictEqual(asm.getAppSetting('testing', 'testing'), mockStorageItem.settings.testing);
assert.throws(() => asm.getAppSetting('fake', 'testing'), { name: 'Error', message: 'No App found by the provided id.' });
assert.throws(() => asm.getAppSetting('testing', 'fake'), {
name: 'Error',
message: 'No setting found for the App by the provided id.',
});
assert.throws(() => {
asm.getAppSetting('testing', 'testing').value = 'testing';
});
});
it('updatingSettingViaAppSettingsManager', async () => {
const asm = new AppSettingsManager(mockManager);
const updateSettingSpy = mock.method(mockStorage, 'updateSetting');
const callSpy = mock.method(mockApp, 'call');
const doOnAppSettingsChangeSpy = mock.method((mockBridges as TestsAppBridges).getAppDetailChangesBridge(), 'doOnAppSettingsChange');
await assert.rejects(() => asm.updateAppSetting('fake', TestData.getSetting()), {
name: 'Error',
message: 'No App found by the provided id.',
});
await assert.rejects(() => asm.updateAppSetting('testing', TestData.getSetting('fake')), {
name: 'Error',
message: 'No setting found for the App by the provided id.',
});
const set = TestData.getSetting('testing');
await assert.doesNotReject(() => asm.updateAppSetting('testing', set));
assert.strictEqual(updateSettingSpy.mock.calls.length, 1);
assert.strictEqual(updateSettingSpy.mock.calls[0].arguments[0], 'test_underscore_id');
const settingArg = updateSettingSpy.mock.calls[0].arguments[1] as any;
assert.ok(Object.keys(set).every((k) => (settingArg as any)[k] === (set as any)[k]));
assert.strictEqual(doOnAppSettingsChangeSpy.mock.calls.length, 1);
assert.deepStrictEqual(doOnAppSettingsChangeSpy.mock.calls[0].arguments, ['testing', set]);
const onsettingupdatedCalls = callSpy.mock.calls.filter((c: any) => c.arguments[0] === AppMethod.ONSETTINGUPDATED);
assert.strictEqual(onsettingupdatedCalls.length, 1);
assert.deepStrictEqual(onsettingupdatedCalls[0].arguments, [AppMethod.ONSETTINGUPDATED, set]);
});
});

View File

@@ -0,0 +1,27 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands';
import type { AppManager } from '../../../src/server/AppManager';
import { AppSlashCommand } from '../../../src/server/managers/AppSlashCommand';
import type { IAppStorageItem } from '../../../src/server/storage';
import { TestData } from '../../test-data/utilities';
describe('AppSlashCommand', () => {
it('ensureAppSlashCommand', () => {
const mockApp = TestData.getMockApp({ info: { id: 'test', name: 'TestApp' } } as IAppStorageItem, {} as AppManager);
assert.doesNotThrow(() => new AppSlashCommand(mockApp, {} as ISlashCommand));
const ascr = new AppSlashCommand(mockApp, {} as ISlashCommand);
assert.strictEqual(ascr.isRegistered, false);
assert.strictEqual(ascr.isEnabled, false);
assert.strictEqual(ascr.isDisabled, false);
ascr.hasBeenRegistered();
assert.strictEqual(ascr.isDisabled, false);
assert.strictEqual(ascr.isEnabled, true);
assert.strictEqual(ascr.isRegistered, true);
});
});

View File

@@ -0,0 +1,454 @@
import * as assert from 'node:assert';
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
import type { ISlashCommandPreviewItem } from '@rocket.chat/apps-engine/definition/slashcommands';
import { SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands';
import type { AppManager } from '../../../src/server/AppManager';
import type { ProxiedApp } from '../../../src/server/ProxiedApp';
import type { AppBridges } from '../../../src/server/bridges';
import type {
AppApiManager,
AppExternalComponentManager,
AppSchedulerManager,
AppVideoConfProviderManager,
} from '../../../src/server/managers';
import { AppSlashCommandManager } from '../../../src/server/managers';
import { AppSlashCommand } from '../../../src/server/managers/AppSlashCommand';
import type { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager';
import { Room } from '../../../src/server/rooms/Room';
import type { AppLogStorage, IAppStorageItem } from '../../../src/server/storage';
import { TestsAppBridges } from '../../test-data/bridges/appBridges';
import { TestsAppLogStorage } from '../../test-data/storage/logStorage';
import { TestData } from '../../test-data/utilities';
describe('AppSlashCommandManager', () => {
let mockBridges: TestsAppBridges;
let mockApp: ProxiedApp;
let mockManager: AppManager;
function setupMocks() {
mockBridges = new TestsAppBridges();
const bri = mockBridges;
mockManager.getBridges = function _refreshedGetBridges(): AppBridges {
return bri;
};
mock.method(mockBridges.getCommandBridge(), 'doDoesCommandExist');
mock.method(mockBridges.getCommandBridge(), 'doRegisterCommand');
mock.method(mockBridges.getCommandBridge(), 'doUnregisterCommand');
mock.method(mockBridges.getCommandBridge(), 'doEnableCommand');
mock.method(mockBridges.getCommandBridge(), 'doDisableCommand');
}
beforeEach(() => {
mockBridges = new TestsAppBridges();
mockApp = TestData.getMockApp({ info: { id: 'testing', name: 'TestApp' } } as IAppStorageItem, {} as AppManager);
const bri = mockBridges;
const app = mockApp;
mockManager = {
getBridges(): AppBridges {
return bri;
},
getCommandManager() {
return {} as AppSlashCommandManager;
},
getExternalComponentManager(): AppExternalComponentManager {
return {} as AppExternalComponentManager;
},
getApiManager() {
return {} as AppApiManager;
},
getOneById(appId: string): ProxiedApp {
return appId === 'failMePlease' ? undefined : app;
},
getLogStorage(): AppLogStorage {
return new TestsAppLogStorage();
},
getSchedulerManager() {
return {} as AppSchedulerManager;
},
getUIActionButtonManager() {
return {} as UIActionButtonManager;
},
getVideoConfProviderManager() {
return {} as AppVideoConfProviderManager;
},
} as AppManager;
});
afterEach(() => {
mock.restoreAll();
});
it('basicAppSlashCommandManager', () => {
setupMocks();
assert.throws(() => new AppSlashCommandManager({} as AppManager));
assert.doesNotThrow(() => new AppSlashCommandManager(mockManager));
const ascm = new AppSlashCommandManager(mockManager);
assert.strictEqual((ascm as any).manager, mockManager);
assert.strictEqual((ascm as any).bridge, mockBridges.getCommandBridge());
assert.ok((ascm as any).providedCommands !== undefined);
assert.strictEqual((ascm as any).providedCommands.size, 0);
assert.ok((ascm as any).modifiedCommands !== undefined);
assert.strictEqual((ascm as any).modifiedCommands.size, 0);
assert.ok((ascm as any).touchedCommandsToApps !== undefined);
assert.strictEqual((ascm as any).touchedCommandsToApps.size, 0);
assert.ok((ascm as any).appsTouchedCommands !== undefined);
assert.strictEqual((ascm as any).appsTouchedCommands.size, 0);
});
it('canCommandBeTouchedBy', () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
assert.strictEqual(ascm.canCommandBeTouchedBy('testing', 'command'), true);
(ascm as any).touchedCommandsToApps.set('just-a-test', 'anotherAppId');
assert.strictEqual(ascm.canCommandBeTouchedBy('testing', 'just-a-test'), false);
});
it('isAlreadyDefined', () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
const reg = new Map<string, AppSlashCommand>();
reg.set('command', new AppSlashCommand(mockApp, TestData.getSlashCommand('command')));
assert.strictEqual(ascm.isAlreadyDefined('command'), false);
(ascm as any).providedCommands.set('testing', reg);
assert.strictEqual(ascm.isAlreadyDefined('command'), true);
assert.strictEqual(ascm.isAlreadyDefined('cOMMand'), true);
assert.strictEqual(ascm.isAlreadyDefined(' command '), true);
assert.strictEqual(ascm.isAlreadyDefined('c0mmand'), false);
});
it('setAsTouched', () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
assert.doesNotThrow(() => (ascm as any).setAsTouched('testing', 'command'));
assert.strictEqual((ascm as any).appsTouchedCommands.has('testing'), true);
assert.ok((ascm as any).appsTouchedCommands.get('testing').length > 0);
assert.strictEqual((ascm as any).appsTouchedCommands.get('testing').length, 1);
assert.strictEqual((ascm as any).touchedCommandsToApps.has('command'), true);
assert.strictEqual((ascm as any).touchedCommandsToApps.get('command'), 'testing');
assert.doesNotThrow(() => (ascm as any).setAsTouched('testing', 'command'));
assert.strictEqual((ascm as any).appsTouchedCommands.get('testing').length, 1);
});
it('registerCommand', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
const doRegisterCommandSpy = (mockBridges.getCommandBridge() as any).doRegisterCommand;
const regInfo = new AppSlashCommand(mockApp, TestData.getSlashCommand('command'));
await assert.doesNotReject(() => (ascm as any).registerCommand('testing', regInfo));
assert.strictEqual(doRegisterCommandSpy.mock.calls.length, 1);
assert.deepStrictEqual(doRegisterCommandSpy.mock.calls[0].arguments, [regInfo.slashCommand, 'testing']);
assert.strictEqual(regInfo.isRegistered, true);
assert.strictEqual(regInfo.isDisabled, false);
assert.strictEqual(regInfo.isEnabled, true);
});
it('addCommand', async () => {
setupMocks();
const cmd = TestData.getSlashCommand('my-cmd');
const ascm = new AppSlashCommandManager(mockManager);
await assert.doesNotReject(async () => ascm.addCommand('testing', cmd));
assert.strictEqual(mockBridges.getCommandBridge().commands.size, 1);
assert.strictEqual((ascm as any).providedCommands.size, 1);
assert.strictEqual((ascm as any).touchedCommandsToApps.get('my-cmd'), 'testing');
assert.strictEqual((ascm as any).appsTouchedCommands.get('testing').length, 1);
await assert.rejects(() => ascm.addCommand('another-app', cmd), {
name: 'CommandHasAlreadyBeenTouched',
message: 'The command "my-cmd" has already been touched by another App.',
});
await assert.rejects(() => ascm.addCommand('testing', cmd), {
name: 'CommandAlreadyExists',
message: 'The command "my-cmd" already exists in the system.',
});
await assert.rejects(() => ascm.addCommand('failMePlease', TestData.getSlashCommand('yet-another')), {
name: 'Error',
message: 'App must exist in order for a command to be added.',
});
await assert.doesNotReject(async () => ascm.addCommand('testing', TestData.getSlashCommand('another-command')));
assert.strictEqual((ascm as any).providedCommands.size, 1);
assert.strictEqual((ascm as any).providedCommands.get('testing').size, 2);
await assert.rejects(() => ascm.addCommand('even-another-app', TestData.getSlashCommand('it-exists')), {
name: 'CommandAlreadyExists',
message: 'The command "it-exists" already exists in the system.',
});
});
it('failToModifyAnotherAppsCommand', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
await ascm.addCommand('other-app', TestData.getSlashCommand('my-cmd'));
await assert.rejects(() => ascm.modifyCommand('testing', TestData.getSlashCommand('my-cmd')), {
name: 'CommandHasAlreadyBeenTouched',
message: 'The command "my-cmd" has already been touched by another App.',
});
});
it('failToModifyNonExistantAppCommand', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
await assert.rejects(() => ascm.modifyCommand('failMePlease', TestData.getSlashCommand('yet-another')), {
name: 'Error',
message: 'App must exist in order to modify a command.',
});
});
it('modifyMyCommand', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
await assert.rejects(() => ascm.modifyCommand('testing', TestData.getSlashCommand()), {
name: 'Error',
message: 'You must first register a command before you can modify it.',
});
await ascm.addCommand('testing', TestData.getSlashCommand('the-cmd'));
await assert.doesNotReject(() => ascm.modifyCommand('testing', TestData.getSlashCommand('the-cmd')));
});
it('modifySystemCommand', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
await assert.doesNotReject(() => ascm.modifyCommand('brand-new-id', TestData.getSlashCommand('it-exists')));
assert.strictEqual((ascm as any).modifiedCommands.size, 1);
assert.ok((ascm as any).modifiedCommands.get('it-exists') !== undefined);
assert.strictEqual((ascm as any).touchedCommandsToApps.get('it-exists'), 'brand-new-id');
});
it('enableMyCommand', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
const doDoesCommandExistSpy = (mockBridges.getCommandBridge() as any).doDoesCommandExist;
await assert.rejects(() => ascm.enableCommand('testing', 'doesnt-exist'), {
name: 'Error',
message: 'The command "doesnt-exist" does not exist to enable.',
});
await ascm.addCommand('testing', TestData.getSlashCommand('command'));
await assert.doesNotReject(() => ascm.enableCommand('testing', 'command'));
assert.strictEqual((ascm as any).providedCommands.get('testing').get('command').isDisabled, false);
assert.strictEqual((ascm as any).providedCommands.get('testing').get('command').isEnabled, true);
await ascm.addCommand('testing', TestData.getSlashCommand('another-command'));
(ascm as any).providedCommands.get('testing').get('another-command').isRegistered = true;
await assert.doesNotReject(() => ascm.enableCommand('testing', 'another-command'));
assert.strictEqual(doDoesCommandExistSpy.mock.calls.length, 3);
});
it('enableSystemCommand', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
const doEnableCommandSpy = (mockBridges.getCommandBridge() as any).doEnableCommand;
const doDoesCommandExistSpy = (mockBridges.getCommandBridge() as any).doDoesCommandExist;
await assert.doesNotReject(() => ascm.enableCommand('testing', 'it-exists'));
assert.strictEqual(doEnableCommandSpy.mock.calls.length, 1);
assert.deepStrictEqual(doEnableCommandSpy.mock.calls[0].arguments, ['it-exists', 'testing']);
assert.strictEqual(doDoesCommandExistSpy.mock.calls.length, 1);
});
it('failToEnableAnotherAppsCommand', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
await ascm.addCommand('another-app', TestData.getSlashCommand('command'));
await assert.rejects(() => ascm.enableCommand('my-app', 'command'), {
name: 'CommandHasAlreadyBeenTouched',
message: 'The command "command" has already been touched by another App.',
});
});
it('disableMyCommand', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
const doDoesCommandExistSpy = (mockBridges.getCommandBridge() as any).doDoesCommandExist;
await assert.rejects(() => ascm.disableCommand('testing', 'doesnt-exist'), {
name: 'Error',
message: 'The command "doesnt-exist" does not exist to disable.',
});
await ascm.addCommand('testing', TestData.getSlashCommand('command'));
await assert.doesNotReject(() => ascm.disableCommand('testing', 'command'));
assert.strictEqual((ascm as any).providedCommands.get('testing').get('command').isDisabled, true);
assert.strictEqual((ascm as any).providedCommands.get('testing').get('command').isEnabled, false);
await ascm.addCommand('testing', TestData.getSlashCommand('another-command'));
(ascm as any).providedCommands.get('testing').get('another-command').isRegistered = true;
await assert.doesNotReject(() => ascm.disableCommand('testing', 'another-command'));
assert.strictEqual(doDoesCommandExistSpy.mock.calls.length, 3);
});
it('disableSystemCommand', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
const doDisableCommandSpy = (mockBridges.getCommandBridge() as any).doDisableCommand;
const doDoesCommandExistSpy = (mockBridges.getCommandBridge() as any).doDoesCommandExist;
await assert.doesNotReject(() => ascm.disableCommand('testing', 'it-exists'));
assert.strictEqual(doDisableCommandSpy.mock.calls.length, 1);
assert.deepStrictEqual(doDisableCommandSpy.mock.calls[0].arguments, ['it-exists', 'testing']);
assert.strictEqual(doDoesCommandExistSpy.mock.calls.length, 1);
});
it('failToDisableAnotherAppsCommand', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
await ascm.addCommand('another-app', TestData.getSlashCommand('command'));
await assert.rejects(() => ascm.disableCommand('my-app', 'command'), {
name: 'CommandHasAlreadyBeenTouched',
message: 'The command "command" has already been touched by another App.',
});
});
it('registerCommands', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
const doRegisterCommandSpy = (mockBridges.getCommandBridge() as any).doRegisterCommand;
const registerCommandSpy = mock.method(ascm as any, 'registerCommand');
await ascm.addCommand('testing', TestData.getSlashCommand('enabled-command'));
const enabledRegInfo = (ascm as any).providedCommands.get('testing').get('enabled-command') as AppSlashCommand;
await ascm.addCommand('testing', TestData.getSlashCommand('disabled-command'));
await ascm.disableCommand('testing', 'disabled-command');
const disabledRegInfo = (ascm as any).providedCommands.get('testing').get('disabled-command') as AppSlashCommand;
await assert.doesNotReject(() => ascm.registerCommands('non-existant'));
await assert.doesNotReject(() => ascm.registerCommands('testing'));
assert.strictEqual(enabledRegInfo.isRegistered, true);
assert.strictEqual(disabledRegInfo.isRegistered, false);
assert.strictEqual(
registerCommandSpy.mock.calls.filter((c: any) => c.arguments[0] === 'testing' && c.arguments[1] === enabledRegInfo).length,
1,
);
assert.strictEqual(
doRegisterCommandSpy.mock.calls.filter((c: any) => c.arguments[0] === enabledRegInfo.slashCommand && c.arguments[1] === 'testing')
.length,
1,
);
});
it('unregisterCommands', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
const doUnregisterCommandSpy = (mockBridges.getCommandBridge() as any).doUnregisterCommand;
await ascm.addCommand('testing', TestData.getSlashCommand('command'));
await ascm.modifyCommand('testing', TestData.getSlashCommand('it-exists'));
await assert.doesNotReject(() => ascm.unregisterCommands('non-existant'));
await assert.doesNotReject(() => ascm.unregisterCommands('testing'));
assert.strictEqual(doUnregisterCommandSpy.mock.calls.length, 1);
});
it('executeCommands', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
await ascm.addCommand('testing', TestData.getSlashCommand('command'));
await ascm.addCommand('testing', TestData.getSlashCommand('not-registered'));
await ascm.addCommand('testing', TestData.getSlashCommand('disabled-command'));
await ascm.disableCommand('testing', 'not-registered');
await ascm.registerCommands('testing');
(ascm as any).providedCommands.get('testing').get('disabled-command').isDisabled = true;
await ascm.modifyCommand('testing', TestData.getSlashCommand('it-exists'));
const context = new SlashCommandContext(TestData.getUser(), TestData.getRoom(), []);
await assert.doesNotReject(() => ascm.executeCommand('nope', context));
await assert.doesNotReject(() => ascm.executeCommand('it-exists', context));
await assert.doesNotReject(() => ascm.executeCommand('command', context));
await assert.doesNotReject(() => ascm.executeCommand('not-registered', context));
await assert.doesNotReject(() => ascm.executeCommand('disabled-command', context));
const classContext = new SlashCommandContext(TestData.getUser(), new Room(TestData.getRoom(), mockManager), []);
await assert.doesNotReject(() => ascm.executeCommand('it-exists', classContext));
// set it up for no "no app failure"
const failedItems = new Map<string, AppSlashCommand>();
const asm = new AppSlashCommand(mockApp, TestData.getSlashCommand('failure'));
asm.hasBeenRegistered();
failedItems.set('failure', asm);
(ascm as any).providedCommands.set('failMePlease', failedItems);
(ascm as any).touchedCommandsToApps.set('failure', 'failMePlease');
await assert.rejects(() => ascm.executeCommand('failure', context));
});
it('getPreviews', async () => {
setupMocks();
const ascm = new AppSlashCommandManager(mockManager);
await ascm.addCommand('testing', TestData.getSlashCommand('command'));
await ascm.addCommand('testing', TestData.getSlashCommand('not-registered'));
await ascm.addCommand('testing', TestData.getSlashCommand('disabled-command'));
await ascm.disableCommand('testing', 'not-registered');
await ascm.registerCommands('testing');
(ascm as any).providedCommands.get('testing').get('disabled-command').isDisabled = true;
await ascm.modifyCommand('testing', TestData.getSlashCommand('it-exists'));
const context = new SlashCommandContext(TestData.getUser(), TestData.getRoom(), ['testing']);
await assert.doesNotReject(() => ascm.getPreviews('nope', context));
await assert.doesNotReject(() => ascm.getPreviews('it-exists', context));
await assert.doesNotReject(() => ascm.getPreviews('command', context));
await assert.doesNotReject(() => ascm.getPreviews('not-registered', context));
await assert.doesNotReject(() => ascm.getPreviews('disabled-command', context));
const classContext = new SlashCommandContext(TestData.getUser(), new Room(TestData.getRoom(), mockManager), []);
await assert.doesNotReject(() => ascm.getPreviews('it-exists', classContext));
// set it up for no "no app failure"
const failedItems = new Map<string, AppSlashCommand>();
const asm = new AppSlashCommand(mockApp, TestData.getSlashCommand('failure'));
asm.hasBeenRegistered();
failedItems.set('failure', asm);
(ascm as any).providedCommands.set('failMePlease', failedItems);
(ascm as any).touchedCommandsToApps.set('failure', 'failMePlease');
await assert.doesNotReject(() => ascm.getPreviews('failure', context));
});
it('executePreview', async () => {
setupMocks();
const previewItem = {} as ISlashCommandPreviewItem;
const ascm = new AppSlashCommandManager(mockManager);
await ascm.addCommand('testing', TestData.getSlashCommand('command'));
await ascm.addCommand('testing', TestData.getSlashCommand('not-registered'));
await ascm.addCommand('testing', TestData.getSlashCommand('disabled-command'));
await ascm.disableCommand('testing', 'not-registered');
await ascm.registerCommands('testing');
(ascm as any).providedCommands.get('testing').get('disabled-command').isDisabled = true;
await ascm.modifyCommand('testing', TestData.getSlashCommand('it-exists'));
const context = new SlashCommandContext(TestData.getUser(), TestData.getRoom(), ['testing']);
await assert.doesNotReject(() => ascm.executePreview('nope', previewItem, context));
await assert.doesNotReject(() => ascm.executePreview('it-exists', previewItem, context));
await assert.doesNotReject(() => ascm.executePreview('command', previewItem, context));
await assert.doesNotReject(() => ascm.executePreview('not-registered', previewItem, context));
await assert.doesNotReject(() => ascm.executePreview('disabled-command', previewItem, context));
const classContext = new SlashCommandContext(TestData.getUser(), new Room(TestData.getRoom(), mockManager), []);
await assert.doesNotReject(() => ascm.executePreview('it-exists', previewItem, classContext));
// set it up for no "no app failure"
const failedItems = new Map<string, AppSlashCommand>();
const asm = new AppSlashCommand(mockApp, TestData.getSlashCommand('failure'));
asm.hasBeenRegistered();
failedItems.set('failure', asm);
(ascm as any).providedCommands.set('failMePlease', failedItems);
(ascm as any).touchedCommandsToApps.set('failure', 'failMePlease');
await ascm.executePreview('nope', previewItem, context).catch(() => {});
await assert.doesNotReject(() => ascm.executePreview('failure', previewItem, context));
});
});

View File

@@ -0,0 +1,21 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders';
import type { ProxiedApp } from '../../../src/server/ProxiedApp';
import { AppVideoConfProvider } from '../../../src/server/managers/AppVideoConfProvider';
describe('AppVideoConfProvider', () => {
it('ensureAppVideoConfManager', () => {
const mockApp = {} as ProxiedApp;
assert.doesNotThrow(() => new AppVideoConfProvider(mockApp, {} as IVideoConfProvider));
const ascr = new AppVideoConfProvider(mockApp, {} as IVideoConfProvider);
assert.strictEqual(ascr.isRegistered, false);
ascr.hasBeenRegistered();
assert.strictEqual(ascr.isRegistered, true);
});
});

View File

@@ -0,0 +1,351 @@
import * as assert from 'node:assert';
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
import type { AppManager } from '../../../src/server/AppManager';
import type { ProxiedApp } from '../../../src/server/ProxiedApp';
import type { AppBridges } from '../../../src/server/bridges';
import type { AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSlashCommandManager } from '../../../src/server/managers';
import { AppVideoConfProviderManager } from '../../../src/server/managers';
import { AppVideoConfProvider } from '../../../src/server/managers/AppVideoConfProvider';
import type { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager';
import type { AppLogStorage, IAppStorageItem } from '../../../src/server/storage';
import { TestsAppBridges } from '../../test-data/bridges/appBridges';
import { TestsAppLogStorage } from '../../test-data/storage/logStorage';
import { TestData } from '../../test-data/utilities';
describe('AppVideoConfProviderManager', () => {
let mockBridges: TestsAppBridges;
let mockApp: ProxiedApp;
let mockManager: AppManager;
beforeEach(() => {
mockBridges = new TestsAppBridges();
mockApp = TestData.getMockApp({ info: { id: 'testing', name: 'testing' } } as IAppStorageItem, {} as AppManager);
const bri = mockBridges;
const app = mockApp;
mockManager = {
getBridges(): AppBridges {
return bri;
},
getCommandManager() {
return {} as AppSlashCommandManager;
},
getExternalComponentManager(): AppExternalComponentManager {
return {} as AppExternalComponentManager;
},
getApiManager() {
return {} as AppApiManager;
},
getOneById(appId: string): ProxiedApp {
return appId === 'failMePlease' ? undefined : app;
},
getLogStorage(): AppLogStorage {
return new TestsAppLogStorage();
},
getSchedulerManager() {
return {} as AppSchedulerManager;
},
getUIActionButtonManager() {
return {} as UIActionButtonManager;
},
getVideoConfProviderManager() {
return {} as AppVideoConfProviderManager;
},
} as AppManager;
});
afterEach(() => {
mock.restoreAll();
});
it('basicAppVideoConfProviderManager', () => {
assert.throws(() => new AppVideoConfProviderManager({} as AppManager));
assert.doesNotThrow(() => new AppVideoConfProviderManager(mockManager));
const manager = new AppVideoConfProviderManager(mockManager);
assert.strictEqual((manager as any).manager, mockManager);
assert.ok((manager as any).videoConfProviders !== undefined);
assert.strictEqual((manager as any).videoConfProviders.size, 0);
});
it('addProvider', () => {
const provider = TestData.getVideoConfProvider();
const manager = new AppVideoConfProviderManager(mockManager);
assert.doesNotThrow(() => manager.addProvider('testing', provider));
assert.strictEqual((manager as any).videoConfProviders.size, 1);
assert.throws(() => manager.addProvider('failMePlease', provider), {
name: 'Error',
message: 'App must exist in order for a video conference provider to be added.',
});
assert.strictEqual((manager as any).videoConfProviders.size, 1);
});
it('ignoreAppsWithoutProviders', async () => {
const manager = new AppVideoConfProviderManager(mockManager);
await assert.doesNotReject(() => manager.registerProviders('non-existant'));
});
it('registerProviders', async () => {
const manager = new AppVideoConfProviderManager(mockManager);
manager.addProvider('firstApp', TestData.getVideoConfProvider());
const appInfo = (manager as any).videoConfProviders.get('firstApp') as Map<string, AppVideoConfProvider>;
assert.ok(appInfo !== undefined);
const regInfo = appInfo.get('test');
assert.ok(regInfo !== undefined);
assert.strictEqual(regInfo.isRegistered, false);
await assert.doesNotReject(() => manager.registerProviders('firstApp'));
assert.strictEqual(regInfo.isRegistered, true);
});
it('registerTwoProviders', async () => {
const manager = new AppVideoConfProviderManager(mockManager);
manager.addProvider('firstApp', TestData.getVideoConfProvider());
manager.addProvider('firstApp', TestData.getVideoConfProvider('another-test'));
const firstApp = (manager as any).videoConfProviders.get('firstApp') as Map<string, AppVideoConfProvider>;
assert.ok(firstApp !== undefined);
const firstRegInfo = firstApp.get('test');
assert.ok(firstRegInfo !== undefined);
const secondRegInfo = firstApp.get('another-test');
assert.ok(secondRegInfo !== undefined);
assert.strictEqual(firstRegInfo.isRegistered, false);
assert.strictEqual(secondRegInfo.isRegistered, false);
await assert.doesNotReject(() => manager.registerProviders('firstApp'));
assert.strictEqual(firstRegInfo.isRegistered, true);
assert.strictEqual(secondRegInfo.isRegistered, true);
});
it('registerProvidersFromMultipleApps', async () => {
const manager = new AppVideoConfProviderManager(mockManager);
manager.addProvider('firstApp', TestData.getVideoConfProvider());
manager.addProvider('firstApp', TestData.getVideoConfProvider('another-test'));
manager.addProvider('secondApp', TestData.getVideoConfProvider('test3'));
const firstApp = (manager as any).videoConfProviders.get('firstApp') as Map<string, AppVideoConfProvider>;
assert.ok(firstApp !== undefined);
const firstRegInfo = firstApp.get('test');
const secondRegInfo = firstApp.get('another-test');
assert.ok(firstRegInfo !== undefined);
assert.ok(secondRegInfo !== undefined);
const secondApp = (manager as any).videoConfProviders.get('secondApp') as Map<string, AppVideoConfProvider>;
assert.ok(secondApp !== undefined);
const thirdRegInfo = secondApp.get('test3');
assert.ok(thirdRegInfo !== undefined);
assert.strictEqual(firstRegInfo.isRegistered, false);
assert.strictEqual(secondRegInfo.isRegistered, false);
await assert.doesNotReject(() => manager.registerProviders('firstApp'));
assert.strictEqual(firstRegInfo.isRegistered, true);
assert.strictEqual(secondRegInfo.isRegistered, true);
assert.strictEqual(thirdRegInfo.isRegistered, false);
await assert.doesNotReject(() => manager.registerProviders('secondApp'));
assert.strictEqual(thirdRegInfo.isRegistered, true);
});
it('failToRegisterSameProvider', () => {
const manager = new AppVideoConfProviderManager(mockManager);
manager.addProvider('firstApp', TestData.getVideoConfProvider());
assert.throws(() => manager.addProvider('secondApp', TestData.getVideoConfProvider('test')), {
name: 'VideoConfProviderAlreadyExists',
message: `The video conference provider "test" was already registered by another App.`,
});
});
it('unregisterProviders', async () => {
const manager = new AppVideoConfProviderManager(mockManager);
manager.addProvider('testing', TestData.getVideoConfProvider());
const regInfo = (manager as any).videoConfProviders.get('testing').get('test') as AppVideoConfProvider;
await assert.doesNotReject(() => manager.registerProviders('testing'));
await assert.doesNotReject(() => manager.unregisterProviders('non-existant'));
assert.strictEqual(regInfo.isRegistered, true);
await assert.doesNotReject(() => manager.unregisterProviders('testing'));
assert.strictEqual(regInfo.isRegistered, false);
});
it('failToGenerateUrlWithoutProvider', async () => {
const manager = new AppVideoConfProviderManager(mockManager);
const call = TestData.getVideoConfData();
await assert.rejects(() => manager.generateUrl('test', call), {
name: 'VideoConfProviderNotRegistered',
message: `The video conference provider "test" is not registered in the system.`,
});
manager.addProvider('testing', TestData.getVideoConfProvider());
await assert.rejects(() => manager.generateUrl('test', call), {
name: 'VideoConfProviderNotRegistered',
message: `The video conference provider "test" is not registered in the system.`,
});
});
it('generateUrl', async () => {
const manager = new AppVideoConfProviderManager(mockManager);
manager.addProvider('testing', TestData.getVideoConfProvider());
await manager.registerProviders('testing');
const call = TestData.getVideoConfData();
mock.method(AppVideoConfProvider.prototype, 'runGenerateUrl', () => 'test/first-call');
const url = await manager.generateUrl('test', call);
assert.strictEqual(url, 'test/first-call');
});
it('generateUrlWithMultipleProvidersAvailable', async () => {
const manager = new AppVideoConfProviderManager(mockManager);
manager.addProvider('testing', TestData.getVideoConfProvider());
manager.addProvider('testing', TestData.getVideoConfProvider('test2'));
await manager.registerProviders('testing');
manager.addProvider('secondApp', TestData.getVideoConfProvider('differentProvider'));
await manager.registerProviders('secondApp');
const call = TestData.getVideoConfData();
const testProvider = (manager as any).videoConfProviders.get('testing').get('test') as AppVideoConfProvider;
const test2Provider = (manager as any).videoConfProviders.get('testing').get('test2') as AppVideoConfProvider;
const differentProvider = (manager as any).videoConfProviders.get('secondApp').get('differentprovider') as AppVideoConfProvider;
mock.method(testProvider, 'runGenerateUrl', () => 'test/first-call');
mock.method(test2Provider, 'runGenerateUrl', () => 'test2/first-call');
mock.method(differentProvider, 'runGenerateUrl', () => 'differentProvider/first-call');
assert.strictEqual(await manager.generateUrl('test', call), 'test/first-call');
assert.strictEqual(await manager.generateUrl('test2', call), 'test2/first-call');
assert.strictEqual(await manager.generateUrl('differentProvider', call), 'differentProvider/first-call');
});
it('failToGenerateUrlWithUnknownProvider', async () => {
const call = TestData.getVideoConfData();
const manager = new AppVideoConfProviderManager(mockManager);
await assert.rejects(() => manager.generateUrl('unknownProvider', call), {
name: 'VideoConfProviderNotRegistered',
message: `The video conference provider "unknownProvider" is not registered in the system.`,
});
});
it('failToGenerateUrlWithUnregisteredProvider', async () => {
const call = TestData.getVideoConfData();
const manager = new AppVideoConfProviderManager(mockManager);
manager.addProvider('unregisteredApp', TestData.getVideoConfProvider('unregisteredProvider'));
await assert.rejects(() => manager.generateUrl('unregisteredProvider', call), {
name: 'VideoConfProviderNotRegistered',
message: `The video conference provider "unregisteredProvider" is not registered in the system.`,
});
});
it('failToCustomizeUrlWithoutProvider', async () => {
const manager = new AppVideoConfProviderManager(mockManager);
const call = TestData.getVideoConfDataExtended();
const user = TestData.getVideoConferenceUser();
await assert.rejects(() => manager.customizeUrl('test', call, user, {}), {
name: 'VideoConfProviderNotRegistered',
message: `The video conference provider "test" is not registered in the system.`,
});
manager.addProvider('testing', TestData.getVideoConfProvider());
await assert.rejects(() => manager.customizeUrl('test', call, user, {}), {
name: 'VideoConfProviderNotRegistered',
message: `The video conference provider "test" is not registered in the system.`,
});
});
it('customizeUrl', async () => {
const manager = new AppVideoConfProviderManager(mockManager);
manager.addProvider('testing', TestData.getVideoConfProvider());
await manager.registerProviders('testing');
const call = TestData.getVideoConfDataExtended();
const user = TestData.getVideoConferenceUser();
const cases: any = [
{
name: 'test',
call,
user,
options: {},
runCustomizeUrl: 'test/first-call#caller',
result: 'test/first-call#caller',
},
{
name: 'test',
call,
user: undefined,
options: {},
runCustomizeUrl: 'test/first-call#',
result: 'test/first-call#',
},
];
for (const c of cases) {
mock.method(AppVideoConfProvider.prototype, 'runCustomizeUrl', () => c.runCustomizeUrl);
assert.strictEqual(await manager.customizeUrl(c.name, c.call, c.user, c.options), c.result);
}
});
it('customizeUrlWithMultipleProvidersAvailable', async () => {
const manager = new AppVideoConfProviderManager(mockManager);
manager.addProvider('testing', TestData.getVideoConfProvider());
manager.addProvider('testing', TestData.getVideoConfProvider('test2'));
await manager.registerProviders('testing');
manager.addProvider('secondApp', TestData.getVideoConfProvider('differentProvider'));
await manager.registerProviders('secondApp');
const call = TestData.getVideoConfDataExtended();
const user = TestData.getVideoConferenceUser();
const testProvider = (manager as any).videoConfProviders.get('testing').get('test') as AppVideoConfProvider;
const test2Provider = (manager as any).videoConfProviders.get('testing').get('test2') as AppVideoConfProvider;
const differentProvider = (manager as any).videoConfProviders.get('secondApp').get('differentprovider') as AppVideoConfProvider;
mock.method(testProvider, 'runCustomizeUrl', (_call: any, user: any) => (user ? 'test/first-call#caller' : 'test/first-call#'));
mock.method(test2Provider, 'runCustomizeUrl', (_call: any, user: any) => (user ? 'test2/first-call#caller' : 'test2/first-call#'));
mock.method(differentProvider, 'runCustomizeUrl', (_call: any, user: any) =>
user ? 'differentProvider/first-call#caller' : 'differentProvider/first-call#',
);
assert.strictEqual(await manager.customizeUrl('test', call, user, {}), 'test/first-call#caller');
assert.strictEqual(await manager.customizeUrl('test', call, undefined, {}), 'test/first-call#');
assert.strictEqual(await manager.customizeUrl('test2', call, user, {}), 'test2/first-call#caller');
assert.strictEqual(await manager.customizeUrl('test2', call, undefined, {}), 'test2/first-call#');
assert.strictEqual(await manager.customizeUrl('differentProvider', call, user, {}), 'differentProvider/first-call#caller');
assert.strictEqual(await manager.customizeUrl('differentProvider', call, undefined, {}), 'differentProvider/first-call#');
});
it('failToCustomizeUrlWithUnknownProvider', async () => {
const call = TestData.getVideoConfDataExtended();
const user = TestData.getVideoConferenceUser();
const manager = new AppVideoConfProviderManager(mockManager);
await assert.rejects(() => manager.customizeUrl('unknownProvider', call, user, {}), {
name: 'VideoConfProviderNotRegistered',
message: `The video conference provider "unknownProvider" is not registered in the system.`,
});
});
it('failToCustomizeUrlWithUnregisteredProvider', async () => {
const call = TestData.getVideoConfDataExtended();
const user = TestData.getVideoConferenceUser();
const manager = new AppVideoConfProviderManager(mockManager);
manager.addProvider('unregisteredApp', TestData.getVideoConfProvider('unregisteredProvider'));
await assert.rejects(() => manager.customizeUrl('unregisteredProvider', call, user, {}), {
name: 'VideoConfProviderNotRegistered',
message: `The video conference provider "unregisteredProvider" is not registered in the system.`,
});
});
});

View File

@@ -0,0 +1,311 @@
import * as assert from 'node:assert';
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import type { IUIActionButtonDescriptor } from '@rocket.chat/apps-engine/definition/ui';
import { UIActionButtonContext } from '@rocket.chat/apps-engine/definition/ui';
import type { AppManager } from '../../../src/server/AppManager';
import type { ProxiedApp } from '../../../src/server/ProxiedApp';
import type { AppActivationBridge, AppBridges } from '../../../src/server/bridges';
import { AppPermissionManager } from '../../../src/server/managers/AppPermissionManager';
import { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager';
import { AppPermissions } from '../../../src/server/permissions/AppPermissions';
import { TestsAppBridges } from '../../test-data/bridges/appBridges';
describe('UIActionButtonManager', () => {
let mockBridges: TestsAppBridges;
let mockApp: ProxiedApp;
let mockApp2: ProxiedApp;
let mockManager: AppManager;
let mockActivationBridge: AppActivationBridge;
let hasPermissionSpy: ReturnType<typeof mock.method>;
let notifyAboutErrorSpy: ReturnType<typeof mock.method>;
let doActionsChangedSpy: ReturnType<typeof mock.method>;
beforeEach(() => {
mockBridges = new TestsAppBridges();
mockActivationBridge = mockBridges.getAppActivationBridge();
mockApp = {
getID() {
return 'testing-app';
},
getName() {
return 'Test App';
},
getStatus() {
return Promise.resolve(AppStatus.AUTO_ENABLED);
},
} as ProxiedApp;
mockApp2 = {
getID() {
return 'testing-app-2';
},
getName() {
return 'Test App 2';
},
getStatus() {
return Promise.resolve(AppStatus.AUTO_ENABLED);
},
} as ProxiedApp;
const bri = mockBridges;
const app = mockApp;
const app2 = mockApp2;
mockManager = {
getBridges(): AppBridges {
return bri;
},
getOneById: (appId: string): ProxiedApp | undefined => {
if (appId === 'testing-app') {
return app;
}
if (appId === 'testing-app-2') {
return app2;
}
return undefined;
},
} as AppManager;
notifyAboutErrorSpy = mock.method(AppPermissionManager, 'notifyAboutError');
hasPermissionSpy = mock.method(AppPermissionManager, 'hasPermission');
doActionsChangedSpy = mock.method(mockActivationBridge, 'doActionsChanged');
});
afterEach(() => {
mock.restoreAll();
});
it('basicUIActionButtonManager', () => {
assert.doesNotThrow(() => new UIActionButtonManager(mockManager));
const manager = new UIActionButtonManager(mockManager);
assert.strictEqual((manager as any).manager, mockManager);
assert.strictEqual((manager as any).activationBridge, mockActivationBridge);
assert.ok((manager as any).registeredActionButtons !== undefined);
assert.strictEqual((manager as any).registeredActionButtons.size, 0);
});
it('registerActionButtonWithPermission', () => {
hasPermissionSpy.mock.mockImplementation(() => true);
const manager = new UIActionButtonManager(mockManager);
const button: IUIActionButtonDescriptor = {
actionId: 'test-action',
context: UIActionButtonContext.MESSAGE_ACTION,
labelI18n: 'test.label',
};
const result = manager.registerActionButton('testing-app', button);
assert.strictEqual(result, true);
assert.strictEqual(hasPermissionSpy.mock.calls.length, 1);
assert.deepStrictEqual(hasPermissionSpy.mock.calls[0].arguments, ['testing-app', AppPermissions.ui.registerButtons]);
assert.ok(doActionsChangedSpy.mock.calls.length > 0);
assert.strictEqual((manager as any).registeredActionButtons.size, 1);
assert.strictEqual((manager as any).registeredActionButtons.get('testing-app').size, 1);
assert.strictEqual((manager as any).registeredActionButtons.get('testing-app').get('test-action'), button);
});
it('registerActionButtonWithoutPermission', () => {
hasPermissionSpy.mock.mockImplementation(() => false);
notifyAboutErrorSpy.mock.mockImplementation(() => {});
const manager = new UIActionButtonManager(mockManager);
const button: IUIActionButtonDescriptor = {
actionId: 'test-action',
context: UIActionButtonContext.MESSAGE_ACTION,
labelI18n: 'test.label',
};
const result = manager.registerActionButton('testing-app', button);
assert.strictEqual(result, false);
assert.strictEqual(hasPermissionSpy.mock.calls.length, 1);
assert.deepStrictEqual(hasPermissionSpy.mock.calls[0].arguments, ['testing-app', AppPermissions.ui.registerButtons]);
assert.ok(notifyAboutErrorSpy.mock.calls.length > 0);
assert.strictEqual(doActionsChangedSpy.mock.calls.length, 0);
assert.strictEqual((manager as any).registeredActionButtons.size, 0);
});
it('registerMultipleButtonsForSameApp', () => {
hasPermissionSpy.mock.mockImplementation(() => true);
const manager = new UIActionButtonManager(mockManager);
const button1: IUIActionButtonDescriptor = {
actionId: 'action-1',
context: UIActionButtonContext.MESSAGE_ACTION,
labelI18n: 'test.label1',
};
const button2: IUIActionButtonDescriptor = {
actionId: 'action-2',
context: UIActionButtonContext.ROOM_ACTION,
labelI18n: 'test.label2',
};
manager.registerActionButton('testing-app', button1);
manager.registerActionButton('testing-app', button2);
assert.strictEqual((manager as any).registeredActionButtons.size, 1);
assert.strictEqual((manager as any).registeredActionButtons.get('testing-app').size, 2);
assert.strictEqual((manager as any).registeredActionButtons.get('testing-app').get('action-1'), button1);
assert.strictEqual((manager as any).registeredActionButtons.get('testing-app').get('action-2'), button2);
});
it('clearAppActionButtons', () => {
hasPermissionSpy.mock.mockImplementation(() => true);
const manager = new UIActionButtonManager(mockManager);
const button: IUIActionButtonDescriptor = {
actionId: 'test-action',
context: UIActionButtonContext.MESSAGE_ACTION,
labelI18n: 'test.label',
};
manager.registerActionButton('testing-app', button);
assert.strictEqual((manager as any).registeredActionButtons.get('testing-app').size, 1);
manager.clearAppActionButtons('testing-app');
assert.strictEqual((manager as any).registeredActionButtons.get('testing-app').size, 0);
assert.strictEqual(doActionsChangedSpy.mock.calls.length, 2);
});
it('getAppActionButtons', () => {
hasPermissionSpy.mock.mockImplementation(() => true);
const manager = new UIActionButtonManager(mockManager);
const button: IUIActionButtonDescriptor = {
actionId: 'test-action',
context: UIActionButtonContext.MESSAGE_ACTION,
labelI18n: 'test.label',
};
manager.registerActionButton('testing-app', button);
const buttons = manager.getAppActionButtons('testing-app');
assert.ok(buttons !== undefined);
assert.strictEqual(buttons?.size, 1);
assert.strictEqual(buttons?.get('test-action'), button);
const nonExistentButtons = manager.getAppActionButtons('non-existent');
assert.strictEqual(nonExistentButtons, undefined);
});
it('getAllActionButtonsFromEnabledApp', async () => {
hasPermissionSpy.mock.mockImplementation(() => true);
mock.method(mockApp, 'getStatus', () => Promise.resolve(AppStatus.AUTO_ENABLED));
const manager = new UIActionButtonManager(mockManager);
const button: IUIActionButtonDescriptor = {
actionId: 'test-action',
context: UIActionButtonContext.MESSAGE_ACTION,
labelI18n: 'test.label',
};
manager.registerActionButton('testing-app', button);
const allButtons = await manager.getAllActionButtons();
assert.ok(allButtons !== undefined);
assert.strictEqual(allButtons.length, 1);
assert.strictEqual(allButtons[0].actionId, 'test-action');
assert.strictEqual(allButtons[0].appId, 'testing-app');
assert.strictEqual(allButtons[0].context, UIActionButtonContext.MESSAGE_ACTION);
assert.strictEqual(allButtons[0].labelI18n, 'test.label');
});
it('getAllActionButtonsFromDisabledApp', async () => {
hasPermissionSpy.mock.mockImplementation(() => true);
mock.method(mockApp, 'getStatus', () => Promise.resolve(AppStatus.DISABLED));
const manager = new UIActionButtonManager(mockManager);
const button: IUIActionButtonDescriptor = {
actionId: 'test-action',
context: UIActionButtonContext.MESSAGE_ACTION,
labelI18n: 'test.label',
};
manager.registerActionButton('testing-app', button);
const allButtons = await manager.getAllActionButtons();
assert.ok(allButtons !== undefined);
assert.strictEqual(allButtons.length, 0);
});
it('getAllActionButtonsFromNonExistentApp', async () => {
hasPermissionSpy.mock.mockImplementation(() => true);
const manager = new UIActionButtonManager(mockManager);
const button: IUIActionButtonDescriptor = {
actionId: 'test-action',
context: UIActionButtonContext.MESSAGE_ACTION,
labelI18n: 'test.label',
};
manager.registerActionButton('non-existent-app', button);
const allButtons = await manager.getAllActionButtons();
assert.ok(allButtons !== undefined);
assert.strictEqual(allButtons.length, 0);
});
it('getAllActionButtonsWithStatusError', async () => {
hasPermissionSpy.mock.mockImplementation(() => true);
mock.method(mockApp, 'getStatus', () => Promise.reject(new Error('Status error')));
const manager = new UIActionButtonManager(mockManager);
const button: IUIActionButtonDescriptor = {
actionId: 'test-action',
context: UIActionButtonContext.MESSAGE_ACTION,
labelI18n: 'test.label',
};
manager.registerActionButton('testing-app', button);
const allButtons = await manager.getAllActionButtons();
assert.ok(allButtons !== undefined);
assert.strictEqual(allButtons.length, 0);
});
it('getAllActionButtonsFromMultipleApps', async () => {
hasPermissionSpy.mock.mockImplementation(() => true);
const button1: IUIActionButtonDescriptor = {
actionId: 'action-1',
context: UIActionButtonContext.MESSAGE_ACTION,
labelI18n: 'test.label1',
};
const button2: IUIActionButtonDescriptor = {
actionId: 'action-2',
context: UIActionButtonContext.ROOM_ACTION,
labelI18n: 'test.label2',
};
const manager = new UIActionButtonManager(mockManager);
manager.registerActionButton('testing-app', button1);
manager.registerActionButton('testing-app-2', button2);
const allButtons = await manager.getAllActionButtons();
assert.ok(allButtons !== undefined);
assert.strictEqual(allButtons.length, 2);
const app1Button = allButtons.find((b) => b.appId === 'testing-app');
const app2Button = allButtons.find((b) => b.appId === 'testing-app-2');
assert.ok(app1Button !== undefined);
assert.strictEqual(app1Button!.actionId, 'action-1');
assert.ok(app2Button !== undefined);
assert.strictEqual(app2Button!.actionId, 'action-2');
});
});

View File

@@ -0,0 +1,99 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import { Utilities } from '../../../src/server/misc/Utilities';
describe('Utilities', () => {
const expectedInfo = {
id: '614055e2-3dba-41fb-be48-c1ff146f5932',
name: 'Testing App',
nameSlug: 'testing-app',
description: 'A Rocket.Chat Application used to test out the various features.',
version: '0.0.8',
requiredApiVersion: '>=0.9.6',
author: {
name: 'Bradley Hilton',
homepage: 'https://github.com/RocketChat/Rocket.Chat.Apps-ts-definitions',
support: 'https://github.com/RocketChat/Rocket.Chat.Apps-ts-definitions/issues',
},
classFile: 'TestingApp.ts',
iconFile: 'testing.jpg',
};
it('testDeepClone', () => {
assert.doesNotThrow(() => Utilities.deepClone(expectedInfo));
const info = Utilities.deepClone(expectedInfo);
assert.deepStrictEqual(info, expectedInfo);
info.name = 'New Testing App';
assert.strictEqual(info.name, 'New Testing App');
assert.strictEqual(info.author.name, expectedInfo.author.name);
});
it('testDeepFreeze', () => {
const testInfo = {
id: '614055e2-3dba-41fb-be48-c1ff146f5932',
name: 'Testing App',
nameSlug: 'testing-app',
description: 'A Rocket.Chat Application used to test out the various features.',
version: '0.0.8',
requiredApiVersion: '>=0.9.6',
author: {
name: 'Bradley Hilton',
homepage: 'https://github.com/RocketChat/Rocket.Chat.Apps-ts-definitions',
support: 'https://github.com/RocketChat/Rocket.Chat.Apps-ts-definitions/issues',
},
classFile: 'TestingApp.ts',
iconFile: 'testing.jpg',
};
assert.doesNotThrow(() => {
testInfo.name = 'New Testing App';
});
assert.doesNotThrow(() => {
testInfo.author.name = 'Bradley H';
});
assert.strictEqual(testInfo.name, 'New Testing App');
assert.strictEqual(testInfo.author.name, 'Bradley H');
assert.doesNotThrow(() => Utilities.deepFreeze(testInfo));
assert.throws(() => {
testInfo.name = 'Old Testing App';
});
assert.throws(() => {
testInfo.author.name = 'Bradley';
});
assert.strictEqual(testInfo.name, 'New Testing App');
assert.strictEqual(testInfo.author.name, 'Bradley H');
});
it('testDeepCloneAndFreeze', () => {
const testInfo = {
id: '614055e2-3dba-41fb-be48-c1ff146f5932',
name: 'Testing App',
nameSlug: 'testing-app',
description: 'A Rocket.Chat Application used to test out the various features.',
version: '0.0.8',
requiredApiVersion: '>=0.9.6',
author: {
name: 'Bradley H',
homepage: 'https://github.com/RocketChat/Rocket.Chat.Apps-ts-definitions',
support: 'https://github.com/RocketChat/Rocket.Chat.Apps-ts-definitions/issues',
},
classFile: 'TestingApp.ts',
iconFile: 'testing.jpg',
};
assert.doesNotThrow(() => Utilities.deepCloneAndFreeze({}));
const info = Utilities.deepCloneAndFreeze(testInfo);
assert.deepStrictEqual(info, testInfo);
assert.notStrictEqual(info, testInfo);
assert.strictEqual(info.author.name, testInfo.author.name);
assert.strictEqual(info.author.name, 'Bradley H');
assert.throws(() => {
info.author.name = 'Bradley Hilton';
});
});
});

View File

@@ -0,0 +1,97 @@
/* eslint-disable dot-notation -- we avoid the dot notation here when testing private methods */
import * as assert from 'node:assert';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { describe, it, afterEach, mock, before, after } from 'node:test';
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import { type RpcStatusType, SuccessObject } from 'jsonrpc-lite';
import type { AppManager } from '../../../src/server/AppManager';
import type { IParseAppPackageResult } from '../../../src/server/compiler';
import { AppApiManager } from '../../../src/server/managers';
import { DenoRuntimeSubprocessController } from '../../../src/server/runtime/deno/AppsEngineDenoRuntime';
import type { IAppStorageItem } from '../../../src/server/storage';
import { TestInfastructureSetup } from '../../test-data/utilities';
describe('DenoRuntimeSubprocessController', () => {
const rpcTypeRequest = 'request' as RpcStatusType.request;
let manager: AppManager;
let controller: DenoRuntimeSubprocessController;
let appPackage: IParseAppPackageResult;
let appStorageItem: IAppStorageItem;
before(
async () => {
const infrastructure = new TestInfastructureSetup();
manager = infrastructure.getMockManager();
const api = new AppApiManager(manager);
manager.getApiManager = () => api;
const appPackageBuffer = await fs.readFile(path.join(__dirname, '../../test-data/apps/hello-world-test_0.0.1.zip'));
appPackage = await manager.getParser().unpackageApp(appPackageBuffer);
await fs.unlink(path.join(manager.getTempFilePath(), 'deno-runtime')).catch(function noop() {});
appStorageItem = {
id: 'hello-world-test',
status: AppStatus.MANUALLY_ENABLED,
} as IAppStorageItem;
controller = new DenoRuntimeSubprocessController(manager, appPackage, appStorageItem);
await controller.setupApp();
},
{ timeout: 60_000 },
);
afterEach(() => {
mock.restoreAll();
});
after(
async () => {
await controller?.stopApp();
await fs.unlink(path.join(manager.getTempFilePath(), 'deno-runtime')).catch((reason) => {
console.warn('Failed to delete temporary Deno runtime symlink', reason);
});
},
{ timeout: 30_000 },
);
it('correctly identifies a call to the message bridge', { timeout: 15_000 }, async () => {
const messageBridge = manager.getBridges().getMessageBridge();
const doCreateSpy = mock.method(messageBridge, 'doCreate', () => Promise.resolve('random-message-id'));
const messageParam = {
room: { id: '123' },
sender: { id: '456' },
text: 'Hello World',
alias: 'alias',
avatarUrl: 'https://avatars.com/123',
};
const response = await controller['handleBridgeMessage']({
type: rpcTypeRequest,
payload: {
jsonrpc: '2.0',
id: 'requestId',
method: 'bridges:getMessageBridge:doCreate',
params: [messageParam, 'APP_ID'],
serialize: () => '',
},
});
assert.ok(response instanceof SuccessObject);
const { id, result } = response;
assert.strictEqual(doCreateSpy.mock.calls.length, 1);
assert.deepStrictEqual(doCreateSpy.mock.calls[0].arguments, [messageParam, '9c1d62ca-e40f-456f-8601-17c823a16c68']);
assert.strictEqual(id, 'requestId');
assert.strictEqual(result, 'random-message-id');
});
});

View File

@@ -0,0 +1,237 @@
import * as assert from 'node:assert';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import { describe, it, before, after } from 'node:test';
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms';
import { kSecureFields } from '../../../src/lib/SecureFields';
import type { AppManager } from '../../../src/server/AppManager';
import type { IParseAppPackageResult } from '../../../src/server/compiler';
import { AppApiManager } from '../../../src/server/managers';
import { DenoRuntimeSubprocessController } from '../../../src/server/runtime/deno/AppsEngineDenoRuntime';
import type { IAppStorageItem } from '../../../src/server/storage';
import { TestInfastructureSetup } from '../../test-data/utilities';
/**
* These tests verify end-to-end codec compatibility between Node and Deno, specifically
* the '@@SecureFields' mechanism introduced to guard access to sensitive room fields
* (e.g. abacAttributes) behind app permissions.
*
* The flow being tested:
* 1. Node encodes a room object that includes a '@@SecureFields' descriptor for abacAttributes
* using the SECURE_FIELDS_HANDLER_EXT msgpack extension type.
* 2. Deno receives and decodes the message. Its codec calls applySecureFields(), which:
* - Checks the running app's declared permissions.
* - If the app has 'abac.read', it merges abacAttributes into the plain room object.
* - Otherwise it strips the field entirely.
* 3. The Deno app's checkPreRoomCreatePrevent handler returns Array.isArray(room.abacAttributes),
* letting Node observe whether the field was received or withheld.
*
* Two app fixtures are used:
* - secure-fields-test-with-abac_0.0.1.zip → declares { name: 'abac.read' }
* - secure-fields-test-no-abac_0.0.1.zip → declares no permissions
*/
describe('@@SecureFields codec compatibility (Node → Deno)', () => {
/** A minimal room that carries abacAttributes as a secure field. */
const roomWithSecureField = {
id: 'room-secure-fields-test',
type: RoomType.CHANNEL,
slugifiedName: 'general',
displayName: 'General',
[kSecureFields]: [
{
permission: 'abac.read',
name: 'abacAttributes',
value: [{ key: 'department', values: ['support', 'engineering'] }],
},
],
};
/** A room with several regular (non-secured) fields plus the secure abacAttributes field. */
const roomWithMixedFields = {
id: 'room-mixed-fields-test',
type: RoomType.CHANNEL,
slugifiedName: 'mixed',
displayName: 'Mixed',
customFields: { foo: 'bar' },
messageCount: 42,
[kSecureFields]: [
{
permission: 'abac.read',
name: 'abacAttributes',
value: [{ key: 'tenant', values: ['alpha'] }],
},
],
};
// -------------------------------------------------------------------------
// Shared helpers
// -------------------------------------------------------------------------
function buildManager(): AppManager {
const infrastructure = new TestInfastructureSetup();
const manager = infrastructure.getMockManager();
const api = new AppApiManager(manager);
manager.getApiManager = () => api;
return manager;
}
async function parseAppPackage(manager: AppManager, zipName: string): Promise<IParseAppPackageResult> {
const buf = await fs.readFile(path.join(__dirname, '../../test-data/apps', zipName));
return manager.getParser().unpackageApp(buf);
}
// -------------------------------------------------------------------------
// App WITH abac.read permission
// -------------------------------------------------------------------------
describe('app that declares abac.read permission', () => {
let manager: AppManager;
let controller: DenoRuntimeSubprocessController;
let appPackage: IParseAppPackageResult;
let appStorageItem: IAppStorageItem;
before(
async () => {
manager = buildManager();
appPackage = await parseAppPackage(manager, 'secure-fields-test-with-abac_0.0.1.zip');
appStorageItem = {
id: 'secure-fields-test-with-abac',
status: AppStatus.MANUALLY_ENABLED,
} as IAppStorageItem;
controller = new DenoRuntimeSubprocessController(manager, appPackage, appStorageItem);
await controller.setupApp();
},
{ timeout: 60_000 },
);
after(
async () => {
await controller?.stopApp();
await fs.unlink(path.join(os.tmpdir(), 'deno-runtime')).catch(() => undefined);
},
{ timeout: 30_000 },
);
it('receives abacAttributes when the room is encoded with @@SecureFields', { timeout: 15_000 }, async () => {
/**
* The app's checkPreRoomCreatePrevent returns Array.isArray(room.abacAttributes).
* Because the app has abac.read, Deno's applySecureFields should attach
* abacAttributes to the decoded room object, making the result `true`.
*/
const result = await controller.sendRequest({
method: 'app:checkPreRoomCreatePrevent',
params: [roomWithSecureField],
});
assert.strictEqual(result, true, 'App with abac.read should receive abacAttributes from a @@SecureFields-encoded room');
});
it('still receives regular (non-secured) room fields alongside the secure field', { timeout: 15_000 }, async () => {
/**
* Verify that applying secure fields does not discard ordinary room properties.
* The handler returns `true` only when abacAttributes is an array AND the
* room reached the handler with its other fields intact (the Room constructor
* uses Object.assign, so any missing fields would surface as assertion
* failures in a more detailed handler – here we at minimum confirm abacAttributes
* was applied without corrupting the object).
*/
const result = await controller.sendRequest({
method: 'app:checkPreRoomCreatePrevent',
params: [roomWithMixedFields],
});
assert.strictEqual(result, true, 'abacAttributes should be applied to a room that also carries non-secure fields');
});
it('does not throw when the room carries no @@SecureFields descriptor', { timeout: 15_000 }, async () => {
/**
* Plain rooms (without @@SecureFields) must still be decodable and routable
* to the handler. abacAttributes will be absent, so the handler returns false,
* but no codec error should be raised.
*/
const plainRoom = {
id: 'plain-room',
type: RoomType.CHANNEL,
slugifiedName: 'plain',
};
const result = await controller.sendRequest({
method: 'app:checkPreRoomCreatePrevent',
params: [plainRoom],
});
assert.strictEqual(result, false, 'A plain room (no @@SecureFields) should be decoded normally; abacAttributes absent → false');
});
});
// -------------------------------------------------------------------------
// App WITHOUT abac.read permission
// -------------------------------------------------------------------------
describe('app that does NOT declare abac.read permission', () => {
let manager: AppManager;
let controller: DenoRuntimeSubprocessController;
let appPackage: IParseAppPackageResult;
let appStorageItem: IAppStorageItem;
before(
async () => {
manager = buildManager();
appPackage = await parseAppPackage(manager, 'secure-fields-test-no-abac_0.0.1.zip');
appStorageItem = {
id: 'secure-fields-test-no-abac',
status: AppStatus.MANUALLY_ENABLED,
} as IAppStorageItem;
controller = new DenoRuntimeSubprocessController(manager, appPackage, appStorageItem);
await controller.setupApp();
},
{ timeout: 60_000 },
);
after(
async () => {
await controller?.stopApp();
await fs.unlink(path.join(os.tmpdir(), 'deno-runtime')).catch(() => undefined);
},
{ timeout: 30_000 },
);
it('does not receive abacAttributes when the app lacks abac.read permission', { timeout: 15_000 }, async () => {
/**
* The room is encoded with @@SecureFields for abacAttributes, but this app
* does not declare abac.read. Deno's applySecureFields should withhold the
* field, so the handler returns `false`.
*/
const result = await controller.sendRequest({
method: 'app:checkPreRoomCreatePrevent',
params: [roomWithSecureField],
});
assert.strictEqual(result, false, 'App without abac.read must not receive abacAttributes from a @@SecureFields-encoded room');
});
it('still decodes regular (non-secured) room fields correctly', { timeout: 15_000 }, async () => {
/**
* Even though abacAttributes is withheld, the remaining room properties
* (id, type, slugifiedName, customFields, messageCount, …) must survive
* the round-trip unaltered. The handler only inspects abacAttributes, so
* we verify this indirectly: if the room object were corrupted entirely,
* Deno would throw an error instead of returning a clean `false`.
*/
const result = await controller.sendRequest({
method: 'app:checkPreRoomCreatePrevent',
params: [roomWithMixedFields],
});
assert.strictEqual(result, false, 'Regular room fields should survive the codec round-trip even when a secure field is withheld');
});
});
});

View File

@@ -0,0 +1,271 @@
import * as assert from 'node:assert';
import type { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:stream';
import { describe, it, beforeEach, afterEach, mock, type Mock } from 'node:test';
import debugFactory from 'debug';
import type { BaseRuntimeSubprocessController } from '../../../../src/server/runtime/base/BaseRuntimeSubprocessController';
import { COMMAND_PING, LivenessManager } from '../../../../src/server/runtime/base/LivenessManager';
import { ProcessMessenger } from '../../../../src/server/runtime/base/ProcessMessenger';
describe('LivenessManager ping mechanism', () => {
const PING_INTERVAL_MS = 100;
const PING_TIMEOUT_MS = 50;
const CONSECUTIVE_TIMEOUT_LIMIT = 3;
const MAX_RESTARTS = Infinity;
const RESTART_ATTEMPT_DELAY_MS = 10;
let mockController: BaseRuntimeSubprocessController;
let mockMessenger: ProcessMessenger;
let mockSubprocess: ChildProcess;
let debug: debug.Debugger;
let livenessManager: LivenessManager;
let controllerEventEmitter: EventEmitter;
let subprocessEventEmitter: EventEmitter;
let sendMock: Mock<ProcessMessenger['send']>;
beforeEach(() => {
debug = debugFactory('test:liveness-manager');
mock.timers.enable({ apis: ['setTimeout', 'setInterval', 'Date'] });
mock.timers.setTime(0);
// Create event emitters for controller and subprocess
controllerEventEmitter = new EventEmitter();
subprocessEventEmitter = new EventEmitter();
// Mock controller
mockController = {
getProcessState: () => 'ready',
restartApp: async () => Promise.resolve(),
stopApp: async () => Promise.resolve(),
on: (event: string, listener: (...args: any[]) => void) => {
controllerEventEmitter.on(event, listener);
return mockController;
},
once: (event: string, listener: (...args: any[]) => void) => {
controllerEventEmitter.once(event, listener);
return mockController;
},
off: (event: string, listener: (...args: any[]) => void) => {
controllerEventEmitter.off(event, listener);
return mockController;
},
emit: (event: string, ...args: any[]) => {
return controllerEventEmitter.emit(event, ...args);
},
addListener: () => mockController,
removeListener: () => mockController,
removeAllListeners: () => mockController,
setMaxListeners: () => mockController,
getMaxListeners: () => 10,
listeners: (): any[] => [],
rawListeners: (): any[] => [],
listenerCount: (): number => 0,
prependListener: () => mockController,
prependOnceListener: () => mockController,
eventNames: (): string[] => [],
} as unknown as BaseRuntimeSubprocessController;
// Mock subprocess
mockSubprocess = {
pid: 12345,
once: (event: string, listener: (...args: any[]) => void) => {
subprocessEventEmitter.once(event, listener);
return mockSubprocess;
},
on: () => mockSubprocess,
off: () => mockSubprocess,
emit: (event: string, ...args: any[]) => {
return subprocessEventEmitter.emit(event, ...args);
},
} as unknown as ChildProcess;
// Mock messenger
mockMessenger = new ProcessMessenger();
mockMessenger.setReceiver(mockSubprocess);
sendMock = mock.method(mockMessenger, 'send', () => Promise.resolve());
// Create LivenessManager with fast ping options for testing
livenessManager = new LivenessManager(
{
controller: mockController,
messenger: mockMessenger,
debug,
},
{
pingTimeoutInMS: PING_TIMEOUT_MS,
pingIntervalInMS: PING_INTERVAL_MS,
consecutiveTimeoutLimit: CONSECUTIVE_TIMEOUT_LIMIT,
maxRestarts: MAX_RESTARTS,
restartAttemptDelayInMS: RESTART_ATTEMPT_DELAY_MS,
},
);
livenessManager.attach(mockSubprocess);
});
afterEach(() => {
// Stop the liveness manager to clean up intervals
livenessManager.stop();
// Clear all event listeners
controllerEventEmitter.removeAllListeners();
subprocessEventEmitter.removeAllListeners();
mock.timers.reset();
sendMock.mock.restore();
});
it('should update lastHeartbeatTimestamp when heartbeat event is emitted', () => {
controllerEventEmitter.emit('constructed');
const initialTimestamp = livenessManager.getRuntimeData().lastHeartbeatTimestamp;
assert.strictEqual(initialTimestamp, 0);
mock.timers.tick(50);
controllerEventEmitter.emit('heartbeat');
const newTimestamp = livenessManager.getRuntimeData().lastHeartbeatTimestamp;
assert.strictEqual(newTimestamp, 50);
});
it('should ping when heartbeat is stale', () => {
controllerEventEmitter.emit('constructed');
mock.timers.tick(PING_INTERVAL_MS);
assert.strictEqual(sendMock.mock.calls.length, 1);
assert.deepStrictEqual(sendMock.mock.calls[0].arguments, [COMMAND_PING]);
});
it('should not ping when heartbeat is recent', () => {
controllerEventEmitter.emit('constructed');
// Wait half the interval to update the heartbeat
mock.timers.tick(PING_INTERVAL_MS / 2);
controllerEventEmitter.emit('heartbeat');
// Wait for the rest of the interval that would trigger the ping
mock.timers.tick(PING_INTERVAL_MS / 2);
// Ping should not have been sent
assert.strictEqual(sendMock.mock.calls.length, 0);
});
it('should handle successful ping/pong', () => {
controllerEventEmitter.emit('constructed');
// Wait for the full interval
mock.timers.tick(PING_INTERVAL_MS);
// Verify ping was sent
assert.strictEqual(sendMock.mock.calls.length, 1);
assert.deepStrictEqual(sendMock.mock.calls[0].arguments, [COMMAND_PING]);
// Advance time to simulate latency
mock.timers.tick(20);
// Emit pong response
controllerEventEmitter.emit('pong');
// Verify consecutive timeout count was reset
const runtimeData = livenessManager.getRuntimeData();
assert.strictEqual(runtimeData.pingTimeoutConsecutiveCount, 0);
// Verify heartbeat timestamp was updated
const newTimestamp = runtimeData.lastHeartbeatTimestamp;
assert.strictEqual(newTimestamp, PING_INTERVAL_MS + 20);
});
it('should keep track of consecutive timeouts, and clear the count on a heartbeat', () => {
controllerEventEmitter.emit('constructed');
// Wait for the full interval
mock.timers.tick(PING_INTERVAL_MS);
// Verify ping was sent
assert.strictEqual(sendMock.mock.calls.length, 1);
assert.deepStrictEqual(sendMock.mock.calls[0].arguments, [COMMAND_PING]);
// Timeout the ping
mock.timers.tick(PING_TIMEOUT_MS);
// Verify consecutive timeout count incremented
let runtimeData = livenessManager.getRuntimeData();
assert.strictEqual(runtimeData.pingTimeoutConsecutiveCount, 1);
// Tick the rest of the interval to next ping
mock.timers.tick(PING_INTERVAL_MS - PING_TIMEOUT_MS);
// Verify ping was sent
assert.strictEqual(sendMock.mock.calls.length, 2);
// Timeout the ping
mock.timers.tick(PING_TIMEOUT_MS);
// Verify consecutive timeout count incremented
runtimeData = livenessManager.getRuntimeData();
assert.strictEqual(runtimeData.pingTimeoutConsecutiveCount, 2);
controllerEventEmitter.emit('heartbeat');
mock.timers.tick(PING_INTERVAL_MS - PING_TIMEOUT_MS);
// Shouldn't have called ping due to recent heartbeat
assert.strictEqual(sendMock.mock.calls.length, 2);
// Verify consecutive timeout count was reset
runtimeData = livenessManager.getRuntimeData();
assert.strictEqual(runtimeData.pingTimeoutConsecutiveCount, 0);
});
it('should call restart when consecutive timeout count reaches limit from options', async () => {
(livenessManager as any).options.consecutiveTimeoutLimit = 2;
const restartSpy = mock.method(livenessManager as any, 'restartProcess', () => Promise.resolve());
controllerEventEmitter.emit('constructed');
// Wait for the full interval
mock.timers.tick(PING_INTERVAL_MS);
// Verify ping was sent
assert.strictEqual(sendMock.mock.calls.length, 1);
assert.deepStrictEqual(sendMock.mock.calls[0].arguments, [COMMAND_PING]);
// Timeout the ping
mock.timers.tick(PING_TIMEOUT_MS);
// Verify consecutive timeout count incremented
let runtimeData = livenessManager.getRuntimeData();
assert.strictEqual(runtimeData.pingTimeoutConsecutiveCount, 1);
// Wait for ping handler to finish
await livenessManager.getPendingPing();
// Tick the rest of the interval to next ping
mock.timers.tick(PING_INTERVAL_MS - PING_TIMEOUT_MS);
// Verify ping was sent
assert.strictEqual(sendMock.mock.calls.length, 2);
// Timeout the ping
mock.timers.tick(PING_TIMEOUT_MS);
// Wait for ping handler to finish
await livenessManager.getPendingPing();
// Verify consecutive timeout count incremented
runtimeData = livenessManager.getRuntimeData();
assert.strictEqual(runtimeData.pingTimeoutConsecutiveCount, 2);
// Verify that restart has been called due to reaching consecutive timeout limit
assert.strictEqual(restartSpy.mock.calls.length, 1);
assert.deepStrictEqual(restartSpy.mock.calls[0].arguments, ['Too many pings timed out']);
restartSpy.mock.restore();
});
});

View File

@@ -0,0 +1,107 @@
import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import type { AppImplements } from '../../../../src/server/compiler/AppImplements';
import type { IParseAppPackageResult } from '../../../../src/server/compiler/IParseAppPackageResult';
import { bundleLegacyApp } from '../../../../src/server/runtime/base/bundler';
function makeAppPackage(classFile: string, files: { [key: string]: string }): IParseAppPackageResult {
return {
info: {
id: 'test-app',
name: 'Test App',
nameSlug: 'test-app',
version: '0.0.1',
description: 'Test App',
requiredApiVersion: '*',
classFile,
iconFile: 'icon.png',
implements: [],
author: { name: 'Test', homepage: 'https://test.com', support: 'https://test.com' },
},
files,
languageContent: {},
implemented: {} as AppImplements,
};
}
describe('bundleLegacyApp', () => {
it('bundles a single-file app into one output file', async () => {
const appPackage = makeAppPackage('app.js', {
'app.js': 'module.exports = { hello: "world" };',
});
await bundleLegacyApp(appPackage);
// After bundling, files should contain only the single bundled entry
assert.strictEqual(Object.keys(appPackage.files).length, 1);
assert.ok(appPackage.files['app.js'] !== undefined);
assert.strictEqual(typeof appPackage.files['app.js'], 'string');
assert.ok(appPackage.files['app.js'].length > 0);
});
it('includes code from relative imports', async () => {
const appPackage = makeAppPackage('app.js', {
'app.js': 'var utils = require("./utils"); module.exports = utils;',
'utils.js': 'module.exports = { value: 42 };',
});
await bundleLegacyApp(appPackage);
assert.strictEqual(Object.keys(appPackage.files).length, 1);
// The bundled output should contain the inlined value from utils.js
assert.ok(appPackage.files['app.js'].includes('42'));
});
it('resolves directory index imports (require("./dir") → dir/index.js)', async () => {
const appPackage = makeAppPackage('app.js', {
'app.js': 'var lib = require("./lib"); module.exports = lib;',
'lib/index.js': 'module.exports = { fromIndex: true };',
});
await bundleLegacyApp(appPackage);
assert.strictEqual(Object.keys(appPackage.files).length, 1);
assert.ok(appPackage.files['app.js'].includes('fromIndex'));
});
it('marks @rocket.chat/apps-engine/* imports as external', async () => {
const appPackage = makeAppPackage('app.js', {
'app.js': ['var AppInterface = require("@rocket.chat/apps-engine/definition/AppInterface");', 'module.exports = AppInterface;'].join(
'\n',
),
});
await bundleLegacyApp(appPackage);
assert.strictEqual(Object.keys(appPackage.files).length, 1);
// The apps-engine import should remain as an external require, not be inlined
assert.ok(appPackage.files['app.js'].includes('@rocket.chat/apps-engine'));
});
it('handles deeply nested relative imports', async () => {
const appPackage = makeAppPackage('app.js', {
'app.js': 'var a = require("./commands/run"); module.exports = a;',
'commands/run.js': 'var helper = require("../helpers/format"); module.exports = helper;',
'helpers/format.js': 'module.exports = { formatted: true };',
});
await bundleLegacyApp(appPackage);
assert.strictEqual(Object.keys(appPackage.files).length, 1);
assert.ok(appPackage.files['app.js'].includes('formatted'));
});
it('replaces the files record with only the bundled classFile entry', async () => {
const appPackage = makeAppPackage('main.js', {
'main.js': 'var dep = require("./dep"); module.exports = dep;',
'dep.js': 'module.exports = "dep-value";',
'unused.js': 'module.exports = "unused";',
});
await bundleLegacyApp(appPackage);
// Only the classFile key should remain after bundling
assert.deepStrictEqual(Object.keys(appPackage.files), ['main.js']);
});
});