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
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:
277
packages/apps/tests/server/AppManager.test.ts
Normal file
277
packages/apps/tests/server/AppManager.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
24
packages/apps/tests/server/compiler/AppCompiler.test.ts
Normal file
24
packages/apps/tests/server/compiler/AppCompiler.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
21
packages/apps/tests/server/compiler/AppImplements.test.ts
Normal file
21
packages/apps/tests/server/compiler/AppImplements.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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.');
|
||||
});
|
||||
});
|
||||
@@ -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.');
|
||||
});
|
||||
});
|
||||
13
packages/apps/tests/server/errors/CompilerError.test.ts
Normal file
13
packages/apps/tests/server/errors/CompilerError.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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.');
|
||||
});
|
||||
});
|
||||
13
packages/apps/tests/server/errors/MustExtendAppError.test.ts
Normal file
13
packages/apps/tests/server/errors/MustExtendAppError.test.ts
Normal 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.');
|
||||
});
|
||||
});
|
||||
@@ -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.');
|
||||
});
|
||||
});
|
||||
@@ -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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
92
packages/apps/tests/server/logging/AppConsole.test.ts
Normal file
92
packages/apps/tests/server/logging/AppConsole.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
24
packages/apps/tests/server/managers/AppApi.test.ts
Normal file
24
packages/apps/tests/server/managers/AppApi.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
219
packages/apps/tests/server/managers/AppApiManager.test.ts
Normal file
219
packages/apps/tests/server/managers/AppApiManager.test.ts
Normal 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: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
140
packages/apps/tests/server/managers/AppRuntimeManager.test.ts
Normal file
140
packages/apps/tests/server/managers/AppRuntimeManager.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
152
packages/apps/tests/server/managers/AppSettingsManager.test.ts
Normal file
152
packages/apps/tests/server/managers/AppSettingsManager.test.ts
Normal 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]);
|
||||
});
|
||||
});
|
||||
27
packages/apps/tests/server/managers/AppSlashCommand.test.ts
Normal file
27
packages/apps/tests/server/managers/AppSlashCommand.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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.`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
99
packages/apps/tests/server/misc/Utilities.test.ts
Normal file
99
packages/apps/tests/server/misc/Utilities.test.ts
Normal 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';
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
271
packages/apps/tests/server/runtime/deno/LivenessManager.test.ts
Normal file
271
packages/apps/tests/server/runtime/deno/LivenessManager.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
107
packages/apps/tests/server/runtime/deno/bundleLegacyApp.test.ts
Normal file
107
packages/apps/tests/server/runtime/deno/bundleLegacyApp.test.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
2
packages/apps/tests/test-data/README.md
Normal file
2
packages/apps/tests/test-data/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# What is this folder?
|
||||
This contains data, classes, and implementations of pieces that the user of this package would have to implement.
|
||||
BIN
packages/apps/tests/test-data/apps/hello-world-test_0.0.1.zip
Normal file
BIN
packages/apps/tests/test-data/apps/hello-world-test_0.0.1.zip
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
packages/apps/tests/test-data/apps/testing-app_0.0.8.zip
Normal file
BIN
packages/apps/tests/test-data/apps/testing-app_0.0.8.zip
Normal file
Binary file not shown.
29
packages/apps/tests/test-data/bridges/OAuthAppsBridge.ts
Normal file
29
packages/apps/tests/test-data/bridges/OAuthAppsBridge.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { IOAuthApp, IOAuthAppParams } from '@rocket.chat/apps-engine/definition/accessors/IOAuthApp';
|
||||
|
||||
import { OAuthAppsBridge } from '../../../src/server/bridges/OAuthAppsBridge';
|
||||
|
||||
export class TestOAuthAppsBridge extends OAuthAppsBridge {
|
||||
protected create(oAuthApp: IOAuthAppParams, appId: string): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected getById(id: string, appId: string): Promise<IOAuthApp> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected getByName(name: string, appId: string): Promise<Array<IOAuthApp>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected update(oAuthApp: IOAuthAppParams, id: string, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected delete(id: string, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected purge(appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
26
packages/apps/tests/test-data/bridges/activationBridge.ts
Normal file
26
packages/apps/tests/test-data/bridges/activationBridge.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
|
||||
|
||||
import type { ProxiedApp } from '../../../src/server/ProxiedApp';
|
||||
import { AppActivationBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestsActivationBridge extends AppActivationBridge {
|
||||
public async appAdded(app: ProxiedApp): Promise<void> {
|
||||
console.log(`The App ${app.getName()} (${app.getID()}) has been added.`);
|
||||
}
|
||||
|
||||
public async appUpdated(app: ProxiedApp): Promise<void> {
|
||||
console.log(`The App ${app.getName()} (${app.getID()}) has been updated.`);
|
||||
}
|
||||
|
||||
public async appRemoved(app: ProxiedApp): Promise<void> {
|
||||
console.log(`The App ${app.getName()} (${app.getID()}) has been removed.`);
|
||||
}
|
||||
|
||||
public async appStatusChanged(app: ProxiedApp, status: AppStatus): Promise<void> {
|
||||
console.log(`The App ${app.getName()} (${app.getID()}) status has changed to: ${status}`);
|
||||
}
|
||||
|
||||
protected async actionsChanged(): Promise<void> {
|
||||
console.log('The actions have changed.');
|
||||
}
|
||||
}
|
||||
38
packages/apps/tests/test-data/bridges/apiBridge.ts
Normal file
38
packages/apps/tests/test-data/bridges/apiBridge.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { IApi } from '@rocket.chat/apps-engine/definition/api';
|
||||
|
||||
import { ApiBridge } from '../../../src/server/bridges';
|
||||
import type { AppApi } from '../../../src/server/managers/AppApi';
|
||||
import { TestData } from '../utilities';
|
||||
|
||||
export class TestsApiBridge extends ApiBridge {
|
||||
public apis: Map<string, Map<string, IApi>>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.apis = new Map<string, Map<string, IApi>>();
|
||||
this.apis.set('appId', new Map<string, IApi>());
|
||||
this.apis.get('appId').set('it-exists', TestData.getApi('it-exists'));
|
||||
}
|
||||
|
||||
public async registerApi(api: AppApi, appId: string): Promise<void> {
|
||||
if (!this.apis.has(appId)) {
|
||||
this.apis.set(appId, new Map<string, IApi>());
|
||||
}
|
||||
|
||||
if (this.apis.get(appId)) {
|
||||
api.api.endpoints.forEach((endpoint) => {
|
||||
if (this.apis.get(appId).has(endpoint.path)) {
|
||||
throw new Error(`Api "${api.endpoint.path}" has already been registered for app ${appId}.`);
|
||||
}
|
||||
});
|
||||
|
||||
api.api.endpoints.forEach((endpoint) => {
|
||||
this.apis.get(appId).set(api.endpoint.path, api.api);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async unregisterApis(appId: string): Promise<void> {
|
||||
this.apis.delete(appId);
|
||||
}
|
||||
}
|
||||
255
packages/apps/tests/test-data/bridges/appBridges.ts
Normal file
255
packages/apps/tests/test-data/bridges/appBridges.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { TestOAuthAppsBridge } from './OAuthAppsBridge';
|
||||
import { TestsActivationBridge } from './activationBridge';
|
||||
import { TestsApiBridge } from './apiBridge';
|
||||
import { TestsAppDetailChangesBridge } from './appDetailChanges';
|
||||
import { TestAppCloudWorkspaceBridge } from './cloudBridge';
|
||||
import { TestsCommandBridge } from './commandBridge';
|
||||
import { TestContactBridge } from './contactBridge';
|
||||
import { TestsEmailBridge } from './emailBridge';
|
||||
import { TestsEnvironmentalVariableBridge } from './environmentalVariableBridge';
|
||||
import { TestExperimentalBridge } from './experimentalBridge';
|
||||
import { TestsHttpBridge } from './httpBridge';
|
||||
import { TestsInternalBridge } from './internalBridge';
|
||||
import { TestsInternalFederationBridge } from './internalFederationBridge';
|
||||
import { TestLivechatBridge } from './livechatBridge';
|
||||
import { TestsMessageBridge } from './messageBridge';
|
||||
import { TestsModerationBridge } from './moderationBridge';
|
||||
import { TestOutboundCommunicationBridge } from './outboundComms';
|
||||
import { TestsPersisBridge } from './persisBridge';
|
||||
import { TestsRoleBridge } from './roleBridge';
|
||||
import { TestsRoomBridge } from './roomBridge';
|
||||
import { TestSchedulerBridge } from './schedulerBridge';
|
||||
import { TestsServerSettingBridge } from './serverSettingBridge';
|
||||
import { TestsThreadBridge } from './threadBridge';
|
||||
import { TestsUiIntegrationBridge } from './uiIntegrationBridge';
|
||||
import { TestUploadBridge } from './uploadBridge';
|
||||
import { TestsUserBridge } from './userBridge';
|
||||
import { TestsVideoConferenceBridge } from './videoConferenceBridge';
|
||||
import { AppBridges } from '../../../src/server/bridges';
|
||||
import type {
|
||||
AppActivationBridge,
|
||||
AppDetailChangesBridge,
|
||||
ContactBridge,
|
||||
EnvironmentalVariableBridge,
|
||||
ExperimentalBridge,
|
||||
HttpBridge,
|
||||
IInternalBridge,
|
||||
IListenerBridge,
|
||||
LivechatBridge,
|
||||
MessageBridge,
|
||||
ModerationBridge,
|
||||
OutboundMessageBridge,
|
||||
PersistenceBridge,
|
||||
RoleBridge,
|
||||
RoomBridge,
|
||||
SchedulerBridge,
|
||||
ServerSettingBridge,
|
||||
UiInteractionBridge,
|
||||
UploadBridge,
|
||||
UserBridge,
|
||||
VideoConferenceBridge,
|
||||
} from '../../../src/server/bridges';
|
||||
import type { CloudWorkspaceBridge } from '../../../src/server/bridges/CloudWorkspaceBridge';
|
||||
import type { EmailBridge } from '../../../src/server/bridges/EmailBridge';
|
||||
import type { IInternalFederationBridge } from '../../../src/server/bridges/IInternalFederationBridge';
|
||||
import type { OAuthAppsBridge } from '../../../src/server/bridges/OAuthAppsBridge';
|
||||
import type { ThreadBridge } from '../../../src/server/bridges/ThreadBridge';
|
||||
|
||||
export class TestsAppBridges extends AppBridges {
|
||||
private readonly appDetails: TestsAppDetailChangesBridge;
|
||||
|
||||
private readonly cmdBridge: TestsCommandBridge;
|
||||
|
||||
private readonly apiBridge: TestsApiBridge;
|
||||
|
||||
private readonly setsBridge: TestsServerSettingBridge;
|
||||
|
||||
private readonly envBridge: TestsEnvironmentalVariableBridge;
|
||||
|
||||
private readonly rlActBridge: TestsActivationBridge;
|
||||
|
||||
private readonly msgBridge: TestsMessageBridge;
|
||||
|
||||
private readonly moderationBridge: TestsModerationBridge;
|
||||
|
||||
private readonly persisBridge: TestsPersisBridge;
|
||||
|
||||
private readonly roleBridge: TestsRoleBridge;
|
||||
|
||||
private readonly roomBridge: TestsRoomBridge;
|
||||
|
||||
private readonly internalBridge: TestsInternalBridge;
|
||||
|
||||
private readonly userBridge: TestsUserBridge;
|
||||
|
||||
private readonly httpBridge: TestsHttpBridge;
|
||||
|
||||
private readonly livechatBridge: TestLivechatBridge;
|
||||
|
||||
private readonly uploadBridge: TestUploadBridge;
|
||||
|
||||
private readonly emailBridge: EmailBridge;
|
||||
|
||||
private readonly contactBridge: ContactBridge;
|
||||
|
||||
private readonly uiIntegrationBridge: TestsUiIntegrationBridge;
|
||||
|
||||
private readonly schedulerBridge: TestSchedulerBridge;
|
||||
|
||||
private readonly cloudWorkspaceBridge: TestAppCloudWorkspaceBridge;
|
||||
|
||||
private readonly videoConfBridge: TestsVideoConferenceBridge;
|
||||
|
||||
private readonly oauthBridge: OAuthAppsBridge;
|
||||
|
||||
private readonly internalFederationBridge: IInternalFederationBridge;
|
||||
|
||||
private readonly threadBridge: ThreadBridge;
|
||||
|
||||
private readonly outboundCommsBridge: TestOutboundCommunicationBridge;
|
||||
|
||||
private readonly experimentalBridge: TestExperimentalBridge;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.appDetails = new TestsAppDetailChangesBridge();
|
||||
this.cmdBridge = new TestsCommandBridge();
|
||||
this.apiBridge = new TestsApiBridge();
|
||||
this.setsBridge = new TestsServerSettingBridge();
|
||||
this.envBridge = new TestsEnvironmentalVariableBridge();
|
||||
this.rlActBridge = new TestsActivationBridge();
|
||||
this.msgBridge = new TestsMessageBridge();
|
||||
this.moderationBridge = new TestsModerationBridge();
|
||||
this.persisBridge = new TestsPersisBridge();
|
||||
this.roleBridge = new TestsRoleBridge();
|
||||
this.roomBridge = new TestsRoomBridge();
|
||||
this.internalBridge = new TestsInternalBridge();
|
||||
this.userBridge = new TestsUserBridge();
|
||||
this.httpBridge = new TestsHttpBridge();
|
||||
this.livechatBridge = new TestLivechatBridge();
|
||||
this.uploadBridge = new TestUploadBridge();
|
||||
this.uiIntegrationBridge = new TestsUiIntegrationBridge();
|
||||
this.schedulerBridge = new TestSchedulerBridge();
|
||||
this.cloudWorkspaceBridge = new TestAppCloudWorkspaceBridge();
|
||||
this.videoConfBridge = new TestsVideoConferenceBridge();
|
||||
this.oauthBridge = new TestOAuthAppsBridge();
|
||||
this.internalFederationBridge = new TestsInternalFederationBridge();
|
||||
this.threadBridge = new TestsThreadBridge();
|
||||
this.emailBridge = new TestsEmailBridge();
|
||||
this.contactBridge = new TestContactBridge();
|
||||
this.outboundCommsBridge = new TestOutboundCommunicationBridge();
|
||||
this.experimentalBridge = new TestExperimentalBridge();
|
||||
}
|
||||
|
||||
public getCommandBridge(): TestsCommandBridge {
|
||||
return this.cmdBridge;
|
||||
}
|
||||
|
||||
public getApiBridge(): TestsApiBridge {
|
||||
return this.apiBridge;
|
||||
}
|
||||
|
||||
public getServerSettingBridge(): ServerSettingBridge {
|
||||
return this.setsBridge;
|
||||
}
|
||||
|
||||
public getEnvironmentalVariableBridge(): EnvironmentalVariableBridge {
|
||||
return this.envBridge;
|
||||
}
|
||||
|
||||
public getAppDetailChangesBridge(): AppDetailChangesBridge {
|
||||
return this.appDetails;
|
||||
}
|
||||
|
||||
public getHttpBridge(): HttpBridge {
|
||||
return this.httpBridge;
|
||||
}
|
||||
|
||||
public getListenerBridge(): IListenerBridge {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getMessageBridge(): MessageBridge {
|
||||
return this.msgBridge;
|
||||
}
|
||||
|
||||
public getModerationBridge(): ModerationBridge {
|
||||
return this.moderationBridge;
|
||||
}
|
||||
|
||||
public getPersistenceBridge(): PersistenceBridge {
|
||||
return this.persisBridge;
|
||||
}
|
||||
|
||||
public getAppActivationBridge(): AppActivationBridge {
|
||||
return this.rlActBridge;
|
||||
}
|
||||
|
||||
public getThreadBridge(): ThreadBridge {
|
||||
return this.threadBridge;
|
||||
}
|
||||
|
||||
public getRoleBridge(): RoleBridge {
|
||||
return this.roleBridge;
|
||||
}
|
||||
|
||||
public getRoomBridge(): RoomBridge {
|
||||
return this.roomBridge;
|
||||
}
|
||||
|
||||
public getInternalBridge(): IInternalBridge {
|
||||
return this.internalBridge;
|
||||
}
|
||||
|
||||
public getUserBridge(): UserBridge {
|
||||
return this.userBridge;
|
||||
}
|
||||
|
||||
public getLivechatBridge(): LivechatBridge {
|
||||
return this.livechatBridge;
|
||||
}
|
||||
|
||||
public getEmailBridge(): EmailBridge {
|
||||
return this.emailBridge;
|
||||
}
|
||||
|
||||
public getUploadBridge(): UploadBridge {
|
||||
return this.uploadBridge;
|
||||
}
|
||||
|
||||
public getUiInteractionBridge(): UiInteractionBridge {
|
||||
return this.uiIntegrationBridge;
|
||||
}
|
||||
|
||||
public getSchedulerBridge(): SchedulerBridge {
|
||||
return this.schedulerBridge;
|
||||
}
|
||||
|
||||
public getCloudWorkspaceBridge(): CloudWorkspaceBridge {
|
||||
return this.cloudWorkspaceBridge;
|
||||
}
|
||||
|
||||
public getVideoConferenceBridge(): VideoConferenceBridge {
|
||||
return this.videoConfBridge;
|
||||
}
|
||||
|
||||
public getOAuthAppsBridge(): OAuthAppsBridge {
|
||||
return this.oauthBridge;
|
||||
}
|
||||
|
||||
public getInternalFederationBridge(): IInternalFederationBridge {
|
||||
return this.internalFederationBridge;
|
||||
}
|
||||
|
||||
public getContactBridge(): ContactBridge {
|
||||
return this.contactBridge;
|
||||
}
|
||||
|
||||
public getOutboundMessageBridge(): OutboundMessageBridge {
|
||||
return this.outboundCommsBridge;
|
||||
}
|
||||
|
||||
public getExperimentalBridge(): ExperimentalBridge {
|
||||
return this.experimentalBridge;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ISetting } from '@rocket.chat/apps-engine/definition/settings';
|
||||
|
||||
import { AppDetailChangesBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestsAppDetailChangesBridge extends AppDetailChangesBridge {
|
||||
public onAppSettingsChange(appId: string, setting: ISetting): void {}
|
||||
}
|
||||
16
packages/apps/tests/test-data/bridges/cloudBridge.ts
Normal file
16
packages/apps/tests/test-data/bridges/cloudBridge.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { IWorkspaceToken } from '@rocket.chat/apps-engine/definition/cloud/IWorkspaceToken';
|
||||
|
||||
import { CloudWorkspaceBridge } from '../../../src/server/bridges/CloudWorkspaceBridge';
|
||||
|
||||
export class TestAppCloudWorkspaceBridge extends CloudWorkspaceBridge {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public async getWorkspaceToken(scope: string, appId: string): Promise<IWorkspaceToken> {
|
||||
return {
|
||||
token: 'mock-workspace-token',
|
||||
expiresAt: new Date(Date.now() + 10000),
|
||||
};
|
||||
}
|
||||
}
|
||||
42
packages/apps/tests/test-data/bridges/commandBridge.ts
Normal file
42
packages/apps/tests/test-data/bridges/commandBridge.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors';
|
||||
import type { ISlashCommand, SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands';
|
||||
|
||||
import { CommandBridge } from '../../../src/server/bridges';
|
||||
import { TestData } from '../utilities';
|
||||
|
||||
export class TestsCommandBridge extends CommandBridge {
|
||||
public commands: Map<string, (context: SlashCommandContext, read: IRead, modify: IModify, http: IHttp, persis: IPersistence) => void>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.commands = new Map<
|
||||
string,
|
||||
(context: SlashCommandContext, read: IRead, modify: IModify, http: IHttp, persis: IPersistence) => void
|
||||
>();
|
||||
this.commands.set('it-exists', TestData.getSlashCommand('it-exists').executor);
|
||||
}
|
||||
|
||||
public async doesCommandExist(command: string, appId: string): Promise<boolean> {
|
||||
return this.commands.has(command);
|
||||
}
|
||||
|
||||
public async enableCommand(command: string, appId: string): Promise<void> {}
|
||||
|
||||
public async disableCommand(command: string, appId: string): Promise<void> {}
|
||||
|
||||
public async modifyCommand(command: ISlashCommand, appId: string): Promise<void> {}
|
||||
|
||||
public restoreCommand(comand: string, appId: string): void {}
|
||||
|
||||
public async registerCommand(command: ISlashCommand, appId: string): Promise<void> {
|
||||
if (this.commands.has(command.command)) {
|
||||
throw new Error(`Command "${command.command}" has already been registered.`);
|
||||
}
|
||||
|
||||
this.commands.set(command.command, command.executor);
|
||||
}
|
||||
|
||||
public async unregisterCommand(command: string, appId: string): Promise<void> {
|
||||
this.commands.delete(command);
|
||||
}
|
||||
}
|
||||
23
packages/apps/tests/test-data/bridges/contactBridge.ts
Normal file
23
packages/apps/tests/test-data/bridges/contactBridge.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat';
|
||||
|
||||
import { ContactBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestContactBridge extends ContactBridge {
|
||||
protected addContactEmail(contactId: ILivechatContact['_id'], email: string, appId: string): Promise<ILivechatContact> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected getById(id: ILivechatContact['_id']): Promise<ILivechatContact> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected verifyContact(verifyContactChannelParams: {
|
||||
contactId: string;
|
||||
field: string;
|
||||
value: string;
|
||||
visitorId: string;
|
||||
roomId: string;
|
||||
}): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
9
packages/apps/tests/test-data/bridges/emailBridge.ts
Normal file
9
packages/apps/tests/test-data/bridges/emailBridge.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { IEmail } from '@rocket.chat/apps-engine/definition/email';
|
||||
|
||||
import { EmailBridge } from '../../../src/server/bridges/EmailBridge';
|
||||
|
||||
export class TestsEmailBridge extends EmailBridge {
|
||||
protected sendEmail(email: IEmail, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { EnvironmentalVariableBridge } from '../../../src/server/bridges/EnvironmentalVariableBridge';
|
||||
|
||||
export class TestsEnvironmentalVariableBridge extends EnvironmentalVariableBridge {
|
||||
public getValueByName(envVarName: string, appId: string): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public isReadable(envVarName: string, appId: string): Promise<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public isSet(envVarName: string, appId: string): Promise<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ExperimentalBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestExperimentalBridge extends ExperimentalBridge {}
|
||||
16
packages/apps/tests/test-data/bridges/httpBridge.ts
Normal file
16
packages/apps/tests/test-data/bridges/httpBridge.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { IHttpResponse } from '@rocket.chat/apps-engine/definition/accessors';
|
||||
|
||||
import type { IHttpBridgeRequestInfo } from '../../../src/server/bridges';
|
||||
import { HttpBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestsHttpBridge extends HttpBridge {
|
||||
public call(info: IHttpBridgeRequestInfo): Promise<IHttpResponse> {
|
||||
return Promise.resolve({
|
||||
url: info.url,
|
||||
method: info.method,
|
||||
statusCode: 200,
|
||||
headers: info.request.headers,
|
||||
content: info.request.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
17
packages/apps/tests/test-data/bridges/internalBridge.ts
Normal file
17
packages/apps/tests/test-data/bridges/internalBridge.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { ISetting } from '@rocket.chat/apps-engine/definition/settings';
|
||||
|
||||
import type { IInternalBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestsInternalBridge implements IInternalBridge {
|
||||
public doGetUsernamesOfRoomByIdSync(roomId: string): Array<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public doGetUsernamesOfRoomById(roomId: string): Promise<Array<string>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public doGetWorkspacePublicKey(): Promise<ISetting> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { IInternalFederationBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestsInternalFederationBridge implements IInternalFederationBridge {
|
||||
public async getPrivateKey(): Promise<string> {
|
||||
return 'fake_private-key';
|
||||
}
|
||||
|
||||
public async getPublicKey(): Promise<string> {
|
||||
return 'fake_public-key';
|
||||
}
|
||||
}
|
||||
124
packages/apps/tests/test-data/bridges/livechatBridge.ts
Normal file
124
packages/apps/tests/test-data/bridges/livechatBridge.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { IExtraRoomParams } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator';
|
||||
import type {
|
||||
IDepartment,
|
||||
IVisitorExternalIdentifier,
|
||||
ILivechatMessage,
|
||||
ILivechatRoom,
|
||||
ILivechatTransferData,
|
||||
IVisitor,
|
||||
ResolveVisitorContactData,
|
||||
} from '@rocket.chat/apps-engine/definition/livechat';
|
||||
import type { IMessage } from '@rocket.chat/apps-engine/definition/messages';
|
||||
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
|
||||
|
||||
import { LivechatBridge } from '../../../src/server/bridges/LivechatBridge';
|
||||
|
||||
export class TestLivechatBridge extends LivechatBridge {
|
||||
public findDepartmentsEnabledWithAgents(appId: string): Promise<Array<IDepartment>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public isOnline(departmentId?: string): boolean {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public isOnlineAsync(departmentId?: string): Promise<boolean> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public createMessage(message: ILivechatMessage, appId: string): Promise<string> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public getMessageById(messageId: string, appId: string): Promise<ILivechatMessage> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public updateMessage(message: ILivechatMessage, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public createVisitor(visitor: IVisitor, appId: string): Promise<string> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public createAndReturnVisitor(visitor: IVisitor, appId: string): Promise<IVisitor | undefined> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public transferVisitor(visitor: IVisitor, transferData: ILivechatTransferData, appId: string): Promise<boolean> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public findVisitors(query: object, appId: string): Promise<Array<IVisitor>> {
|
||||
console.warn('The method AppLivechatBridge.findVisitors is deprecated. Please consider using its alternatives');
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public findVisitorById(id: string, appId: string): Promise<IVisitor | undefined> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public findVisitorByEmail(email: string, appId: string): Promise<IVisitor | undefined> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public findVisitorByToken(token: string, appId: string): Promise<IVisitor | undefined> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public findVisitorByPhoneNumber(phoneNumber: string, appId: string): Promise<IVisitor | undefined> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public resolveVisitor(
|
||||
externalId: IVisitorExternalIdentifier,
|
||||
contactData: ResolveVisitorContactData | undefined,
|
||||
appId: string,
|
||||
): Promise<IVisitor | undefined> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public updateVisitorExternalId(
|
||||
visitorId: string,
|
||||
externalId: Omit<IVisitorExternalIdentifier, 'appId'>,
|
||||
appId: string,
|
||||
): Promise<IVisitor | undefined> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public createRoom(visitor: IVisitor, agent: IUser, appId: string, extraParams?: IExtraRoomParams): Promise<ILivechatRoom> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public closeRoom(room: ILivechatRoom, comment: string, closer: IUser | undefined, appId: string): Promise<boolean> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public findRooms(visitor: IVisitor, departmentId: string | null, appId: string): Promise<Array<ILivechatRoom>> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public findOpenRoomsByAgentId(agentId: string, appId: string): Promise<ILivechatRoom[]> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public countOpenRoomsByAgentId(agentId: string, appId: string): Promise<number> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public findDepartmentByIdOrName(value: string, appId: string): Promise<IDepartment | undefined> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public _fetchLivechatRoomMessages(appId: string, roomId: string): Promise<Array<IMessage>> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public setCustomFields(
|
||||
data: { token: IVisitor['token']; key: string; value: string; overwrite: boolean },
|
||||
appId: string,
|
||||
): Promise<number> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
}
|
||||
44
packages/apps/tests/test-data/bridges/messageBridge.ts
Normal file
44
packages/apps/tests/test-data/bridges/messageBridge.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { IMessage, Reaction } from '@rocket.chat/apps-engine/definition/messages';
|
||||
import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms';
|
||||
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
|
||||
|
||||
import { MessageBridge } from '../../../src/server/bridges';
|
||||
import type { ITypingDescriptor } from '../../../src/server/bridges/MessageBridge';
|
||||
|
||||
export class TestsMessageBridge extends MessageBridge {
|
||||
public create(message: IMessage, appId: string): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getById(messageId: string, appId: string): Promise<IMessage> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public update(message: IMessage, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public notifyUser(user: IUser, message: IMessage, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public notifyRoom(room: IRoom, message: IMessage, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public delete(message: IMessage, user: IUser, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public typing(options: ITypingDescriptor): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public addReaction(_messageId: string, _userId: string, _reaction: Reaction): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public removeReaction(_messageId: string, _userId: string, _reaction: Reaction): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
18
packages/apps/tests/test-data/bridges/moderationBridge.ts
Normal file
18
packages/apps/tests/test-data/bridges/moderationBridge.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { IMessage } from '@rocket.chat/apps-engine/definition/messages';
|
||||
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
|
||||
|
||||
import { ModerationBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestsModerationBridge extends ModerationBridge {
|
||||
public report(messageId: string, description: string, userId: string, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public dismissReportsByMessageId(messageId: IMessage['id'], reason: string, action: string, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public dismissReportsByUserId(userId: IUser['id'], reason: string, action: string, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
21
packages/apps/tests/test-data/bridges/outboundComms.ts
Normal file
21
packages/apps/tests/test-data/bridges/outboundComms.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type {
|
||||
IOutboundEmailMessageProvider,
|
||||
IOutboundMessageProviders,
|
||||
IOutboundPhoneMessageProvider,
|
||||
} from '@rocket.chat/apps-engine/definition/outboundCommunication';
|
||||
|
||||
import { OutboundMessageBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestOutboundCommunicationBridge extends OutboundMessageBridge {
|
||||
protected async registerPhoneProvider(provider: IOutboundPhoneMessageProvider, appId: string): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
protected async registerEmailProvider(provider: IOutboundEmailMessageProvider, appId: string): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
protected async unRegisterProvider(provider: IOutboundMessageProviders, appId: string): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
46
packages/apps/tests/test-data/bridges/persisBridge.ts
Normal file
46
packages/apps/tests/test-data/bridges/persisBridge.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata';
|
||||
|
||||
import { PersistenceBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestsPersisBridge extends PersistenceBridge {
|
||||
public purge(appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public create(data: any, appId: string): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public createWithAssociations(data: object, associations: Array<RocketChatAssociationRecord>, appId: string): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public readById(id: string, appId: string): Promise<object> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public readByAssociations(associations: Array<RocketChatAssociationRecord>, appId: string): Promise<Array<object>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public remove(id: string, appId: string): Promise<object> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public removeByAssociations(associations: Array<RocketChatAssociationRecord>, appId: string): Promise<Array<object>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public update(id: string, data: object, upsert: boolean, appId: string): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public updateByAssociations(
|
||||
associations: Array<RocketChatAssociationRecord>,
|
||||
data: object,
|
||||
upsert: boolean,
|
||||
appId: string,
|
||||
): Promise<string> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
}
|
||||
13
packages/apps/tests/test-data/bridges/roleBridge.ts
Normal file
13
packages/apps/tests/test-data/bridges/roleBridge.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { IRole } from '@rocket.chat/apps-engine/definition/roles';
|
||||
|
||||
import { RoleBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestsRoleBridge extends RoleBridge {
|
||||
public getOneByIdOrName(idOrName: IRole['id'] | IRole['name'], appId: string): Promise<IRole | null> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getCustomRoles(appId: string): Promise<Array<IRole>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
81
packages/apps/tests/test-data/bridges/roomBridge.ts
Normal file
81
packages/apps/tests/test-data/bridges/roomBridge.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { IMessage, IMessageRaw } from '@rocket.chat/apps-engine/definition/messages';
|
||||
import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms';
|
||||
import type { IRoomRaw } from '@rocket.chat/apps-engine/definition/rooms/IRoomRaw';
|
||||
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
|
||||
|
||||
import { RoomBridge } from '../../../src/server/bridges';
|
||||
import type { GetMessagesOptions, GetRoomsOptions, GetRoomsFilters } from '../../../src/server/bridges/RoomBridge';
|
||||
|
||||
export class TestsRoomBridge extends RoomBridge {
|
||||
public create(room: IRoom, members: Array<string>, appId: string): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getById(roomId: string, appId: string): Promise<IRoom> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getCreatorById(roomId: string, appId: string): Promise<IUser> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getByName(roomName: string, appId: string): Promise<IRoom> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getCreatorByName(roomName: string, appId: string): Promise<IUser> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getDirectByUsernames(username: Array<string>, appId: string): Promise<IRoom> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public getMembers(roomName: string, appId: string): Promise<Array<IUser>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getAllRooms(filter: GetRoomsFilters, options: GetRoomsOptions, appId: string): Promise<IRoomRaw[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getMessages(roomId: string, options: GetMessagesOptions, appId: string): Promise<IMessageRaw[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public update(room: IRoom, members: Array<string>, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public createDiscussion(room: IRoom, parentMessage: IMessage, reply: string, members: Array<string>, appId: string): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public delete(roomId: string, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getLeaders(roomId: string, appId: string): Promise<Array<IUser>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getModerators(roomId: string, appId: string): Promise<Array<IUser>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getOwners(roomId: string, appId: string): Promise<Array<IUser>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public removeUsers(roomId: string, usernames: string[], appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public getUnreadByUser(roomId: string, uid: string, options: GetMessagesOptions, appId: string): Promise<IMessageRaw[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected getUserUnreadMessageCount(roomId: string, uid: string, appId: string): Promise<number> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
25
packages/apps/tests/test-data/bridges/schedulerBridge.ts
Normal file
25
packages/apps/tests/test-data/bridges/schedulerBridge.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { IOnetimeSchedule, IProcessor, IRecurringSchedule } from '@rocket.chat/apps-engine/definition/scheduler';
|
||||
|
||||
import { SchedulerBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestSchedulerBridge extends SchedulerBridge {
|
||||
public async registerProcessors(processors: Array<IProcessor>, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public async scheduleOnce(job: IOnetimeSchedule, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public async scheduleRecurring(job: IRecurringSchedule, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public async cancelJob(jobId: string, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public async cancelAllJobs(appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
42
packages/apps/tests/test-data/bridges/serverSettingBridge.ts
Normal file
42
packages/apps/tests/test-data/bridges/serverSettingBridge.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { SettingType, type ISetting } from '@rocket.chat/apps-engine/definition/settings';
|
||||
|
||||
import { ServerSettingBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestsServerSettingBridge extends ServerSettingBridge {
|
||||
public getAll(appId: string): Promise<Array<ISetting>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getOneById(id: string, appId: string): Promise<ISetting> {
|
||||
return Promise.resolve({
|
||||
id,
|
||||
packageValue: 'packageValue',
|
||||
value: 'value',
|
||||
i18nLabel: 'i18nLabel',
|
||||
i18nDescription: 'i18nDescription',
|
||||
required: true,
|
||||
public: true,
|
||||
type: SettingType.STRING,
|
||||
});
|
||||
}
|
||||
|
||||
public hideGroup(name: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public hideSetting(id: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public isReadableById(id: string, appId: string): Promise<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public updateOne(setting: ISetting, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public incrementValue(id: ISetting['id'], value: number, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
9
packages/apps/tests/test-data/bridges/threadBridge.ts
Normal file
9
packages/apps/tests/test-data/bridges/threadBridge.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { IMessage } from '@rocket.chat/apps-engine/definition/messages';
|
||||
|
||||
import { ThreadBridge } from '../../../src/server/bridges/ThreadBridge';
|
||||
|
||||
export class TestsThreadBridge extends ThreadBridge {
|
||||
public getById(messageId: string, appId: string): Promise<Array<IMessage>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
10
packages/apps/tests/test-data/bridges/uiIntegrationBridge.ts
Normal file
10
packages/apps/tests/test-data/bridges/uiIntegrationBridge.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { IUIKitInteractionParam } from '@rocket.chat/apps-engine/definition/accessors/IUIController';
|
||||
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
|
||||
|
||||
import { UiInteractionBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestsUiIntegrationBridge extends UiInteractionBridge {
|
||||
public async notifyUser(user: IUser, interaction: IUIKitInteractionParam, appId: string) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
18
packages/apps/tests/test-data/bridges/uploadBridge.ts
Normal file
18
packages/apps/tests/test-data/bridges/uploadBridge.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { IUpload } from '@rocket.chat/apps-engine/definition/uploads';
|
||||
import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails';
|
||||
|
||||
import { UploadBridge } from '../../../src/server/bridges/UploadBridge';
|
||||
|
||||
export class TestUploadBridge extends UploadBridge {
|
||||
public getById(id: string, appId: string): Promise<IUpload> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public getBuffer(upload: IUpload, appId: string): Promise<Buffer> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public createUpload(details: IUploadDetails, buffer: Buffer, appId: string): Promise<IUpload> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
}
|
||||
61
packages/apps/tests/test-data/bridges/userBridge.ts
Normal file
61
packages/apps/tests/test-data/bridges/userBridge.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { IUser, UserType } from '@rocket.chat/apps-engine/definition/users';
|
||||
|
||||
import { UserBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestsUserBridge extends UserBridge {
|
||||
public getById(id: string, appId: string): Promise<IUser> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getByUsername(username: string, appId: string): Promise<IUser> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public create(user: Partial<IUser>): Promise<string> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public getActiveUserCount(): Promise<number> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public remove(user: IUser, appId: string): Promise<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getAppUser(appId?: string): Promise<IUser> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public async update(user: IUser, updates: Partial<IUser>, appId: string): Promise<boolean> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public async deleteUsersCreatedByApp(appId: string, type: UserType.BOT): Promise<boolean> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
protected getUserUnreadMessageCount(uid: string, appId: string): Promise<number> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected getUserRoomIds(userId: string, appId: string): Promise<string[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected deactivate(userId: IUser['id'], confirmRelinquish: boolean, appId: string): Promise<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected setActiveState(
|
||||
userId: IUser['id'],
|
||||
state: Pick<IUser, 'statusDefault' | 'statusSource' | 'statusText' | 'statusExpiresAt' | 'statusId'>,
|
||||
appId: string,
|
||||
): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected endActiveState(userId: IUser['id'], appId: string, statusId?: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders';
|
||||
import type { VideoConference, AppVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences';
|
||||
|
||||
import { VideoConferenceBridge } from '../../../src/server/bridges';
|
||||
|
||||
export class TestsVideoConferenceBridge extends VideoConferenceBridge {
|
||||
public getById(callId: string, appId: string): Promise<VideoConference> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public create(call: AppVideoConference, appId: string): Promise<string> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
public update(call: VideoConference, appId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected registerProvider(info: IVideoConfProvider, appId: string): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
protected unRegisterProvider(info: IVideoConfProvider, appId: string): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
5
packages/apps/tests/test-data/misc/fake-library-file.d.ts
vendored
Normal file
5
packages/apps/tests/test-data/misc/fake-library-file.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export declare class FakeLibraryClass {
|
||||
startWork(): boolean;
|
||||
|
||||
stopWorking(): boolean;
|
||||
}
|
||||
20
packages/apps/tests/test-data/storage/TestSourceStorage.ts
Normal file
20
packages/apps/tests/test-data/storage/TestSourceStorage.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { IAppStorageItem } from '../../../src/server/storage';
|
||||
import { AppSourceStorage } from '../../../src/server/storage';
|
||||
|
||||
export class TestSourceStorage extends AppSourceStorage {
|
||||
public async store(item: IAppStorageItem, zip: Buffer): Promise<string> {
|
||||
return 'app_package_path';
|
||||
}
|
||||
|
||||
public async fetch(item: IAppStorageItem): Promise<Buffer> {
|
||||
return Buffer.from('buffer');
|
||||
}
|
||||
|
||||
public async update(item: IAppStorageItem, zip: Buffer): Promise<string> {
|
||||
return 'updated_path';
|
||||
}
|
||||
|
||||
public async remove(item: IAppStorageItem): Promise<void> {
|
||||
// yup
|
||||
}
|
||||
}
|
||||
28
packages/apps/tests/test-data/storage/logStorage.ts
Normal file
28
packages/apps/tests/test-data/storage/logStorage.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { ILoggerStorageEntry } from '../../../src/server/logging';
|
||||
import type { IAppLogStorageFindOptions } from '../../../src/server/storage';
|
||||
import { AppLogStorage } from '../../../src/server/storage';
|
||||
|
||||
export class TestsAppLogStorage extends AppLogStorage {
|
||||
constructor() {
|
||||
super('nothing');
|
||||
}
|
||||
|
||||
public findPaginated(
|
||||
query: { [field: string]: any },
|
||||
options?: IAppLogStorageFindOptions,
|
||||
): Promise<{ logs: ILoggerStorageEntry[]; total: number }> {
|
||||
return Promise.resolve({ logs: [], total: 0 });
|
||||
}
|
||||
|
||||
public storeEntries(logEntry: ILoggerStorageEntry): Promise<ILoggerStorageEntry> {
|
||||
return Promise.resolve({} as ILoggerStorageEntry);
|
||||
}
|
||||
|
||||
public getEntriesFor(appId: string): Promise<Array<ILoggerStorageEntry>> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
public removeEntriesFor(appId: string): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
110
packages/apps/tests/test-data/storage/storage.ts
Normal file
110
packages/apps/tests/test-data/storage/storage.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
|
||||
import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
|
||||
import type { ISetting } from '@rocket.chat/apps-engine/definition/settings';
|
||||
|
||||
import type { IMarketplaceInfo } from '../../../src/server/marketplace';
|
||||
import type { IAppStorageItem } from '../../../src/server/storage';
|
||||
import { AppMetadataStorage } from '../../../src/server/storage';
|
||||
import { AppInstallationSource } from '../../../src/server/storage/IAppStorageItem';
|
||||
|
||||
export class TestsAppStorage extends AppMetadataStorage {
|
||||
private db = new Map<string, IAppStorageItem>();
|
||||
|
||||
constructor() {
|
||||
super('in-memory');
|
||||
}
|
||||
|
||||
public create(item: IAppStorageItem): Promise<IAppStorageItem> {
|
||||
for (const [id, value] of this.db) {
|
||||
if (id === item.id || item.info.nameSlug === value.info.nameSlug) {
|
||||
return Promise.reject(new Error('App already exists.'));
|
||||
}
|
||||
}
|
||||
|
||||
const stored = { ...item, _id: item._id ?? item.id, createdAt: new Date(), updatedAt: new Date() };
|
||||
this.db.set(stored.id, stored);
|
||||
return Promise.resolve(stored);
|
||||
}
|
||||
|
||||
public retrieveOne(id: string): Promise<IAppStorageItem | null> {
|
||||
return Promise.resolve(this.db.get(id) ?? null);
|
||||
}
|
||||
|
||||
public retrieveAll(): Promise<Map<string, IAppStorageItem>> {
|
||||
return Promise.resolve(new Map(this.db));
|
||||
}
|
||||
|
||||
public retrieveAllPrivate(): Promise<Map<string, IAppStorageItem>> {
|
||||
const items = new Map<string, IAppStorageItem>();
|
||||
for (const [id, item] of this.db) {
|
||||
if (item.installationSource === AppInstallationSource.PRIVATE) {
|
||||
items.set(id, item);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(items);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.db.clear();
|
||||
}
|
||||
|
||||
public remove(id: string): Promise<{ success: boolean }> {
|
||||
this.db.delete(id);
|
||||
return Promise.resolve({ success: true });
|
||||
}
|
||||
|
||||
public updatePartialAndReturnDocument(
|
||||
item: Partial<IAppStorageItem>,
|
||||
_options?: { unsetPermissionsGranted?: boolean },
|
||||
): Promise<IAppStorageItem> {
|
||||
const lookupId = item.id ?? item._id;
|
||||
if (!lookupId) {
|
||||
return Promise.reject(new Error('Cannot update: item has no id.'));
|
||||
}
|
||||
|
||||
const existing = this.db.get(lookupId);
|
||||
if (!existing) {
|
||||
return Promise.reject(new Error(`App not found: ${lookupId}`));
|
||||
}
|
||||
|
||||
const updated = { ...existing, ...item, updatedAt: new Date() };
|
||||
this.db.set(updated.id, updated);
|
||||
return Promise.resolve(updated);
|
||||
}
|
||||
|
||||
public updateStatus(id: string, status: AppStatus): Promise<boolean> {
|
||||
const existing = this.db.get(id);
|
||||
if (!existing) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
this.db.set(id, { ...existing, status, updatedAt: new Date() });
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
public updateSetting(id: string, setting: ISetting): Promise<boolean> {
|
||||
const existing = this.db.get(id);
|
||||
if (!existing) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
this.db.set(id, { ...existing, settings: { ...existing.settings, [setting.id]: setting }, updatedAt: new Date() });
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
public updateAppInfo(id: string, info: IAppInfo): Promise<boolean> {
|
||||
const existing = this.db.get(id);
|
||||
if (!existing) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
this.db.set(id, { ...existing, info, updatedAt: new Date() });
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
public updateMarketplaceInfo(id: string, marketplaceInfo: IMarketplaceInfo[]): Promise<boolean> {
|
||||
const existing = this.db.get(id);
|
||||
if (!existing) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
this.db.set(id, { ...existing, marketplaceInfo, updatedAt: new Date() });
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
729
packages/apps/tests/test-data/utilities.ts
Normal file
729
packages/apps/tests/test-data/utilities.ts
Normal file
@@ -0,0 +1,729 @@
|
||||
import * as os from 'node:os';
|
||||
|
||||
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
|
||||
import type { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors';
|
||||
import { HttpStatusCode } from '@rocket.chat/apps-engine/definition/accessors';
|
||||
import type { IApi, IApiRequest, IApiResponse } from '@rocket.chat/apps-engine/definition/api';
|
||||
import { ApiSecurity, ApiVisibility } from '@rocket.chat/apps-engine/definition/api';
|
||||
import type { IApiEndpointInfo } from '@rocket.chat/apps-engine/definition/api/IApiEndpointInfo';
|
||||
import type { IMessage, IMessageAttachment, IMessageRaw } from '@rocket.chat/apps-engine/definition/messages';
|
||||
import type {
|
||||
IOutboundEmailMessageProvider,
|
||||
IOutboundMessage,
|
||||
IOutboundPhoneMessageProvider,
|
||||
ProviderMetadata,
|
||||
} from '@rocket.chat/apps-engine/definition/outboundCommunication';
|
||||
import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms';
|
||||
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms';
|
||||
import type { ISetting } from '@rocket.chat/apps-engine/definition/settings';
|
||||
import { SettingType } from '@rocket.chat/apps-engine/definition/settings';
|
||||
import type {
|
||||
ISlashCommand,
|
||||
ISlashCommandPreview,
|
||||
ISlashCommandPreviewItem,
|
||||
SlashCommandContext,
|
||||
} from '@rocket.chat/apps-engine/definition/slashcommands';
|
||||
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
|
||||
import { UserStatusConnection, UserType } from '@rocket.chat/apps-engine/definition/users';
|
||||
import type {
|
||||
IVideoConferenceOptions,
|
||||
IVideoConfProvider,
|
||||
VideoConfData,
|
||||
VideoConfDataExtended,
|
||||
} from '@rocket.chat/apps-engine/definition/videoConfProviders';
|
||||
import type { AppVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/AppVideoConference';
|
||||
import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference';
|
||||
import { VideoConferenceStatus } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference';
|
||||
import type { IVideoConferenceUser } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConferenceUser';
|
||||
|
||||
import { TestsAppBridges } from './bridges/appBridges';
|
||||
import { TestSourceStorage } from './storage/TestSourceStorage';
|
||||
import { TestsAppLogStorage } from './storage/logStorage';
|
||||
import { TestsAppStorage } from './storage/storage';
|
||||
import type { AppOutboundCommunicationProviderManager } from '../../server/managers/AppOutboundCommunicationProviderManager';
|
||||
import type { AppManager } from '../../src/server/AppManager';
|
||||
import { ProxiedApp } from '../../src/server/ProxiedApp';
|
||||
import type { AppBridges } from '../../src/server/bridges';
|
||||
import { AppResourceBridge } from '../../src/server/bridges/AppResourceBridge';
|
||||
import { AppPackageParser } from '../../src/server/compiler';
|
||||
import type {
|
||||
AppExternalComponentManager,
|
||||
AppSchedulerManager,
|
||||
AppSettingsManager,
|
||||
AppSlashCommandManager,
|
||||
AppVideoConfProviderManager,
|
||||
} from '../../src/server/managers';
|
||||
import type { AppRuntimeManager } from '../../src/server/managers/AppRuntimeManager';
|
||||
import type { UIActionButtonManager } from '../../src/server/managers/UIActionButtonManager';
|
||||
import type { IMarketplaceInfo, IMarketplaceSubscriptionInfo } from '../../src/server/marketplace';
|
||||
import { MarketplacePurchaseType } from '../../src/server/marketplace/MarketplacePurchaseType';
|
||||
import { MarketplaceSubscriptionStatus } from '../../src/server/marketplace/MarketplaceSubscriptionStatus';
|
||||
import { MarketplaceSubscriptionType } from '../../src/server/marketplace/MarketplaceSubscriptionType';
|
||||
import type { IRuntimeController } from '../../src/server/runtime/IRuntimeController';
|
||||
import type { AppLogStorage, AppMetadataStorage, AppSourceStorage, IAppStorageItem } from '../../src/server/storage';
|
||||
import { AppInstallationSource } from '../../src/server/storage/IAppStorageItem';
|
||||
|
||||
export class TestInfastructureSetup {
|
||||
private appStorage: TestsAppStorage;
|
||||
|
||||
private logStorage: TestsAppLogStorage;
|
||||
|
||||
private bridges: TestsAppBridges;
|
||||
|
||||
private sourceStorage: TestSourceStorage;
|
||||
|
||||
private appManager: AppManager;
|
||||
|
||||
private runtimeManager: AppRuntimeManager;
|
||||
|
||||
constructor() {
|
||||
this.appStorage = new TestsAppStorage();
|
||||
this.logStorage = new TestsAppLogStorage();
|
||||
this.bridges = new TestsAppBridges();
|
||||
this.sourceStorage = new TestSourceStorage();
|
||||
this.runtimeManager = {
|
||||
startRuntimeForApp: async () => {
|
||||
return TestData.getMockRuntimeController('test');
|
||||
},
|
||||
runInSandbox: async () => {
|
||||
return {} as unknown as Promise<unknown>;
|
||||
},
|
||||
stopRuntime: () => {},
|
||||
} as unknown as AppRuntimeManager;
|
||||
|
||||
this.appManager = {
|
||||
getParser() {
|
||||
if (!this.parser) {
|
||||
this.parser = new AppPackageParser();
|
||||
}
|
||||
|
||||
return this.parser;
|
||||
},
|
||||
getBridges: () => {
|
||||
return this.bridges as AppBridges;
|
||||
},
|
||||
getCommandManager() {
|
||||
return {} as AppSlashCommandManager;
|
||||
},
|
||||
getExternalComponentManager() {
|
||||
return {} as AppExternalComponentManager;
|
||||
},
|
||||
getOneById(appId: string): ProxiedApp {
|
||||
return appId === 'failMePlease'
|
||||
? undefined
|
||||
: TestData.getMockApp({ info: { id: appId, name: 'testing' } } as IAppStorageItem, this);
|
||||
},
|
||||
getLogStorage(): AppLogStorage {
|
||||
return new TestsAppLogStorage();
|
||||
},
|
||||
getSchedulerManager() {
|
||||
return {} as AppSchedulerManager;
|
||||
},
|
||||
getUIActionButtonManager() {
|
||||
return {} as UIActionButtonManager;
|
||||
},
|
||||
getVideoConfProviderManager() {
|
||||
return {} as AppVideoConfProviderManager;
|
||||
},
|
||||
getOutboundCommunicationProviderManager() {
|
||||
return {} as AppOutboundCommunicationProviderManager;
|
||||
},
|
||||
getSettingsManager() {
|
||||
return {} as AppSettingsManager;
|
||||
},
|
||||
getAppResourceBridge() {
|
||||
return new AppResourceBridge(this as unknown as AppManager);
|
||||
},
|
||||
getRuntime: () => {
|
||||
return this.runtimeManager;
|
||||
},
|
||||
getTempFilePath: this.getTempFilePath,
|
||||
} as unknown as AppManager;
|
||||
}
|
||||
|
||||
public getTempFilePath(): string {
|
||||
return os.tmpdir();
|
||||
}
|
||||
|
||||
public getAppStorage(): AppMetadataStorage {
|
||||
return this.appStorage;
|
||||
}
|
||||
|
||||
public getLogStorage(): AppLogStorage {
|
||||
return this.logStorage;
|
||||
}
|
||||
|
||||
public getAppBridges(): AppBridges {
|
||||
return this.bridges;
|
||||
}
|
||||
|
||||
public getSourceStorage(): AppSourceStorage {
|
||||
return this.sourceStorage;
|
||||
}
|
||||
|
||||
public getMockManager(): AppManager {
|
||||
return this.appManager;
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date();
|
||||
|
||||
const DEFAULT_ATTACHMENT = {
|
||||
color: '#00b2b2',
|
||||
collapsed: false,
|
||||
text: 'Just an attachment that is used for testing',
|
||||
timestampLink: 'https://google.com/',
|
||||
thumbnailUrl: 'https://avatars0.githubusercontent.com/u/850391?s=88&v=4',
|
||||
author: {
|
||||
name: 'Author Name',
|
||||
link: 'https://github.com/graywolf336',
|
||||
icon: 'https://avatars0.githubusercontent.com/u/850391?s=88&v=4',
|
||||
},
|
||||
title: {
|
||||
value: 'Attachment Title',
|
||||
link: 'https://github.com/RocketChat',
|
||||
displayDownloadLink: false,
|
||||
},
|
||||
imageUrl: 'https://rocket.chat/images/default/logo.svg',
|
||||
audioUrl: 'http://www.w3schools.com/tags/horse.mp3',
|
||||
videoUrl: 'http://www.w3schools.com/tags/movie.mp4',
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: 'Test',
|
||||
value: 'Testing out something or other',
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: 'Another Test',
|
||||
value: '[Link](https://google.com/) something and this and that.',
|
||||
},
|
||||
],
|
||||
};
|
||||
export class TestData {
|
||||
public static getDate(): Date {
|
||||
return date;
|
||||
}
|
||||
|
||||
public static getSetting(id?: string): ISetting {
|
||||
return {
|
||||
id: id || 'testing',
|
||||
type: SettingType.STRING,
|
||||
packageValue: 'The packageValue',
|
||||
required: false,
|
||||
public: false,
|
||||
i18nLabel: 'Testing',
|
||||
};
|
||||
}
|
||||
|
||||
public static getUser(id?: string, username?: string): IUser {
|
||||
return {
|
||||
id: id || 'BBxwgCBzLeMC6esTb',
|
||||
username: username || 'testing-user',
|
||||
name: 'Testing User',
|
||||
emails: [],
|
||||
type: UserType.USER,
|
||||
isEnabled: true,
|
||||
roles: ['admin'],
|
||||
status: 'online',
|
||||
statusConnection: UserStatusConnection.ONLINE,
|
||||
utcOffset: -5,
|
||||
createdAt: date,
|
||||
updatedAt: new Date(),
|
||||
lastLoginAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
public static getRoom(id?: string, slugifiedName?: string): IRoom {
|
||||
return {
|
||||
id: id || 'bTse6CMeLzBCgwxBB',
|
||||
slugifiedName: slugifiedName || 'testing-room',
|
||||
displayName: 'Testing Room',
|
||||
type: RoomType.CHANNEL,
|
||||
creator: TestData.getUser(),
|
||||
usernames: [TestData.getUser().username],
|
||||
isDefault: true,
|
||||
isReadOnly: false,
|
||||
displaySystemMessages: true,
|
||||
messageCount: 145,
|
||||
createdAt: date,
|
||||
updatedAt: new Date(),
|
||||
lastModifiedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
public static getMessage(id?: string, text?: string): IMessage {
|
||||
return {
|
||||
id: id || '4bShvoOXqB',
|
||||
room: TestData.getRoom(),
|
||||
sender: TestData.getUser(),
|
||||
text: 'This is just a test, do not be alarmed',
|
||||
createdAt: date,
|
||||
updatedAt: new Date(),
|
||||
editor: TestData.getUser(),
|
||||
editedAt: new Date(),
|
||||
emoji: ':see_no_evil:',
|
||||
avatarUrl: 'https://avatars0.githubusercontent.com/u/850391?s=88&v=4',
|
||||
alias: 'Testing Bot',
|
||||
attachments: [this.createAttachment()],
|
||||
};
|
||||
}
|
||||
|
||||
public static getMessageRaw(id?: string, text?: string): IMessageRaw {
|
||||
const editorUser = TestData.getUser();
|
||||
const senderUser = TestData.getUser();
|
||||
|
||||
return {
|
||||
id: id || '4bShvoOXqB',
|
||||
roomId: TestData.getRoom().id,
|
||||
sender: {
|
||||
_id: senderUser.id,
|
||||
username: senderUser.username,
|
||||
name: senderUser?.name,
|
||||
},
|
||||
text: text || 'This is just a test, do not be alarmed',
|
||||
createdAt: date,
|
||||
updatedAt: new Date(),
|
||||
editor: {
|
||||
_id: editorUser.id,
|
||||
username: editorUser.username,
|
||||
},
|
||||
editedAt: new Date(),
|
||||
emoji: ':see_no_evil:',
|
||||
avatarUrl: 'https://avatars0.githubusercontent.com/u/850391?s=88&v=4',
|
||||
alias: 'Testing Bot',
|
||||
attachments: [this.createAttachment()],
|
||||
};
|
||||
}
|
||||
|
||||
private static createAttachment(attachment?: IMessageAttachment): IMessageAttachment {
|
||||
attachment = attachment || DEFAULT_ATTACHMENT;
|
||||
return {
|
||||
timestamp: new Date(),
|
||||
...attachment,
|
||||
};
|
||||
}
|
||||
|
||||
public static getSlashCommand(command?: string): ISlashCommand {
|
||||
return {
|
||||
command: command || 'testing-cmd',
|
||||
i18nParamsExample: 'justATest',
|
||||
i18nDescription: 'justATest_Description',
|
||||
permission: 'create-c',
|
||||
providesPreview: true,
|
||||
executor: (context: SlashCommandContext, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise<void> => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
previewer: (
|
||||
context: SlashCommandContext,
|
||||
read: IRead,
|
||||
modify: IModify,
|
||||
http: IHttp,
|
||||
persis: IPersistence,
|
||||
): Promise<ISlashCommandPreview> => {
|
||||
return Promise.resolve({
|
||||
i18nTitle: 'my i18nTitle',
|
||||
items: [],
|
||||
} as ISlashCommandPreview);
|
||||
},
|
||||
executePreviewItem: (
|
||||
item: ISlashCommandPreviewItem,
|
||||
context: SlashCommandContext,
|
||||
read: IRead,
|
||||
modify: IModify,
|
||||
http: IHttp,
|
||||
persis: IPersistence,
|
||||
): Promise<void> => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static getApi(
|
||||
path = 'testing-path',
|
||||
visibility: ApiVisibility = ApiVisibility.PUBLIC,
|
||||
security: ApiSecurity = ApiSecurity.UNSECURE,
|
||||
): IApi {
|
||||
return {
|
||||
visibility,
|
||||
security,
|
||||
endpoints: [
|
||||
{
|
||||
path,
|
||||
// The move to the Deno runtime now requires us to manually set what methods are available
|
||||
_availableMethods: ['get'],
|
||||
get(
|
||||
request: IApiRequest,
|
||||
endpoint: IApiEndpointInfo,
|
||||
read: IRead,
|
||||
modify: IModify,
|
||||
http: IHttp,
|
||||
persis: IPersistence,
|
||||
): Promise<IApiResponse> {
|
||||
return Promise.resolve({
|
||||
status: HttpStatusCode.OK,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
public static getVideoConfProvider(name = 'test'): IVideoConfProvider {
|
||||
return {
|
||||
name,
|
||||
|
||||
async generateUrl(call: VideoConfData): Promise<string> {
|
||||
return `${name}/${call._id}`;
|
||||
},
|
||||
|
||||
async customizeUrl(
|
||||
call: VideoConfDataExtended,
|
||||
user: IVideoConferenceUser | undefined,
|
||||
options: IVideoConferenceOptions,
|
||||
): Promise<string> {
|
||||
return `${name}/${call._id}#${user ? user.username : ''}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static getInvalidConfProvider(name = 'invalid'): IVideoConfProvider {
|
||||
return {
|
||||
name,
|
||||
|
||||
async isFullyConfigured(): Promise<boolean> {
|
||||
return false;
|
||||
},
|
||||
|
||||
async generateUrl(call: VideoConfData): Promise<string> {
|
||||
return ``;
|
||||
},
|
||||
|
||||
async customizeUrl(
|
||||
call: VideoConfDataExtended,
|
||||
user: IVideoConferenceUser | undefined,
|
||||
options: IVideoConferenceOptions,
|
||||
): Promise<string> {
|
||||
return ``;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static getFullVideoConfProvider(name = 'test'): IVideoConfProvider {
|
||||
return {
|
||||
name,
|
||||
|
||||
capabilities: {
|
||||
mic: true,
|
||||
cam: true,
|
||||
title: true,
|
||||
},
|
||||
|
||||
async isFullyConfigured(): Promise<boolean> {
|
||||
return true;
|
||||
},
|
||||
|
||||
async generateUrl(call: VideoConfData): Promise<string> {
|
||||
return `${name}/${call._id}`;
|
||||
},
|
||||
|
||||
async customizeUrl(
|
||||
call: VideoConfDataExtended,
|
||||
user: IVideoConferenceUser | undefined,
|
||||
options: IVideoConferenceOptions,
|
||||
): Promise<string> {
|
||||
return `${name}/${call._id}#${user ? user.username : ''}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static getVideoConferenceUser(): IVideoConferenceUser {
|
||||
return {
|
||||
_id: 'callerId',
|
||||
username: 'caller',
|
||||
name: 'John Caller',
|
||||
};
|
||||
}
|
||||
|
||||
public static getVideoConfData(): VideoConfData {
|
||||
return {
|
||||
_id: 'first-call',
|
||||
type: 'videoconference',
|
||||
rid: 'roomId',
|
||||
createdBy: this.getVideoConferenceUser(),
|
||||
title: 'Test Call',
|
||||
};
|
||||
}
|
||||
|
||||
public static getVideoConfDataExtended(providerName = 'test'): VideoConfDataExtended {
|
||||
return {
|
||||
...this.getVideoConfData(),
|
||||
url: '${providerName}/first-call',
|
||||
};
|
||||
}
|
||||
|
||||
public static getAppVideoConference(): AppVideoConference {
|
||||
return {
|
||||
rid: 'roomId',
|
||||
createdBy: 'userId',
|
||||
title: 'Video Conference',
|
||||
providerName: 'test',
|
||||
};
|
||||
}
|
||||
|
||||
public static getVideoConference(): VideoConference {
|
||||
return {
|
||||
_id: 'first-call',
|
||||
_updatedAt: new Date(),
|
||||
type: 'videoconference',
|
||||
rid: 'roomId',
|
||||
users: [
|
||||
{
|
||||
_id: 'johnId',
|
||||
name: 'John Doe',
|
||||
username: 'mrdoe',
|
||||
ts: new Date(),
|
||||
},
|
||||
{
|
||||
_id: 'janeId',
|
||||
name: 'Jane Doe',
|
||||
username: 'msdoe',
|
||||
ts: new Date(),
|
||||
},
|
||||
],
|
||||
status: VideoConferenceStatus.STARTED,
|
||||
messages: {
|
||||
started: 'messageId',
|
||||
},
|
||||
url: 'video-conf/first-call',
|
||||
createdBy: {
|
||||
_id: 'johnId',
|
||||
name: 'John Doe',
|
||||
username: 'mrdoe',
|
||||
},
|
||||
createdAt: new Date(),
|
||||
title: 'Video Conference',
|
||||
anonymousUsers: 0,
|
||||
providerName: 'test',
|
||||
};
|
||||
}
|
||||
|
||||
public static getOutboundPhoneMessageProvider(name = 'Test Phone Provider'): IOutboundPhoneMessageProvider {
|
||||
return {
|
||||
type: 'phone',
|
||||
appId: `${name}-app-id`,
|
||||
name,
|
||||
supportsTemplates: true,
|
||||
documentationUrl: 'https://rocket.chat',
|
||||
sendOutboundMessage: async (message): Promise<void> => {
|
||||
console.log('Sending message', message);
|
||||
},
|
||||
getProviderMetadata: async (): Promise<ProviderMetadata> => {
|
||||
return {} as ProviderMetadata;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static getOutboundEmailMessageProvider(name = 'Test Email Provider'): IOutboundEmailMessageProvider {
|
||||
return {
|
||||
type: 'email',
|
||||
appId: `${name}-app-id`,
|
||||
name,
|
||||
supportsTemplates: true,
|
||||
documentationUrl: 'https://rocket.chat',
|
||||
sendOutboundMessage: async (message): Promise<void> => {
|
||||
console.log('Sending message', message);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static getOutboundMessage(): IOutboundMessage {
|
||||
return {
|
||||
to: '+123456789',
|
||||
type: 'template',
|
||||
templateProviderPhoneNumber: '+123456789',
|
||||
agentId: 'agent-id',
|
||||
departmentId: 'department-id',
|
||||
template: {
|
||||
name: 'template-name',
|
||||
language: {
|
||||
code: 'en',
|
||||
policy: 'deterministic',
|
||||
},
|
||||
components: [
|
||||
{
|
||||
type: 'body',
|
||||
parameters: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Sample text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
namespace: 'template-namespace',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static getOAuthApp(isToCreate: boolean) {
|
||||
const OAuthApp = {
|
||||
_id: '4526fcab-b068-4dcc-b208-4fff599165b0',
|
||||
name: 'name-test',
|
||||
active: true,
|
||||
clientId: 'clientId-test',
|
||||
clientSecret: 'clientSecret-test',
|
||||
redirectUri: 'redirectUri-test',
|
||||
appId: 'app-123',
|
||||
_createdAt: '2022-07-11T14:30:48.937Z',
|
||||
_createdBy: {
|
||||
_id: 'Em5TQwMD4P7AmTs73',
|
||||
username: 'testa.bot',
|
||||
},
|
||||
_updatedAt: '2022-07-11T14:30:48.937Z',
|
||||
};
|
||||
|
||||
if (isToCreate) {
|
||||
delete OAuthApp._id;
|
||||
delete OAuthApp._createdAt;
|
||||
delete OAuthApp._createdBy;
|
||||
delete OAuthApp._updatedAt;
|
||||
delete OAuthApp.appId;
|
||||
}
|
||||
return OAuthApp;
|
||||
}
|
||||
|
||||
public static getMockRuntimeController(id: string): IRuntimeController {
|
||||
const mock = {
|
||||
getStatus: () => Promise.resolve(AppStatus.AUTO_ENABLED),
|
||||
sendRequest: () => Promise.resolve(undefined),
|
||||
setupApp: () => Promise.resolve(),
|
||||
stopApp: () => Promise.resolve(),
|
||||
getAppId: () => id,
|
||||
on: () => mock,
|
||||
once: () => mock,
|
||||
off: () => mock,
|
||||
emit: () => true,
|
||||
addListener: () => mock,
|
||||
removeListener: () => mock,
|
||||
removeAllListeners: () => mock,
|
||||
setMaxListeners: () => mock,
|
||||
getMaxListeners: () => 10,
|
||||
listeners: () => [],
|
||||
rawListeners: () => [],
|
||||
listenerCount: () => 0,
|
||||
prependListener: () => mock,
|
||||
prependOnceListener: () => mock,
|
||||
eventNames: () => [],
|
||||
} as IRuntimeController;
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
public static getMockApp(storageItem: Partial<IAppStorageItem>, manager: AppManager): ProxiedApp {
|
||||
const { id, name } = storageItem.info || { id: 'test-app', name: 'Test App' };
|
||||
|
||||
return new ProxiedApp(
|
||||
manager,
|
||||
{ id, status: AppStatus.AUTO_ENABLED, info: { id, name }, ...storageItem } as IAppStorageItem,
|
||||
TestData.getMockRuntimeController(id),
|
||||
);
|
||||
}
|
||||
|
||||
public static getMarketplaceSubscriptionInfo(overrides: Partial<IMarketplaceSubscriptionInfo> = {}): IMarketplaceSubscriptionInfo {
|
||||
return {
|
||||
seats: 10,
|
||||
maxSeats: 100,
|
||||
startDate: '2023-01-01',
|
||||
periodEnd: '2023-12-31',
|
||||
isSubscripbedViaBundle: false,
|
||||
typeOf: MarketplaceSubscriptionType.SubscriptionTypeApp,
|
||||
status: MarketplaceSubscriptionStatus.PurchaseSubscriptionStatusActive,
|
||||
license: {
|
||||
license: 'encrypted-license-data',
|
||||
version: 1,
|
||||
expireDate: new Date('2023-01-01'),
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
public static getMarketplaceInfo(overrides: Partial<IMarketplaceInfo> = {}): IMarketplaceInfo {
|
||||
return {
|
||||
id: 'test-app',
|
||||
name: 'Test App',
|
||||
nameSlug: 'test-app',
|
||||
version: '1.0.0',
|
||||
description: 'Test app',
|
||||
author: { name: 'Test Author', support: 'https://test.com', homepage: 'https://test.com' },
|
||||
permissions: [],
|
||||
requiredApiVersion: '1.0.0',
|
||||
classFile: 'main.js',
|
||||
iconFile: 'icon.png',
|
||||
implements: [],
|
||||
categories: [],
|
||||
status: 'active',
|
||||
isVisible: true,
|
||||
isPurchased: false,
|
||||
isSubscribed: false,
|
||||
isBundled: false,
|
||||
createdDate: '2023-01-01',
|
||||
modifiedDate: '2023-01-01',
|
||||
price: 0,
|
||||
purchaseType: MarketplacePurchaseType.PurchaseTypeSubscription,
|
||||
subscriptionInfo: TestData.getMarketplaceSubscriptionInfo(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
public static getAppStorageItem(overrides: Partial<IAppStorageItem> = {}): IAppStorageItem {
|
||||
return {
|
||||
id: 'test-app',
|
||||
status: AppStatus.AUTO_ENABLED,
|
||||
info: {
|
||||
id: 'test-app',
|
||||
name: 'Test App',
|
||||
nameSlug: 'test-app',
|
||||
version: '1.0.0',
|
||||
description: 'Test app',
|
||||
author: { name: 'Test Author', support: 'https://test.com', homepage: 'https://test.com' },
|
||||
permissions: [],
|
||||
requiredApiVersion: '1.0.0',
|
||||
classFile: 'main.js',
|
||||
iconFile: 'icon.png',
|
||||
implements: [],
|
||||
},
|
||||
marketplaceInfo: [TestData.getMarketplaceInfo()],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
installationSource: AppInstallationSource.MARKETPLACE,
|
||||
languageContent: {},
|
||||
settings: {},
|
||||
implemented: {},
|
||||
signature: 'default-signature',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
public static getAppsOverview(subscriptionInfo?: IMarketplaceSubscriptionInfo): Array<{ latest: IMarketplaceInfo }> {
|
||||
return [
|
||||
{
|
||||
latest: TestData.getMarketplaceInfo({
|
||||
subscriptionInfo: subscriptionInfo || TestData.getMarketplaceSubscriptionInfo(),
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export class SimpleClass {
|
||||
private readonly world: string;
|
||||
|
||||
constructor(world = 'Earith') {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
public getWorld(): string {
|
||||
return this.world;
|
||||
}
|
||||
}
|
||||
8
packages/apps/tests/tsconfig.json
Normal file
8
packages/apps/tests/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"useDefineForClassFields": false,
|
||||
"rootDir": "../"
|
||||
},
|
||||
"include": ["./**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user