diff --git a/.changeset/little-steaks-itch.md b/.changeset/little-steaks-itch.md new file mode 100644 index 0000000000000..ee04b7a333402 --- /dev/null +++ b/.changeset/little-steaks-itch.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes incorrect permission checks on workspace registration status, aligning the API and UI hooks with manage-cloud access. diff --git a/.changeset/selfish-jeans-rest.md b/.changeset/selfish-jeans-rest.md new file mode 100644 index 0000000000000..2a854f5d77606 --- /dev/null +++ b/.changeset/selfish-jeans-rest.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Prevents custom status being saved in local storage as `undefined` and breaking the UI when accessing it diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d589614476d6..8b1e812e67ea3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -655,24 +655,26 @@ jobs: # List loaded images docker images - - name: Run federation integration tests with pre-built image + - name: Start federation services working-directory: ./ee/packages/federation-matrix env: ROCKETCHAT_IMAGE: ghcr.io/${{ needs.release-versions.outputs.lowercase-repo }}/rocket.chat:${{ needs.release-versions.outputs.gh-docker-tag }}-amd64 ENTERPRISE_LICENSE_RC1: ZAikY+LLaal7mT6RNYxpyWEmMQyucrl50/7pYBXqHczc90j+RLwF+T0xuCT2pIpKMC5DxcZ1TtkV6MYJk5whrwmap+mQ0FV+VpILJlL0i4T21K4vMfzZXTWm/pzcAy2fMTUNH+mUA9HTBD6lYYh40KnbGXPAd80VbZk0MO/WbWBm2dOT0YCwfvlRyurRqkDAQrftLaffzCNUsMKk0fh+MKs73UDHZQDp1yvs7WoGpPu5ZVi5mTBOt3ZKVz5KjGfClLwJptFPmW1w6nKelAiJBDPpjcX1ylfjxpnBoixko7uN52zlyaeoAYwfRcdDLnZ8k0Ou6tui/vTQUXjGIjHw2AhMaKwonn4E9LYpuA1KEXt08qJL5J3ZtjSCV1T+A9Z3zFhhLgp5dxP/PPUbxDn/P8XKp7nXM9duIfcCMlnea7V8ixEyCHwwvKQaXVVidcsUGtB8CwS0GlsAEBLOzqMehuQUK2rdQ4WgEz3AYveikeVvSzgBHvyXsxssWAThc0Mht0eEJqdDhUB2QeZ2WmPsaSSD639Z4WgjSUoR0zh8bfqepH+2XRcUryXe2yN+iU+3POzi9wfg0k65MxXT8pBg3PD5RHnR8oflEP0tpZts33JiBhYRxX3MKplAFm4dMuphTsDJTh+e534pT7IPuZF79QSVaLEWZfVVVb7nGFtmMwA= QASE_TESTOPS_JEST_API_TOKEN: ${{ secrets.QASE_TESTOPS_JEST_API_TOKEN }} PR_NUMBER: ${{ github.event.number }} - run: yarn test:integration --image "${ROCKETCHAT_IMAGE}" + run: yarn test:integration --start-containers-only --image "${ROCKETCHAT_IMAGE}" - - name: Show rc server logs if tests failed - if: failure() + - name: Run federation integration tests working-directory: ./ee/packages/federation-matrix - run: docker compose -f docker-compose.test.yml logs rc1-prebuilt + env: + QASE_TESTOPS_JEST_API_TOKEN: ${{ secrets.QASE_TESTOPS_JEST_API_TOKEN }} + PR_NUMBER: ${{ github.event.number }} + run: yarn test:integration --ci - - name: Show hs server logs if tests failed + - name: Show federation integration tests logs if: failure() working-directory: ./ee/packages/federation-matrix - run: docker compose -f docker-compose.test.yml logs hs1 + run: yarn test:integration --logs - name: Show mongo logs if tests failed if: failure() diff --git a/apps/meteor/app/api/server/v1/cloud.ts b/apps/meteor/app/api/server/v1/cloud.ts index 369169987c100..acf62e7f1bb62 100644 --- a/apps/meteor/app/api/server/v1/cloud.ts +++ b/apps/meteor/app/api/server/v1/cloud.ts @@ -2,7 +2,7 @@ import { isCloudConfirmationPollProps, isCloudCreateRegistrationIntentProps, isC import { CloudWorkspaceRegistrationError } from '../../../../lib/errors/CloudWorkspaceRegistrationError'; import { SystemLogger } from '../../../../server/lib/logger/system'; -import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { getCheckoutUrl } from '../../../cloud/server/functions/getCheckoutUrl'; import { getConfirmationPoll } from '../../../cloud/server/functions/getConfirmationPoll'; import { @@ -88,7 +88,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - if (!(await hasRoleAsync(this.userId, 'admin'))) { + if (!(await hasPermissionAsync(this.userId, 'manage-cloud'))) { return API.v1.forbidden(); } diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts index 8ddb44d45c81b..5d43e122c7a7e 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts @@ -154,7 +154,7 @@ export class SAMLUtils { const buffer = Buffer.from(base64Data, 'base64'); zlib.inflateRaw(buffer, (err, decoded) => { if (err) { - this.log(`Error while inflating. ${err}`); + this.log({ msg: 'Error while inflating.', err }); return reject(errorCallback(err)); } @@ -426,7 +426,7 @@ export class SAMLUtils { const attributeList = new Map(); for (const attributeName of userDataMap.attributeList) { if (profile[attributeName] === undefined) { - this.log(`SAML user profile is missing the attribute ${attributeName}.`); + this.log({ msg: 'SAML user profile is missing the attribute.', attribute: attributeName }); continue; } attributeList.set(attributeName, profile[attributeName]); diff --git a/apps/meteor/app/push/server/fcm.ts b/apps/meteor/app/push/server/fcm.ts index ff9e8644db5e9..d31cd07e003d6 100644 --- a/apps/meteor/app/push/server/fcm.ts +++ b/apps/meteor/app/push/server/fcm.ts @@ -103,7 +103,7 @@ async function fetchWithRetry(url: string, _removeToken: () => void, options: Ex } const error: FCMError = await response.json(); - logger.error('sendFCM error', error); + logger.error({ msg: 'sendFCM error', err: error }); return response; } @@ -164,7 +164,7 @@ export const sendFCM = function ({ userTokens, notification, _removeToken, optio return; } - logger.debug('sendFCM', tokens, notification); + logger.debug({ msg: 'sendFCM', tokens, notification }); const messages = getFCMMessagesFromPushData(tokens, notification); const headers = { @@ -181,7 +181,7 @@ export const sendFCM = function ({ userTokens, notification, _removeToken, optio const url = `https://fcm.googleapis.com/v1/projects/${options.gcm.projectNumber}/messages:send`; for (const fcmRequest of messages) { - logger.debug('sendFCM message', fcmRequest); + logger.debug({ msg: 'sendFCM message', request: fcmRequest }); const removeToken = () => { const { token } = fcmRequest.message; @@ -191,7 +191,7 @@ export const sendFCM = function ({ userTokens, notification, _removeToken, optio const response = fetchWithRetry(url, removeToken, { method: 'POST', headers, body: JSON.stringify(fcmRequest) }); response.catch((err) => { - logger.error('sendFCM error', err); + logger.error({ msg: 'sendFCM error', err }); }); } }; diff --git a/apps/meteor/app/push/server/methods.ts b/apps/meteor/app/push/server/methods.ts index 47d0eb4dfacd1..bc5102848756a 100644 --- a/apps/meteor/app/push/server/methods.ts +++ b/apps/meteor/app/push/server/methods.ts @@ -100,11 +100,11 @@ export const pushUpdate = async (options: PushUpdateOptions): Promise({ async 'raix:push-update'(options) { - logger.debug('Got push token from app:', options); + logger.debug({ msg: 'Got push token from app', options }); check(options, { id: Match.Optional(String), @@ -137,7 +137,7 @@ Meteor.methods({ throw new Meteor.Error(403, 'Forbidden access'); } - logger.debug(`Settings userId "${this.userId}" for app:`, id); + logger.debug({ msg: 'Setting userId for app', userId: this.userId, appId: id }); const found = await AppsTokens.updateOne({ _id: id }, { $set: { userId: this.userId } }); return !!found; diff --git a/apps/meteor/app/utils/lib/templateVarHandler.ts b/apps/meteor/app/utils/lib/templateVarHandler.ts index cf8279930bda0..047ad2415c56c 100644 --- a/apps/meteor/app/utils/lib/templateVarHandler.ts +++ b/apps/meteor/app/utils/lib/templateVarHandler.ts @@ -9,7 +9,7 @@ export const templateVarHandler = function (variable: string, object: Record { + it('should not call API and return error state when user does not have manage-cloud permission', async () => { + const mockGetRegistrationStatus = jest.fn(); + + const { result } = renderHook(() => useRegistrationStatus(), { + wrapper: mockAppRoot().withEndpoint('GET', '/v1/cloud.registrationStatus', mockGetRegistrationStatus).withJohnDoe().build(), + }); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expect(result.current.canViewRegistrationStatus).toBe(false); + expect(result.current.isRegistered).toBeFalsy(); + expect(mockGetRegistrationStatus).not.toHaveBeenCalled(); + }); + + it('should call API and return isRegistered as true and canViewRegistrationStatus as true when workspace is registered and user has manage-cloud permission', async () => { + const mockGetRegistrationStatus = jest.fn().mockResolvedValue({ + registrationStatus: { + workspaceRegistered: true, + }, + }); + + const { result } = renderHook(() => useRegistrationStatus(), { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/cloud.registrationStatus', mockGetRegistrationStatus) + .withPermission('manage-cloud') + .withJohnDoe() + .build(), + }); + + await waitFor(() => { + expect(mockGetRegistrationStatus).toHaveBeenCalled(); + }); + + expect(result.current.isRegistered).toBe(true); + expect(result.current.canViewRegistrationStatus).toBe(true); + }); + + it('should call API, return isRegistered as false and canViewRegistrationStatus as true when workspace is not registered and user has manage-cloud permission', async () => { + const mockGetRegistrationStatus = jest.fn().mockResolvedValue({ + registrationStatus: { + workspaceRegistered: false, + }, + }); + + const { result } = renderHook(() => useRegistrationStatus(), { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/cloud.registrationStatus', mockGetRegistrationStatus) + .withPermission('manage-cloud') + .withJohnDoe() + .build(), + }); + + await waitFor(() => { + expect(mockGetRegistrationStatus).toHaveBeenCalled(); + }); + + expect(result.current.isRegistered).toBe(false); + expect(result.current.canViewRegistrationStatus).toBe(true); + }); +}); diff --git a/apps/meteor/client/hooks/useRegistrationStatus.ts b/apps/meteor/client/hooks/useRegistrationStatus.ts index 34bcf31ae38cb..522d493caeff6 100644 --- a/apps/meteor/client/hooks/useRegistrationStatus.ts +++ b/apps/meteor/client/hooks/useRegistrationStatus.ts @@ -4,17 +4,18 @@ import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; type useRegistrationStatusReturnType = { + canViewRegistrationStatus: boolean; isRegistered?: boolean; } & UseQueryResult>; export const useRegistrationStatus = (): useRegistrationStatusReturnType => { const getRegistrationStatus = useEndpoint('GET', '/v1/cloud.registrationStatus'); - const canViewregistrationStatus = usePermission('manage-cloud'); + const canViewRegistrationStatus = usePermission('manage-cloud'); const queryResult = useQuery({ queryKey: ['getRegistrationStatus'], queryFn: () => { - if (!canViewregistrationStatus) { + if (!canViewRegistrationStatus) { throw new Error('unauthorized api call'); } return getRegistrationStatus(); @@ -22,5 +23,9 @@ export const useRegistrationStatus = (): useRegistrationStatusReturnType => { staleTime: Infinity, }); - return { isRegistered: !queryResult.isPending && queryResult.data?.registrationStatus?.workspaceRegistered, ...queryResult }; + return { + canViewRegistrationStatus, + isRegistered: !queryResult.isPending && queryResult.data?.registrationStatus?.workspaceRegistered, + ...queryResult, + }; }; diff --git a/apps/meteor/client/navbar/NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx b/apps/meteor/client/navbar/NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx index c483d331f52b6..7e017b88750b9 100644 --- a/apps/meteor/client/navbar/NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx +++ b/apps/meteor/client/navbar/NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx @@ -35,8 +35,8 @@ type EditStatusModalProps = { const EditStatusModal = ({ onClose, userStatus, userStatusText }: EditStatusModalProps): ReactElement => { const allowUserStatusMessageChange = useSetting('Accounts_AllowUserStatusMessageChange'); const dispatchToastMessage = useToastMessageDispatch(); - const [customStatus, setCustomStatus] = useLocalStorage('Local_Custom_Status', ''); - const initialStatusText = customStatus || userStatusText; + const [customStatus, setCustomStatus] = useLocalStorage('Local_Custom_Status', ''); + const initialStatusText = customStatus || userStatusText || ''; const t = useTranslation(); const modalId = useId(); diff --git a/apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx b/apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx index 2efa553b291eb..f3f34299556b4 100644 --- a/apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx +++ b/apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx @@ -54,7 +54,7 @@ const SubscriptionPage = () => { const showLicense = useShowLicense(); const router = useRouter(); const { data: enterpriseData } = useIsEnterprise(); - const { isRegistered } = useRegistrationStatus(); + const { canViewRegistrationStatus } = useRegistrationStatus(); const { data: licensesData, isLoading: isLicenseLoading } = useLicenseWithCloudAnnouncement({ loadValues: true }); const syncLicenseUpdate = useWorkspaceSync(); const invalidateLicenseQuery = useInvalidateLicense(); @@ -108,7 +108,7 @@ const SubscriptionPage = () => { - {isRegistered && ( + {canViewRegistrationStatus && ( diff --git a/apps/meteor/client/views/admin/workspace/VersionCard/VersionCard.tsx b/apps/meteor/client/views/admin/workspace/VersionCard/VersionCard.tsx index a0cfdef382c20..398bab1b684ad 100644 --- a/apps/meteor/client/views/admin/workspace/VersionCard/VersionCard.tsx +++ b/apps/meteor/client/views/admin/workspace/VersionCard/VersionCard.tsx @@ -47,7 +47,7 @@ const VersionCard = ({ serverInfo }: VersionCardProps): ReactElement => { const formatDate = useFormatDate(); const { data: licenseData, isPending, refetch: refetchLicense } = useLicense({ loadValues: true }); - const { isRegistered } = useRegistrationStatus(); + const { isRegistered, canViewRegistrationStatus } = useRegistrationStatus(); const { license, limits } = licenseData || {}; const isAirgapped = license?.information?.offline; @@ -82,7 +82,7 @@ const VersionCard = ({ serverInfo }: VersionCardProps): ReactElement => { action: () => void; label: ReactNode; } = useMemo(() => { - if (!isRegistered) { + if (canViewRegistrationStatus && !isRegistered) { return { action: () => { const handleModalClose = (): void => { @@ -107,7 +107,7 @@ const VersionCard = ({ serverInfo }: VersionCardProps): ReactElement => { if (isOverLimits) { return { path: '/admin/subscription', label: t('Manage_subscription') }; } - }, [isRegistered, versionStatus, isOverLimits, t, setModal, refetchLicense]); + }, [canViewRegistrationStatus, isRegistered, versionStatus, isOverLimits, t, setModal, refetchLicense]); const actionItems = useMemo(() => { return ( @@ -154,19 +154,30 @@ const VersionCard = ({ serverInfo }: VersionCardProps): ReactElement => { ), }, - isRegistered - ? { - icon: 'check', - label: t('Workspace_registered'), - } - : { - danger: true, - icon: 'warning', - label: t('Workspace_not_registered'), - }, + canViewRegistrationStatus && + (isRegistered + ? { + icon: 'check', + label: t('Workspace_registered'), + } + : { + danger: true, + icon: 'warning', + label: t('Workspace_not_registered'), + }), ].filter(Boolean) as VersionActionItem[] ).sort((a) => (a.danger ? -1 : 1)); - }, [isOverLimits, t, isAirgapped, versions, versionStatus?.label, versionStatus?.expiration, formatDate, isRegistered]); + }, [ + isOverLimits, + t, + isAirgapped, + versions, + versionStatus?.label, + versionStatus?.expiration, + formatDate, + canViewRegistrationStatus, + isRegistered, + ]); if (isPending && !licenseData) { return ( diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts index b90d139e8d3b6..c8631f8e6165c 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts @@ -46,7 +46,7 @@ export class AutoCloseOnHoldSchedulerClass { throw new Error('AutoCloseOnHoldScheduler is not running'); } - this.logger.debug(`Scheduling room ${roomId} to be closed in ${timeout} seconds`); + this.logger.debug({ msg: 'Scheduling room to be closed', roomId, timeoutSeconds: timeout }); await this.unscheduleRoom(roomId); const jobName = `${SCHEDULER_NAME}-${roomId}`; @@ -60,13 +60,13 @@ export class AutoCloseOnHoldSchedulerClass { if (!this.running) { throw new Error('AutoCloseOnHoldScheduler is not running'); } - this.logger.debug(`Unscheduling room ${roomId}`); + this.logger.debug({ msg: 'Unscheduling room', roomId }); const jobName = `${SCHEDULER_NAME}-${roomId}`; await this.scheduler.cancel({ name: jobName }); } private async executeJob({ attrs: { data } }: any = {}): Promise { - this.logger.debug(`Executing job for room ${data.roomId}`); + this.logger.debug({ msg: 'Executing job for room', roomId: data.roomId }); const { roomId, comment } = data; const [room, user] = await Promise.all([LivechatRooms.findOneById(roomId), this.getSchedulerUser()]); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index e0e26cff0248c..64fb8982bcbeb 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -56,7 +56,7 @@ export class AutoTransferChatSchedulerClass { } public async scheduleRoom(roomId: string, timeout: number): Promise { - this.logger.debug(`Scheduling room ${roomId} to be transferred in ${timeout} seconds`); + this.logger.debug({ msg: 'Scheduling room to be transferred', roomId, timeoutSeconds: timeout }); await this.unscheduleRoom(roomId); const jobName = `${SCHEDULER_NAME}-${roomId}`; @@ -69,7 +69,7 @@ export class AutoTransferChatSchedulerClass { } public async unscheduleRoom(roomId: string): Promise { - this.logger.debug(`Unscheduling room ${roomId}`); + this.logger.debug({ msg: 'Unscheduling room', roomId }); const jobName = `${SCHEDULER_NAME}-${roomId}`; await LivechatRooms.unsetAutoTransferOngoingById(roomId); @@ -77,7 +77,7 @@ export class AutoTransferChatSchedulerClass { } private async transferRoom(roomId: string): Promise { - this.logger.debug(`Transferring room ${roomId}`); + this.logger.debug({ msg: 'Transferring room', roomId }); const room = await LivechatRooms.findOneById(roomId, { _id: 1, v: 1, @@ -97,7 +97,7 @@ export class AutoTransferChatSchedulerClass { const timeoutDuration = settings.get('Livechat_auto_transfer_chat_timeout').toString(); if (!RoutingManager.getConfig()?.autoAssignAgent) { - this.logger.debug(`Auto-assign agent is disabled, returning room ${roomId} as inquiry`); + this.logger.debug({ msg: 'Auto-assign agent is disabled, returning room as inquiry', roomId }); await returnRoomAsInquiry(room, departmentId, { scope: 'autoTransferUnansweredChatsToQueue', @@ -109,11 +109,15 @@ export class AutoTransferChatSchedulerClass { const agent = await RoutingManager.getNextAgent(departmentId, ignoreAgentId); if (!agent) { - this.logger.error(`No agent found to transfer room ${room._id} which hasn't been answered in ${timeoutDuration} seconds`); + this.logger.error({ + msg: 'No agent found to transfer unanswered room', + roomId: room._id, + timeoutSeconds: timeoutDuration, + }); return; } - this.logger.debug(`Transferring room ${roomId} to agent ${agent.agentId}`); + this.logger.debug({ msg: 'Transferring room to agent', roomId, agentId: agent.agentId }); const transferredBy = await this.getSchedulerUser(); @@ -134,7 +138,7 @@ export class AutoTransferChatSchedulerClass { await Promise.all([LivechatRooms.setAutoTransferredAtById(roomId), this.unscheduleRoom(roomId)]); } catch (error) { - this.logger.error(`Error while executing job ${SCHEDULER_NAME} for room ${roomId}:`, error); + this.logger.error({ msg: 'Error while executing auto-transfer job', schedulerName: SCHEDULER_NAME, roomId, err: error }); } } } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts index 7b6996978e5a6..2d1842da65fcf 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts @@ -76,7 +76,7 @@ export class OmnichannelQueueInactivityMonitorClass { async scheduleInquiry(inquiryId: string, time: Date): Promise { await this.stopInquiry(inquiryId); - this.logger.debug(`Scheduling automatic close of inquiry ${inquiryId} at ${time}`); + this.logger.debug({ msg: 'Scheduling automatic close of inquiry', inquiryId, scheduledAt: time }); const name = this.getName(inquiryId); this.scheduler.define(name, this.bindedCloseRoom); @@ -118,12 +118,12 @@ export class OmnichannelQueueInactivityMonitorClass { const room = await LivechatRooms.findOneById(inquiry.rid); if (!room) { - this.logger.error(`Unable to find room ${inquiry.rid} for inquiry ${inquiryId} to close in queue inactivity monitor`); + this.logger.error({ msg: 'Unable to find room to close in queue inactivity monitor', inquiryId, roomId: inquiry.rid }); return; } await Promise.all([this.closeRoomAction(room), this.stopInquiry(inquiryId)]); - this.logger.info(`Closed room ${inquiry.rid} for inquiry ${inquiryId} due to inactivity`); + this.logger.info({ msg: 'Closed room due to queue inactivity', roomId: inquiry.rid, inquiryId }); } } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts index d9e78ea4026fd..b8fa97ad93032 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts @@ -98,7 +98,7 @@ export class VisitorInactivityMonitor { user: this.user, }); void notifyOnRoomChangedById(room._id); - this.logger.info(`Room ${room._id} closed`); + this.logger.info({ msg: 'Closed room due to visitor inactivity', roomId: room._id }); } async placeRoomOnHold(room: IOmnichannelRoom) { @@ -128,12 +128,12 @@ export class VisitorInactivityMonitor { await LivechatRooms.findAbandonedOpenRooms(new Date(), extraQuery).forEach((room) => { switch (action) { case 'close': { - this.logger.info(`Closing room ${room._id}`); + this.logger.info({ msg: 'Closing room due to abandoned visitor', roomId: room._id }); promises.push(this.closeRooms(room)); break; } case 'on-hold': { - this.logger.info(`Placing room ${room._id} on hold`); + this.logger.info({ msg: 'Placing room on hold due to abandoned visitor', roomId: room._id }); promises.push(this.placeRoomOnHold(room)); break; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts index 8c5f9b4927f24..b4ff69668f38a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts @@ -33,7 +33,7 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE comment: string, onHoldBy: Pick, ) { - this.logger.debug(`Attempting to place room ${room._id} on hold by user ${onHoldBy?._id}`); + this.logger.debug({ msg: 'Attempting to place room on hold', roomId: room._id, userId: onHoldBy?._id }); const { _id: roomId } = room; @@ -80,7 +80,7 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE resumeBy: Pick, clientAction = false, ) { - this.logger.debug(`Attempting to resume room ${room._id} on hold by user ${resumeBy?._id}`); + this.logger.debug({ msg: 'Attempting to resume room on hold', roomId: room._id, userId: resumeBy?._id }); if (!room || !isOmnichannelRoom(room)) { throw new Error('error-invalid-room'); @@ -97,13 +97,13 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE const { _id: roomId, servedBy } = room; if (!servedBy) { - this.logger.error(`No serving agent found for room ${roomId}`); + this.logger.error({ msg: 'No serving agent found for room', roomId }); throw new Error('error-room-not-served'); } const inquiry = await LivechatInquiry.findOneByRoomId(roomId, {}); if (!inquiry) { - this.logger.error(`No inquiry found for room ${roomId}`); + this.logger.error({ msg: 'No inquiry found for room', roomId }); throw new Error('error-invalid-inquiry'); } @@ -156,7 +156,7 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE return; } catch (e) { - this.logger.error(`Agent ${servingAgent._id} is not available to take the inquiry ${inquiry._id}`, e); + this.logger.error({ msg: 'Agent is not available to take inquiry', agentId: servingAgent._id, inquiryId: inquiry._id, err: e }); if (clientAction) { // if the action was triggered by the client, we should throw the error // so the client can handle it and show the error message to the user @@ -182,7 +182,7 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE room: Pick; inquiry: ILivechatInquiryRecord; }): Promise { - this.logger.debug(`Attempting to remove current agent from room ${room._id}`); + this.logger.debug({ msg: 'Attempting to remove current agent from room', roomId: room._id }); const { _id: roomId } = room; diff --git a/apps/meteor/ee/server/lib/ldap/copyCustomFieldsLDAP.ts b/apps/meteor/ee/server/lib/ldap/copyCustomFieldsLDAP.ts index 3be64c25bffeb..d6a79e176934f 100644 --- a/apps/meteor/ee/server/lib/ldap/copyCustomFieldsLDAP.ts +++ b/apps/meteor/ee/server/lib/ldap/copyCustomFieldsLDAP.ts @@ -56,7 +56,7 @@ export const copyCustomFieldsLDAP = ( Object.entries(map).forEach(([ldapField, userField]) => { if (!getNestedProp(customFields, userField)) { - logger.debug(`User attribute does not exist: ${userField}`); + logger.debug({ msg: 'User attribute does not exist', userField }); return; } diff --git a/apps/meteor/ee/server/lib/oauth/Manager.ts b/apps/meteor/ee/server/lib/oauth/Manager.ts index b75c8aa9a7a5e..11da104222b74 100644 --- a/apps/meteor/ee/server/lib/oauth/Manager.ts +++ b/apps/meteor/ee/server/lib/oauth/Manager.ts @@ -22,7 +22,7 @@ export class OAuthEEManager { const userChannelAdmin = await Users.findOneByUsernameIgnoringCase(channelsAdmin); if (!userChannelAdmin) { - logger.error(`could not create channel, user not found: ${channelsAdmin}`); + logger.error({ msg: 'could not create channel, user not found', channelsAdmin }); return; } @@ -38,7 +38,7 @@ export class OAuthEEManager { if (!room) { const createdRoom = await createRoom('c', channel, userChannelAdmin, [], false, false); if (!createdRoom?.rid) { - logger.error(`could not create channel ${channel}`); + logger.error({ msg: 'could not create channel', channel }); return; } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index d33faf9c0e72b..f3119ef09c0b7 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -99,7 +99,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/favicon": "workspace:^", "@rocket.chat/federation-matrix": "workspace:^", - "@rocket.chat/federation-sdk": "0.3.8", + "@rocket.chat/federation-sdk": "0.3.9", "@rocket.chat/fuselage": "^0.70.0", "@rocket.chat/fuselage-forms": "~0.1.1", "@rocket.chat/fuselage-hooks": "~0.38.1", diff --git a/apps/meteor/server/lib/callbacks/callbacksBase.ts b/apps/meteor/server/lib/callbacks/callbacksBase.ts index 47ad59bc3fd77..b1d89a78e1e87 100644 --- a/apps/meteor/server/lib/callbacks/callbacksBase.ts +++ b/apps/meteor/server/lib/callbacks/callbacksBase.ts @@ -63,7 +63,7 @@ export class Callbacks< const wrapCallback = (callback: Callback) => async (item: unknown, constant?: unknown): Promise => { - this.logger?.debug(`Executing callback with id ${callback.id} for hook ${callback.hook}`); + this.logger?.debug({ msg: 'Executing callback', id: callback.id, hook: callback.hook }); return (await this.runOne(callback, item, constant)) ?? item; }; diff --git a/apps/meteor/server/lib/migrations.ts b/apps/meteor/server/lib/migrations.ts index 12d33bf26608e..fd8c1a468bab4 100644 --- a/apps/meteor/server/lib/migrations.ts +++ b/apps/meteor/server/lib/migrations.ts @@ -146,7 +146,12 @@ async function migrate(direction: 'up' | 'down', migration: IMigration): Promise throw new Error(`Cannot migrate ${direction} on version ${migration.version}`); } - log.startup(`Running ${direction}() on version ${migration.version}${migration.name ? `(${migration.name})` : ''}`); + log.startup({ + msg: 'Running migration', + direction, + version: migration.version, + name: migration.name, + }); await migration[direction]?.(migration); } @@ -181,9 +186,13 @@ export async function migrateDatabase(targetVersion: 'latest' | number, subcomma // const { version } = orderedMigrations[orderedMigrations.length - 1]; if (!(await lock())) { - const msg = `Not migrating, control is locked. Attempt ${currentAttempt}/${maxAttempts}`; if (currentAttempt <= maxAttempts) { - log.warn(`${msg}. Trying again in ${retryInterval} seconds.`); + log.warn({ + msg: 'Not migrating, control is locked. Will retry.', + retryIntervalSeconds: retryInterval, + attempt: currentAttempt, + maxAttempts, + }); await sleep(retryInterval * 1000); @@ -212,7 +221,10 @@ export async function migrateDatabase(targetVersion: 'latest' | number, subcomma } if (subcommands?.includes('rerun')) { - log.startup(`Rerunning version ${targetVersion}`); + log.startup({ + msg: 'Rerunning migration', + targetVersion, + }); const migration = orderedMigrations.find((migration) => migration.version === targetVersion); if (!migration) { @@ -232,7 +244,10 @@ export async function migrateDatabase(targetVersion: 'latest' | number, subcomma } if (currentVersion === version) { - log.startup(`Not migrating, already at version ${version}`); + log.startup({ + msg: 'Already at target migration version', + version, + }); unlock(currentVersion); return true; } @@ -247,7 +262,11 @@ export async function migrateDatabase(targetVersion: 'latest' | number, subcomma throw new Error(`Can't find migration version ${version}`); } - log.startup(`Migrating from version ${orderedMigrations[startIdx].version} -> ${orderedMigrations[endIdx].version}`); + log.startup({ + msg: 'Migrating between versions', + fromVersion: orderedMigrations[startIdx].version, + toVersion: orderedMigrations[endIdx].version, + }); try { const migrations = []; diff --git a/apps/meteor/server/services/calendar/service.ts b/apps/meteor/server/services/calendar/service.ts index b6b469ddd46fa..0ddf9c9aec06f 100644 --- a/apps/meteor/server/services/calendar/service.ts +++ b/apps/meteor/server/services/calendar/service.ts @@ -331,7 +331,12 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe status: event.previousStatus, }); } else { - logger.debug(`Not restoring status for user ${event.uid}: current=${user.status}, stored=${event.previousStatus}`); + logger.debug({ + msg: 'Not restoring status for user', + userId: event.uid, + currentStatus: user.status, + previousStatus: event.previousStatus, + }); } } diff --git a/apps/meteor/server/services/calendar/statusEvents/applyStatusChange.ts b/apps/meteor/server/services/calendar/statusEvents/applyStatusChange.ts index 860d5df5e4d3b..251af36abadd6 100644 --- a/apps/meteor/server/services/calendar/statusEvents/applyStatusChange.ts +++ b/apps/meteor/server/services/calendar/statusEvents/applyStatusChange.ts @@ -20,7 +20,14 @@ export async function applyStatusChange({ status?: UserStatus; shouldScheduleRemoval?: boolean; }): Promise { - logger.debug(`Applying status change for event ${eventId} at ${startTime} ${endTime ? `to ${endTime}` : ''} to ${status}`); + logger.debug({ + msg: 'Applying status change for event', + eventId, + uid, + startTime, + endTime, + status: status ?? UserStatus.BUSY, + }); const user = await Users.findOneById(uid, { projection: { roles: 1, username: 1, name: 1, status: 1 } }); if (!user || user.status === UserStatus.OFFLINE) { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/edit-status-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/edit-status-modal.ts index 71ca5ceb99f42..a2db20bc1020e 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/modals/edit-status-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/edit-status-modal.ts @@ -15,8 +15,10 @@ export class EditStatusModal extends Modal { return this.root.getByRole('textbox', { name: 'Status message' }); } - async changeStatusMessage(statusMessage: string): Promise { - await this.statusMessageInput.fill(statusMessage); + async changeStatusMessage(statusMessage?: string): Promise { + if (statusMessage !== undefined) { + await this.statusMessageInput.fill(statusMessage); + } await this.save(); await this.toastMessages.dismissToast(); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts b/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts index 1365b8a13ee6c..79948e29717bd 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts @@ -242,7 +242,7 @@ export class Navbar { await this.getUserProfileMenuOption(status).click(); } - async changeUserCustomStatus(text: string): Promise { + async changeUserCustomStatus(text?: string): Promise { await this.btnUserMenu.click(); await this.getUserProfileMenuOption('Custom Status').click(); await this.modals.editStatus.changeStatusMessage(text); diff --git a/apps/meteor/tests/e2e/presence.spec.ts b/apps/meteor/tests/e2e/presence.spec.ts index 56359475e6447..42e37ed710e77 100644 --- a/apps/meteor/tests/e2e/presence.spec.ts +++ b/apps/meteor/tests/e2e/presence.spec.ts @@ -64,5 +64,16 @@ test.describe.serial('Presence', () => { await poHomeChannel.navbar.btnUserMenu.click(); await expect(async () => expect(poHomeChannel.navbar.userMenu).not.toContainText(customStatus)).toPass(); }); + + test('should not save custom status as `undefined` if nothing changes', async ({ page }) => { + await test.step('change to empty status', async () => { + await poHomeChannel.navbar.changeUserCustomStatus(); + expect(await page.evaluate(() => localStorage.getItem('fuselage-localStorage-Local_Custom_Status'))).not.toBe('undefined'); + }); + await test.step('save without changes', async () => { + await poHomeChannel.navbar.changeUserCustomStatus(); + expect(await page.evaluate(() => localStorage.getItem('fuselage-localStorage-Local_Custom_Status'))).not.toBe('undefined'); + }); + }); }); }); diff --git a/apps/meteor/tests/end-to-end/api/cloud.ts b/apps/meteor/tests/end-to-end/api/cloud.ts index dc6d08e990925..ab6ce13be0bd0 100644 --- a/apps/meteor/tests/end-to-end/api/cloud.ts +++ b/apps/meteor/tests/end-to-end/api/cloud.ts @@ -178,4 +178,50 @@ describe('[Cloud]', function () { }); }); }); + + describe('[/cloud.registrationStatus]', () => { + before(async () => { + return updatePermission('manage-cloud', ['admin']); + }); + + after(async () => { + return updatePermission('manage-cloud', ['admin']); + }); + + it('should fail if user is not authenticated', async () => { + return request + .get(api('cloud.registrationStatus')) + .expect('Content-Type', 'application/json') + .expect(401) + .expect((res: Response) => { + expect(res.body).to.have.property('status', 'error'); + expect(res.body).to.have.property('message', 'You must be logged in to do this.'); + }); + }); + + it('should fail when user does not have the manage-cloud permission', async () => { + await updatePermission('manage-cloud', []); + return request + .get(api('cloud.registrationStatus')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + }); + }); + + it('should return registration status when user has the manage-cloud permission', async () => { + await updatePermission('manage-cloud', ['admin']); + return request + .get(api('cloud.registrationStatus')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('registrationStatus'); + }); + }); + }); }); diff --git a/ee/packages/federation-matrix/jest.config.federation.ts b/ee/packages/federation-matrix/jest.config.federation.ts index 85e34daae709c..aa685c2daf12d 100644 --- a/ee/packages/federation-matrix/jest.config.federation.ts +++ b/ee/packages/federation-matrix/jest.config.federation.ts @@ -55,7 +55,6 @@ export default { complete: true, }, }, - debug: true, }, ] as [string, { [x: string]: unknown }], ] diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index 529f74173da43..7e205339a76df 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -22,7 +22,7 @@ "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/federation-sdk": "0.3.8", + "@rocket.chat/federation-sdk": "0.3.9", "@rocket.chat/http-router": "workspace:^", "@rocket.chat/license": "workspace:^", "@rocket.chat/models": "workspace:^", diff --git a/ee/packages/federation-matrix/tests/helper/synapse-client.ts b/ee/packages/federation-matrix/tests/helper/synapse-client.ts index 17b61af16cc69..8a23b43c3f3a9 100644 --- a/ee/packages/federation-matrix/tests/helper/synapse-client.ts +++ b/ee/packages/federation-matrix/tests/helper/synapse-client.ts @@ -8,6 +8,12 @@ import * as fs from 'fs'; import * as path from 'path'; import { createClient, type MatrixClient, KnownMembership, type Room, type RoomMember, Visibility } from 'matrix-js-sdk'; +import { logger } from 'matrix-js-sdk/lib/logger'; + +logger.debug = () => void 0; +logger.info = () => void 0; +logger.warn = () => void 0; +logger.error = () => void 0; /** * Creates a promise that resolves after the specified delay. diff --git a/ee/packages/federation-matrix/tests/scripts/run-integration-tests.sh b/ee/packages/federation-matrix/tests/scripts/run-integration-tests.sh index b2b5e8bd7b293..64fe8b066e6a5 100755 --- a/ee/packages/federation-matrix/tests/scripts/run-integration-tests.sh +++ b/ee/packages/federation-matrix/tests/scripts/run-integration-tests.sh @@ -20,7 +20,6 @@ PACKAGE_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" DOCKER_COMPOSE_FILE="$PACKAGE_ROOT/docker-compose.test.yml" MAX_WAIT_TIME=240 # 4 minutes CHECK_INTERVAL=5 # Check every 5 seconds -RC1_CONTAINER="rc1" # Build configuration # Use a temporary directory outside the repo to avoid symlink traversal issues during Meteor build @@ -34,9 +33,30 @@ USE_PREBUILT_IMAGE=false PREBUILT_IMAGE="" INTERRUPTED=false NO_TEST=false +CI=false +LOGS=false +START_CONTAINERS=true + while [[ $# -gt 0 ]]; do case $1 in + --start-containers-only) + NO_TEST=true + KEEP_RUNNING=true + shift + ;; + --ci) + CI=true + KEEP_RUNNING=true + START_CONTAINERS=false + shift + ;; + --logs) + LOGS=true + NO_TEST=true + START_CONTAINERS=false + shift + ;; --keep-running) KEEP_RUNNING=true shift @@ -63,11 +83,14 @@ while [[ $# -gt 0 ]]; do --help|-h) echo "Usage: $0 [OPTIONS]" echo "Options:" - echo " --keep-running Keep Docker containers running after tests complete" - echo " --element Include Element web client in the test environment" - echo " --no-test Start containers and skip running tests" - echo " --image [IMAGE] Use a pre-built Docker image instead of building locally" - echo " --help, -h Show this help message" + echo " --start-containers-only Start containers and skip running tests" + echo " --ci Run tests in CI mode (keep containers running, no cleanup)" + echo " --logs Show logs of rc1 and hs1 containers" + echo " --keep-running Keep Docker containers running after tests complete" + echo " --element Include Element web client in the test environment" + echo " --no-test Start containers and skip running tests" + echo " --image [IMAGE] Use a pre-built Docker image instead of building locally" + echo " --help, -h Show this help message" echo "" echo "By default, builds Rocket.Chat locally and runs the 'test' profile" echo "Use --image to test against a pre-built image (e.g., --image rocketchat/rocket.chat:latest)" @@ -101,8 +124,30 @@ log_error() { echo -e "${RED}❌ [$(date '+%Y-%m-%d %H:%M:%S')] $1${NC}" } +docker_logs() { + echo "" + echo "ROCKET.CHAT (rc1) LOGS:" + echo "----------------------------------------" + docker compose -f "$DOCKER_COMPOSE_FILE" --profile "$COMPOSE_PROFILE" logs rc1 + + echo "" + echo "SYNAPSE (hs1) LOGS:" + echo "----------------------------------------" + docker compose -f "$DOCKER_COMPOSE_FILE" --profile "$COMPOSE_PROFILE" logs hs1 + + echo "" + echo "==========================================" +} + # Cleanup function cleanup() { + if [ "$CI" = true ]; then + # Exit with the test result code + if [ -n "${TEST_EXIT_CODE:-}" ]; then + exit $TEST_EXIT_CODE + fi + fi + # Show container logs if tests failed if [ -n "${TEST_EXIT_CODE:-}" ] && [ "$TEST_EXIT_CODE" -ne 0 ]; then echo "" @@ -110,26 +155,7 @@ cleanup() { echo "CONTAINER LOGS (Test Failed)" echo "==========================================" - echo "" - echo "ROCKET.CHAT (rc1) LOGS:" - echo "----------------------------------------" - if docker ps -q -f name=rc1 | grep -q .; then - docker logs rc1 2>&1 | sed 's/^/ /' - else - echo " Rocket.Chat container not found or no logs" - fi - - echo "" - echo "SYNAPSE (hs1) LOGS:" - echo "----------------------------------------" - if docker ps -q -f name=hs1 | grep -q .; then - docker logs hs1 2>&1 | sed 's/^/ /' - else - echo " Synapse container not found or no logs" - fi - - echo "" - echo "==========================================" + docker_logs fi if [ "$KEEP_RUNNING" = true ]; then @@ -141,20 +167,12 @@ cleanup() { if [ "$INCLUDE_ELEMENT" = true ]; then log_info " - Element: https://element" fi - if [ "$INCLUDE_ELEMENT" = true ]; then - log_info "To stop containers manually, run: docker compose -f $DOCKER_COMPOSE_FILE --profile element down -v" - else - log_info "To stop containers manually, run: docker compose -f $DOCKER_COMPOSE_FILE --profile test down -v" - fi + log_info "To stop containers manually, run: docker compose -f \"$DOCKER_COMPOSE_FILE\" --profile \"$COMPOSE_PROFILE\" down -v" else log_info "Cleaning up services..." - if [ -f "$DOCKER_COMPOSE_FILE" ]; then - if [ "$INCLUDE_ELEMENT" = true ]; then - docker compose -f "$DOCKER_COMPOSE_FILE" --profile "element" down -v 2>/dev/null || true - else - docker compose -f "$DOCKER_COMPOSE_FILE" --profile "test" down -v 2>/dev/null || true - fi - fi + + docker compose -f "$DOCKER_COMPOSE_FILE" --profile "$COMPOSE_PROFILE" down -v 2>/dev/null || true + log_success "Cleanup completed" fi @@ -181,145 +199,153 @@ if [ ! -f "$DOCKER_COMPOSE_FILE" ]; then exit 1 fi -# Build Rocket.Chat locally if not using pre-built image -if [ "$USE_PREBUILT_IMAGE" = false ]; then - log_info "🚀 Building Rocket.Chat locally..." - log_info "=====================================" - - # Clean up any existing build - log_info "Cleaning up previous build..." - rm -rf "$BUILD_DIR" - - # Build the project - log_info "Building packages from project root..." - cd "$ROCKETCHAT_ROOT" - yarn build - - # Build the Meteor bundle (must be run from the meteor directory) - log_info "Building Meteor bundle..." - cd "$ROCKETCHAT_ROOT/apps/meteor" - METEOR_DISABLE_OPTIMISTIC_CACHING=1 meteor build --server-only --directory "$BUILD_DIR" - - log_success "Build completed!" +if [ "$INCLUDE_ELEMENT" = true ]; then + COMPOSE_PROFILE="element" else - log_info "🚀 Using pre-built image: $PREBUILT_IMAGE" - log_info "=====================================" + COMPOSE_PROFILE="test" fi -log_info "🚀 Starting Federation Integration Tests" -log_info "=====================================" +if [ "$START_CONTAINERS" = true ]; then + # Build Rocket.Chat locally if not using pre-built image + if [ "$USE_PREBUILT_IMAGE" = false ]; then + log_info "🚀 Building Rocket.Chat locally..." + log_info "=====================================" -BUILD_PARAM="" + # Clean up any existing build + log_info "Cleaning up previous build..." + rm -rf "$BUILD_DIR" -# Set environment variables for Docker Compose -if [ "$USE_PREBUILT_IMAGE" = true ]; then - export ROCKETCHAT_IMAGE="$PREBUILT_IMAGE" - log_info "Using pre-built image: $PREBUILT_IMAGE" -else - export ROCKETCHAT_BUILD_CONTEXT="$BUILD_DIR" - export ROCKETCHAT_DOCKERFILE="$ROCKETCHAT_ROOT/apps/meteor/.docker/Dockerfile.alpine" - BUILD_PARAM="--build" - log_info "Building from local context: $BUILD_DIR" -fi + # Build the project + log_info "Building packages from project root..." + cd "$ROCKETCHAT_ROOT" + yarn build -# Start services -if [ "$INCLUDE_ELEMENT" = true ]; then - log_info "Starting all federation services including Element web client..." - docker compose -f "$DOCKER_COMPOSE_FILE" --profile "element" up -d $BUILD_PARAM -else - log_info "Starting federation services (test profile only)..." - docker compose -f "$DOCKER_COMPOSE_FILE" --profile "test" up -d $BUILD_PARAM -fi + # Build the Meteor bundle (must be run from the meteor directory) + log_info "Building Meteor bundle..." + cd "$ROCKETCHAT_ROOT/apps/meteor" + METEOR_DISABLE_OPTIMISTIC_CACHING=1 meteor build --server-only --directory "$BUILD_DIR" -# Wait for rc1 container to be running -log_info "Waiting for rc1 container to start..." -timeout=60 -while [ $timeout -gt 0 ] && [ "$INTERRUPTED" = false ]; do - if docker ps --filter "name=$RC1_CONTAINER" --filter "status=running" | grep -q "$RC1_CONTAINER"; then - log_success "rc1 container is running" - break + log_success "Build completed!" + else + log_info "🚀 Using pre-built image: $PREBUILT_IMAGE" + log_info "=====================================" fi - sleep 2 - timeout=$((timeout - 2)) -done -if [ "$INTERRUPTED" = true ]; then - log_info "Container startup interrupted by user" - exit 130 -fi + log_info "🚀 Starting Federation Integration Tests" + log_info "=====================================" -if [ $timeout -le 0 ]; then - log_error "rc1 container failed to start within 60 seconds" - exit 1 -fi + BUILD_PARAM="" -# Wait for both Rocket.Chat and Synapse to be ready -log_info "Waiting for Rocket.Chat and Synapse servers to be ready..." - -# Function to wait for a service to be ready -wait_for_service() { - local url=$1 - local name=$2 - local host=$3 - local elapsed=0 - local ca_cert="${CA_CERT:-$PACKAGE_ROOT/docker-compose/traefik/certs/ca/rootCA.crt}" - - # Derive host/port from URL when not explicitly provided - local host_with_port="${url#*://}" - host_with_port="${host_with_port%%/*}" - if [ -z "$host" ]; then - host="${host_with_port%%:*}" - fi - local port - if [[ "$host_with_port" == *:* ]]; then - port="${host_with_port##*:}" + # Set environment variables for Docker Compose + if [ "$USE_PREBUILT_IMAGE" = true ]; then + export ROCKETCHAT_IMAGE="$PREBUILT_IMAGE" + log_info "Using pre-built image: $PREBUILT_IMAGE" else - if [[ "$url" == https://* ]]; then - port=443 - else - port=80 - fi + export ROCKETCHAT_BUILD_CONTEXT="$BUILD_DIR" + export ROCKETCHAT_DOCKERFILE="$ROCKETCHAT_ROOT/apps/meteor/.docker/Dockerfile.alpine" + BUILD_PARAM="--build" + log_info "Building from local context: $BUILD_DIR" fi - log_info "Checking $name at $url (host $host -> 127.0.0.1:$port)..." + # Start services + if [ "$INCLUDE_ELEMENT" = true ]; then + log_info "Starting all federation services including Element web client..." + else + log_info "Starting federation services (test profile only)..." + fi - while [ $elapsed -lt $MAX_WAIT_TIME ] && [ "$INTERRUPTED" = false ]; do - # Capture curl output and error for debugging - curl_output=$(curl -fsS --cacert "$ca_cert" --resolve "${host}:${port}:127.0.0.1" "$url" 2>&1) - curl_exit_code=$? + docker compose -f "$DOCKER_COMPOSE_FILE" --profile "$COMPOSE_PROFILE" up -d $BUILD_PARAM - if [ $curl_exit_code -eq 0 ]; then - log_success "$name is ready!" - return 0 + # Wait for rc1 container to be running + log_info "Waiting for rc1 container to start..." + timeout=60 + while [ $timeout -gt 0 ] && [ "$INTERRUPTED" = false ]; do + if docker compose -f "$DOCKER_COMPOSE_FILE" ps rc1 --filter "status=running" | grep -q "rc1"; then + log_success "rc1 container is running" + break fi - - log_info "$name not ready yet, waiting... (${elapsed}s/${MAX_WAIT_TIME}s)" - log_info "Curl error: $curl_output" - sleep $CHECK_INTERVAL - elapsed=$((elapsed + CHECK_INTERVAL)) + sleep 2 + timeout=$((timeout - 2)) done if [ "$INTERRUPTED" = true ]; then - log_info "Service check interrupted by user" - return 1 + log_info "Container startup interrupted by user" + exit 130 fi - log_error "$name failed to become ready within ${MAX_WAIT_TIME} seconds" - return 1 -} + if [ $timeout -le 0 ]; then + log_error "rc1 container failed to start within 60 seconds" + exit 1 + fi -# Wait for Rocket.Chat -if ! wait_for_service "https://rc1/api/info" "Rocket.Chat" "rc1"; then - log_error "Last 50 lines of rc1 logs:" - docker logs --tail 50 "$RC1_CONTAINER" 2>&1 | sed 's/^/ /' - exit 1 -fi + # Wait for both Rocket.Chat and Synapse to be ready + log_info "Waiting for Rocket.Chat and Synapse servers to be ready..." + + # Function to wait for a service to be ready + wait_for_service() { + local url=$1 + local name=$2 + local host=$3 + local elapsed=0 + local ca_cert="${CA_CERT:-$PACKAGE_ROOT/docker-compose/traefik/certs/ca/rootCA.crt}" + + # Derive host/port from URL when not explicitly provided + local host_with_port="${url#*://}" + host_with_port="${host_with_port%%/*}" + if [ -z "$host" ]; then + host="${host_with_port%%:*}" + fi + local port + if [[ "$host_with_port" == *:* ]]; then + port="${host_with_port##*:}" + else + if [[ "$url" == https://* ]]; then + port=443 + else + port=80 + fi + fi -# Wait for Synapse -if ! wait_for_service "https://hs1/_matrix/client/versions" "Synapse" "hs1"; then - log_error "Last 50 lines of hs1 logs:" - docker logs --tail 50 "hs1" 2>&1 | sed 's/^/ /' - exit 1 + log_info "Checking $name at $url (host $host -> 127.0.0.1:$port)..." + + while [ $elapsed -lt $MAX_WAIT_TIME ] && [ "$INTERRUPTED" = false ]; do + # Capture curl output and error for debugging + curl_output=$(curl -fsS --cacert "$ca_cert" --resolve "${host}:${port}:127.0.0.1" "$url" 2>&1) + curl_exit_code=$? + + if [ $curl_exit_code -eq 0 ]; then + log_success "$name is ready!" + return 0 + fi + + log_info "$name not ready yet, waiting... (${elapsed}s/${MAX_WAIT_TIME}s)" + log_info "Curl error: $curl_output" + sleep $CHECK_INTERVAL + elapsed=$((elapsed + CHECK_INTERVAL)) + done + + if [ "$INTERRUPTED" = true ]; then + log_info "Service check interrupted by user" + return 1 + fi + + log_error "$name failed to become ready within ${MAX_WAIT_TIME} seconds" + return 1 + } + + # Wait for Rocket.Chat + if ! wait_for_service "https://rc1/api/info" "Rocket.Chat" "rc1"; then + log_error "Last 50 lines of rc1 logs:" + docker compose -f "$DOCKER_COMPOSE_FILE" logs --tail 50 rc1 + exit 1 + fi + + # Wait for Synapse + if ! wait_for_service "https://hs1/_matrix/client/versions" "Synapse" "hs1"; then + log_error "Last 50 lines of hs1 logs:" + docker compose -f "$DOCKER_COMPOSE_FILE" logs --tail 50 hs1 + exit 1 + fi fi # Run the end-to-end tests @@ -327,8 +353,13 @@ if [ "$NO_TEST" = false ]; then log_info "Running end-to-end tests..." cd "$PACKAGE_ROOT" + set +e IS_EE=true NODE_EXTRA_CA_CERTS=$(pwd)/docker-compose/traefik/certs/ca/rootCA.crt yarn test:federation TEST_EXIT_CODE=$? + set -e +elif [ "$LOGS" = true ]; then + docker_logs + exit 0 else log_info "No-test mode: skipping test execution" log_info "Services are ready and running. You can now:" diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 2f64d2f435a3c..333ad1da9e9f6 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/federation-sdk": "0.3.8", + "@rocket.chat/federation-sdk": "0.3.9", "@rocket.chat/http-router": "workspace:^", "@rocket.chat/icons": "~0.46.0", "@rocket.chat/media-signaling": "workspace:^", diff --git a/yarn.lock b/yarn.lock index d5299688aead2..1651c307f759f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8266,7 +8266,7 @@ __metadata: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.3.8" + "@rocket.chat/federation-sdk": "npm:0.3.9" "@rocket.chat/http-router": "workspace:^" "@rocket.chat/icons": "npm:~0.46.0" "@rocket.chat/jest-presets": "workspace:~" @@ -8472,7 +8472,7 @@ __metadata: "@rocket.chat/ddp-client": "workspace:^" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.3.8" + "@rocket.chat/federation-sdk": "npm:0.3.9" "@rocket.chat/http-router": "workspace:^" "@rocket.chat/license": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -8498,9 +8498,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/federation-sdk@npm:0.3.8": - version: 0.3.8 - resolution: "@rocket.chat/federation-sdk@npm:0.3.8" +"@rocket.chat/federation-sdk@npm:0.3.9": + version: 0.3.9 + resolution: "@rocket.chat/federation-sdk@npm:0.3.9" dependencies: "@datastructures-js/priority-queue": "npm:^6.3.5" "@noble/ed25519": "npm:^3.0.0" @@ -8513,7 +8513,7 @@ __metadata: zod: "npm:^3.24.1" peerDependencies: typescript: ~5.9.2 - checksum: 10/c8a3e8d7bdf68798d20d1d42c7bafd354c934e9d80b94796ef09f073585b1de501ba181b059a3c4b40b92660fd8ee04db6535610932bc64c1c2d147f3a24286c + checksum: 10/8bf215c37fa3c181d12731a2b2f5068656b64736c05560a070ad8dde6177e48cd262a2a26d8cc56e8cee7850f25a3bc5dd069537c4a1ee3f638b0d94cb11519c languageName: node linkType: hard @@ -9140,7 +9140,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/favicon": "workspace:^" "@rocket.chat/federation-matrix": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.3.8" + "@rocket.chat/federation-sdk": "npm:0.3.9" "@rocket.chat/fuselage": "npm:^0.70.0" "@rocket.chat/fuselage-forms": "npm:~0.1.1" "@rocket.chat/fuselage-hooks": "npm:~0.38.1"