feat: license add-ons (external modules) (#33433)
parent
2806cb5d3e
commit
81998f3450
@ -0,0 +1,8 @@ |
||||
--- |
||||
'@rocket.chat/core-typings': minor |
||||
'@rocket.chat/apps-engine': minor |
||||
'@rocket.chat/license': minor |
||||
'@rocket.chat/meteor': minor |
||||
--- |
||||
|
||||
Added support for interacting with add-ons issued in the license |
||||
@ -1,25 +1,49 @@ |
||||
import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; |
||||
import { Apps } from '@rocket.chat/core-services'; |
||||
import { License } from '@rocket.chat/license'; |
||||
import type { LicenseModule } from '@rocket.chat/core-typings'; |
||||
import { License, type LicenseImp } from '@rocket.chat/license'; |
||||
|
||||
import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; |
||||
|
||||
export const canEnableApp = async (app: IAppStorageItem): Promise<boolean> => { |
||||
type _canEnableAppDependencies = { |
||||
Apps: typeof Apps; |
||||
License: LicenseImp; |
||||
}; |
||||
|
||||
export const _canEnableApp = async ({ Apps, License }: _canEnableAppDependencies, app: IAppStorageItem): Promise<void> => { |
||||
if (!(await Apps.isInitialized())) { |
||||
return false; |
||||
throw new Error('apps-engine-not-initialized'); |
||||
} |
||||
|
||||
// Migrated apps were installed before the validation was implemented
|
||||
// so they're always allowed to be enabled
|
||||
if (app.migrated) { |
||||
return true; |
||||
return; |
||||
} |
||||
|
||||
if (app.info.addon && !License.hasModule(app.info.addon as LicenseModule)) { |
||||
throw new Error('app-addon-not-valid'); |
||||
} |
||||
|
||||
const source = getInstallationSourceFromAppStorageItem(app); |
||||
switch (source) { |
||||
case 'private': |
||||
return !(await License.shouldPreventAction('privateApps')); |
||||
if (await License.shouldPreventAction('privateApps')) { |
||||
throw new Error('license-prevented'); |
||||
} |
||||
|
||||
break; |
||||
default: |
||||
return !(await License.shouldPreventAction('marketplaceApps')); |
||||
if (await License.shouldPreventAction('marketplaceApps')) { |
||||
throw new Error('license-prevented'); |
||||
} |
||||
|
||||
if (app.marketplaceInfo?.isEnterpriseOnly && !License.hasValidLicense()) { |
||||
throw new Error('invalid-license'); |
||||
} |
||||
|
||||
break; |
||||
} |
||||
}; |
||||
|
||||
export const canEnableApp = async (app: IAppStorageItem): Promise<void> => _canEnableApp({ Apps, License }, app); |
||||
|
||||
@ -1,42 +0,0 @@ |
||||
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; |
||||
import type { IMarketplaceInfo } from '@rocket.chat/apps-engine/server/marketplace'; |
||||
import type { Logger } from '@rocket.chat/logger'; |
||||
|
||||
import { getMarketplaceAppInfo } from './appInfo'; |
||||
|
||||
export const appEnableCheck = async ({ |
||||
baseUrl, |
||||
headers, |
||||
appId, |
||||
version, |
||||
logger, |
||||
status, |
||||
marketplaceInfo, |
||||
}: { |
||||
baseUrl: string; |
||||
headers: Record<string, any>; |
||||
appId: string; |
||||
version: string; |
||||
logger: Logger; |
||||
status: AppStatus; |
||||
marketplaceInfo?: IMarketplaceInfo; |
||||
}) => { |
||||
let isAppEnterpriseOnly = false; |
||||
|
||||
if (marketplaceInfo?.isEnterpriseOnly !== undefined) { |
||||
isAppEnterpriseOnly = marketplaceInfo.isEnterpriseOnly; |
||||
} else { |
||||
try { |
||||
const { isEnterpriseOnly } = await getMarketplaceAppInfo({ baseUrl, headers, appId, version }); |
||||
|
||||
isAppEnterpriseOnly = !!isEnterpriseOnly; |
||||
} catch (error: any) { |
||||
logger.error('Error getting the app info from the Marketplace:', error.message); |
||||
throw new Error(error.message); |
||||
} |
||||
} |
||||
|
||||
if (![AppStatus.DISABLED, AppStatus.MANUALLY_DISABLED].includes(status) && isAppEnterpriseOnly) { |
||||
throw new Error('Invalid environment for enabling enterprise app'); |
||||
} |
||||
}; |
||||
@ -0,0 +1,46 @@ |
||||
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; |
||||
import type { LicenseImp } from '@rocket.chat/license'; |
||||
|
||||
import { i18n } from '../../../../server/lib/i18n'; |
||||
import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins'; |
||||
import { Apps } from '../../apps'; |
||||
|
||||
type OnModuleCallbackParameter = Parameters<Parameters<LicenseImp['onModule']>[0]>[0]; |
||||
|
||||
export async function _disableAppsWithAddonsCallback( |
||||
deps: { Apps: typeof Apps; sendMessagesToAdmins: typeof sendMessagesToAdmins }, |
||||
{ module, external, valid }: OnModuleCallbackParameter, |
||||
) { |
||||
if (!external || valid) return; |
||||
|
||||
const enabledApps = await deps.Apps.installedApps({ enabled: true }); |
||||
|
||||
if (!enabledApps) return; |
||||
|
||||
const affectedApps: string[] = []; |
||||
|
||||
await Promise.all( |
||||
enabledApps.map(async (app) => { |
||||
if (app.getInfo().addon !== module) return; |
||||
|
||||
affectedApps.push(app.getName()); |
||||
|
||||
return deps.Apps.getManager()?.disable(app.getID(), AppStatus.DISABLED, false); |
||||
}), |
||||
); |
||||
|
||||
if (!affectedApps.length) return; |
||||
|
||||
await deps.sendMessagesToAdmins({ |
||||
msgs: async ({ adminUser }) => ({ |
||||
msg: i18n.t('App_has_been_disabled_addon_message', { |
||||
lng: adminUser.language || 'en', |
||||
count: affectedApps.length, |
||||
appNames: affectedApps, |
||||
}), |
||||
}), |
||||
}); |
||||
} |
||||
|
||||
export const disableAppsWithAddonsCallback = (ctx: OnModuleCallbackParameter) => |
||||
_disableAppsWithAddonsCallback({ Apps, sendMessagesToAdmins }, ctx); |
||||
@ -0,0 +1,20 @@ |
||||
import { License } from '@rocket.chat/license'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { Apps } from '../apps'; |
||||
import { disableAppsWithAddonsCallback } from '../lib/apps/disableAppsWithAddonsCallback'; |
||||
|
||||
Meteor.startup(() => { |
||||
async function migratePrivateAppsCallback() { |
||||
if (!Apps.isInitialized) return; |
||||
|
||||
void Apps.migratePrivateApps(); |
||||
void Apps.disableMarketplaceApps(); |
||||
} |
||||
|
||||
License.onInvalidateLicense(migratePrivateAppsCallback); |
||||
License.onRemoveLicense(migratePrivateAppsCallback); |
||||
|
||||
// Disable apps that depend on add-ons (external modules) if they are invalidated
|
||||
License.onModule(disableAppsWithAddonsCallback); |
||||
}); |
||||
@ -1 +0,0 @@ |
||||
import './trialExpiration'; |
||||
@ -1,16 +0,0 @@ |
||||
import { License } from '@rocket.chat/license'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { Apps } from '../../apps'; |
||||
|
||||
Meteor.startup(async () => { |
||||
const updateAppsCallback = async () => { |
||||
if (!Apps.isInitialized) return; |
||||
|
||||
void Apps.migratePrivateApps(); |
||||
void Apps.disableMarketplaceApps(); |
||||
}; |
||||
|
||||
License.onInvalidateLicense(updateAppsCallback); |
||||
License.onRemoveLicense(updateAppsCallback); |
||||
}); |
||||
@ -0,0 +1,233 @@ |
||||
import { expect, spy } from 'chai'; |
||||
import proxyquire from 'proxyquire'; |
||||
|
||||
import type { AppServerOrchestrator } from '../../../../../../ee/server/apps/orchestrator'; |
||||
|
||||
const { _disableAppsWithAddonsCallback } = proxyquire |
||||
.noCallThru() |
||||
.load('../../../../../../ee/server/lib/apps/disableAppsWithAddonsCallback', { |
||||
'../../apps': {}, |
||||
'../../../../server/lib/sendMessagesToAdmins': { sendMessagesToAdmins: () => undefined }, |
||||
'../../../../server/lib/i18n': { |
||||
i18n: { t: () => undefined }, |
||||
}, |
||||
}); |
||||
|
||||
/** |
||||
* I've used named "empty" functions to spy on as it is easier to |
||||
* troubleshoot if the assertion fails. |
||||
* If we use `spy()` instead, there is no clear indication on the |
||||
* error message which of the spy assertions failed |
||||
*/ |
||||
|
||||
describe('disableAppsWithAddonsCallback', () => { |
||||
function sendMessagesToAdmins(): any { |
||||
return undefined; |
||||
} |
||||
|
||||
it('should not execute anything if not external module', async () => { |
||||
function installedApps() { |
||||
return []; |
||||
} |
||||
|
||||
function getManagerDisable() { |
||||
return undefined; |
||||
} |
||||
|
||||
const mockManager = { |
||||
disable: spy(getManagerDisable), |
||||
}; |
||||
|
||||
const AppsMock = { |
||||
installedApps: spy(installedApps), |
||||
getManager: () => mockManager, |
||||
} as unknown as AppServerOrchestrator; |
||||
|
||||
await _disableAppsWithAddonsCallback({ Apps: AppsMock, sendMessagesToAdmins }, { module: 'auditing', external: false, valid: true }); |
||||
|
||||
expect(AppsMock.installedApps).to.not.have.been.called(); |
||||
expect(AppsMock.getManager()?.disable).to.not.have.been.called(); |
||||
}); |
||||
|
||||
it('should not execute anything if module is external and valid', async () => { |
||||
function installedApps() { |
||||
return []; |
||||
} |
||||
|
||||
function getManagerDisable() { |
||||
return undefined; |
||||
} |
||||
|
||||
const mockManager = { |
||||
disable: spy(getManagerDisable), |
||||
}; |
||||
|
||||
const AppsMock = { |
||||
installedApps: spy(installedApps), |
||||
getManager: () => mockManager, |
||||
} as unknown as AppServerOrchestrator; |
||||
|
||||
await _disableAppsWithAddonsCallback({ Apps: AppsMock, sendMessagesToAdmins }, { module: 'auditing', external: true, valid: true }); |
||||
|
||||
expect(AppsMock.installedApps).to.not.have.been.called(); |
||||
expect(AppsMock.getManager()?.disable).to.not.have.been.called(); |
||||
}); |
||||
|
||||
it('should not throw if there are no apps installed that are enabled', async () => { |
||||
function installedApps() { |
||||
return []; |
||||
} |
||||
|
||||
function getManagerDisable() { |
||||
return undefined; |
||||
} |
||||
|
||||
const mockManager = { |
||||
disable: spy(getManagerDisable), |
||||
}; |
||||
|
||||
const AppsMock = { |
||||
installedApps: spy(installedApps), |
||||
getManager: () => mockManager, |
||||
} as unknown as AppServerOrchestrator; |
||||
|
||||
await expect( |
||||
_disableAppsWithAddonsCallback({ Apps: AppsMock, sendMessagesToAdmins }, { module: 'auditing', external: true, valid: false }), |
||||
).to.not.eventually.be.rejected; |
||||
|
||||
expect(AppsMock.installedApps).to.have.been.called(); |
||||
expect(AppsMock.getManager()?.disable).to.not.have.been.called(); |
||||
}); |
||||
|
||||
it('should only disable apps that require addons', async () => { |
||||
function installedApps() { |
||||
return [ |
||||
{ |
||||
getInfo: () => ({}), |
||||
getName: () => 'Test App Without Addon', |
||||
getID() { |
||||
return 'test-app-without-addon'; |
||||
}, |
||||
}, |
||||
{ |
||||
getInfo: () => ({ addon: 'chat.rocket.test-addon' }), |
||||
getName: () => 'Test App WITH Addon', |
||||
getID() { |
||||
return 'test-app-with-addon'; |
||||
}, |
||||
}, |
||||
]; |
||||
} |
||||
|
||||
function getManagerDisable() { |
||||
return undefined; |
||||
} |
||||
|
||||
const mockManager = { |
||||
disable: spy(getManagerDisable), |
||||
}; |
||||
|
||||
const AppsMock = { |
||||
installedApps: spy(installedApps), |
||||
getManager: () => mockManager, |
||||
} as unknown as AppServerOrchestrator; |
||||
|
||||
await expect( |
||||
_disableAppsWithAddonsCallback( |
||||
{ Apps: AppsMock, sendMessagesToAdmins }, |
||||
{ module: 'chat.rocket.test-addon', external: true, valid: false }, |
||||
), |
||||
).to.not.eventually.be.rejected; |
||||
|
||||
expect(AppsMock.installedApps).to.have.been.called(); |
||||
expect(AppsMock.getManager()?.disable).to.have.been.called.once; |
||||
expect(AppsMock.getManager()?.disable).to.have.been.called.with('test-app-with-addon'); |
||||
}); |
||||
|
||||
it('should not send messages to admins if no app was disabled', async () => { |
||||
function installedApps() { |
||||
return [ |
||||
{ |
||||
getInfo: () => ({}), |
||||
getName: () => 'Test App Without Addon', |
||||
getID() { |
||||
return 'test-app-without-addon'; |
||||
}, |
||||
}, |
||||
]; |
||||
} |
||||
|
||||
function getManagerDisable() { |
||||
return undefined; |
||||
} |
||||
|
||||
const mockManager = { |
||||
disable: spy(getManagerDisable), |
||||
}; |
||||
|
||||
const AppsMock = { |
||||
installedApps: spy(installedApps), |
||||
getManager: () => mockManager, |
||||
} as unknown as AppServerOrchestrator; |
||||
|
||||
const sendMessagesToAdminsSpy = spy(sendMessagesToAdmins); |
||||
|
||||
await expect( |
||||
_disableAppsWithAddonsCallback( |
||||
{ Apps: AppsMock, sendMessagesToAdmins: sendMessagesToAdminsSpy }, |
||||
{ module: 'chat.rocket.test-addon', external: true, valid: false }, |
||||
), |
||||
).to.not.eventually.be.rejected; |
||||
|
||||
expect(AppsMock.installedApps).to.have.been.called(); |
||||
expect(AppsMock.getManager()?.disable).to.not.have.been.called(); |
||||
expect(sendMessagesToAdminsSpy).to.not.have.been.called(); |
||||
}); |
||||
|
||||
it('should send messages to admins if some app has been disabled', async () => { |
||||
function installedApps() { |
||||
return [ |
||||
{ |
||||
getInfo: () => ({}), |
||||
getName: () => 'Test App Without Addon', |
||||
getID() { |
||||
return 'test-app-without-addon'; |
||||
}, |
||||
}, |
||||
{ |
||||
getInfo: () => ({ addon: 'chat.rocket.test-addon' }), |
||||
getName: () => 'Test App WITH Addon', |
||||
getID() { |
||||
return 'test-app-with-addon'; |
||||
}, |
||||
}, |
||||
]; |
||||
} |
||||
|
||||
function getManagerDisable() { |
||||
return undefined; |
||||
} |
||||
|
||||
const mockManager = { |
||||
disable: spy(getManagerDisable), |
||||
}; |
||||
|
||||
const AppsMock = { |
||||
installedApps: spy(installedApps), |
||||
getManager: () => mockManager, |
||||
} as unknown as AppServerOrchestrator; |
||||
|
||||
const sendMessagesToAdminsSpy = spy(sendMessagesToAdmins); |
||||
|
||||
await expect( |
||||
_disableAppsWithAddonsCallback( |
||||
{ Apps: AppsMock, sendMessagesToAdmins: sendMessagesToAdminsSpy }, |
||||
{ module: 'chat.rocket.test-addon', external: true, valid: false }, |
||||
), |
||||
).to.not.eventually.be.rejected; |
||||
|
||||
expect(AppsMock.installedApps).to.have.been.called(); |
||||
expect(AppsMock.getManager()?.disable).to.have.been.called.once; |
||||
expect(sendMessagesToAdminsSpy).to.have.been.called(); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,129 @@ |
||||
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; |
||||
import type { IMarketplaceInfo } from '@rocket.chat/apps-engine/server/marketplace'; |
||||
import { AppInstallationSource, type IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; |
||||
import type { Apps } from '@rocket.chat/core-services'; |
||||
import type { LicenseImp } from '@rocket.chat/license'; |
||||
import { expect } from 'chai'; |
||||
|
||||
import { _canEnableApp } from '../../../../../ee/app/license/server/canEnableApp'; |
||||
|
||||
const getDefaultApp = (): IAppStorageItem => ({ |
||||
_id: '6706d9258e0ca97c2f0cc885', |
||||
id: '2e14ff6e-b4d5-4c4c-b12b-b1b1d15ec630', |
||||
info: { |
||||
id: '2e14ff6e-b4d5-4c4c-b12b-b1b1d15ec630', |
||||
version: '0.0.1', |
||||
requiredApiVersion: '^1.19.0', |
||||
iconFile: 'icon.png', |
||||
author: { name: 'a', homepage: 'a', support: 'a' }, |
||||
name: 'Add-on test', |
||||
nameSlug: 'add-on-test', |
||||
classFile: 'AddOnTestApp.js', |
||||
description: 'a', |
||||
implements: [], |
||||
iconFileContent: '', |
||||
}, |
||||
status: AppStatus.UNKNOWN, |
||||
settings: {}, |
||||
implemented: {}, |
||||
installationSource: AppInstallationSource.PRIVATE, |
||||
languageContent: {}, |
||||
sourcePath: 'GridFS:/6706d9258e0ca97c2f0cc880', |
||||
signature: '', |
||||
createdAt: new Date('2024-10-09T19:27:33.923Z'), |
||||
updatedAt: new Date('2024-10-09T19:27:33.923Z'), |
||||
}); |
||||
|
||||
// We will be passing promises to the `expect` function
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */ |
||||
|
||||
describe('canEnableApp', () => { |
||||
it('should throw the message "apps-engine-not-initialized" when appropriate', () => { |
||||
const AppsMock = { |
||||
isInitialized() { |
||||
return false; |
||||
}, |
||||
} as unknown as typeof Apps; |
||||
|
||||
const LicenseMock = {} as unknown as LicenseImp; |
||||
|
||||
const deps = { Apps: AppsMock, License: LicenseMock }; |
||||
|
||||
return expect(_canEnableApp(deps, getDefaultApp())).to.eventually.be.rejectedWith('apps-engine-not-initialized'); |
||||
}); |
||||
|
||||
const AppsMock = { |
||||
isInitialized() { |
||||
return true; |
||||
}, |
||||
} as unknown as typeof Apps; |
||||
|
||||
const LicenseMock = { |
||||
hasModule() { |
||||
return false; |
||||
}, |
||||
shouldPreventAction() { |
||||
return true; |
||||
}, |
||||
hasValidLicense() { |
||||
return false; |
||||
}, |
||||
} as unknown as LicenseImp; |
||||
|
||||
const deps = { Apps: AppsMock, License: LicenseMock }; |
||||
|
||||
it('should throw the message "app-addon-not-valid" when appropriate', () => { |
||||
const app = getDefaultApp(); |
||||
app.info.addon = 'chat.rocket.test-addon'; |
||||
|
||||
return expect(_canEnableApp(deps, app)).to.eventually.be.rejectedWith('app-addon-not-valid'); |
||||
}); |
||||
|
||||
it('should throw the message "license-prevented" when appropriate', () => { |
||||
const privateApp = getDefaultApp(); |
||||
const marketplaceApp = getDefaultApp(); |
||||
marketplaceApp.installationSource = AppInstallationSource.MARKETPLACE; |
||||
|
||||
return Promise.all([ |
||||
expect(_canEnableApp(deps, privateApp)).to.eventually.be.rejectedWith('license-prevented'), |
||||
expect(_canEnableApp(deps, marketplaceApp)).to.eventually.be.rejectedWith('license-prevented'), |
||||
]); |
||||
}); |
||||
|
||||
it('should throw the message "invalid-license" when appropriate', () => { |
||||
const License = { ...LicenseMock, shouldPreventAction: () => false } as unknown as LicenseImp; |
||||
|
||||
const app = getDefaultApp(); |
||||
app.installationSource = AppInstallationSource.MARKETPLACE; |
||||
app.marketplaceInfo = { isEnterpriseOnly: true } as IMarketplaceInfo; |
||||
|
||||
const deps = { Apps: AppsMock, License }; |
||||
|
||||
return expect(_canEnableApp(deps, app)).to.eventually.be.rejectedWith('invalid-license'); |
||||
}); |
||||
|
||||
it('should not throw if app is migrated', () => { |
||||
const app = getDefaultApp(); |
||||
app.migrated = true; |
||||
|
||||
return expect(_canEnableApp(deps, app)).to.not.eventually.be.rejected; |
||||
}); |
||||
|
||||
it('should not throw if license allows it', () => { |
||||
const License = { |
||||
hasModule() { |
||||
return true; |
||||
}, |
||||
shouldPreventAction() { |
||||
return false; |
||||
}, |
||||
hasValidLicense() { |
||||
return true; |
||||
}, |
||||
} as unknown as LicenseImp; |
||||
|
||||
const deps = { Apps: AppsMock, License }; |
||||
|
||||
return expect(_canEnableApp(deps, getDefaultApp())).to.not.eventually.be.rejected; |
||||
}); |
||||
}); |
||||
@ -0,0 +1,101 @@ |
||||
import type { LicenseModule } from '@rocket.chat/core-typings'; |
||||
|
||||
import { MockedLicenseBuilder, getReadyLicenseManager } from '../__tests__/MockedLicenseBuilder'; |
||||
|
||||
describe('getModules', () => { |
||||
it('should return internal and external', async () => { |
||||
const licenseManager = await getReadyLicenseManager(); |
||||
|
||||
const modules = ['auditing', 'livechat-enterprise', 'ldap-enterprise', 'chat.rocket.test-addon'] as LicenseModule[]; |
||||
|
||||
const license = await new MockedLicenseBuilder().withGratedModules(modules).sign(); |
||||
|
||||
await expect(licenseManager.setLicense(license)).resolves.toBe(true); |
||||
|
||||
expect(licenseManager.getModules()).toContain('auditing'); |
||||
expect(licenseManager.getModules()).toContain('livechat-enterprise'); |
||||
expect(licenseManager.getModules()).toContain('ldap-enterprise'); |
||||
expect(licenseManager.getModules()).toContain('chat.rocket.test-addon'); |
||||
}); |
||||
}); |
||||
|
||||
describe('getModuleDefinition', () => { |
||||
it('should not return `external` property for an internal module', async () => { |
||||
const licenseManager = await getReadyLicenseManager(); |
||||
|
||||
const license = await new MockedLicenseBuilder().withGratedModules(['auditing', 'chat.rocket.test-addon']).sign(); |
||||
|
||||
await licenseManager.setLicense(license); |
||||
|
||||
const module = licenseManager.getModuleDefinition('auditing'); |
||||
|
||||
expect(module).toMatchObject({ module: 'auditing' }); |
||||
}); |
||||
|
||||
it('should return `undefined` for a non-existing module', async () => { |
||||
const licenseManager = await getReadyLicenseManager(); |
||||
|
||||
const license = await new MockedLicenseBuilder().withGratedModules(['auditing', 'chat.rocket.test-addon']).sign(); |
||||
|
||||
await licenseManager.setLicense(license); |
||||
|
||||
const module = licenseManager.getModuleDefinition('livechat-enterprise'); |
||||
|
||||
expect(module).toBeUndefined(); |
||||
}); |
||||
|
||||
it('should return `undefined` if there is no license available', async () => { |
||||
const licenseManager = await getReadyLicenseManager(); |
||||
|
||||
const module = licenseManager.getModuleDefinition('livechat-enterprise'); |
||||
|
||||
expect(module).toBeUndefined(); |
||||
}); |
||||
|
||||
it('should return `external` property for an external module', async () => { |
||||
const licenseManager = await getReadyLicenseManager(); |
||||
|
||||
const license = await new MockedLicenseBuilder().withGratedModules(['auditing', 'chat.rocket.test-addon']).sign(); |
||||
|
||||
await licenseManager.setLicense(license); |
||||
|
||||
const module = licenseManager.getModuleDefinition('chat.rocket.test-addon'); |
||||
|
||||
expect(module).toMatchObject({ module: 'chat.rocket.test-addon', external: true }); |
||||
}); |
||||
}); |
||||
|
||||
describe('getExternalModules', () => { |
||||
it('should return only external modules', async () => { |
||||
const licenseManager = await getReadyLicenseManager(); |
||||
|
||||
const license = await new MockedLicenseBuilder().withGratedModules(['auditing', 'chat.rocket.test-addon']).sign(); |
||||
|
||||
await licenseManager.setLicense(license); |
||||
|
||||
const modules = licenseManager.getExternalModules(); |
||||
|
||||
expect(modules).toHaveLength(1); |
||||
expect(modules[0]).toMatchObject({ external: true, module: 'chat.rocket.test-addon' }); |
||||
}); |
||||
|
||||
it('should return empty array if no external module is present', async () => { |
||||
const licenseManager = await getReadyLicenseManager(); |
||||
|
||||
const license = await new MockedLicenseBuilder().withGratedModules(['auditing', 'livechat-enterprise']).sign(); |
||||
|
||||
await licenseManager.setLicense(license); |
||||
|
||||
const modules = licenseManager.getExternalModules(); |
||||
|
||||
expect(modules).toHaveLength(0); |
||||
}); |
||||
|
||||
it('should return empty array if license is not available', async () => { |
||||
const licenseManager = await getReadyLicenseManager(); |
||||
|
||||
const modules = licenseManager.getExternalModules(); |
||||
|
||||
expect(modules).toHaveLength(0); |
||||
}); |
||||
}); |
||||
@ -1,24 +1,29 @@ |
||||
export type LicenseModule = |
||||
| 'auditing' |
||||
| 'canned-responses' |
||||
| 'ldap-enterprise' |
||||
| 'livechat-enterprise' |
||||
| 'voip-enterprise' |
||||
| 'omnichannel-mobile-enterprise' |
||||
| 'engagement-dashboard' |
||||
| 'push-privacy' |
||||
| 'scalability' |
||||
| 'teams-mention' |
||||
| 'saml-enterprise' |
||||
| 'oauth-enterprise' |
||||
| 'device-management' |
||||
| 'federation' |
||||
| 'videoconference-enterprise' |
||||
| 'message-read-receipt' |
||||
| 'outlook-calendar' |
||||
| 'hide-watermark' |
||||
| 'custom-roles' |
||||
| 'accessibility-certification' |
||||
| 'unlimited-presence' |
||||
| 'contact-id-verification' |
||||
| 'teams-voip'; |
||||
export const CoreModules = [ |
||||
'auditing', |
||||
'canned-responses', |
||||
'ldap-enterprise', |
||||
'livechat-enterprise', |
||||
'voip-enterprise', |
||||
'omnichannel-mobile-enterprise', |
||||
'engagement-dashboard', |
||||
'push-privacy', |
||||
'scalability', |
||||
'teams-mention', |
||||
'saml-enterprise', |
||||
'oauth-enterprise', |
||||
'device-management', |
||||
'federation', |
||||
'videoconference-enterprise', |
||||
'message-read-receipt', |
||||
'outlook-calendar', |
||||
'hide-watermark', |
||||
'custom-roles', |
||||
'accessibility-certification', |
||||
'unlimited-presence', |
||||
'contact-id-verification', |
||||
'teams-voip', |
||||
] as const; |
||||
|
||||
export type InternalModuleName = (typeof CoreModules)[number]; |
||||
export type ExternalModuleName = `${string}.${string}`; |
||||
export type LicenseModule = InternalModuleName | ExternalModuleName; |
||||
|
||||
Loading…
Reference in new issue