diff --git a/.gitignore b/.gitignore index 76fa46cbf..3549bc155 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ jspm_packages /lib .DS_Store +.idea/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..9b77117b3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 4, + "useTabs": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 160 +} diff --git a/package-lock.json b/package-lock.json index 4fac427af..3e532bad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps-engine", - "version": "1.34.0", + "version": "1.35.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7952,6 +7952,27 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, + "vm2": { + "version": "3.9.11", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.11.tgz", + "integrity": "sha512-PFG8iJRSjvvBdisowQ7iVF580DXb1uCIiGaXgm7tynMR1uTBlv7UJlB1zdv5KJ+Tmq1f0Upnj3fayoEOPpCBKg==", + "requires": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "dependencies": { + "acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==" + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + } + } + }, "vscode-textmate": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.5.0.tgz", diff --git a/package.json b/package.json index 0f08b9d6e..7415ccb3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps-engine", - "version": "1.34.0", + "version": "1.35.0", "description": "The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.", "main": "index", "typings": "index", @@ -91,7 +91,8 @@ "lodash.clonedeep": "^4.5.0", "semver": "^5.7.1", "stack-trace": "0.0.10", - "uuid": "^3.4.0" + "uuid": "^3.4.0", + "vm2": "^3.9.11" }, "nyc": { "include": [ diff --git a/src/definition/accessors/IRoomRead.ts b/src/definition/accessors/IRoomRead.ts index 613538bea..2ccd18fb3 100644 --- a/src/definition/accessors/IRoomRead.ts +++ b/src/definition/accessors/IRoomRead.ts @@ -61,4 +61,28 @@ export interface IRoomRead { * @returns the room */ getDirectByUsernames(usernames: Array): Promise; + + /** + * Get a list of the moderators of a given room + * + * @param roomId the room's id + * @returns a list of the users with the moderator role in the room + */ + getModerators(roomId: string): Promise>; + + /** + * Get a list of the owners of a given room + * + * @param roomId the room's id + * @returns a list of the users with the owner role in the room + */ + getOwners(roomId: string): Promise>; + + /** + * Get a list of the leaders of a given room + * + * @param roomId the room's id + * @returns a list of the users with the leader role in the room + */ + getLeaders(roomId: string): Promise>; } diff --git a/src/definition/package.json b/src/definition/package.json index f1b8f10f6..2e60f3760 100644 --- a/src/definition/package.json +++ b/src/definition/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps-ts-definition", - "version": "1.33.0-alpha", + "version": "1.35.0-alpha", "description": "Contains the TypeScript definitions for the Rocket.Chat Applications.", "main": "index.js", "typings": "index", diff --git a/src/server/AppManager.ts b/src/server/AppManager.ts index dbe500cf1..7dac3a270 100644 --- a/src/server/AppManager.ts +++ b/src/server/AppManager.ts @@ -25,6 +25,7 @@ import { IMarketplaceInfo } from './marketplace'; import { DisabledApp } from './misc/DisabledApp'; import { defaultPermissions } from './permissions/AppPermissions'; import { ProxiedApp } from './ProxiedApp'; +import { AppsEngineEmptyRuntime } from './runtime/AppsEngineEmptyRuntime'; import { AppLogStorage, AppMetadataStorage, IAppStorageItem } from './storage'; import { AppSourceStorage } from './storage/AppSourceStorage'; @@ -240,7 +241,7 @@ export class AppManager { app.getLogger().error(e); this.logStorage.storeEntries(app.getID(), app.getLogger()); - const prl = new ProxiedApp(this, item, app, () => ''); + const prl = new ProxiedApp(this, item, app, new AppsEngineEmptyRuntime(app)); this.apps.set(item.id, prl); aff.setApp(prl); } diff --git a/src/server/ProxiedApp.ts b/src/server/ProxiedApp.ts index 81034cca8..2e60fd316 100644 --- a/src/server/ProxiedApp.ts +++ b/src/server/ProxiedApp.ts @@ -1,5 +1,3 @@ -import * as vm from 'vm'; - import { IAppAccessors, ILogger } from '../definition/accessors'; import { App } from '../definition/App'; import { AppStatus } from '../definition/AppStatus'; @@ -10,23 +8,27 @@ import { AppManager } from './AppManager'; import { NotEnoughMethodArgumentsError } from './errors'; import { AppConsole } from './logging'; import { AppLicenseValidationResult } from './marketplace/license'; -import { Utilities } from './misc/Utilities'; +import { AppsEngineRuntime } from './runtime/AppsEngineRuntime'; import { IAppStorageItem } from './storage'; -export const ROCKETCHAT_APP_EXECUTION_PREFIX = '$RocketChat_App$'; - export class ProxiedApp implements IApp { private previousStatus: AppStatus; private latestLicenseValidationResult: AppLicenseValidationResult; - constructor(private readonly manager: AppManager, - private storageItem: IAppStorageItem, - private readonly app: App, - private readonly customRequire: (mod: string) => {}) { + constructor( + private readonly manager: AppManager, + private storageItem: IAppStorageItem, + private readonly app: App, + private readonly runtime: AppsEngineRuntime, + ) { this.previousStatus = storageItem.status; } + public getRuntime(): AppsEngineRuntime { + return this.runtime; + } + public getApp(): App { return this.app; } @@ -51,12 +53,6 @@ export class ProxiedApp implements IApp { return typeof (this.app as any)[method] === 'function'; } - public makeContext(data: object): vm.Context { - return Utilities.buildDefaultAppContext(Object.assign({}, { - require: this.customRequire, - }, data)); - } - public setupLogger(method: AppMethod): AppConsole { const logger = new AppConsole(method); // Set the logger to our new one @@ -65,13 +61,6 @@ export class ProxiedApp implements IApp { return logger; } - public runInContext(codeToRun: string, context: vm.Context): any { - return vm.runInContext(codeToRun, context, { - timeout: 1000, - filename: `${ ROCKETCHAT_APP_EXECUTION_PREFIX }_${ this.getName() }.ts`, - }); - } - public async call(method: AppMethod, ...args: Array): Promise { if (typeof (this.app as any)[method] !== 'function') { throw new Error(`The App ${this.app.getName()} (${this.app.getID()}` @@ -89,8 +78,10 @@ export class ProxiedApp implements IApp { let result; try { - // tslint:disable-next-line:max-line-length - result = await this.runInContext(`app.${method}.apply(app, args)`, this.makeContext({ app: this.app, args })) as Promise; + result = await this.runtime.runInSandbox( + `module.exports = app.${method}.apply(app, args)`, + { app: this.app, args }, + ); logger.debug(`'${method}' was successfully called! The result is:`, result); } catch (e) { logger.error(e); diff --git a/src/server/accessors/RoomRead.ts b/src/server/accessors/RoomRead.ts index 49ca68e01..137669c42 100644 --- a/src/server/accessors/RoomRead.ts +++ b/src/server/accessors/RoomRead.ts @@ -6,7 +6,7 @@ import { IUser } from '../../definition/users'; import { RoomBridge } from '../bridges'; export class RoomRead implements IRoomRead { - constructor(private roomBridge: RoomBridge, private appId: string) { } + constructor(private roomBridge: RoomBridge, private appId: string) {} public getById(id: string): Promise { return this.roomBridge.doGetById(id, this.appId); @@ -35,4 +35,16 @@ export class RoomRead implements IRoomRead { public getDirectByUsernames(usernames: Array): Promise { return this.roomBridge.doGetDirectByUsernames(usernames, this.appId); } + + public getModerators(roomId: string): Promise> { + return this.roomBridge.doGetModerators(roomId, this.appId); + } + + public getOwners(roomId: string): Promise> { + return this.roomBridge.doGetOwners(roomId, this.appId); + } + + public getLeaders(roomId: string): Promise> { + return this.roomBridge.doGetLeaders(roomId, this.appId); + } } diff --git a/src/server/bridges/RoomBridge.ts b/src/server/bridges/RoomBridge.ts index e81d4935f..1a97ff017 100644 --- a/src/server/bridges/RoomBridge.ts +++ b/src/server/bridges/RoomBridge.ts @@ -55,8 +55,13 @@ export abstract class RoomBridge extends BaseBridge { } } - public async doCreateDiscussion(room: IRoom, parentMessage: IMessage | undefined, - reply: string | undefined, members: Array, appId: string): Promise { + public async doCreateDiscussion( + room: IRoom, + parentMessage: IMessage | undefined, + reply: string | undefined, + members: Array, + appId: string, + ): Promise { if (this.hasWritePermission(appId)) { return this.createDiscussion(room, parentMessage, reply, members, appId); } @@ -68,6 +73,24 @@ export abstract class RoomBridge extends BaseBridge { } } + public async doGetModerators(roomId: string, appId: string): Promise> { + if (this.hasReadPermission(appId)) { + return this.getModerators(roomId, appId); + } + } + + public async doGetOwners(roomId: string, appId: string): Promise> { + if (this.hasReadPermission(appId)) { + return this.getOwners(roomId, appId); + } + } + + public async doGetLeaders(roomId: string, appId: string): Promise> { + if (this.hasReadPermission(appId)) { + return this.getLeaders(roomId, appId); + } + } + protected abstract create(room: IRoom, members: Array, appId: string): Promise; protected abstract getById(roomId: string, appId: string): Promise; protected abstract getByName(roomName: string, appId: string): Promise; @@ -76,19 +99,29 @@ export abstract class RoomBridge extends BaseBridge { protected abstract getDirectByUsernames(usernames: Array, appId: string): Promise; protected abstract getMembers(roomId: string, appId: string): Promise>; protected abstract update(room: IRoom, members: Array, appId: string): Promise; - protected abstract createDiscussion(room: IRoom, parentMessage: IMessage | undefined, - reply: string | undefined, members: Array, appId: string): Promise; + protected abstract createDiscussion( + room: IRoom, + parentMessage: IMessage | undefined, + reply: string | undefined, + members: Array, + appId: string, + ): Promise; protected abstract delete(room: string, appId: string): Promise; + protected abstract getModerators(roomId: string, appId: string): Promise>; + protected abstract getOwners(roomId: string, appId: string): Promise>; + protected abstract getLeaders(roomId: string, appId: string): Promise>; private hasWritePermission(appId: string): boolean { if (AppPermissionManager.hasPermission(appId, AppPermissions.room.write)) { return true; } - AppPermissionManager.notifyAboutError(new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.room.write], - })); + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.room.write], + }), + ); return false; } @@ -98,10 +131,12 @@ export abstract class RoomBridge extends BaseBridge { return true; } - AppPermissionManager.notifyAboutError(new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.room.read], - })); + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.room.read], + }), + ); return false; } diff --git a/src/server/compiler/AppCompiler.ts b/src/server/compiler/AppCompiler.ts index 923a42bd7..75df3eadf 100644 --- a/src/server/compiler/AppCompiler.ts +++ b/src/server/compiler/AppCompiler.ts @@ -1,14 +1,14 @@ import * as path from 'path'; -import * as vm from 'vm'; import { App } from '../../definition/App'; import { AppMethod } from '../../definition/metadata'; import { AppAccessors } from '../accessors'; import { AppManager } from '../AppManager'; -import { MustContainFunctionError, MustExtendAppError } from '../errors'; +import { MustContainFunctionError } from '../errors'; import { AppConsole } from '../logging'; -import { Utilities } from '../misc/Utilities'; import { ProxiedApp } from '../ProxiedApp'; +import { getRuntime } from '../runtime'; +import { buildCustomRequire } from '../runtime/require'; import { IAppStorageItem } from '../storage'; import { IParseAppPackageResult } from './IParseAppPackageResult'; @@ -29,31 +29,30 @@ export class AppCompiler { `Could not find the classFile (${ storage.info.classFile }) file.`); } - const exports = {}; - const customRequire = Utilities.buildCustomRequire(files, storage.info.id); - const context = Utilities.buildDefaultAppContext({ require: customRequire, exports, process: {}, console }); + const Runtime = getRuntime(); - const script = new vm.Script(files[path.normalize(storage.info.classFile)]); - const result = script.runInContext(context); + const customRequire = buildCustomRequire(files, storage.info.id); + const result = Runtime.runCode(files[path.normalize(storage.info.classFile)], { + require: customRequire, + }); if (typeof result !== 'function') { // tslint:disable-next-line:max-line-length throw new Error(`The App's main class for ${ storage.info.name } is not valid ("${ storage.info.classFile }").`); } - const appAccessors = new AppAccessors(manager, storage.info.id); const logger = new AppConsole(AppMethod._CONSTRUCTOR); - const rl = vm.runInNewContext('new App(info, rcLogger, appAccessors);', Utilities.buildDefaultAppContext({ + const rl = Runtime.runCode('exports.app = new App(info, rcLogger, appAccessors);', { rcLogger: logger, info: storage.info, App: result, - process: {}, appAccessors, - }), { timeout: 1000, filename: `App_${ storage.info.nameSlug }.js` }); + }, { timeout: 1000, filename: `App_${ storage.info.nameSlug }.js` }); - if (!(rl instanceof App)) { - throw new MustExtendAppError(); - } + // TODO: app is importing the Class App internally so it's not same object to compare. Need to find a way to make this test + // if (!(rl instanceof App)) { + // throw new MustExtendAppError(); + // } if (typeof rl.getName !== 'function') { throw new MustContainFunctionError(storage.info.classFile, 'getName'); @@ -79,7 +78,8 @@ export class AppCompiler { throw new MustContainFunctionError(storage.info.classFile, 'getRequiredApiVersion'); } - const app = new ProxiedApp(manager, storage, rl as App, customRequire); + // TODO: Fix this type cast from to any to the right one + const app = new ProxiedApp(manager, storage, rl as App, new Runtime(rl as App, customRequire as any)); manager.getLogStorage().storeEntries(app.getID(), logger); diff --git a/src/server/compiler/modules/index.ts b/src/server/compiler/modules/index.ts index 86822fef8..f4829f1e7 100644 --- a/src/server/compiler/modules/index.ts +++ b/src/server/compiler/modules/index.ts @@ -44,8 +44,8 @@ const proxyHandlers = { querystring: defaultHandler, }; -export function requireNativeModule(module: AllowedInternalModules, appId: string) { - const requiredModule = require(module); +export function requireNativeModule(module: AllowedInternalModules, appId: string, requirer: any) { + const requiredModule = requirer(module); return new Proxy( requiredModule, diff --git a/src/server/managers/AppApi.ts b/src/server/managers/AppApi.ts index 5d814165a..f4ed8fe2a 100644 --- a/src/server/managers/AppApi.ts +++ b/src/server/managers/AppApi.ts @@ -75,24 +75,22 @@ export class AppApi { hash: this.hash, }; - const runContext = this.app.makeContext({ - endpoint: this.endpoint, - args: [ - request, - endpoint, - accessors.getReader(this.app.getID()), - accessors.getModifier(this.app.getID()), - accessors.getHttp(this.app.getID()), - accessors.getPersistence(this.app.getID()), - ], - }); - const logger = this.app.setupLogger(AppMethod._API_EXECUTOR); logger.debug(`${ path }'s ${ method } is being executed...`, request); - const runCode = `endpoint.${ method }.apply(endpoint, args)`; + const runCode = `module.exports = endpoint.${ method }.apply(endpoint, args)`; try { - const result: IApiResponse = await this.app.runInContext(runCode, runContext); + const result: IApiResponse = await this.app.getRuntime().runInSandbox(runCode, { + endpoint: this.endpoint, + args: [ + request, + endpoint, + accessors.getReader(this.app.getID()), + accessors.getModifier(this.app.getID()), + accessors.getHttp(this.app.getID()), + accessors.getPersistence(this.app.getID()), + ], + }); logger.debug(`${ path }'s ${ method } was successfully executed.`); logStorage.storeEntries(this.app.getID(), logger); return result; diff --git a/src/server/managers/AppPermissionManager.ts b/src/server/managers/AppPermissionManager.ts index 7dfd56e2e..5975278dc 100644 --- a/src/server/managers/AppPermissionManager.ts +++ b/src/server/managers/AppPermissionManager.ts @@ -1,7 +1,7 @@ import { IPermission } from '../../definition/permissions/IPermission'; import { getPermissionsByAppId } from '../AppManager'; import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { ROCKETCHAT_APP_EXECUTION_PREFIX } from '../ProxiedApp'; +import { APPS_ENGINE_RUNTIME_FILE_PREFIX } from '../runtime/AppsEngineRuntime'; export class AppPermissionManager { /** @@ -33,7 +33,7 @@ export class AppPermissionManager { private static getCallStack(): string { const stack = new Error().stack.toString().split('\n'); - const appStackIndex = stack.findIndex((position) => position.includes(ROCKETCHAT_APP_EXECUTION_PREFIX)); + const appStackIndex = stack.findIndex((position) => position.includes(APPS_ENGINE_RUNTIME_FILE_PREFIX)); return stack.slice(4, appStackIndex).join('\n'); } diff --git a/src/server/managers/AppSchedulerManager.ts b/src/server/managers/AppSchedulerManager.ts index 086310d77..9bd3f8ba8 100644 --- a/src/server/managers/AppSchedulerManager.ts +++ b/src/server/managers/AppSchedulerManager.ts @@ -63,23 +63,21 @@ export class AppSchedulerManager { return; } - const context = app.makeContext({ - processor, - args: [ - jobContext, - this.accessors.getReader(appId), - this.accessors.getModifier(appId), - this.accessors.getHttp(appId), - this.accessors.getPersistence(appId), - ], - }); - const logger = app.setupLogger(AppMethod._JOB_PROCESSOR); logger.debug(`Job processor ${processor.id} is being executed...`); try { - const codeToRun = `processor.processor.apply(null, args)`; - await app.runInContext(codeToRun, context); + const codeToRun = `module.exports = processor.processor.apply(null, args)`; + await app.getRuntime().runInSandbox(codeToRun, { + processor, + args: [ + jobContext, + this.accessors.getReader(appId), + this.accessors.getModifier(appId), + this.accessors.getHttp(appId), + this.accessors.getPersistence(appId), + ], + }); logger.debug(`Job processor ${processor.id} was sucessfully executed`); } catch (e) { logger.error(e); diff --git a/src/server/managers/AppSlashCommand.ts b/src/server/managers/AppSlashCommand.ts index 4fef5cf07..ddb35ca70 100644 --- a/src/server/managers/AppSlashCommand.ts +++ b/src/server/managers/AppSlashCommand.ts @@ -72,38 +72,30 @@ export class AppSlashCommand { return; } - const runContext = this.app.makeContext({ - slashCommand: this.slashCommand, - args: [ - ...runContextArgs, - context, - accessors.getReader(this.app.getID()), - accessors.getModifier(this.app.getID()), - accessors.getHttp(this.app.getID()), - accessors.getPersistence(this.app.getID()), - ], - }); - const logger = this.app.setupLogger(method); logger.debug(`${ command }'s ${ method } is being executed...`, context); - let result: void | ISlashCommandPreview; try { - const runCode = `slashCommand.${ method }.apply(slashCommand, args)`; - result = await this.app.runInContext(runCode, runContext); + const runCode = `module.exports = slashCommand.${ method }.apply(slashCommand, args)`; + const result = await this.app.getRuntime().runInSandbox(runCode, { + slashCommand: this.slashCommand, + args: [ + ...runContextArgs, + context, + accessors.getReader(this.app.getID()), + accessors.getModifier(this.app.getID()), + accessors.getHttp(this.app.getID()), + accessors.getPersistence(this.app.getID()), + ], + }); + logger.debug(`${ command }'s ${ method } was successfully executed.`); + return result; } catch (e) { logger.error(e); logger.debug(`${ command }'s ${ method } was unsuccessful.`); - } - - try { + } finally { await logStorage.storeEntries(this.app.getID(), logger); - } catch (e) { - // Don't care, at the moment. - // TODO: Evaluate to determine if we do care } - - return result; } } diff --git a/src/server/managers/AppVideoConfProvider.ts b/src/server/managers/AppVideoConfProvider.ts index e61531917..648c6d604 100644 --- a/src/server/managers/AppVideoConfProvider.ts +++ b/src/server/managers/AppVideoConfProvider.ts @@ -32,7 +32,7 @@ export class AppVideoConfProvider { return true; } - return await this.runTheCode(AppMethod._VIDEOCONF_IS_CONFIGURED, logStorage, accessors, []) as boolean; + return !!await this.runTheCode(AppMethod._VIDEOCONF_IS_CONFIGURED, logStorage, accessors, []) as boolean; } public async runGenerateUrl( @@ -64,7 +64,7 @@ export class AppVideoConfProvider { return; } - const runContext = this.app.makeContext({ + const runContext = { provider: this.provider, args: [ ...runContextArgs, @@ -73,15 +73,15 @@ export class AppVideoConfProvider { accessors.getHttp(this.app.getID()), accessors.getPersistence(this.app.getID()), ], - }); + }; const logger = this.app.setupLogger(method); logger.debug(`Executing ${ method } on video conference provider...`); let result: string | undefined; try { - const runCode = `provider.${ method }.apply(provider, args)`; - result = await this.app.runInContext(runCode, runContext); + const runCode = `module.exports = provider.${ method }.apply(provider, args)`; + result = await this.app.getRuntime().runInSandbox(runCode, runContext); logger.debug(`Video Conference Provider's ${ method } was successfully executed.`); } catch (e) { logger.error(e); diff --git a/src/server/misc/Utilities.ts b/src/server/misc/Utilities.ts index 3c6ae37eb..18410ef07 100644 --- a/src/server/misc/Utilities.ts +++ b/src/server/misc/Utilities.ts @@ -1,9 +1,4 @@ import cloneDeep = require('lodash.clonedeep'); -import * as path from 'path'; -import * as timers from 'timers'; -import * as vm from 'vm'; - -import { AllowedInternalModules, requireNativeModule } from '../compiler/modules'; export class Utilities { public static deepClone(item: T): T { @@ -27,77 +22,6 @@ export class Utilities { return Utilities.deepFreeze(Utilities.deepClone(item)); } - /** - * Keeps compatibility with apps compiled and stored in the database - * with previous Apps-Engine versions - */ - public static transformFallbackModuleForCustomRequire(moduleName: string): string { - return path.normalize(moduleName).replace(/\.\.?\//g, '').replace(/^\//, '') + '.ts'; - } - - public static transformModuleForCustomRequire(moduleName: string): string { - return path.normalize(moduleName).replace(/\.\.?\//g, '').replace(/^\//, '') + '.js'; - } - - public static allowedInternalModuleRequire(moduleName: string): moduleName is AllowedInternalModules { - return moduleName in AllowedInternalModules; - } - - public static buildCustomRequire(files: { [s: string]: string }, appId: string, currentPath: string = '.'): (mod: string) => {} { - return function _requirer(mod: string): any { - // Keep compatibility with apps importing apps-ts-definition - if (mod.startsWith('@rocket.chat/apps-ts-definition/')) { - mod = path.normalize(mod); - mod = mod.replace('@rocket.chat/apps-ts-definition/', '../../definition/'); - return require(mod); - } - - if (mod.startsWith('@rocket.chat/apps-engine/definition/')) { - mod = path.normalize(mod); - mod = mod.replace('@rocket.chat/apps-engine/definition/', '../../definition/'); - return require(mod); - } - - if (Utilities.allowedInternalModuleRequire(mod)) { - return requireNativeModule(mod, appId); - } - - if (currentPath !== '.') { - mod = path.join(currentPath, mod); - } - - const transformedModule = Utilities.transformModuleForCustomRequire(mod); - const fallbackModule = Utilities.transformFallbackModuleForCustomRequire(mod); - - const filename = files[transformedModule] ? transformedModule : files[fallbackModule] ? fallbackModule : undefined; - let fileExport; - - if (filename) { - fileExport = {}; - - const context = vm.createContext({ - require: Utilities.buildCustomRequire(files, appId, path.dirname(filename) + '/'), - console, - exports: fileExport, - process: {}, - }); - - vm.runInContext(files[filename], context); - } - - return fileExport; - }; - } - - public static buildDefaultAppContext(injectables: unknown): vm.Context { - const defaultContextProperties = { - ...timers, - Buffer, - }; - - return vm.createContext(Object.assign({}, defaultContextProperties, injectables)); - } - public static omit(object: { [key: string]: any }, keys: Array) { const cloned = this.deepClone(object); for (const key of keys) { diff --git a/src/server/runtime/AppsEngineEmptyRuntime.ts b/src/server/runtime/AppsEngineEmptyRuntime.ts new file mode 100644 index 000000000..b1ba45691 --- /dev/null +++ b/src/server/runtime/AppsEngineEmptyRuntime.ts @@ -0,0 +1,16 @@ +import { App } from '../../definition/App'; +import { AppsEngineRuntime, IAppsEngineRuntimeOptions } from './AppsEngineRuntime'; + +export class AppsEngineEmptyRuntime extends AppsEngineRuntime { + + public static runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { + throw new Error('Empty runtime does not support code execution'); + } + constructor(readonly app: App) { + super(app, () => {}); + } + + public async runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { + return Promise.reject(new Error('Empty runtime does not support execution')); + } +} diff --git a/src/server/runtime/AppsEngineNodeRuntime.ts b/src/server/runtime/AppsEngineNodeRuntime.ts new file mode 100644 index 000000000..315e114a7 --- /dev/null +++ b/src/server/runtime/AppsEngineNodeRuntime.ts @@ -0,0 +1,45 @@ +import * as timers from 'timers'; +import * as vm from 'vm'; + +import { App } from '../../definition/App'; +import { APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT, AppsEngineRuntime, getFilenameForApp, IAppsEngineRuntimeOptions } from './AppsEngineRuntime'; + +export class AppsEngineNodeRuntime extends AppsEngineRuntime { + public static defaultRuntimeOptions = { + timeout: APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT, + }; + + public static defaultContext = { + ...timers, + Buffer, + console, + process: {}, + exports: {}, + }; + public static runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { + return vm.runInNewContext( + code, + { ...AppsEngineNodeRuntime.defaultContext, ...sandbox }, + { ...AppsEngineNodeRuntime.defaultRuntimeOptions, ...options || {} }, + ); + } + + constructor(private readonly app: App, private readonly customRequire: (mod: string) => any) { + super(app, customRequire); + } + + public async runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { + sandbox ??= {}; + + const result = await vm.runInNewContext(code, { + ...AppsEngineNodeRuntime.defaultContext, + ...sandbox, + require: this.customRequire, + }, { + ...AppsEngineNodeRuntime.defaultRuntimeOptions, + filename: getFilenameForApp(options?.filename || this.app.getName()), + }); + + return result; + } +} diff --git a/src/server/runtime/AppsEngineRuntime.ts b/src/server/runtime/AppsEngineRuntime.ts new file mode 100644 index 000000000..33d7a889d --- /dev/null +++ b/src/server/runtime/AppsEngineRuntime.ts @@ -0,0 +1,23 @@ +import { App } from '../../definition/App'; + +export const APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT = 1000; + +export const APPS_ENGINE_RUNTIME_FILE_PREFIX = '$RocketChat_App$'; + +export function getFilenameForApp(filename: string): string { + return `${ APPS_ENGINE_RUNTIME_FILE_PREFIX }_${ filename }`; +} + +export abstract class AppsEngineRuntime { + public static runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { + throw new Error(`Can't call this method on abstract class. Override it in a proper runtime class.`); + } + constructor(app: App, customRequire: (module: string) => any) {} + public abstract runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise; +} + +export interface IAppsEngineRuntimeOptions { + timeout?: number; + filename?: string; + returnAllExports?: boolean; +} diff --git a/src/server/runtime/AppsEngineVM2Runtime.spec.ts b/src/server/runtime/AppsEngineVM2Runtime.spec.ts new file mode 100644 index 000000000..ac227dc4f --- /dev/null +++ b/src/server/runtime/AppsEngineVM2Runtime.spec.ts @@ -0,0 +1,105 @@ +import { + AsyncTest, + Expect, + Setup, + SetupFixture, + Test, + TestCase, + TestFixture, +} from 'alsatian'; +import { App } from '../../definition/App'; +import { AppsEngineVM2Runtime } from './AppsEngineVM2Runtime'; + +@TestFixture('AppsEngineVM2Runtine') +export class AppsEngineVM2RuntineTestFixture { + private app = { + getName: () => 'app-name', + }; + + @SetupFixture + public setupFixture() {} + + @Setup + public setup() {} + + @TestCase( + `module.exports = () => { return 'Hello World First case'};`, + { someSandbox: true }, + { + timeout: 10, + filename: 'filename.ts', + returnAllExports: true, + }, + 'Hello World First case', + ) + @TestCase( + `module.exports = { method: () => { return 'Hello World Second case' } }`, + null, + { + timeout: 10, + returnAllExports: false, + }, + 'Hello World Second case', + ) + @TestCase( + `module.exports = () => { return 'Hello World Third case'};`, + { someSandbox: true }, + null, + 'Hello World Third case', + ) + @TestCase( + `module.exports = () => { return 'Hello World Fourth case'};`, + null, + null, + ) + @TestCase( + `module.exports = () => { return 'Hello World Fifth case'};`, + { require: () => 'module' }, + null, + 'Hello World Fourth case', + ) + @Test('AppsEngineVM2Runtime.runCode') + public runCodeTest(...args: any) { + const code = args[0]; + const sandbox = args[1]; + const options = args[2]; + const response = args[3]; + + const result = AppsEngineVM2Runtime.runCode(code, sandbox, options); + + if (result) { + if (result instanceof Function) { + Expect(result()).toBe(response); + } + } + } + + @TestCase( + `module.exports = () => { return 'Hello World'};`, + { someSandbox: true }, + { + timeout: 10, + filename: 'filename.ts', + returnAllExports: true, + }, + 'Hello World', + ) + @AsyncTest( + 'new AppsEngineVM2Runtime().runInSandbox(code, sandbox, options)', + ) + public async runInSandbox(...args: any) { + const code = args[0]; + const sandbox = args[1]; + const options = args[2]; + const response = args[3]; + + const instance = new AppsEngineVM2Runtime( + this.app as App, + (mod: string) => mod, + ); + + const result = await instance.runInSandbox(code, sandbox, options); + + Expect(result()).toBe(response); + } +} diff --git a/src/server/runtime/AppsEngineVM2Runtime.ts b/src/server/runtime/AppsEngineVM2Runtime.ts new file mode 100644 index 000000000..e2593190e --- /dev/null +++ b/src/server/runtime/AppsEngineVM2Runtime.ts @@ -0,0 +1,87 @@ +import * as path from 'path'; +import * as timers from 'timers'; +import { NodeVM, NodeVMOptions } from 'vm2'; +import { App } from '../../definition/App'; +import { APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT, AppsEngineRuntime, getFilenameForApp, IAppsEngineRuntimeOptions } from './AppsEngineRuntime'; + +export class AppsEngineVM2Runtime extends AppsEngineRuntime { + public static defaultNodeVMOptions: NodeVMOptions = { + console: 'inherit', + // wrapper: 'none', + timeout: APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT, + // we don't need any compiling happening + compiler: (code: string, filename: string) => code, + // We keep require inaccessible here as we expect it to be provided + // require: false, + sandbox: { + Buffer, + ...timers, + }, + }; + + public static runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { + const vmOptions = { + ...AppsEngineVM2Runtime.defaultNodeVMOptions, + timeout: options?.timeout, + sandbox: { + ...AppsEngineVM2Runtime.defaultNodeVMOptions.sandbox, + ...(sandbox || {}), + }, + }; + + const resolve = sandbox && sandbox.require; + if (resolve instanceof Function) { + vmOptions.require = { + external: ['@rocket.chat/apps-engine', 'uuid'], + builtin: ['path', 'url', 'crypto', 'buffer', 'stream', 'net', 'http', 'https', 'zlib', 'util', 'punycode', 'os', 'querystring'], + resolve: (moduleName, p) => { + return path.resolve(p + '/npm/node_modules/' + moduleName); + }, + context: 'sandbox', + }; + + delete sandbox.require; + } + const vm = new NodeVM(vmOptions); + + const app = vm.run(code, { + filename: options?.filename || 'app.js', + require: (mod: string) => resolve(mod, vm.require.bind(vm)), + } as any); + // Get first exported object, vm2 does not return the last value when it's an assignment as intern vm + // so we use the first exported value as the class. + return options?.returnAllExports ? app : app && app[Object.keys(app)[0]]; + } + + private vm: NodeVM; + + constructor(private readonly app: App, customRequire: (mod: string) => any) { + super(app, customRequire); + + this.vm = new NodeVM({ + ...AppsEngineVM2Runtime.defaultNodeVMOptions, + require: { customRequire }, + }); + } + + public async runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { + sandbox ??= {}; + + this.vm.setGlobals(sandbox); + + const result = await this.vm.run(code, { + filename: getFilenameForApp(options?.filename || this.app.getName()), + }); + + // Clean up the sandbox after the code has run + this.vm.setGlobals( + Object.keys(sandbox).reduce((acc, key) => { + acc[key] = undefined; + + return acc; + }, {} as typeof sandbox), + ); + + return result; + } +} diff --git a/src/server/runtime/index.ts b/src/server/runtime/index.ts new file mode 100644 index 000000000..9581c9e65 --- /dev/null +++ b/src/server/runtime/index.ts @@ -0,0 +1,17 @@ +import { AppsEngineNodeRuntime } from './AppsEngineNodeRuntime'; +import { AppsEngineVM2Runtime } from './AppsEngineVM2Runtime'; + +export type AvailableRuntime = typeof AppsEngineNodeRuntime | typeof AppsEngineVM2Runtime; + +export function _getRuntime(requiredEnv: string = 'vm2'): AvailableRuntime { + switch (requiredEnv) { + case 'vm2': + return AppsEngineVM2Runtime; + default: + return AppsEngineNodeRuntime; + } +} + +export function getRuntime() { + return _getRuntime(process.env?.ROCKETCHAT_APPS_ENGINE_RUNTIME); +} diff --git a/src/server/runtime/require.ts b/src/server/runtime/require.ts new file mode 100644 index 000000000..b5057b82f --- /dev/null +++ b/src/server/runtime/require.ts @@ -0,0 +1,71 @@ +import * as path from 'path'; +import { getRuntime } from '.'; + +import { AllowedInternalModules, requireNativeModule } from '../compiler/modules'; + +/** + * Keeps compatibility with apps compiled and stored in the database + * with previous Apps-Engine versions + */ +export function transformFallbackModuleForCustomRequire(moduleName: string): string { + return path.normalize(moduleName).replace(/\.\.?\//g, '').replace(/^\//, '') + '.ts'; +} + +export function transformModuleForCustomRequire(moduleName: string): string { + return path.normalize(moduleName).replace(/\.\.?\//g, '').replace(/^\//, '') + '.js'; +} + +export function allowedInternalModuleRequire(moduleName: string): moduleName is AllowedInternalModules { + return moduleName in AllowedInternalModules; +} + +export function buildCustomRequire(files: { [s: string]: string }, appId: string, currentPath: string = '.'): (mod: string, require: any) => {} { + return function _requirer(mod: string, requirer: any) { + // Keep compatibility with apps importing apps-ts-definition + if (mod.startsWith('@rocket.chat/apps-ts-definition/')) { + if (requirer) { + return requirer(mod); + } + mod = path.normalize(mod); + mod = mod.replace('@rocket.chat/apps-ts-definition/', '../../definition/'); + return require(mod); + } + + if (mod.startsWith('@rocket.chat/apps-engine/definition/')) { + if (requirer) { + return requirer(mod); + } + mod = path.normalize(mod); + mod = mod.replace('@rocket.chat/apps-engine/definition/', '../../definition/'); + return require(mod); + } + + if (allowedInternalModuleRequire(mod)) { + // TODO: Need to use the vm2 require in this function and evaluate the necessity of the proxies + return requireNativeModule(mod, appId, requirer); + } + + if (currentPath !== '.') { + mod = path.join(currentPath, mod); + } + + const transformedModule = transformModuleForCustomRequire(mod); + const fallbackModule = transformFallbackModuleForCustomRequire(mod); + + const filename = files[transformedModule] ? transformedModule : files[fallbackModule] ? fallbackModule : undefined; + + if (!filename) { + return; + } + + const Runtime = getRuntime(); + + // TODO: specify correct file name + return Runtime.runCode(files[filename], { + require: buildCustomRequire(files, appId, path.dirname(filename) + '/'), + }, { + returnAllExports: true, + filename, + }); + }; +} diff --git a/tests/server/accessors/AppAccessors.spec.ts b/tests/server/accessors/AppAccessors.spec.ts index a437bd0b0..40240edc2 100644 --- a/tests/server/accessors/AppAccessors.spec.ts +++ b/tests/server/accessors/AppAccessors.spec.ts @@ -1,5 +1,4 @@ import { Expect, Setup, SetupFixture, Test } from 'alsatian'; -import * as vm from 'vm'; import { AppStatus } from '../../../src/definition/AppStatus'; import { AppMethod } from '../../../src/definition/metadata'; @@ -10,6 +9,7 @@ import { AppConsole } from '../../../src/server/logging'; import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSettingsManager, AppSlashCommandManager, AppVideoConfProviderManager } from '../../../src/server/managers'; import { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager'; import { ProxiedApp } from '../../../src/server/ProxiedApp'; +import { AppsEngineRuntime } from '../../../src/server/runtime/AppsEngineRuntime'; import { AppLogStorage } from '../../../src/server/storage'; import { TestsAppBridges } from '../../test-data/bridges/appBridges'; import { TestsAppLogStorage } from '../../test-data/storage/logStorage'; @@ -28,6 +28,9 @@ export class AppAccessorsTestFixture { this.mockBridges = new TestsAppBridges(); this.mockApp = { + getRuntime() { + return {} as AppsEngineRuntime; + }, getID() { return 'testing'; }, @@ -37,13 +40,6 @@ export class AppAccessorsTestFixture { hasMethod(method: AppMethod): boolean { return true; }, - makeContext(data: object): vm.Context { - return {} as vm.Context; - }, - runInContext(codeToRun: string, context: vm.Context): any { - return AppAccessorsTestFixture.doThrow ? - Promise.reject('You told me so') : Promise.resolve(); - }, setupLogger(method: AppMethod): AppConsole { return new AppConsole(method); }, diff --git a/tests/server/compiler/AppFabricationFulfillment.spec.ts b/tests/server/compiler/AppFabricationFulfillment.spec.ts index aa13b11dc..f7c72c545 100644 --- a/tests/server/compiler/AppFabricationFulfillment.spec.ts +++ b/tests/server/compiler/AppFabricationFulfillment.spec.ts @@ -5,6 +5,7 @@ import { AppInterface, IAppInfo } from '../../../src/definition/metadata'; import { AppManager } from '../../../src/server/AppManager'; import { AppFabricationFulfillment } from '../../../src/server/compiler'; import { ProxiedApp } from '../../../src/server/ProxiedApp'; +import { AppsEngineEmptyRuntime } from '../../../src/server/runtime/AppsEngineEmptyRuntime'; import { IAppStorageItem } from '../../../src/server/storage'; export class AppFabricationFulfillmentTestFixture { @@ -39,7 +40,12 @@ export class AppFabricationFulfillmentTestFixture { Expect(() => aff.setImplementedInterfaces(expectedInter)).not.toThrow(); Expect(aff.getImplementedInferfaces()).toEqual(expectedInter); - const fakeApp = new ProxiedApp({} as AppManager, { status: AppStatus.UNKNOWN } as IAppStorageItem, {} as App, (mod: string) => mod); + const fakeApp = new ProxiedApp( + {} as AppManager, + { status: AppStatus.UNKNOWN } as IAppStorageItem, + {} as App, + new AppsEngineEmptyRuntime(null), + ); Expect(() => aff.setApp(fakeApp)).not.toThrow(); Expect(aff.getApp()).toEqual(fakeApp); } diff --git a/tests/server/managers/AppApiManager.spec.ts b/tests/server/managers/AppApiManager.spec.ts index 3f481fb4b..b2ea5fc79 100644 --- a/tests/server/managers/AppApiManager.spec.ts +++ b/tests/server/managers/AppApiManager.spec.ts @@ -1,6 +1,5 @@ // tslint:disable:max-line-length import { AsyncTest, Expect, FunctionSpy, RestorableFunctionSpy, Setup, SetupFixture, SpyOn, Teardown, Test } from 'alsatian'; -import * as vm from 'vm'; import { RequestMethod } from '../../../src/definition/accessors'; import { IApi, IApiRequest } from '../../../src/definition/api'; @@ -14,6 +13,7 @@ import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppSche import { AppApi } from '../../../src/server/managers/AppApi'; import { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager'; import { ProxiedApp } from '../../../src/server/ProxiedApp'; +import { AppsEngineRuntime } from '../../../src/server/runtime/AppsEngineRuntime'; import { AppLogStorage } from '../../../src/server/storage'; import { TestsAppBridges } from '../../test-data/bridges/appBridges'; import { TestsAppLogStorage } from '../../test-data/storage/logStorage'; @@ -32,6 +32,11 @@ export class AppApiManagerTestFixture { this.mockBridges = new TestsAppBridges(); this.mockApp = { + getRuntime() { + return { + runInSandbox: () => Promise.resolve(true), + } as AppsEngineRuntime; + }, getID() { return 'testing'; }, @@ -41,13 +46,6 @@ export class AppApiManagerTestFixture { hasMethod(method: AppMethod): boolean { return true; }, - makeContext(data: object): vm.Context { - return {} as vm.Context; - }, - runInContext(codeToRun: string, context: vm.Context): any { - return AppApiManagerTestFixture.doThrow ? - Promise.reject('You told me so') : Promise.resolve(); - }, setupLogger(method: AppMethod): AppConsole { return new AppConsole(method); }, @@ -186,7 +184,6 @@ export class AppApiManagerTestFixture { ascm.addApi('testing', TestData.getApi('api3')); ascm.registerApis('testing'); - SpyOn(this.mockApp, 'runInContext'); const request: IApiRequest = { method: RequestMethod.GET, headers: {}, @@ -200,8 +197,6 @@ export class AppApiManagerTestFixture { await Expect(async () => await ascm.executeApi('testing', 'api1', request)).not.toThrowAsync(); await Expect(async () => await ascm.executeApi('testing', 'api2', request)).not.toThrowAsync(); await Expect(async () => await ascm.executeApi('testing', 'api3', request)).not.toThrowAsync(); - - Expect(this.mockApp.runInContext).toHaveBeenCalled().exactly(3); } @Test() diff --git a/tests/server/managers/AppSlashCommandManager.spec.ts b/tests/server/managers/AppSlashCommandManager.spec.ts index 17ce88be7..4ed76eada 100644 --- a/tests/server/managers/AppSlashCommandManager.spec.ts +++ b/tests/server/managers/AppSlashCommandManager.spec.ts @@ -1,7 +1,6 @@ // tslint:disable:max-line-length import { AsyncTest, Expect, FunctionSpy, RestorableFunctionSpy, Setup, SetupFixture, SpyOn, Teardown, Test } from 'alsatian'; -import * as vm from 'vm'; import { AppStatus } from '../../../src/definition/AppStatus'; import { AppMethod } from '../../../src/definition/metadata'; import { ISlashCommandPreviewItem, SlashCommandContext } from '../../../src/definition/slashcommands'; @@ -18,6 +17,7 @@ import { AppSlashCommand } from '../../../src/server/managers/AppSlashCommand'; import { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager'; import { ProxiedApp } from '../../../src/server/ProxiedApp'; import { Room } from '../../../src/server/rooms/Room'; +import { AppsEngineRuntime } from '../../../src/server/runtime/AppsEngineRuntime'; import { AppLogStorage } from '../../../src/server/storage'; export class AppSlashCommandManagerTestFixture { @@ -33,6 +33,9 @@ export class AppSlashCommandManagerTestFixture { this.mockBridges = new TestsAppBridges(); this.mockApp = { + getRuntime() { + return {} as AppsEngineRuntime; + }, getID() { return 'testing'; }, @@ -42,13 +45,6 @@ export class AppSlashCommandManagerTestFixture { hasMethod(method: AppMethod): boolean { return true; }, - makeContext(data: object): vm.Context { - return {} as vm.Context; - }, - runInContext(codeToRun: string, context: vm.Context): any { - return AppSlashCommandManagerTestFixture.doThrow ? - Promise.reject('You told me so') : Promise.resolve(); - }, setupLogger(method: AppMethod): AppConsole { return new AppConsole(method); }, @@ -345,7 +341,6 @@ export class AppSlashCommandManagerTestFixture { ascm.modifyCommand('testing', TestData.getSlashCommand('it-exists')); const context = new SlashCommandContext(TestData.getUser(), TestData.getRoom(), []); - SpyOn(this.mockApp, 'runInContext'); await Expect(async () => await ascm.executeCommand('nope', context)).not.toThrowAsync(); await Expect(async () => await ascm.executeCommand('it-exists', context)).not.toThrowAsync(); @@ -368,8 +363,6 @@ export class AppSlashCommandManagerTestFixture { AppSlashCommandManagerTestFixture.doThrow = true; await Expect(async () => await ascm.executeCommand('command', context)).not.toThrowAsync(); AppSlashCommandManagerTestFixture.doThrow = false; - - Expect(this.mockApp.runInContext).toHaveBeenCalled().exactly(4); } @AsyncTest() diff --git a/tests/server/managers/AppVideoConfProviderManager.spec.ts b/tests/server/managers/AppVideoConfProviderManager.spec.ts index cfc43cbe6..23fa62ea0 100644 --- a/tests/server/managers/AppVideoConfProviderManager.spec.ts +++ b/tests/server/managers/AppVideoConfProviderManager.spec.ts @@ -1,4 +1,4 @@ -import { AsyncTest, Expect, Setup, SetupFixture, Teardown, Test } from 'alsatian'; +import { AsyncTest, Expect, Setup, SetupFixture, SpyOn, Teardown, Test } from 'alsatian'; import { TestsAppBridges } from '../../test-data/bridges/appBridges'; import { TestsAppLogStorage } from '../../test-data/storage/logStorage'; import { TestData } from '../../test-data/utilities'; @@ -220,6 +220,8 @@ export class AppVideoConfProviderManagerTestFixture { const statusFull = await manager.isFullyConfigured('full'); await Expect(statusFull).toBe(true); + SpyOn(AppVideoConfProvider.prototype, 'runIsFullyConfigured').andReturn(false); + const statusInvalid = await manager.isFullyConfigured('invalid'); await Expect(statusInvalid).toBe(false); } @@ -232,6 +234,7 @@ export class AppVideoConfProviderManagerTestFixture { const call = TestData.getVideoConfData(); + SpyOn(AppVideoConfProvider.prototype, 'runGenerateUrl').andReturn('test/first-call'); const url = await manager.generateUrl('test', call); await Expect(url).toBe('test/first-call'); } @@ -247,14 +250,28 @@ export class AppVideoConfProviderManagerTestFixture { const call = TestData.getVideoConfData(); - const url = await manager.generateUrl('test', call); - await Expect(url).toBe('test/first-call'); - - const url2 = await manager.generateUrl('test2', call); - await Expect(url2).toBe('test2/first-call'); + const cases: any = [ + { + name: 'test', call, + runGenerateUrl: 'test/first-call', + result: 'test/first-call', + }, + { + name: 'test2', call, + runGenerateUrl: 'test2/first-call', + result: 'test2/first-call', + }, + { + name: 'differentProvider', call, + runGenerateUrl: 'differentProvider/first-call', + result: 'differentProvider/first-call', + }, + ]; - const url3 = await manager.generateUrl('differentProvider', call); - await Expect(url3).toBe('differentProvider/first-call'); + for (const c of cases ) { + SpyOn(AppVideoConfProvider.prototype, 'runGenerateUrl').andReturn(c.runGenerateUrl); + await Expect(await manager.generateUrl(c.name, c.call)).toBe(c.result); + } } @AsyncTest() @@ -288,7 +305,6 @@ export class AppVideoConfProviderManagerTestFixture { await Expect(async () => await manager.customizeUrl('test', call, user, {})) .toThrowErrorAsync(VideoConfProviderNotRegisteredError, `The video conference provider "test" is not registered in the system.`); } - @AsyncTest() public async customizeUrl() { const manager = new AppVideoConfProviderManager(this.mockManager); @@ -298,8 +314,24 @@ export class AppVideoConfProviderManagerTestFixture { const call = TestData.getVideoConfDataExtended(); const user = TestData.getVideoConferenceUser(); - await Expect(await manager.customizeUrl('test', call, user, {})).toBe('test/first-call#caller'); - await Expect(await manager.customizeUrl('test', call, undefined, {})).toBe('test/first-call#'); + 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 ) { + SpyOn(AppVideoConfProvider.prototype, 'runCustomizeUrl').andReturn(c.runCustomizeUrl); + await Expect(await manager.customizeUrl(c.name, c.call, c.user, c.options)).toBe(c.result); + } + } @AsyncTest() @@ -314,14 +346,43 @@ export class AppVideoConfProviderManagerTestFixture { const call = TestData.getVideoConfDataExtended(); const user = TestData.getVideoConferenceUser(); - await Expect(await manager.customizeUrl('test', call, user, {})).toBe('test/first-call#caller'); - await Expect(await manager.customizeUrl('test', call, undefined, {})).toBe('test/first-call#'); - - await Expect(await manager.customizeUrl('test2', call, user, {})).toBe('test2/first-call#caller'); - await Expect(await manager.customizeUrl('test2', call, undefined, {})).toBe('test2/first-call#'); + const cases = [ + { + 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#', + }, + { + name: 'test2', call, user, options: {}, + runCustomizeUrl: 'test2/first-call#caller', + result: 'test2/first-call#caller', + }, + { + name: 'test2', call, user: undefined, options: {}, + runCustomizeUrl: 'test2/first-call#', + result: 'test2/first-call#', + }, + { + name: 'differentProvider', call, user, options: {}, + runCustomizeUrl: 'differentProvider/first-call#caller', + result: 'differentProvider/first-call#caller', + }, + { + name: 'differentProvider', call, user: undefined, options: {}, + runCustomizeUrl: 'differentProvider/first-call#', + result: 'differentProvider/first-call#', + }, + ]; - await Expect(await manager.customizeUrl('differentProvider', call, user, {})).toBe('differentProvider/first-call#caller'); - await Expect(await manager.customizeUrl('differentProvider', call, undefined, {})).toBe('differentProvider/first-call#'); + for (const c of cases ) { + SpyOn(AppVideoConfProvider.prototype, 'runCustomizeUrl').andReturn(c.runCustomizeUrl); + await Expect(await manager.customizeUrl(c.name, c.call, c.user, c.options)).toBe(c.result); + } } @AsyncTest() diff --git a/tests/test-data/bridges/roomBridge.ts b/tests/test-data/bridges/roomBridge.ts index b28103a22..1159e2456 100644 --- a/tests/test-data/bridges/roomBridge.ts +++ b/tests/test-data/bridges/roomBridge.ts @@ -43,4 +43,16 @@ export class TestsRoomBridge extends RoomBridge { public delete(roomId: string, appId: string): Promise { throw new Error('Method not implemented.'); } + + public getLeaders(roomId: string, appId: string): Promise> { + throw new Error('Method not implemented.'); + } + + public getModerators(roomId: string, appId: string): Promise> { + throw new Error('Method not implemented.'); + } + + public getOwners(roomId: string, appId: string): Promise> { + throw new Error('Method not implemented.'); + } } diff --git a/tests/test-data/utilities.ts b/tests/test-data/utilities.ts index d6a9393fa..19d9f8c68 100644 --- a/tests/test-data/utilities.ts +++ b/tests/test-data/utilities.ts @@ -353,7 +353,8 @@ export class TestData { return new ProxiedApp({} as AppManager, { status: AppStatus.UNKNOWN } as IAppStorageItem, { getName() { return 'testing'; }, getID() { return 'testing'; }, - } as App, (mod: string) => mod); + getRuntime() { return ({ runInSandbox: (mod: string) => mod }); }, + } as unknown as App, { runInSandbox: (mod: string) => mod } as any); } }