From f63b8601056211e2b191f6ca8ca29de5ee472a45 Mon Sep 17 00:00:00 2001 From: Matheus Cardoso Date: Mon, 29 Jun 2026 11:26:17 -0300 Subject: [PATCH] regression: private room access denial shows delayed generic error --- .../views/room/hooks/useOpenRoom.spec.ts | 57 +++++++++++++++++++ .../client/views/room/hooks/useOpenRoom.ts | 10 +++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/hooks/useOpenRoom.spec.ts b/apps/meteor/client/views/room/hooks/useOpenRoom.spec.ts index ee74b74b09728..82a160f1e9118 100644 --- a/apps/meteor/client/views/room/hooks/useOpenRoom.spec.ts +++ b/apps/meteor/client/views/room/hooks/useOpenRoom.spec.ts @@ -3,6 +3,7 @@ import { renderHook, waitFor } from '@testing-library/react'; import { useOpenRoom } from './useOpenRoom'; import { createFakeRoom, createFakeSubscription } from '../../../../tests/mocks/data'; +import { RoomNotFoundError } from '../../../lib/errors/RoomNotFoundError'; import { Rooms, Subscriptions } from '../../../stores'; jest.mock('../../../../app/ui-utils/client', () => ({ @@ -76,4 +77,60 @@ describe('useOpenRoom', () => { expect(result.current.data?.rid).toBe(dmRid); }); }); + + describe('error classification', () => { + it('maps error-no-permission to RoomNotFoundError without retrying (regression for #40991)', async () => { + const getRoomByTypeAndName = jest.fn().mockImplementation(() => { + throw Object.assign(new Error('No permission'), { error: 'error-no-permission' }); + }); + + const { result } = renderHook(() => useOpenRoom({ type: 'p', reference: 'private-channel' }), { + wrapper: mockAppRoot().withJohnDoe().withMethod('getRoomByTypeAndName', getRoomByTypeAndName).build(), + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(result.current.error).toBeInstanceOf(RoomNotFoundError); + expect(getRoomByTypeAndName).toHaveBeenCalledTimes(1); + }); + + it('maps error-invalid-room to RoomNotFoundError without retrying for non-DM rooms', async () => { + const getRoomByTypeAndName = jest.fn().mockImplementation(() => { + throw Object.assign(new Error('Invalid room'), { error: 'error-invalid-room' }); + }); + + const { result } = renderHook(() => useOpenRoom({ type: 'c', reference: 'missing-channel' }), { + wrapper: mockAppRoot().withJohnDoe().withMethod('getRoomByTypeAndName', getRoomByTypeAndName).build(), + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(result.current.error).toBeInstanceOf(RoomNotFoundError); + expect(getRoomByTypeAndName).toHaveBeenCalledTimes(1); + }); + + it('retries unclassified transient errors and recovers on a later attempt', async () => { + const channelRid = 'channel-rid-transient'; + const channelName = 'flaky-channel'; + const channelRoom = createFakeRoom({ _id: channelRid, t: 'c', name: channelName }); + + const getRoomByTypeAndName = jest + .fn() + .mockImplementationOnce(() => { + throw new Error('network down'); + }) + .mockImplementation(() => channelRoom); + + const { result } = renderHook(() => useOpenRoom({ type: 'c', reference: channelName }), { + wrapper: mockAppRoot() + .withJohnDoe() + .withPermission('preview-c-room') + .withMethod('getRoomByTypeAndName', getRoomByTypeAndName) + .build(), + }); + + // Backoff is Math.min(1000 * 2 ** attempt, 5000); first retry fires after ~1s. + await waitFor(() => expect(result.current.isSuccess).toBe(true), { timeout: 3000 }); + expect(result.current.data?.rid).toBe(channelRid); + expect(getRoomByTypeAndName).toHaveBeenCalledTimes(2); + }); + }); }); diff --git a/apps/meteor/client/views/room/hooks/useOpenRoom.ts b/apps/meteor/client/views/room/hooks/useOpenRoom.ts index 50fbae6be3a21..ed6e6547751ae 100644 --- a/apps/meteor/client/views/room/hooks/useOpenRoom.ts +++ b/apps/meteor/client/views/room/hooks/useOpenRoom.ts @@ -78,9 +78,15 @@ export function useOpenRoom({ type, reference }: { type: RoomType; reference: st try { roomData = await getRoomByTypeAndName(type, reference); } catch (error) { - const isDefinitivelyNotFound = error && typeof error === 'object' && 'error' in error && error.error === 'error-invalid-room'; + const errorCode = error && typeof error === 'object' && 'error' in error ? error.error : undefined; - if (!isDefinitivelyNotFound) { + // "No permission" means the room exists but the user can't see it — surface the + // not-found/no-access screen rather than retrying it as a transient failure. + if (errorCode === 'error-no-permission') { + throw new RoomNotFoundError(undefined, { type, reference }); + } + + if (errorCode !== 'error-invalid-room') { throw error; }