Skip to content

Commit

Permalink
fix: retry connect when failed with token expired error (#1107)
Browse files Browse the repository at this point in the history
## Changes
- When attempting to `connect` with an expired or invalid session token,
an error will be returned.
In such cases, the error message is checked, and if a `SessionHandler`
is available, it will extract a valid session token and retry the
`connect`.
- The `eventHandlers.connection.onFailed(SendbirdError)` will not be
called when userId and appId are missing.
This should not be triggered as it is not related to connection errors.
(In the first place, this logic should not be reached without an app ID
and user ID.)
- Added `eventHandlers.connection.onConnected(User)`


## Recording

https://github.com/sendbird/sendbird-uikit-react/assets/26326015/ab529a97-7fe8-4e47-aa2c-9047886f22c8


- ticket: [AC-2476]





[AC-2476]:
https://sendbird.atlassian.net/browse/AC-2476?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
bang9 authored May 27, 2024
1 parent f08ebaa commit 65a503d
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 188 deletions.
4 changes: 0 additions & 4 deletions src/lib/MediaQueryContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,8 @@ const MediaQueryProvider = (props: MediaQueryProviderProps): React.ReactElement
if (typeof breakpoint === 'boolean') {
setIsMobile(breakpoint);
if (breakpoint) {
logger?.info?.('MediaQueryProvider: isMobile: true');
addClassNameToBody();
} else {
logger?.info?.('MediaQueryProvider: isMobile: false');
removeClassNameFromBody();
}
} else {
Expand All @@ -63,11 +61,9 @@ const MediaQueryProvider = (props: MediaQueryProviderProps): React.ReactElement
if (mq.matches) {
setIsMobile(true);
addClassNameToBody();
logger?.info?.('MediaQueryProvider: isMobile: true');
} else {
setIsMobile(false);
removeClassNameFromBody();
logger?.info?.('MediaQueryProvider: isMobile: false');
}
}
};
Expand Down
1 change: 1 addition & 0 deletions src/lib/hooks/useConnect/__test__/data.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const mockSdk = {
updateCurrentUserInfo: jest.fn().mockImplementation((user) => Promise.resolve(user)),
setSessionHandler: jest.fn(),
addExtension: jest.fn(),
addSendbirdExtensions: jest.fn(),
getUIKitConfiguration: jest.fn().mockImplementation(() => Promise.resolve({})),
} as unknown as ConnectTypes['sdk'];

Expand Down
67 changes: 35 additions & 32 deletions src/lib/hooks/useConnect/__test__/setupConnection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,27 @@
/* eslint-disable global-require */
import { SDK_ACTIONS } from '../../../dux/sdk/actionTypes';
import { USER_ACTIONS } from '../../../dux/user/actionTypes';
import { getMissingParamError, setUpConnection, setUpParams } from '../setupConnection';
import { getMissingParamError, setUpConnection, initSDK, getConnectSbError } from '../setupConnection';
import { SetupConnectionTypes } from '../types';
import { generateSetUpConnectionParams, mockSdk, mockUser, mockUser2 } from './data.mocks';

import { SendbirdError } from '@sendbird/chat';

// todo: combine after mock-sdk is implemented
jest.mock('@sendbird/chat', () => ({
init: jest.fn().mockImplementation(() => mockSdk),
SendbirdError: jest.fn(),
}));
jest.mock('@sendbird/chat/openChannel', () => ({
OpenChannelModule: jest.fn(),
}));
jest.mock('@sendbird/chat/groupChannel', () => ({
GroupChannelModule: jest.fn(),
}));
jest.mock('@sendbird/chat', () => {
const originalModule = jest.requireActual('@sendbird/chat');
return {
init: jest.fn(() => mockSdk),
...originalModule,
};
});

describe('useConnect/setupConnection', () => {
it('should call SDK_ERROR when there is no appId', async () => {
const setUpConnectionProps = generateSetUpConnectionParams();
const params = { ...setUpConnectionProps, appId: undefined };
const errorMessage = getMissingParamError({ userId: params.userId, appId: params.appId });

await expect(setUpConnection((params as unknown as SetupConnectionTypes)))
.rejects.toMatch(errorMessage);
await expect(setUpConnection(params as unknown as SetupConnectionTypes)).rejects.toMatch(errorMessage);

expect(mockSdk.connect).not.toBeCalledWith({ type: SDK_ACTIONS.SET_SDK_LOADING, payload: true });
expect(setUpConnectionProps.sdkDispatcher).toBeCalledWith({ type: SDK_ACTIONS.SDK_ERROR });
Expand All @@ -38,8 +33,7 @@ describe('useConnect/setupConnection', () => {
const params = { ...setUpConnectionProps, userId: undefined };
const errorMessage = getMissingParamError({ userId: params.userId, appId: params.appId });

await expect(setUpConnection((params as unknown as SetupConnectionTypes)))
.rejects.toMatch(errorMessage);
await expect(setUpConnection(params as unknown as SetupConnectionTypes)).rejects.toMatch(errorMessage);

expect(setUpConnectionProps.sdkDispatcher).toHaveBeenCalledWith({ type: SDK_ACTIONS.SET_SDK_LOADING, payload: true });
expect(mockSdk.connect).not.toBeCalledWith({ type: SDK_ACTIONS.SET_SDK_LOADING, payload: true });
Expand Down Expand Up @@ -137,8 +131,7 @@ describe('useConnect/setupConnection', () => {
nickname: newNickname,
profileUrl: newprofileUrl,
});
expect(mockSdk.updateCurrentUserInfo)
.toHaveBeenCalledWith({ nickname: 'newNickname', profileUrl: 'newprofileUrl' });
expect(mockSdk.updateCurrentUserInfo).toHaveBeenCalledWith({ nickname: 'newNickname', profileUrl: 'newprofileUrl' });
expect(setUpConnectionProps.userDispatcher).toHaveBeenCalledWith({
type: USER_ACTIONS.INIT_USER,
payload: {
Expand All @@ -157,8 +150,7 @@ describe('useConnect/setupConnection', () => {
appId: setUpConnectionProps.appId,
});

await expect(setUpConnection(setUpConnectionProps))
.rejects.toMatch(errorMessage);
await expect(setUpConnection(setUpConnectionProps)).rejects.toMatch(errorMessage);

expect(setUpConnectionProps.sdkDispatcher).toHaveBeenCalledWith({
type: SDK_ACTIONS.SDK_ERROR,
Expand All @@ -170,23 +162,34 @@ describe('useConnect/setupConnection', () => {
const setUpConnectionProps = generateSetUpConnectionParams();
setUpConnectionProps.eventHandlers = { connection: { onFailed: onConnectionFailed } };

// Force a connection failure by providing an invalid userId
const params = { ...setUpConnectionProps, userId: undefined };
const expectedErrorMessage = getMissingParamError({ userId: undefined, appId: params.appId });

const error = new Error('test-error');
// @ts-expect-error
mockSdk.connect.mockRejectedValueOnce(error);
const expected = getConnectSbError(error as SendbirdError);
// // Ensure that the onConnectionFailed callback is called with the correct error message
await expect(setUpConnection((params as unknown as SetupConnectionTypes)))
.rejects.toBe(expectedErrorMessage);
await expect(setUpConnection(setUpConnectionProps)).rejects.toStrictEqual(expected);
// Ensure that onConnectionFailed callback is called with the expected error object
expect(onConnectionFailed).toHaveBeenCalledWith({ message: expectedErrorMessage } as SendbirdError);
expect(onConnectionFailed).toHaveBeenCalledWith(error);
});

it('should call onConnected callback when connection succeeded', async () => {
const setUpConnectionProps = generateSetUpConnectionParams();
setUpConnectionProps.eventHandlers = { connection: { onConnected: jest.fn() } };

const user = { userId: 'test-user-id', nickname: 'test-nickname', profileUrl: 'test-profile-url' };
// @ts-expect-error
mockSdk.connect.mockResolvedValueOnce(user);

await expect(setUpConnection(setUpConnectionProps)).resolves.toStrictEqual(undefined);
expect(setUpConnectionProps.eventHandlers.connection.onConnected).toHaveBeenCalledWith(user);
});
});

describe('useConnect/setupConnection/setUpParams', () => {
describe('useConnect/setupConnection/initSDK', () => {
it('should call init with correct appId', async () => {
const setUpConnectionProps = generateSetUpConnectionParams();
const { appId, customApiHost, customWebSocketHost } = setUpConnectionProps;
const newSdk = setUpParams({ appId, customApiHost, customWebSocketHost });
const newSdk = initSDK({ appId, customApiHost, customWebSocketHost });
// @ts-ignore
expect(require('@sendbird/chat').init).toBeCalledWith({
appId,
Expand All @@ -205,7 +208,7 @@ describe('useConnect/setupConnection/setUpParams', () => {
it('should call init with correct customApiHost & customWebSocketHost', async () => {
const setUpConnectionProps = generateSetUpConnectionParams();
const { appId, customApiHost, customWebSocketHost } = setUpConnectionProps;
const newSdk = setUpParams({ appId, customApiHost, customWebSocketHost });
const newSdk = initSDK({ appId, customApiHost, customWebSocketHost });
// @ts-ignore
expect(require('@sendbird/chat').init).toBeCalledWith({
appId,
Expand All @@ -226,7 +229,7 @@ describe('useConnect/setupConnection/setUpParams', () => {
it('should call init with sdkInitParams', async () => {
const setUpConnectionProps = generateSetUpConnectionParams();
const { appId, sdkInitParams } = setUpConnectionProps;
const newSdk = setUpParams({ appId, sdkInitParams });
const newSdk = initSDK({ appId, sdkInitParams });
// @ts-ignore
expect(require('@sendbird/chat').init).toBeCalledWith({
appId,
Expand All @@ -246,7 +249,7 @@ describe('useConnect/setupConnection/setUpParams', () => {
it('should call init with customExtensionParams', async () => {
const setUpConnectionProps = generateSetUpConnectionParams();
const { appId, customExtensionParams } = setUpConnectionProps;
const newSdk = setUpParams({ appId, customExtensionParams });
const newSdk = initSDK({ appId, customExtensionParams });
// @ts-ignore
expect(require('@sendbird/chat').init).toBeCalledWith({
appId,
Expand Down
1 change: 0 additions & 1 deletion src/lib/hooks/useConnect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export default function useConnect(triggerTypes: TriggerTypes, staticTypes: Stat
eventHandlers,
initializeMessageTemplatesInfo,
} = staticTypes;
logger?.info?.('SendbirdProvider | useConnect', { ...triggerTypes, ...staticTypes });

// Note: This is a workaround to prevent the creation of multiple SDK instances when React strict mode is enabled.
const connectDeps = useRef<{ appId: string, userId: string }>({
Expand Down
Loading

0 comments on commit 65a503d

Please sign in to comment.