diff --git a/src/definition/accessors/IConfigurationExtend.ts b/src/definition/accessors/IConfigurationExtend.ts index 756b4e859..ecb36de99 100644 --- a/src/definition/accessors/IConfigurationExtend.ts +++ b/src/definition/accessors/IConfigurationExtend.ts @@ -5,6 +5,7 @@ import { ISchedulerExtend } from './ISchedulerExtend'; import { ISettingsExtend } from './ISettingsExtend'; import { ISlashCommandsExtend } from './ISlashCommandsExtend'; import { IUIExtend } from './IUIExtend'; +import { IVideoConfProvidersExtend } from './IVideoConfProvidersExtend'; /** * This accessor provides methods for declaring the configuration @@ -29,4 +30,7 @@ export interface IConfigurationExtend { readonly scheduler: ISchedulerExtend; /** Accessor for registering different elements in the host UI */ readonly ui: IUIExtend; + + /** Accessor for declaring the videoconf providers which your App provides. */ + readonly videoConfProviders: IVideoConfProvidersExtend; } diff --git a/src/definition/accessors/IVideoConfProvidersExtend.ts b/src/definition/accessors/IVideoConfProvidersExtend.ts new file mode 100644 index 000000000..452f551fa --- /dev/null +++ b/src/definition/accessors/IVideoConfProvidersExtend.ts @@ -0,0 +1,15 @@ +import { IVideoConfProvider } from '../videoConfProviders'; + +/** + * This accessor provides methods for adding videoconf providers. + * It is provided during the initialization of your App + */ + +export interface IVideoConfProvidersExtend { + /** + * Adds a videoconf provider + * + * @param provider the provider information + */ + provideVideoConfProvider(provider: IVideoConfProvider): Promise; +} diff --git a/src/definition/accessors/index.ts b/src/definition/accessors/index.ts index 470fab568..4c7a0d4d5 100644 --- a/src/definition/accessors/index.ts +++ b/src/definition/accessors/index.ts @@ -45,3 +45,4 @@ export * from './IUploadCreator'; export * from './IUploadRead'; export * from './IUserBuilder'; export * from './IUserRead'; +export * from './IVideoConfProvidersExtend'; diff --git a/src/definition/metadata/AppMethod.ts b/src/definition/metadata/AppMethod.ts index d6f04a041..7153e7a29 100644 --- a/src/definition/metadata/AppMethod.ts +++ b/src/definition/metadata/AppMethod.ts @@ -5,6 +5,8 @@ export enum AppMethod { _COMMAND_PREVIEWER = 'previewer', _COMMAND_PREVIEW_EXECUTOR = 'executePreviewItem', _JOB_PROCESSOR = 'jobProcessor', + _VIDEOCONF_GENERATE_URL = 'generateUrl', + _VIDEOCONF_CUSTOMIZE_URL = 'customizeUrl', INITIALIZE = 'initialize', ONENABLE = 'onEnable', ONDISABLE = 'onDisable', diff --git a/src/definition/videoConfProviders/IVideoConfProvider.ts b/src/definition/videoConfProviders/IVideoConfProvider.ts new file mode 100644 index 000000000..bb2365475 --- /dev/null +++ b/src/definition/videoConfProviders/IVideoConfProvider.ts @@ -0,0 +1,17 @@ +import { INewVideoConference, IVideoConference } from './IVideoConference'; +import { IVideoConferenceOptions } from './IVideoConferenceOptions'; +import { IVideoConferenceUser } from './IVideoConferenceUser'; + +/** + * Represents a video conference provider + */ +export interface IVideoConfProvider { + /** + * The function which gets called when a new video conference url is requested + */ + generateUrl(call: INewVideoConference): Promise; + /** + * The function which gets called whenever a user join url is requested + */ + customizeUrl(call: IVideoConference, user?: IVideoConferenceUser, options?: IVideoConferenceOptions): Promise; +} diff --git a/src/definition/videoConfProviders/IVideoConference.ts b/src/definition/videoConfProviders/IVideoConference.ts new file mode 100644 index 000000000..5adc61421 --- /dev/null +++ b/src/definition/videoConfProviders/IVideoConference.ts @@ -0,0 +1,13 @@ +import type { IVideoConferenceUser } from './IVideoConferenceUser'; + +export interface INewVideoConference { + _id: string; + type: 'direct' | 'videoconference'; + rid: string; + createdBy: IVideoConferenceUser; + title?: string; +} + +export interface IVideoConference extends INewVideoConference { + url: string; +} diff --git a/src/definition/videoConfProviders/IVideoConferenceOptions.ts b/src/definition/videoConfProviders/IVideoConferenceOptions.ts new file mode 100644 index 000000000..9dccc73f5 --- /dev/null +++ b/src/definition/videoConfProviders/IVideoConferenceOptions.ts @@ -0,0 +1,4 @@ +export interface IVideoConferenceOptions { + mic?: boolean; + cam?: boolean; +} diff --git a/src/definition/videoConfProviders/IVideoConferenceUser.ts b/src/definition/videoConfProviders/IVideoConferenceUser.ts new file mode 100644 index 000000000..86ab22435 --- /dev/null +++ b/src/definition/videoConfProviders/IVideoConferenceUser.ts @@ -0,0 +1,5 @@ +export interface IVideoConferenceUser { + _id: string; + username: string; + name: string; +} diff --git a/src/definition/videoConfProviders/index.ts b/src/definition/videoConfProviders/index.ts new file mode 100644 index 000000000..3653718c8 --- /dev/null +++ b/src/definition/videoConfProviders/index.ts @@ -0,0 +1,12 @@ +import { INewVideoConference, IVideoConference } from './IVideoConference'; +import { IVideoConferenceOptions } from './IVideoConferenceOptions'; +import { IVideoConferenceUser } from './IVideoConferenceUser'; +import { IVideoConfProvider } from './IVideoConfProvider'; + +export { + INewVideoConference, + IVideoConference, + IVideoConferenceOptions, + IVideoConferenceUser, + IVideoConfProvider, +}; diff --git a/src/server/AppManager.ts b/src/server/AppManager.ts index 89ea3cf72..63fd08a71 100644 --- a/src/server/AppManager.ts +++ b/src/server/AppManager.ts @@ -11,7 +11,7 @@ import { InvalidLicenseError } from './errors'; import { IGetAppsFilter } from './IGetAppsFilter'; import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppLicenseManager, AppListenerManager, AppSchedulerManager, AppSettingsManager, - AppSlashCommandManager, + AppSlashCommandManager, AppVideoConfProviderManager, } from './managers'; import { UIActionButtonManager } from './managers/UIActionButtonManager'; import { IMarketplaceInfo } from './marketplace'; @@ -60,6 +60,7 @@ export class AppManager { private readonly licenseManager: AppLicenseManager; private readonly schedulerManager: AppSchedulerManager; private readonly uiActionButtonManager: UIActionButtonManager; + private readonly videoConfProviderManager: AppVideoConfProviderManager; private isLoaded: boolean; @@ -106,6 +107,7 @@ export class AppManager { this.licenseManager = new AppLicenseManager(this); this.schedulerManager = new AppSchedulerManager(this); this.uiActionButtonManager = new UIActionButtonManager(this); + this.videoConfProviderManager = new AppVideoConfProviderManager(this); this.isLoaded = false; AppManager.Instance = this; @@ -151,6 +153,10 @@ export class AppManager { return this.commandManager; } + public getVideoConfProviderManager(): AppVideoConfProviderManager { + return this.videoConfProviderManager; + } + public getLicenseManager(): AppLicenseManager { return this.licenseManager; } @@ -870,6 +876,7 @@ export class AppManager { this.accessorManager.purifyApp(app.getID()); await this.schedulerManager.cleanUp(app.getID()); this.uiActionButtonManager.clearAppActionButtons(app.getID()); + this.videoConfProviderManager.unregisterProviders(app.getID()); } /** @@ -936,6 +943,7 @@ export class AppManager { this.apiManager.registerApis(app.getID()); this.listenerManager.registerListeners(app); this.listenerManager.releaseEssentialEvents(app); + this.videoConfProviderManager.registerProviders(app.getID()); } else { await this.purgeAppConfig(app); } diff --git a/src/server/accessors/ConfigurationExtend.ts b/src/server/accessors/ConfigurationExtend.ts index 5e6bad2f6..b656bebf6 100644 --- a/src/server/accessors/ConfigurationExtend.ts +++ b/src/server/accessors/ConfigurationExtend.ts @@ -7,6 +7,7 @@ import { ISettingsExtend, ISlashCommandsExtend, IUIExtend, + IVideoConfProvidersExtend, } from '../../definition/accessors'; export class ConfigurationExtend implements IConfigurationExtend { @@ -18,5 +19,6 @@ export class ConfigurationExtend implements IConfigurationExtend { public readonly externalComponents: IExternalComponentsExtend, public readonly scheduler: ISchedulerExtend, public readonly ui: IUIExtend, + public readonly videoConfProviders: IVideoConfProvidersExtend, ) { } } diff --git a/src/server/accessors/VideoConfProviderExtend.ts b/src/server/accessors/VideoConfProviderExtend.ts new file mode 100644 index 000000000..002072621 --- /dev/null +++ b/src/server/accessors/VideoConfProviderExtend.ts @@ -0,0 +1,12 @@ +import { AppVideoConfProviderManager } from '../managers/AppVideoConfProviderManager'; + +import { IVideoConfProvidersExtend } from '../../definition/accessors'; +import { IVideoConfProvider } from '../../definition/videoConfProviders'; + +export class VideoConfProviderExtend implements IVideoConfProvidersExtend { + constructor(private readonly manager: AppVideoConfProviderManager, private readonly appId: string) { } + + public provideVideoConfProvider(provider: IVideoConfProvider): Promise { + return Promise.resolve(this.manager.addProvider(this.appId, provider)); + } +} diff --git a/src/server/accessors/index.ts b/src/server/accessors/index.ts index 0ca3f47ce..06b512145 100644 --- a/src/server/accessors/index.ts +++ b/src/server/accessors/index.ts @@ -33,6 +33,7 @@ import { SlashCommandsModify } from './SlashCommandsModify'; import { UploadRead } from './UploadRead'; import { UserBuilder } from './UserBuilder'; import { UserRead } from './UserRead'; +import { VideoConfProviderExtend } from './VideoConfProviderExtend'; export { ApiExtend, @@ -70,4 +71,5 @@ export { UserRead, SchedulerExtend, SchedulerModify, + VideoConfProviderExtend, }; diff --git a/src/server/errors/AVideoConfProviderAlreadyExistsError.ts b/src/server/errors/AVideoConfProviderAlreadyExistsError.ts new file mode 100644 index 000000000..5f30df24c --- /dev/null +++ b/src/server/errors/AVideoConfProviderAlreadyExistsError.ts @@ -0,0 +1,4 @@ +export class AVideoConfProviderAlreadyExistsError implements Error { + public name = 'AVideoConfProviderAlreadyExists'; + public message = 'A video conference provider is already registered in the system.'; +} diff --git a/src/server/errors/NoVideoConfProviderRegisteredError.ts b/src/server/errors/NoVideoConfProviderRegisteredError.ts new file mode 100644 index 000000000..850d23704 --- /dev/null +++ b/src/server/errors/NoVideoConfProviderRegisteredError.ts @@ -0,0 +1,4 @@ +export class NoVideoConfProviderRegisteredError implements Error { + public name = 'NoVideoConfProviderRegistered'; + public message = 'There are no video conference providers registered in the system.'; +} diff --git a/src/server/errors/index.ts b/src/server/errors/index.ts index 0837ce92d..76efe4066 100644 --- a/src/server/errors/index.ts +++ b/src/server/errors/index.ts @@ -1,3 +1,4 @@ +import { AVideoConfProviderAlreadyExistsError } from './AVideoConfProviderAlreadyExistsError'; import { CommandAlreadyExistsError } from './CommandAlreadyExistsError'; import { CommandHasAlreadyBeenTouchedError } from './CommandHasAlreadyBeenTouchedError'; import { CompilerError } from './CompilerError'; @@ -5,6 +6,7 @@ import { InvalidLicenseError } from './InvalidLicenseError'; import { MustContainFunctionError } from './MustContainFunctionError'; import { MustExtendAppError } from './MustExtendAppError'; import { NotEnoughMethodArgumentsError } from './NotEnoughMethodArgumentsError'; +import { NoVideoConfProviderRegisteredError } from './NoVideoConfProviderRegisteredError'; import { PathAlreadyExistsError } from './PathAlreadyExistsError'; import { RequiredApiVersionError } from './RequiredApiVersionError'; @@ -18,4 +20,6 @@ export { NotEnoughMethodArgumentsError, RequiredApiVersionError, InvalidLicenseError, + NoVideoConfProviderRegisteredError, + AVideoConfProviderAlreadyExistsError, }; diff --git a/src/server/managers/AppAccessorManager.ts b/src/server/managers/AppAccessorManager.ts index b140030b4..7e023cee0 100644 --- a/src/server/managers/AppAccessorManager.ts +++ b/src/server/managers/AppAccessorManager.ts @@ -35,6 +35,7 @@ import { SlashCommandsModify, UploadRead, UserRead, + VideoConfProviderExtend, } from '../accessors'; import { CloudWorkspaceRead } from '../accessors/CloudWorkspaceRead'; import { UIExtend } from '../accessors/UIExtend'; @@ -87,13 +88,14 @@ export class AppAccessorManager { const htt = new HttpExtend(); const cmds = new SlashCommandsExtend(this.manager.getCommandManager(), appId); + const videoConf = new VideoConfProviderExtend(this.manager.getVideoConfProviderManager(), appId); const apis = new ApiExtend(this.manager.getApiManager(), appId); const sets = new SettingsExtend(rl); const excs = new ExternalComponentsExtend(this.manager.getExternalComponentManager(), appId); const scheduler = new SchedulerExtend(this.manager.getSchedulerManager(), appId); const ui = new UIExtend(this.manager.getUIActionButtonManager(), appId); - this.configExtenders.set(appId, new ConfigurationExtend(htt, sets, cmds, apis, excs, scheduler, ui)); + this.configExtenders.set(appId, new ConfigurationExtend(htt, sets, cmds, apis, excs, scheduler, ui, videoConf)); } return this.configExtenders.get(appId); diff --git a/src/server/managers/AppVideoConfProvider.ts b/src/server/managers/AppVideoConfProvider.ts new file mode 100644 index 000000000..365c13a68 --- /dev/null +++ b/src/server/managers/AppVideoConfProvider.ts @@ -0,0 +1,88 @@ +import { AppMethod } from '../../definition/metadata'; +import type { INewVideoConference, IVideoConference, IVideoConferenceOptions, IVideoConferenceUser, IVideoConfProvider } from '../../definition/videoConfProviders'; + +import { ProxiedApp } from '../ProxiedApp'; +import { AppLogStorage } from '../storage'; +import { AppAccessorManager } from './AppAccessorManager'; + +export class AppVideoConfProvider { + /** + * States whether this provider has been registered into the Rocket.Chat system or not. + */ + public isRegistered: boolean; + + constructor(public app: ProxiedApp, public provider: IVideoConfProvider) { + this.isRegistered = false; + } + + public hasBeenRegistered(): void { + this.isRegistered = true; + } + + public canBeRan(method: AppMethod): boolean { + return this.app.hasMethod(method); + } + + public async runGenerateUrl( + call: INewVideoConference, + logStorage: AppLogStorage, + accessors: AppAccessorManager, + ): Promise { + return await this.runTheCode(AppMethod._VIDEOCONF_GENERATE_URL, logStorage, accessors, [call]); + } + + public async runCustomizeUrl( + call: IVideoConference, + user: IVideoConferenceUser | undefined, + options: IVideoConferenceOptions = {}, + logStorage: AppLogStorage, + accessors: AppAccessorManager, + ): Promise { + return await this.runTheCode(AppMethod._VIDEOCONF_CUSTOMIZE_URL, logStorage, accessors, [call, user, options]); + } + + private async runTheCode( + method: AppMethod._VIDEOCONF_GENERATE_URL | AppMethod._VIDEOCONF_CUSTOMIZE_URL, + logStorage: AppLogStorage, + accessors: AppAccessorManager, + runContextArgs: Array, + ): Promise { + // Ensure the provider has the property before going on + if (typeof this.provider[method] !== 'function') { + return; + } + + const runContext = this.app.makeContext({ + provider: this.provider, + args: [ + ...runContextArgs, + 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(`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); + logger.debug(`Video Conference Provider's ${ method } was successfully executed.`); + } catch (e) { + logger.error(e); + logger.debug(`Video Conference Provider's ${ method } was unsuccessful.`); + } + + try { + 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/AppVideoConfProviderManager.ts b/src/server/managers/AppVideoConfProviderManager.ts new file mode 100644 index 000000000..e431ac55c --- /dev/null +++ b/src/server/managers/AppVideoConfProviderManager.ts @@ -0,0 +1,84 @@ +import type { INewVideoConference, IVideoConference, IVideoConferenceOptions, IVideoConferenceUser, IVideoConfProvider } from '../../definition/videoConfProviders'; +import { AppManager } from '../AppManager'; +import { AVideoConfProviderAlreadyExistsError, NoVideoConfProviderRegisteredError } from '../errors'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissions } from '../permissions/AppPermissions'; +import { AppAccessorManager } from './AppAccessorManager'; +import { AppPermissionManager } from './AppPermissionManager'; +import { AppVideoConfProvider } from './AppVideoConfProvider'; + +export class AppVideoConfProviderManager { + private readonly accessors: AppAccessorManager; + + private videoConfProviders: Map; + + constructor(private readonly manager: AppManager) { + this.accessors = this.manager.getAccessorManager(); + + this.videoConfProviders = new Map(); + } + public addProvider(appId: string, provider: IVideoConfProvider): void { + const app = this.manager.getOneById(appId); + if (!app) { + throw new Error('App must exist in order for a video conference provider to be added.'); + } + + if (!AppPermissionManager.hasPermission(appId, AppPermissions.videoConfProvider.default)) { + throw new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.videoConfProvider.default], + }); + } + + this.videoConfProviders.set(appId, new AppVideoConfProvider(app, provider)); + } + + public registerProviders(appId: string): void { + if (!this.videoConfProviders.has(appId)) { + return; + } + + const providerInfo = this.videoConfProviders.get(appId); + + const registeredProviderInfo = this.retrieveRegisteredProvider(); + if (registeredProviderInfo && registeredProviderInfo !== providerInfo) { + throw new AVideoConfProviderAlreadyExistsError(); + } + + providerInfo.hasBeenRegistered(); + } + + public unregisterProviders(appId: string): void { + if (!this.videoConfProviders.has(appId)) { + return; + } + + this.videoConfProviders.get(appId).isRegistered = false; + } + + public async generateUrl(call: INewVideoConference): Promise { + const providerInfo = this.retrieveRegisteredProvider(); + if (!providerInfo) { + throw new NoVideoConfProviderRegisteredError(); + } + + return providerInfo.runGenerateUrl(call, this.manager.getLogStorage(), this.accessors); + } + + public async customizeUrl(call: IVideoConference, user?: IVideoConferenceUser, options?: IVideoConferenceOptions): Promise { + const providerInfo = this.retrieveRegisteredProvider(); + if (!providerInfo) { + throw new NoVideoConfProviderRegisteredError(); + } + + return providerInfo.runCustomizeUrl(call, user, options, this.manager.getLogStorage(), this.accessors); + } + + private retrieveRegisteredProvider(): AppVideoConfProvider | undefined { + for (const [, provider] of this.videoConfProviders) { + if (provider.isRegistered) { + return provider; + } + } + } +} diff --git a/src/server/managers/index.ts b/src/server/managers/index.ts index 23f3ba7b8..d8763124b 100644 --- a/src/server/managers/index.ts +++ b/src/server/managers/index.ts @@ -6,6 +6,7 @@ import { AppListenerManager } from './AppListenerManager'; import { AppSchedulerManager } from './AppSchedulerManager'; import { AppSettingsManager } from './AppSettingsManager'; import { AppSlashCommandManager } from './AppSlashCommandManager'; +import { AppVideoConfProviderManager } from './AppVideoConfProviderManager'; export { AppAccessorManager, @@ -16,4 +17,5 @@ export { AppSlashCommandManager, AppApiManager, AppSchedulerManager, + AppVideoConfProviderManager, }; diff --git a/src/server/permissions/AppPermissions.ts b/src/server/permissions/AppPermissions.ts index 2c1b876e7..4dc355084 100644 --- a/src/server/permissions/AppPermissions.ts +++ b/src/server/permissions/AppPermissions.ts @@ -84,6 +84,9 @@ export const AppPermissions = { 'command': { default: { name: 'slashcommand' }, }, + 'videoConfProvider': { + default: { name: 'videoconf-provider' }, + }, 'apis': { default: { name: 'api' }, }, @@ -116,5 +119,6 @@ export const defaultPermissions: Array = [ AppPermissions.persistence.default, AppPermissions.env.read, AppPermissions.command.default, + AppPermissions.videoConfProvider.default, AppPermissions.apis.default, ]; diff --git a/tests/server/AppManager.spec.ts b/tests/server/AppManager.spec.ts index cb1b0599d..b43e9ce82 100644 --- a/tests/server/AppManager.spec.ts +++ b/tests/server/AppManager.spec.ts @@ -5,7 +5,7 @@ import { SimpleClass, TestInfastructureSetup } from '../test-data/utilities'; import { AppManager } from '../../src/server/AppManager'; import { AppBridges } from '../../src/server/bridges'; import { AppCompiler, AppPackageParser } from '../../src/server/compiler'; -import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppListenerManager, AppSettingsManager, AppSlashCommandManager } from '../../src/server/managers'; +import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppListenerManager, AppSettingsManager, AppSlashCommandManager, AppVideoConfProviderManager } from '../../src/server/managers'; import { AppLogStorage, AppMetadataStorage, AppSourceStorage } from '../../src/server/storage'; export class AppManagerTestFixture { @@ -95,5 +95,6 @@ export class AppManagerTestFixture { Expect(manager.getExternalComponentManager() instanceof AppExternalComponentManager).toBe(true); Expect(manager.getApiManager() instanceof AppApiManager).toBe(true); Expect(manager.getSettingsManager() instanceof AppSettingsManager).toBe(true); + Expect(manager.getVideoConfProviderManager() instanceof AppVideoConfProviderManager).toBe(true); } } diff --git a/tests/server/accessors/AppAccessors.spec.ts b/tests/server/accessors/AppAccessors.spec.ts index 7011f988c..caf3fd0c7 100644 --- a/tests/server/accessors/AppAccessors.spec.ts +++ b/tests/server/accessors/AppAccessors.spec.ts @@ -7,7 +7,7 @@ import { AppAccessors } from '../../../src/server/accessors'; import { AppManager } from '../../../src/server/AppManager'; import { AppBridges } from '../../../src/server/bridges'; import { AppConsole } from '../../../src/server/logging'; -import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSlashCommandManager } from '../../../src/server/managers'; +import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSlashCommandManager, AppVideoConfProviderManager } from '../../../src/server/managers'; import { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager'; import { ProxiedApp } from '../../../src/server/ProxiedApp'; import { AppLogStorage } from '../../../src/server/storage'; @@ -73,6 +73,9 @@ export class AppAccessorsTestFixture { getUIActionButtonManager() { return {} as UIActionButtonManager; }, + getVideoConfProviderManager() { + return {} as AppVideoConfProviderManager; + }, } as AppManager; this.mockAccessors = new AppAccessorManager(this.mockManager); diff --git a/tests/server/accessors/ConfigurationExtend.spec.ts b/tests/server/accessors/ConfigurationExtend.spec.ts index 226a52fba..80b21d38d 100644 --- a/tests/server/accessors/ConfigurationExtend.spec.ts +++ b/tests/server/accessors/ConfigurationExtend.spec.ts @@ -1,5 +1,5 @@ import { Expect, SetupFixture, Test } from 'alsatian'; -import { IApiExtend, IExternalComponentsExtend, IHttpExtend, ISchedulerExtend, ISettingsExtend, ISlashCommandsExtend, IUIExtend } from '../../../src/definition/accessors'; +import { IApiExtend, IExternalComponentsExtend, IHttpExtend, ISchedulerExtend, ISettingsExtend, ISlashCommandsExtend, IUIExtend, IVideoConfProvidersExtend } from '../../../src/definition/accessors'; import { ConfigurationExtend } from '../../../src/server/accessors'; @@ -11,6 +11,7 @@ export class ConfigurationExtendTestFixture { private externalComponent: IExternalComponentsExtend; private schedulerExtend: ISchedulerExtend; private uiExtend: IUIExtend; + private vcProvidersExtend: IVideoConfProvidersExtend; @SetupFixture public setupFixture() { @@ -21,16 +22,20 @@ export class ConfigurationExtendTestFixture { this.externalComponent = {} as IExternalComponentsExtend; this.schedulerExtend = {} as ISchedulerExtend; this.uiExtend = {} as IUIExtend; + this.vcProvidersExtend = {} as IVideoConfProvidersExtend; } @Test() public useConfigurationExtend() { - Expect(() => new ConfigurationExtend(this.he, this.se, this.sce, this.api, this.externalComponent, this.schedulerExtend, this.uiExtend)).not.toThrow(); + Expect(() => new ConfigurationExtend(this.he, this.se, this.sce, this.api, this.externalComponent, + this.schedulerExtend, this.uiExtend, this.vcProvidersExtend)).not.toThrow(); - const se = new ConfigurationExtend(this.he, this.se, this.sce, this.api, this.externalComponent, this.schedulerExtend, this.uiExtend); + const se = new ConfigurationExtend(this.he, this.se, this.sce, this.api, this.externalComponent, + this.schedulerExtend, this.uiExtend, this.vcProvidersExtend); Expect(se.http).toBeDefined(); Expect(se.settings).toBeDefined(); Expect(se.slashCommands).toBeDefined(); Expect(se.externalComponents).toBeDefined(); + Expect(se.videoConfProviders).toBeDefined(); } } diff --git a/tests/server/accessors/VideoConfProviderExtend.spec.ts b/tests/server/accessors/VideoConfProviderExtend.spec.ts new file mode 100644 index 000000000..b8313e5ec --- /dev/null +++ b/tests/server/accessors/VideoConfProviderExtend.spec.ts @@ -0,0 +1,44 @@ +// tslint:disable:max-line-length + +import { AsyncTest, Expect, Test } from 'alsatian'; +import { IVideoConfProvider } from '../../../src/definition/videoConfProviders'; + +import { VideoConfProviderExtend } from '../../../src/server/accessors'; +import { AVideoConfProviderAlreadyExistsError } from '../../../src/server/errors'; +import { AppVideoConfProviderManager } from '../../../src/server/managers'; + +export class VideoConfProviderExtendAccessorTestFixture { + @Test() + public basicVideoConfProviderExtend() { + Expect(() => new VideoConfProviderExtend({} as AppVideoConfProviderManager, 'testing')).not.toThrow(); + } + + @AsyncTest() + public async provideProviderToVideoConfProviderExtend(): Promise { + let providerAdded: boolean = false; + const mockManager: AppVideoConfProviderManager = { + addProvider(appId: string, provider: IVideoConfProvider) { + if (providerAdded) { + throw new AVideoConfProviderAlreadyExistsError(); + } + + providerAdded = true; + }, + } as AppVideoConfProviderManager; + + const se = new VideoConfProviderExtend(mockManager, 'testing'); + + const mockProvider: IVideoConfProvider = { + async generateUrl(): Promise { + return ''; + }, + async customizeUrl(): Promise { + return ''; + }, + } as IVideoConfProvider; + + await Expect(async () => await se.provideVideoConfProvider(mockProvider)).not.toThrowAsync(); + Expect(providerAdded).toBe(true); + await Expect(async () => await se.provideVideoConfProvider(mockProvider)).toThrowErrorAsync(AVideoConfProviderAlreadyExistsError, 'A video conference provider is already registered in the system.'); + } +} diff --git a/tests/server/managers/AppAccessorManager.spec.ts b/tests/server/managers/AppAccessorManager.spec.ts index 81088e5ef..7ed9b1c13 100644 --- a/tests/server/managers/AppAccessorManager.spec.ts +++ b/tests/server/managers/AppAccessorManager.spec.ts @@ -2,7 +2,7 @@ import { Expect, RestorableFunctionSpy, Setup, SetupFixture, SpyOn, Teardown, Te import { AppManager } from '../../../src/server/AppManager'; import { AppBridges } from '../../../src/server/bridges'; -import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSlashCommandManager } from '../../../src/server/managers'; +import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSlashCommandManager, AppVideoConfProviderManager } from '../../../src/server/managers'; import { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager'; import { ProxiedApp } from '../../../src/server/ProxiedApp'; import { TestsAppBridges } from '../../test-data/bridges/appBridges'; @@ -39,6 +39,9 @@ export class AppAccessorManagerTestFixture { getUIActionButtonManager() { return {} as UIActionButtonManager; }, + getVideoConfProviderManager() { + return {} as AppVideoConfProviderManager; + }, } as AppManager; } diff --git a/tests/server/managers/AppApiManager.spec.ts b/tests/server/managers/AppApiManager.spec.ts index 8137e060a..3f481fb4b 100644 --- a/tests/server/managers/AppApiManager.spec.ts +++ b/tests/server/managers/AppApiManager.spec.ts @@ -10,7 +10,7 @@ import { AppManager } from '../../../src/server/AppManager'; import { AppBridges } from '../../../src/server/bridges'; import { PathAlreadyExistsError } from '../../../src/server/errors'; import { AppConsole } from '../../../src/server/logging'; -import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSlashCommandManager } from '../../../src/server/managers'; +import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSlashCommandManager, AppVideoConfProviderManager } from '../../../src/server/managers'; import { AppApi } from '../../../src/server/managers/AppApi'; import { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager'; import { ProxiedApp } from '../../../src/server/ProxiedApp'; @@ -80,6 +80,9 @@ export class AppApiManagerTestFixture { getUIActionButtonManager() { return {} as UIActionButtonManager; }, + getVideoConfProviderManager() { + return {} as AppVideoConfProviderManager; + }, } as AppManager; this.mockAccessors = new AppAccessorManager(this.mockManager); diff --git a/tests/server/managers/AppSettingsManager.spec.ts b/tests/server/managers/AppSettingsManager.spec.ts index 6a70d5aba..46f644d2f 100644 --- a/tests/server/managers/AppSettingsManager.spec.ts +++ b/tests/server/managers/AppSettingsManager.spec.ts @@ -6,7 +6,7 @@ import { TestData } from '../../test-data/utilities'; import { AppManager } from '../../../src/server/AppManager'; import { AppBridges } from '../../../src/server/bridges'; -import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSettingsManager, AppSlashCommandManager } from '../../../src/server/managers'; +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 { AppMetadataStorage, IAppStorageItem } from '../../../src/server/storage'; @@ -80,6 +80,9 @@ export class AppSettingsManagerTestFixture { getUIActionButtonManager() { return {} as UIActionButtonManager; }, + getVideoConfProviderManager() { + return {} as AppVideoConfProviderManager; + }, } as AppManager; this.mockAccessors = new AppAccessorManager(this.mockManager); diff --git a/tests/server/managers/AppSlashCommandManager.spec.ts b/tests/server/managers/AppSlashCommandManager.spec.ts index f559d0b9c..17ce88be7 100644 --- a/tests/server/managers/AppSlashCommandManager.spec.ts +++ b/tests/server/managers/AppSlashCommandManager.spec.ts @@ -13,7 +13,7 @@ import { AppManager } from '../../../src/server/AppManager'; import { AppBridges } from '../../../src/server/bridges'; import { CommandAlreadyExistsError, CommandHasAlreadyBeenTouchedError } from '../../../src/server/errors'; import { AppConsole } from '../../../src/server/logging'; -import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSlashCommandManager } from '../../../src/server/managers'; +import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSlashCommandManager, AppVideoConfProviderManager } from '../../../src/server/managers'; import { AppSlashCommand } from '../../../src/server/managers/AppSlashCommand'; import { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager'; import { ProxiedApp } from '../../../src/server/ProxiedApp'; @@ -81,6 +81,9 @@ export class AppSlashCommandManagerTestFixture { getUIActionButtonManager() { return {} as UIActionButtonManager; }, + getVideoConfProviderManager() { + return {} as AppVideoConfProviderManager; + }, } as AppManager; this.mockAccessors = new AppAccessorManager(this.mockManager); diff --git a/tests/server/managers/AppVideoConfProvider.spec.ts b/tests/server/managers/AppVideoConfProvider.spec.ts new file mode 100644 index 000000000..a57b16576 --- /dev/null +++ b/tests/server/managers/AppVideoConfProvider.spec.ts @@ -0,0 +1,33 @@ +import { Expect, SetupFixture, Test } from 'alsatian'; +import { AppMethod } from '../../../src/definition/metadata'; +import { IVideoConfProvider } from '../../../src/definition/videoConfProviders'; + +import { AppVideoConfProvider } from '../../../src/server/managers/AppVideoConfProvider'; +import { ProxiedApp } from '../../../src/server/ProxiedApp'; + +export class AppSlashCommandRegistrationTestFixture { + private mockApp: ProxiedApp; + + @SetupFixture + public setupFixture() { + this.mockApp = { + hasMethod(method: AppMethod): boolean { + return true; + }, + } as ProxiedApp; + } + + @Test() + public ensureAppVideoConfManager() { + Expect(() => new AppVideoConfProvider(this.mockApp, {} as IVideoConfProvider)).not.toThrow(); + + const ascr = new AppVideoConfProvider(this.mockApp, {} as IVideoConfProvider); + Expect(ascr.isRegistered).toBe(false); + + ascr.hasBeenRegistered(); + Expect(ascr.isRegistered).toBe(true); + + Expect(ascr.canBeRan(AppMethod._VIDEOCONF_GENERATE_URL)).toBe(true); + Expect(ascr.canBeRan(AppMethod._VIDEOCONF_CUSTOMIZE_URL)).toBe(true); + } +} diff --git a/tests/server/managers/AppVideoConfProviderManager.spec.ts b/tests/server/managers/AppVideoConfProviderManager.spec.ts new file mode 100644 index 000000000..160057312 --- /dev/null +++ b/tests/server/managers/AppVideoConfProviderManager.spec.ts @@ -0,0 +1,186 @@ +import { AsyncTest, Expect, Setup, SetupFixture, Teardown, Test } from 'alsatian'; +import { TestsAppBridges } from '../../test-data/bridges/appBridges'; +import { TestsAppLogStorage } from '../../test-data/storage/logStorage'; +import { TestData } from '../../test-data/utilities'; + +import { AppManager } from '../../../src/server/AppManager'; +import { AppBridges } from '../../../src/server/bridges'; +import { AVideoConfProviderAlreadyExistsError, NoVideoConfProviderRegisteredError } from '../../../src/server/errors'; +import { AppAccessorManager, AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppSlashCommandManager, AppVideoConfProviderManager } from '../../../src/server/managers'; +import { AppVideoConfProvider } from '../../../src/server/managers/AppVideoConfProvider'; +import { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager'; +import { ProxiedApp } from '../../../src/server/ProxiedApp'; +import { AppLogStorage } from '../../../src/server/storage'; + +export class AppVideoConfProviderManagerTestFixture { + public static doThrow: boolean = false; + private mockBridges: TestsAppBridges; + private mockApp: ProxiedApp; + private mockAccessors: AppAccessorManager; + private mockManager: AppManager; + + @SetupFixture + public setupFixture() { + this.mockBridges = new TestsAppBridges(); + + this.mockApp = TestData.getMockApp('testing', 'testing'); + + const bri = this.mockBridges; + const app = this.mockApp; + this.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; + + this.mockAccessors = new AppAccessorManager(this.mockManager); + const ac = this.mockAccessors; + this.mockManager.getAccessorManager = function _getAccessorManager(): AppAccessorManager { + return ac; + }; + } + + @Setup + public setup() { + } + + @Teardown + public teardown() { + } + + @Test() + public basicAppVideoConfProviderManager() { + Expect(() => new AppVideoConfProviderManager({} as AppManager)).toThrow(); + Expect(() => new AppVideoConfProviderManager(this.mockManager)).not.toThrow(); + + const manager = new AppVideoConfProviderManager(this.mockManager); + Expect((manager as any).manager).toBe(this.mockManager); + Expect((manager as any).accessors).toBe(this.mockManager.getAccessorManager()); + Expect((manager as any).videoConfProviders).toBeDefined(); + Expect((manager as any).videoConfProviders.size).toBe(0); + } + + @Test() + public addProvider() { + const provider = TestData.getVideoConfProvider(); + const manager = new AppVideoConfProviderManager(this.mockManager); + + Expect(() => manager.addProvider('testing', provider)).not.toThrow(); + Expect((manager as any).videoConfProviders.size).toBe(1); + Expect(() => manager.addProvider('failMePlease', provider)) + .toThrowError(Error, 'App must exist in order for a video conference provider to be added.'); + Expect((manager as any).videoConfProviders.size).toBe(1); + } + + @Test() + public registerProviders() { + const manager = new AppVideoConfProviderManager(this.mockManager); + + manager.addProvider('testing', TestData.getVideoConfProvider()); + const firstRegInfo = (manager as any).videoConfProviders.get('testing') as AppVideoConfProvider; + + manager.addProvider('testing2', TestData.getVideoConfProvider()); + const secondRegInfo = (manager as any).videoConfProviders.get('testing2') as AppVideoConfProvider; + + Expect(() => manager.registerProviders('non-existant')).not.toThrow(); + Expect(() => manager.registerProviders('testing')).not.toThrow(); + Expect(firstRegInfo.isRegistered).toBe(true); + Expect(secondRegInfo.isRegistered).toBe(false); + + Expect(() => manager.registerProviders('testing2')) + .toThrowError(AVideoConfProviderAlreadyExistsError, 'A video conference provider is already registered in the system.'); + } + + @Test() + public unregisterProviders() { + const manager = new AppVideoConfProviderManager(this.mockManager); + + manager.addProvider('testing', TestData.getVideoConfProvider()); + const regInfo = (manager as any).videoConfProviders.get('testing') as AppVideoConfProvider; + Expect(() => manager.registerProviders('testing')).not.toThrow(); + + Expect(() => manager.unregisterProviders('non-existant')).not.toThrow(); + Expect(regInfo.isRegistered).toBe(true); + Expect(() => manager.unregisterProviders('testing')).not.toThrow(); + Expect(regInfo.isRegistered).toBe(false); + } + + @AsyncTest() + public async failToGenerateUrlWithoutProvider() { + const manager = new AppVideoConfProviderManager(this.mockManager); + + const call = TestData.getVideoConference(); + + await Expect(async () => manager.generateUrl(call)) + .toThrowErrorAsync(NoVideoConfProviderRegisteredError, 'There are no video conference providers registered in the system.'); + + manager.addProvider('testing', TestData.getVideoConfProvider()); + + await Expect(async () => await manager.generateUrl(call)) + .toThrowErrorAsync(NoVideoConfProviderRegisteredError, 'There are no video conference providers registered in the system.'); + } + + @AsyncTest() + public async generateUrl() { + const manager = new AppVideoConfProviderManager(this.mockManager); + manager.addProvider('testing', TestData.getVideoConfProvider()); + manager.registerProviders('testing'); + + const call = TestData.getVideoConference(); + + const url = await manager.generateUrl(call); + await Expect(url).toBe('video-conf/first-call'); + } + + @AsyncTest() + public async failToCustomizeUrlWithoutProvider() { + const manager = new AppVideoConfProviderManager(this.mockManager); + const call = TestData.getVideoConference(); + const user = TestData.getVideoConferenceUser(); + + await Expect(async () => await manager.customizeUrl(call, user, {})) + .toThrowErrorAsync(NoVideoConfProviderRegisteredError, 'There are no video conference providers registered in the system.'); + + manager.addProvider('testing', TestData.getVideoConfProvider()); + + await Expect(async () => await manager.customizeUrl(call, user, {})) + .toThrowErrorAsync(NoVideoConfProviderRegisteredError, 'There are no video conference providers registered in the system.'); + } + + @AsyncTest() + public async customizeUrl() { + const manager = new AppVideoConfProviderManager(this.mockManager); + manager.addProvider('testing', TestData.getVideoConfProvider()); + manager.registerProviders('testing'); + + const call = TestData.getVideoConference(); + const user = TestData.getVideoConferenceUser(); + + await Expect(await manager.customizeUrl(call, user, {})).toBe('video-conf/first-call#caller'); + await Expect(await manager.customizeUrl(call, undefined, {})).toBe('video-conf/first-call#'); + } +} diff --git a/tests/test-data/utilities.ts b/tests/test-data/utilities.ts index b7c0336da..33241ede3 100644 --- a/tests/test-data/utilities.ts +++ b/tests/test-data/utilities.ts @@ -14,8 +14,13 @@ import { TestSourceStorage } from './storage/TestSourceStorage'; import { ApiSecurity, ApiVisibility, IApi, IApiRequest, IApiResponse } from '../../src/definition/api'; import { IApiEndpointInfo } from '../../src/definition/api/IApiEndpointInfo'; +import { App } from '../../src/definition/App'; +import { AppStatus } from '../../src/definition/AppStatus'; +import { INewVideoConference, IVideoConference, IVideoConferenceOptions, IVideoConferenceUser, IVideoConfProvider } from '../../src/definition/videoConfProviders'; +import { AppManager } from '../../src/server/AppManager'; import { AppBridges } from '../../src/server/bridges'; -import { AppLogStorage, AppMetadataStorage, AppSourceStorage } from '../../src/server/storage'; +import { ProxiedApp } from '../../src/server/ProxiedApp'; +import { AppLogStorage, AppMetadataStorage, AppSourceStorage, IAppStorageItem } from '../../src/server/storage'; export class TestInfastructureSetup { private appStorage: TestsAppStorage; @@ -185,6 +190,44 @@ export class TestData { }], }; } + + public static getVideoConfProvider(): IVideoConfProvider { + return { + async generateUrl(call: INewVideoConference): Promise { + return `video-conf/${call._id}`; + }, + + async customizeUrl(call: IVideoConference, user: IVideoConferenceUser | undefined, options: IVideoConferenceOptions): Promise { + return `video-conf/${call._id}#${user ? user.username : ''}`; + }, + }; + } + + public static getVideoConferenceUser(): IVideoConferenceUser { + return { + _id: 'callerId', + username: 'caller', + name: 'John Caller', + }; + } + + public static getVideoConference(): IVideoConference { + return { + _id: 'first-call', + type: 'videoconference', + rid: 'roomId', + url: 'video-conf/first-call', + createdBy: this.getVideoConferenceUser(), + title: 'Test Call', + }; + } + + public static getMockApp(id: string, name: string): ProxiedApp { + return new ProxiedApp({} as AppManager, { status: AppStatus.UNKNOWN } as IAppStorageItem, { + getName() { return 'testing'; }, + getID() { return 'testing'; }, + } as App, (mod: string) => mod); + } } export class SimpleClass {