diff --git a/app/definitions/rest/v1/index.ts b/app/definitions/rest/v1/index.ts index 0642359a1bb..822406e2ac0 100644 --- a/app/definitions/rest/v1/index.ts +++ b/app/definitions/rest/v1/index.ts @@ -21,6 +21,7 @@ import { type PushEndpoints } from './push'; import { type DirectoryEndpoint } from './directory'; import { type AutoTranslateEndpoints } from './autotranslate'; import { type ModerationEndpoints } from './moderation'; +import { type LicensesEndpoints } from './licenses'; export type Endpoints = ChannelsEndpoints & ChatEndpoints & @@ -44,4 +45,5 @@ export type Endpoints = ChannelsEndpoints & PushEndpoints & DirectoryEndpoint & AutoTranslateEndpoints & - ModerationEndpoints; + ModerationEndpoints & + LicensesEndpoints; diff --git a/app/definitions/rest/v1/licenses.ts b/app/definitions/rest/v1/licenses.ts new file mode 100644 index 00000000000..4fd045db54f --- /dev/null +++ b/app/definitions/rest/v1/licenses.ts @@ -0,0 +1,9 @@ +export type LicensesEndpoints = { + 'licenses.info': { + GET: () => { + license: { + activeModules: string[]; + }; + }; + }; +}; diff --git a/app/lib/methods/__tests__/getEnterpriseModules.test.ts b/app/lib/methods/__tests__/getEnterpriseModules.test.ts new file mode 100644 index 00000000000..329fddfe2ac --- /dev/null +++ b/app/lib/methods/__tests__/getEnterpriseModules.test.ts @@ -0,0 +1,109 @@ +import { clearEnterpriseModules, setEnterpriseModules } from '~/actions/enterpriseModules'; +import { selectServerSuccess } from '~/actions/server'; +import { mockedStore } from '~/reducers/mockedStore'; +import { initStore } from '../../store/auxStore'; +import { getEnterpriseModules } from '../enterpriseModules'; + +const mockGet = jest.fn(); +const mockMethodCallWrapper = jest.fn(); +const mockServerUpdate = jest.fn(); + +jest.mock('../../services/sdk', () => ({ + __esModule: true, + default: { + get: (...args: unknown[]) => mockGet(...args), + methodCallWrapper: (...args: unknown[]) => mockMethodCallWrapper(...args) + } +})); + +jest.mock('../../database', () => ({ + __esModule: true, + default: { + servers: { + get: () => ({ + find: () => + Promise.resolve({ + update: (updater: (server: { enterpriseModules?: string }) => void) => { + const server: { enterpriseModules?: string } = {}; + updater(server); + mockServerUpdate(server); + } + }) + }), + write: (writer: () => Promise) => writer() + } + } +})); + +jest.mock('../helpers/log', () => jest.fn()); + +const connectToServerVersion = (version: string) => { + mockedStore.dispatch(selectServerSuccess({ server: 'https://open.rocket.chat', version, name: 'Open' })); +}; + +describe('getEnterpriseModules', () => { + beforeAll(() => { + initStore(mockedStore); + }); + + beforeEach(() => { + jest.clearAllMocks(); + mockedStore.dispatch(clearEnterpriseModules()); + }); + + it('reads active modules from licenses.info on 6.5.0 and later', async () => { + connectToServerVersion('6.5.0'); + mockGet.mockResolvedValue({ success: true, license: { activeModules: ['teams-voip', 'livechat-enterprise'] } }); + + await getEnterpriseModules(); + + expect(mockGet).toHaveBeenCalledWith('licenses.info'); + expect(mockMethodCallWrapper).not.toHaveBeenCalled(); + expect(mockedStore.getState().enterpriseModules).toEqual(['teams-voip', 'livechat-enterprise']); + expect(mockServerUpdate).toHaveBeenCalledWith({ enterpriseModules: 'teams-voip,livechat-enterprise' }); + }); + + it('clears modules when licenses.info is unsuccessful', async () => { + connectToServerVersion('8.9.0'); + mockedStore.dispatch(setEnterpriseModules(['teams-voip'])); + mockGet.mockResolvedValue({ success: false, error: 'unauthorized' }); + + await getEnterpriseModules(); + + expect(mockedStore.getState().enterpriseModules).toEqual([]); + expect(mockServerUpdate).not.toHaveBeenCalled(); + }); + + it('uses the license:getModules method before 6.5.0', async () => { + connectToServerVersion('6.4.1'); + mockMethodCallWrapper.mockResolvedValue(['teams-voip']); + + await getEnterpriseModules(); + + expect(mockMethodCallWrapper).toHaveBeenCalledWith('license:getModules'); + expect(mockGet).not.toHaveBeenCalled(); + expect(mockedStore.getState().enterpriseModules).toEqual(['teams-voip']); + }); + + it('clears modules on servers older than 3.1.0', async () => { + connectToServerVersion('3.0.0'); + mockedStore.dispatch(setEnterpriseModules(['teams-voip'])); + + await getEnterpriseModules(); + + expect(mockGet).not.toHaveBeenCalled(); + expect(mockMethodCallWrapper).not.toHaveBeenCalled(); + expect(mockedStore.getState().enterpriseModules).toEqual([]); + }); + + it('keeps current modules when the request throws', async () => { + connectToServerVersion('8.9.0'); + mockedStore.dispatch(setEnterpriseModules(['teams-voip'])); + mockGet.mockRejectedValue(new Error('network')); + + await getEnterpriseModules(); + + expect(mockedStore.getState().enterpriseModules).toEqual(['teams-voip']); + expect(mockServerUpdate).not.toHaveBeenCalled(); + }); +}); diff --git a/app/lib/methods/enterpriseModules.ts b/app/lib/methods/enterpriseModules.ts index f2a241e49b4..85b0e0f88e1 100644 --- a/app/lib/methods/enterpriseModules.ts +++ b/app/lib/methods/enterpriseModules.ts @@ -29,32 +29,35 @@ export async function setEnterpriseModules() { } } -export function getEnterpriseModules() { - return new Promise(async resolve => { - try { - const { version: serverVersion, server: serverId } = reduxStore.getState().server; - if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '3.1.0')) { - // RC 3.1.0 - const enterpriseModules = await sdk.methodCallWrapper('license:getModules'); - if (enterpriseModules) { - const serversDB = database.servers; - const serversCollection = serversDB.get('servers'); - const server = await serversCollection.find(serverId); - await serversDB.write(async () => { - await server.update(s => { - s.enterpriseModules = enterpriseModules.join(','); - }); - }); - reduxStore.dispatch(setEnterpriseModulesAction(enterpriseModules)); - return resolve(); - } - } +async function fetchEnterpriseModules(serverVersion: string): Promise { + if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '6.5.0')) { + const licensesInfo = await sdk.get('licenses.info'); + return licensesInfo.success ? licensesInfo.license.activeModules : undefined; + } + if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '3.1.0')) { + return sdk.methodCallWrapper('license:getModules'); + } +} + +export async function getEnterpriseModules() { + try { + const { version: serverVersion, server: serverId } = reduxStore.getState().server; + const enterpriseModules = await fetchEnterpriseModules(serverVersion); + if (!enterpriseModules) { reduxStore.dispatch(clearEnterpriseModules()); - } catch (e) { - log(e); + return; } - return resolve(); - }); + const serversDB = database.servers; + const server = await serversDB.get('servers').find(serverId); + await serversDB.write(async () => { + await server.update(s => { + s.enterpriseModules = enterpriseModules.join(','); + }); + }); + reduxStore.dispatch(setEnterpriseModulesAction(enterpriseModules)); + } catch (e) { + log(e); + } } export function isOmnichannelModuleAvailable() {