From de830b603cc6faafbb89bf302829b9d7d4e30942 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Mon, 17 Nov 2025 09:27:34 -0300 Subject: [PATCH] chore(apps): remove CI test dependency on marketplace api (#37272) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/meteor/ee/server/apps/appRequestsCron.ts | 15 +-- .../ee/server/apps/communication/rest.ts | 114 ++++++++---------- apps/meteor/ee/server/apps/cron.ts | 6 +- .../apps/marketplace/MarketplaceAPIClient.ts | 103 ++++++++++++++++ .../ee/server/apps/marketplace/appInstall.ts | 22 ++-- .../apps/marketplace/fetchMarketplaceApps.ts | 13 +- .../marketplace/fetchMarketplaceCategories.ts | 12 +- .../ee/server/apps/marketplace/isTesting.ts | 8 ++ apps/meteor/ee/server/apps/orchestrator.js | 22 ++-- .../apps/server/mocks/orchestrator.mock.js | 10 +- 10 files changed, 217 insertions(+), 108 deletions(-) create mode 100644 apps/meteor/ee/server/apps/marketplace/MarketplaceAPIClient.ts create mode 100644 apps/meteor/ee/server/apps/marketplace/isTesting.ts diff --git a/apps/meteor/ee/server/apps/appRequestsCron.ts b/apps/meteor/ee/server/apps/appRequestsCron.ts index 5e23186baca..032e9ba69e0 100644 --- a/apps/meteor/ee/server/apps/appRequestsCron.ts +++ b/apps/meteor/ee/server/apps/appRequestsCron.ts @@ -1,5 +1,4 @@ import { cronJobs } from '@rocket.chat/cron'; -import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { appRequestNotififyForUsers } from './marketplace/appRequestNotifyUsers'; import { Apps } from './orchestrator'; @@ -21,20 +20,14 @@ const appsNotifyAppRequests = async function _appsNotifyAppRequests() { return; } - const baseUrl = Apps.getMarketplaceUrl(); - if (!baseUrl) { - Apps.debugLog(`could not load marketplace base url to send app requests notifications`); - return; - } - const options = { headers: { Authorization: `Bearer ${token}`, }, }; - const pendingSentUrl = `${baseUrl}/v1/app-request/sent/pending`; - const result = await fetch(pendingSentUrl, options); + const pendingSentUrl = `v1/app-request/sent/pending`; + const result = await Apps.getMarketplaceClient().fetch(pendingSentUrl, options); const { data } = await result.json(); const filtered = installedApps.filter((app) => data.indexOf(app.getID()) !== -1); @@ -42,10 +35,10 @@ const appsNotifyAppRequests = async function _appsNotifyAppRequests() { const appId = app.getID(); const appName = app.getName(); - const usersNotified = await appRequestNotififyForUsers(baseUrl, workspaceUrl, appId, appName) + const usersNotified = await appRequestNotififyForUsers(Apps.getMarketplaceClient().getMarketplaceUrl(), workspaceUrl, appId, appName) .then(async (response) => { // Mark all app requests as sent - await fetch(`${baseUrl}/v1/app-request/markAsSent/${appId}`, { ...options, method: 'POST' }); + await Apps.getMarketplaceClient().fetch(`v1/app-request/markAsSent/${appId}`, { ...options, method: 'POST' }); return response; }) .catch((err) => { diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index 3f001a6805d..4b39c2f7a76 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -34,7 +34,7 @@ import { AppsEngineNoNodesFoundError } from '../../../../server/services/apps-en import { canEnableApp } from '../../../app/license/server/canEnableApp'; import { fetchAppsStatusFromCluster } from '../../../lib/misc/fetchAppsStatusFromCluster'; import { formatAppInstanceForRest } from '../../../lib/misc/formatAppInstanceForRest'; -import { notifyAppInstall } from '../marketplace/appInstall'; +import { notifyMarketplace } from '../marketplace/appInstall'; import { fetchMarketplaceApps } from '../marketplace/fetchMarketplaceApps'; import { fetchMarketplaceCategories } from '../marketplace/fetchMarketplaceCategories'; import { MarketplaceAppsError, MarketplaceConnectionError, MarketplaceUnsupportedVersionError } from '../marketplace/marketplaceErrors'; @@ -120,7 +120,7 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = orchestrator.getMarketplaceClient().getMarketplaceUrl(); const workspaceId = settings.get('Cloud_Workspace_Id'); const { action, appId, appVersion } = this.queryParams; @@ -193,7 +193,7 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = orchestrator.getMarketplaceClient().getMarketplaceUrl(); const workspaceId = settings.get('Cloud_Workspace_Id'); @@ -254,8 +254,6 @@ export class AppsRestApi { { authRequired: true, permissionsRequired: ['manage-apps'] }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); - // Gets the Apps from the marketplace if ('marketplace' in this.queryParams && this.queryParams.marketplace) { apiDeprecationLogger.endpoint(this.route, '7.0.0', this.response, 'Use /apps/marketplace to get the apps list.'); @@ -327,6 +325,8 @@ export class AppsRestApi { const seats = await Users.getActiveLocalUserCount(); + const baseUrl = orchestrator.getMarketplaceClient().getMarketplaceUrl(); + return API.v1.success({ url: `${baseUrl}/apps/${this.queryParams.appId}/${ this.queryParams.purchaseType === 'buy' ? this.queryParams.purchaseType : subscribeRoute @@ -361,27 +361,29 @@ export class AppsRestApi { return API.v1.internalError(); } } else if ('appId' in this.bodyParams && this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) { - const baseUrl = orchestrator.getMarketplaceUrl(); - const headers = getDefaultHeaders(); try { const downloadToken = await getWorkspaceAccessToken(true, 'marketplace:download', false); const marketplaceToken = await getWorkspaceAccessToken(); const [downloadResponse, marketplaceResponse] = await Promise.all([ - fetch(`${baseUrl}/v2/apps/${this.bodyParams.appId}/download/${this.bodyParams.version}?token=${downloadToken}`, { - headers, - }).catch((cause) => { - throw new Error('App package download failed', { cause }); - }), - fetch(`${baseUrl}/v1/apps/${this.bodyParams.appId}?appVersion=${this.bodyParams.version}`, { - headers: { - Authorization: `Bearer ${marketplaceToken}`, - ...headers, - }, - }).catch((cause) => { - throw new Error('App metadata download failed', { cause }); - }), + Apps.getMarketplaceClient() + .fetch(`v2/apps/${this.bodyParams.appId}/download/${this.bodyParams.version}?token=${downloadToken}`, { + headers, + }) + .catch((cause) => { + throw new Error('App package download failed', { cause }); + }), + Apps.getMarketplaceClient() + .fetch(`v1/apps/${this.bodyParams.appId}?appVersion=${this.bodyParams.version}`, { + headers: { + Authorization: `Bearer ${marketplaceToken}`, + ...headers, + }, + }) + .catch((cause) => { + throw new Error('App metadata download failed', { cause }); + }), ]); if (downloadResponse.headers.get('content-type') !== 'application/zip') { @@ -469,7 +471,7 @@ export class AppsRestApi { info.status = await aff.getApp().getStatus(); - void notifyAppInstall(orchestrator.getMarketplaceUrl() as string, 'install', info); + void notifyMarketplace('install', info); try { await canEnableApp(aff.getApp().getStorageItem()); @@ -500,7 +502,7 @@ export class AppsRestApi { return API.v1.failure({ error: 'Invalid request. Please ensure an appId is attached to the request.' }); } - const baseUrl = orchestrator.getMarketplaceUrl(); + const baseUrl = orchestrator.getMarketplaceClient().getMarketplaceUrl(); const workspaceId = settings.get('Cloud_Workspace_Id'); const requester = { @@ -610,8 +612,6 @@ export class AppsRestApi { { authRequired: true, permissionsRequired: ['manage-apps'] }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); - const headers: Record = {}; const token = await getWorkspaceAccessToken(); if (token) { @@ -620,7 +620,7 @@ export class AppsRestApi { let result; try { - const request = await fetch(`${baseUrl}/v1/bundles/${this.urlParams.id}/apps`, { headers }); + const request = await orchestrator.getMarketplaceClient().fetch(`v1/bundles/${this.urlParams.id}/apps`, { headers }); if (request.status !== 200) { orchestrator.getRocketChatLogger().error("Error getting the Bundle's Apps from the Marketplace:", await request.json()); return API.v1.failure(); @@ -641,8 +641,6 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); - const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); if (token) { @@ -651,7 +649,7 @@ export class AppsRestApi { let result; try { - const request = await fetch(`${baseUrl}/v1/featured-apps`, { headers }); + const request = await orchestrator.getMarketplaceClient().fetch(`v1/featured-apps`, { headers }); if (request.status !== 200) { orchestrator.getRocketChatLogger().error('Error getting the Featured Apps from the Marketplace:', await request.json()); return API.v1.failure(); @@ -671,7 +669,6 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); const { appId, q = '', sort = '', limit = 25, offset = 0 } = this.queryParams; const headers = getDefaultHeaders(); @@ -681,9 +678,11 @@ export class AppsRestApi { } try { - const request = await fetch(`${baseUrl}/v1/app-request?appId=${appId}&q=${q}&sort=${sort}&limit=${limit}&offset=${offset}`, { - headers, - }); + const request = await orchestrator + .getMarketplaceClient() + .fetch(`v1/app-request?appId=${appId}&q=${q}&sort=${sort}&limit=${limit}&offset=${offset}`, { + headers, + }); const result = await request.json(); if (!request.ok) { @@ -704,7 +703,6 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); @@ -713,7 +711,7 @@ export class AppsRestApi { } try { - const request = await fetch(`${baseUrl}/v1/app-request/stats`, { headers }); + const request = await orchestrator.getMarketplaceClient().fetch(`v1/app-request/stats`, { headers }); const result = await request.json(); if (!request.ok) { throw new Error(result.error); @@ -733,7 +731,6 @@ export class AppsRestApi { { authRequired: true }, { async post() { - const baseUrl = orchestrator.getMarketplaceUrl(); const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); @@ -744,7 +741,7 @@ export class AppsRestApi { const { unseenRequests } = this.bodyParams; try { - const request = await fetch(`${baseUrl}/v1/app-request/markAsSeen`, { + const request = await orchestrator.getMarketplaceClient().fetch(`v1/app-request/markAsSeen`, { method: 'POST', headers, body: { ids: unseenRequests }, @@ -808,8 +805,6 @@ export class AppsRestApi { { async get() { if (this.queryParams.marketplace && this.queryParams.version) { - const baseUrl = orchestrator.getMarketplaceUrl(); - const headers: Record = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE. const token = await getWorkspaceAccessToken(); if (token) { @@ -818,7 +813,9 @@ export class AppsRestApi { let result: any; try { - const request = await fetch(`${baseUrl}/v1/apps/${this.urlParams.id}?appVersion=${this.queryParams.version}`, { headers }); + const request = await orchestrator + .getMarketplaceClient() + .fetch(`v1/apps/${this.urlParams.id}?appVersion=${this.queryParams.version}`, { headers }); if (request.status !== 200) { orchestrator.getRocketChatLogger().error('Error getting the App information from the Marketplace:', await request.json()); return API.v1.failure(); @@ -832,8 +829,6 @@ export class AppsRestApi { } if (this.queryParams.marketplace && this.queryParams.update && this.queryParams.appVersion) { - const baseUrl = orchestrator.getMarketplaceUrl(); - const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); if (token) { @@ -842,9 +837,11 @@ export class AppsRestApi { let result; try { - const request = await fetch(`${baseUrl}/v1/apps/${this.urlParams.id}/latest?appVersion=${this.queryParams.appVersion}`, { - headers, - }); + const request = await orchestrator + .getMarketplaceClient() + .fetch(`v1/apps/${this.urlParams.id}/latest?appVersion=${this.queryParams.appVersion}`, { + headers, + }); if (request.status !== 200) { orchestrator.getRocketChatLogger().error('Error getting the App update info from the Marketplace:', await request.json()); return API.v1.failure(); @@ -881,18 +878,15 @@ export class AppsRestApi { buff = Buffer.from(await response.arrayBuffer()); } else if (this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) { - const baseUrl = orchestrator.getMarketplaceUrl(); - const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(true, 'marketplace:download', false); try { - const response = await fetch( - `${baseUrl}/v2/apps/${this.bodyParams.appId}/download/${this.bodyParams.version}?token=${token}`, - { + const response = await orchestrator + .getMarketplaceClient() + .fetch(`v2/apps/${this.bodyParams.appId}/download/${this.bodyParams.version}?token=${token}`, { headers, - }, - ); + }); if (response.status !== 200) { orchestrator.getRocketChatLogger().error('Error getting the App from the Marketplace:', await response.text()); @@ -976,7 +970,7 @@ export class AppsRestApi { info.status = await aff.getApp().getStatus(); - void notifyAppInstall(orchestrator.getMarketplaceUrl() as string, 'update', info); + void notifyMarketplace('update', info); void orchestrator.getNotifier().appUpdated(info.id); @@ -1007,7 +1001,7 @@ export class AppsRestApi { return API.v1.failure({ app: info }); } - void notifyAppInstall(orchestrator.getMarketplaceUrl() as string, 'uninstall', info); + void notifyMarketplace('uninstall', info); return API.v1.success({ app: info }); }, @@ -1019,8 +1013,6 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); - const headers: Record = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE. const token = await getWorkspaceAccessToken(); if (token) { @@ -1030,7 +1022,7 @@ export class AppsRestApi { let result; let statusCode; try { - const request = await fetch(`${baseUrl}/v1/apps/${this.urlParams.id}`, { headers }); + const request = await orchestrator.getMarketplaceClient().fetch(`v1/apps/${this.urlParams.id}`, { headers }); statusCode = request.status; result = await request.json(); @@ -1056,8 +1048,6 @@ export class AppsRestApi { { authRequired: true, permissionsRequired: ['manage-apps'] }, { async post() { - const baseUrl = orchestrator.getMarketplaceUrl(); - const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); if (token) { @@ -1072,7 +1062,10 @@ export class AppsRestApi { let result; let statusCode; try { - const request = await fetch(`${baseUrl}/v1/workspaces/${workspaceIdSetting.value}/apps/${this.urlParams.id}`, { headers }); + const request = await orchestrator + .getMarketplaceClient() + .fetch(`v1/workspaces/${workspaceIdSetting.value}/apps/${this.urlParams.id}`, { headers }); + statusCode = request.status; result = await request.json(); @@ -1132,12 +1125,11 @@ export class AppsRestApi { { authRequired: false }, { async get() { - const baseUrl = orchestrator.getMarketplaceUrl(); const appId = this.urlParams.id; const headers = getDefaultHeaders(); try { - const request = await fetch(`${baseUrl}/v1/apps/${appId}/screenshots`, { headers }); + const request = await orchestrator.getMarketplaceClient().fetch(`v1/apps/${appId}/screenshots`, { headers }); const data = await request.json(); return API.v1.success({ diff --git a/apps/meteor/ee/server/apps/cron.ts b/apps/meteor/ee/server/apps/cron.ts index 3ae1afdb5a6..bef6940d478 100644 --- a/apps/meteor/ee/server/apps/cron.ts +++ b/apps/meteor/ee/server/apps/cron.ts @@ -2,7 +2,6 @@ import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; import { cronJobs } from '@rocket.chat/cron'; import { Settings, Users } from '@rocket.chat/models'; -import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Apps } from './orchestrator'; import { getWorkspaceAccessToken } from '../../../app/cloud/server'; @@ -77,12 +76,11 @@ const notifyAdminsAboutRenewedApps = async function _notifyAdminsAboutRenewedApp const appsUpdateMarketplaceInfo = async function _appsUpdateMarketplaceInfo() { const token = await getWorkspaceAccessToken(); - const baseUrl = Apps.getMarketplaceUrl(); const workspaceIdSetting = await Settings.getValueById('Cloud_Workspace_Id'); const currentSeats = await Users.getActiveLocalUserCount(); - const fullUrl = `${baseUrl}/v1/workspaces/${workspaceIdSetting}/apps`; + const fullUrl = `v1/workspaces/${workspaceIdSetting}/apps`; const options = { headers: { Authorization: `Bearer ${token}`, @@ -95,7 +93,7 @@ const appsUpdateMarketplaceInfo = async function _appsUpdateMarketplaceInfo() { let data = []; try { - const response = await fetch(fullUrl, options); + const response = await Apps.getMarketplaceClient().fetch(fullUrl, options); const result = await response.json(); diff --git a/apps/meteor/ee/server/apps/marketplace/MarketplaceAPIClient.ts b/apps/meteor/ee/server/apps/marketplace/MarketplaceAPIClient.ts new file mode 100644 index 00000000000..af95b1979fc --- /dev/null +++ b/apps/meteor/ee/server/apps/marketplace/MarketplaceAPIClient.ts @@ -0,0 +1,103 @@ +import { type ExtendedFetchOptions, Response, serverFetch } from '@rocket.chat/server-fetch'; + +import { isTesting } from './isTesting'; + +export class MarketplaceAPIClient { + #fetchStrategy: (input: string, options?: ExtendedFetchOptions, allowSelfSignedCerts?: boolean) => Promise; + + #marketplaceUrl: string; + + constructor() { + if (typeof process.env.OVERWRITE_INTERNAL_MARKETPLACE_URL === 'string' && process.env.OVERWRITE_INTERNAL_MARKETPLACE_URL !== '') { + this.#marketplaceUrl = process.env.OVERWRITE_INTERNAL_MARKETPLACE_URL; + } else { + this.#marketplaceUrl = 'https://marketplace.rocket.chat'; + } + + if (isTesting()) { + this.#fetchStrategy = mockMarketplaceFetch; + } else { + this.#fetchStrategy = serverFetch; + } + } + + public getMarketplaceUrl(): string { + return this.#marketplaceUrl; + } + + public setStrategy(strategyName: 'default' | 'mock'): void { + switch (strategyName) { + case 'default': + this.#fetchStrategy = serverFetch; + break; + + case 'mock': + this.#fetchStrategy = mockMarketplaceFetch; + break; + + default: + throw new Error('Unknown strategy'); + } + } + + public fetch(input: string, options?: ExtendedFetchOptions, allowSelfSignedCerts?: boolean): ReturnType { + if (!input.startsWith('http://') && !input.startsWith('https://')) { + input = this.getMarketplaceUrl().concat(!input.startsWith('/') ? '/' : '', input); + } + + return this.#fetchStrategy(input, options, allowSelfSignedCerts); + } +} + +/** + * Provide mocked HTTP responses for supported Marketplace API endpoints. + * + * This allows us to prevent actual calls to Marketplace service + * during TEST_MODE (CI, local tests, etc.), i.e., remove our dependency + * an external unrelated service + * + * The response content provided has minimal structure to allow for the program + * to not crash by receiving something different from the expected structure + * + * @param input - The request URL or path used to determine which mock response to return + * @returns A `Response` with status 200 and a JSON body corresponding to the requested marketplace endpoint + * @throws Error when `input` does not match any supported mock endpoint + */ +function mockMarketplaceFetch(input: string, _options?: ExtendedFetchOptions, _allowSelfSignedCerts?: boolean): Promise { + let content: string; + + switch (true) { + // This is not an exhaustive list of endpoints + case input.indexOf('v1/apps') !== -1: + case input.indexOf('v1/categories') !== -1: + case input.indexOf('v1/bundles') !== -1: + content = '[]'; + break; + case input.indexOf('v1/featured-apps') !== -1: + content = '{"sections":[]}'; + break; + case input.indexOf('v1/app-request/stats') !== -1: + content = '{"data":{"totalSeen":0,"totalUnseen":0}}'; + break; + case input.indexOf('v1/app-request/markAsSeen') !== -1: + content = '{"success":false}'; + break; + case input.indexOf('v1/app-request') !== -1: + content = '{"data":[],"meta":{"limit":25,"offset":0,"sort":"","filter":"","total":0}}'; + break; + case input.indexOf('v1/workspaces') !== -1: + content = '{}'; + break; + default: + throw new Error(`Invalid marketplace mock request ${input}`); + } + + const response = new Response(Buffer.from(content), { + headers: { + 'content-type': 'application/json', + }, + status: 200, + }); + + return Promise.resolve(response); +} diff --git a/apps/meteor/ee/server/apps/marketplace/appInstall.ts b/apps/meteor/ee/server/apps/marketplace/appInstall.ts index f58b0e3123a..dabaa481b3a 100644 --- a/apps/meteor/ee/server/apps/marketplace/appInstall.ts +++ b/apps/meteor/ee/server/apps/marketplace/appInstall.ts @@ -1,13 +1,21 @@ import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; -import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { getWorkspaceAccessToken } from '../../../../app/cloud/server'; import { settings } from '../../../../app/settings/server'; import { Info } from '../../../../app/utils/rocketchat.info'; - -type installAction = 'install' | 'update' | 'uninstall'; - -export async function notifyAppInstall(marketplaceBaseUrl: string, action: installAction, appInfo: IAppInfo): Promise { +import { Apps } from '../orchestrator'; + +type MarketplaceNotificationType = 'install' | 'update' | 'uninstall'; + +/** + * Notify the marketplace about an app install, update, or uninstall event. + * + * Attempts to POST a notification to the marketplace client at `v1/apps/{id}/install` containing the action, app metadata, Rocket.Chat and engine versions, and site URL. If a workspace access token is available it is included in the Authorization header. Any errors encountered while obtaining the token, reading settings, or sending the request are ignored. + * + * @param action - The marketplace event type: 'install', 'update', or 'uninstall' + * @param appInfo - App metadata (including `id`, `name`, `nameSlug`, and `version`) to include in the notification + */ +export async function notifyMarketplace(action: MarketplaceNotificationType, appInfo: IAppInfo): Promise { const headers: { Authorization?: string } = {}; try { @@ -34,10 +42,10 @@ export async function notifyAppInstall(marketplaceBaseUrl: string, action: insta siteUrl, }; - const pendingSentUrl = `${marketplaceBaseUrl}/v1/apps/${appInfo.id}/install`; + const pendingSentUrl = `v1/apps/${appInfo.id}/install`; try { - await fetch(pendingSentUrl, { + await Apps.getMarketplaceClient().fetch(pendingSentUrl, { method: 'POST', headers, body: data, diff --git a/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceApps.ts b/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceApps.ts index 60230d08bf5..50dddeb3586 100644 --- a/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceApps.ts +++ b/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceApps.ts @@ -1,5 +1,4 @@ import type { App } from '@rocket.chat/core-typings'; -import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { z } from 'zod'; import { getMarketplaceHeaders } from './getMarketplaceHeaders'; @@ -130,8 +129,16 @@ const fetchMarketplaceAppsSchema = z.array( }), ); +/** + * Fetches marketplace apps available to the workspace. + * + * @param endUserID - Optional end-user identifier used to filter apps returned by the marketplace. + * @returns An array of marketplace `App` objects describing available apps and their latest release metadata. + * @throws MarketplaceConnectionError when the marketplace cannot be reached. + * @throws MarketplaceUnsupportedVersionError when the marketplace reports an unsupported client version. + * @throws MarketplaceAppsError for marketplace-side errors, including invalid apps engine version, internal marketplace errors, or a generic failure to fetch apps. + */ export async function fetchMarketplaceApps({ endUserID }: FetchMarketplaceAppsParams = {}): Promise { - const baseUrl = Apps.getMarketplaceUrl(); const headers = getMarketplaceHeaders(); const token = await getWorkspaceAccessToken(); if (token) { @@ -140,7 +147,7 @@ export async function fetchMarketplaceApps({ endUserID }: FetchMarketplaceAppsPa let request; try { - request = await fetch(`${baseUrl}/v1/apps`, { + request = await Apps.getMarketplaceClient().fetch(`v1/apps`, { headers, params: { ...(endUserID && { endUserID }), diff --git a/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceCategories.ts b/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceCategories.ts index 454ab2d79dd..ebbf6593d6a 100644 --- a/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceCategories.ts +++ b/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceCategories.ts @@ -1,5 +1,4 @@ import type { AppCategory } from '@rocket.chat/core-typings'; -import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { z } from 'zod'; import { getMarketplaceHeaders } from './getMarketplaceHeaders'; @@ -18,8 +17,15 @@ const fetchMarketplaceCategoriesSchema = z.array( }), ); +/** + * Fetches marketplace categories from the marketplace API. + * + * @returns An array of marketplace categories (`AppCategory[]`). + * @throws MarketplaceConnectionError when the HTTP request cannot be made. + * @throws MarketplaceUnsupportedVersionError when the marketplace responds with status 426 and `errorMsg` equals `"unsupported version"`. + * @throws MarketplaceAppsError when the marketplace returns an internal error (specific internal codes) or any other non-successful response. + */ export async function fetchMarketplaceCategories(): Promise { - const baseUrl = Apps.getMarketplaceUrl(); const headers = getMarketplaceHeaders(); const token = await getWorkspaceAccessToken(); if (token) { @@ -28,7 +34,7 @@ export async function fetchMarketplaceCategories(): Promise { let request; try { - request = await fetch(`${baseUrl}/v1/categories`, { headers }); + request = await Apps.getMarketplaceClient().fetch(`v1/categories`, { headers }); } catch (error) { throw new MarketplaceConnectionError('Marketplace_Bad_Marketplace_Connection'); } diff --git a/apps/meteor/ee/server/apps/marketplace/isTesting.ts b/apps/meteor/ee/server/apps/marketplace/isTesting.ts new file mode 100644 index 00000000000..dcc83478370 --- /dev/null +++ b/apps/meteor/ee/server/apps/marketplace/isTesting.ts @@ -0,0 +1,8 @@ +/** + * Determine whether the application is running in testing mode. + * + * @returns `true` if the `TEST_MODE` environment variable equals `'true'`, `false` otherwise. + */ +export function isTesting() { + return process.env.TEST_MODE === 'true'; +} diff --git a/apps/meteor/ee/server/apps/orchestrator.js b/apps/meteor/ee/server/apps/orchestrator.js index 89d308d5e69..743b27639f0 100644 --- a/apps/meteor/ee/server/apps/orchestrator.js +++ b/apps/meteor/ee/server/apps/orchestrator.js @@ -6,6 +6,8 @@ import { AppLogs, Apps as AppsModel, AppsPersistence, Statistics } from '@rocket import { Meteor } from 'meteor/meteor'; import { AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from './communication'; +import { MarketplaceAPIClient } from './marketplace/MarketplaceAPIClient'; +import { isTesting } from './marketplace/isTesting'; import { AppRealLogStorage, AppRealStorage, ConfigurableAppSourceStorage } from './storage'; import { RealAppBridges } from '../../../app/apps/server/bridges'; import { @@ -24,15 +26,13 @@ import { AppThreadsConverter } from '../../../app/apps/server/converters/threads import { settings } from '../../../app/settings/server'; import { canEnableApp } from '../../app/license/server/canEnableApp'; -function isTesting() { - return process.env.TEST_MODE === 'true'; -} - const DISABLED_PRIVATE_APP_INSTALLATION = ['yes', 'true'].includes(String(process.env.DISABLE_PRIVATE_APP_INSTALLATION).toLowerCase()); export class AppServerOrchestrator { constructor() { this._isInitialized = false; + + this.marketplaceClient = new MarketplaceAPIClient(); } initialize() { @@ -42,12 +42,6 @@ export class AppServerOrchestrator { this._rocketchatLogger = new Logger('Rocket.Chat Apps'); - if (typeof process.env.OVERWRITE_INTERNAL_MARKETPLACE_URL === 'string' && process.env.OVERWRITE_INTERNAL_MARKETPLACE_URL !== '') { - this._marketplaceUrl = process.env.OVERWRITE_INTERNAL_MARKETPLACE_URL; - } else { - this._marketplaceUrl = 'https://marketplace.rocket.chat'; - } - this._model = AppsModel; this._logModel = AppLogs; this._persistModel = AppsPersistence; @@ -89,6 +83,10 @@ export class AppServerOrchestrator { this._isInitialized = true; } + getMarketplaceClient() { + return this.marketplaceClient; + } + getModel() { return this._model; } @@ -169,10 +167,6 @@ export class AppServerOrchestrator { } } - getMarketplaceUrl() { - return this._marketplaceUrl; - } - async load() { // Don't try to load it again if it has // already been loaded diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/orchestrator.mock.js b/apps/meteor/tests/unit/app/apps/server/mocks/orchestrator.mock.js index 970993b907c..79e8996598b 100644 --- a/apps/meteor/tests/unit/app/apps/server/mocks/orchestrator.mock.js +++ b/apps/meteor/tests/unit/app/apps/server/mocks/orchestrator.mock.js @@ -2,7 +2,7 @@ export class AppServerOrchestratorMock { constructor() { - this._marketplaceUrl = 'https://marketplace.rocket.chat'; + this._marketplaceClient = {}; this._model = {}; this._logModel = {}; @@ -26,6 +26,10 @@ export class AppServerOrchestratorMock { this._communicators.set('restapi', {}); } + getMarketplaceClient() { + return this._marketplaceClient; + } + getModel() { return this._model; } @@ -76,10 +80,6 @@ export class AppServerOrchestratorMock { } } - getMarketplaceUrl() { - return this._marketplaceUrl; - } - load() { // Don't try to load it again if it has // already been loaded