Gracefully handle different x-version-id being reported (#5927) (#5936)

* Gracefully handle different x-version-id being reported

* feedback review

* Fix access to general entities

(cherry picked from commit 4dc164a679)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Mattermost Build
2022-02-07 21:58:27 +01:00
committed by GitHub
parent 0bbbbb98cb
commit cbdb820cbe
9 changed files with 68 additions and 33 deletions

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import deepEqual from 'deep-equal';
import {batchActions} from 'redux-batched-actions';
import {Client4} from '@client/rest';
@@ -31,7 +32,10 @@ export function startDataCleanup() {
export function loadConfigAndLicense() {
return async (dispatch, getState) => {
const {currentUserId} = getState().entities.users;
const state = getState();
const {currentUserId} = state.entities.users;
const {general} = state.entities;
const actions = [];
try {
const [config, license] = await Promise.all([
@@ -39,23 +43,31 @@ export function loadConfigAndLicense() {
Client4.getClientLicenseOld(),
]);
const actions = [{
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
data: config,
}, {
type: GeneralTypes.CLIENT_LICENSE_RECEIVED,
data: license,
}];
if (!deepEqual(general.config, config)) {
actions.push({
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
data: config,
});
}
if (currentUserId) {
if (license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
dispatch(getDataRetentionPolicy());
} else {
actions.push({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
if (!deepEqual(general.license, license)) {
actions.push({
type: GeneralTypes.CLIENT_LICENSE_RECEIVED,
data: license,
});
if (currentUserId) {
if (license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
dispatch(getDataRetentionPolicy());
} else {
actions.push({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
}
}
}
dispatch(batchActions(actions, 'BATCH_LOAD_CONFIG_AND_LICENSE'));
if (actions.length) {
dispatch(batchActions(actions, 'BATCH_LOAD_CONFIG_AND_LICENSE'));
}
return {config, license};
} catch (error) {

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import deepEqual from 'deep-equal';
import {batchActions} from 'redux-batched-actions';
import {handleCRTPreferenceChange} from '@actions/views/crt';
@@ -121,9 +122,12 @@ export function loadMe(user, deviceToken, skipDispatch = false) {
data.teams = teams;
data.teamMembers = teamMembers;
data.teamUnreads = teamUnreads;
data.config = config;
data.url = Client4.getUrl();
if (!deepEqual(state.entities?.general?.config, config)) {
data.config = config;
}
actions.push({
type: UserTypes.LOGIN,
data,

View File

@@ -38,6 +38,7 @@ import {
handleCallScreenOff,
} from '@mmproducts/calls/store/actions/websockets';
import {getChannelSinceValue} from '@utils/channels';
import {semverFromServerVersion} from '@utils/general';
import websocketClient from '@websocket';
import {handleRefreshAppsBindings} from './apps';
@@ -474,11 +475,8 @@ function handleEvent(msg: WebSocketMessage) {
}
function handleHelloEvent(msg: WebSocketMessage) {
const serverVersion = msg.data.server_version;
if (serverVersion && Client4.serverVersion !== serverVersion) {
Client4.serverVersion = serverVersion;
EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion);
}
const serverVersion = semverFromServerVersion(msg.data.server_version);
EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion);
}
// Helpers

View File

@@ -13,6 +13,7 @@ import ClientError from '@client/rest/error';
import mattermostManaged from '@mattermost-managed';
import {General} from '@mm-redux/constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {semverFromServerVersion} from '@utils/general';
import {t} from '@utils/i18n';
import mattermostBucket from 'app/mattermost_bucket';
@@ -116,7 +117,7 @@ Client4.doFetchWithResponse = async (url, options) => {
Client4.setToken(token);
}
const serverVersion = headers[HEADER_X_VERSION_ID] || headers[HEADER_X_VERSION_ID.toLowerCase()];
const serverVersion = semverFromServerVersion(headers[HEADER_X_VERSION_ID] || headers[HEADER_X_VERSION_ID.toLowerCase()]);
if (serverVersion && !headers['Cache-Control'] && Client4.serverVersion !== serverVersion) {
Client4.serverVersion = serverVersion; /* eslint-disable-line require-atomic-updates */
EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion);

View File

@@ -29,7 +29,7 @@ describe('Fetch', () => {
test('doFetchWithResponse handles title case headers', async () => {
const setToken = jest.spyOn(Client4, 'setToken');
const headers = {
[HEADER_X_VERSION_ID]: 'VersionId',
[HEADER_X_VERSION_ID]: '6.1.0',
[HEADER_X_CLUSTER_ID]: 'ClusterId',
[HEADER_TOKEN]: 'Token',
};
@@ -48,7 +48,7 @@ describe('Fetch', () => {
test('doFetchWithResponse handles lower case headers', async () => {
const setToken = jest.spyOn(Client4, 'setToken');
const headers = {
[HEADER_X_VERSION_ID.toLowerCase()]: 'versionid',
[HEADER_X_VERSION_ID.toLowerCase()]: '6.2.0',
[HEADER_X_CLUSTER_ID.toLowerCase()]: 'clusterid',
[HEADER_TOKEN.toLowerCase()]: 'token',
};
@@ -66,7 +66,7 @@ describe('Fetch', () => {
test('doFetchWithResponse handles server version change', async () => {
const emit = jest.spyOn(EventEmitter, 'emit');
const serverVersion1 = 'version1';
const serverVersion1 = '6.3.0';
const response = {
json: () => Promise.resolve('data'),
ok: true,

View File

@@ -7,6 +7,7 @@ import CookieManager from '@react-native-cookies/cookies';
import {AppState, Dimensions, Keyboard, Linking, Platform} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {getLocales} from 'react-native-localize';
import {valid as validVersion} from 'semver';
import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet} from '@actions/device';
import {dismissAllModals, popToRoot, showOverlay} from '@actions/navigation';
@@ -249,9 +250,11 @@ class GlobalEventHandler {
onServerVersionChanged = async (serverVersion) => {
const {dispatch, getState} = Store.redux;
const state = getState();
if (serverVersion && state.entities.users && state.entities.users.currentUserId) {
const {general, users} = state.entities;
const isValid = validVersion(serverVersion);
const versionDidChange = general?.serverVersion !== serverVersion;
if (isValid && serverVersion && versionDidChange && users?.currentUserId) {
dispatch(setServerVersion(serverVersion));
dispatch(loadConfigAndLicense());
}
};

View File

@@ -147,6 +147,9 @@ describe('GlobalEventHandler', () => {
const currentUserId = 'current-user-id';
Store.redux.getState = jest.fn().mockReturnValue({
entities: {
general: {
serverVersion: '',
},
users: {
currentUserId,
profiles: {
@@ -164,21 +167,18 @@ describe('GlobalEventHandler', () => {
const invalidVersion = 'a.b.c';
await GlobalEventHandler.onServerVersionChanged(invalidVersion);
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenCalledWith('setServerVersion');
expect(dispatch).toHaveBeenCalledWith('loadConfigAndLicense');
expect(dispatch).toHaveBeenCalledTimes(0);
});
it('should dispatch on gte min server version with currentUserId', async () => {
let version = minVersion.version;
await GlobalEventHandler.onServerVersionChanged(version);
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledWith('setServerVersion');
expect(dispatch).toHaveBeenCalledWith('loadConfigAndLicense');
version = semver.coerce(minVersion.major + 1).version;
await GlobalEventHandler.onServerVersionChanged(version);
expect(dispatch).toHaveBeenCalledTimes(4);
expect(dispatch).toHaveBeenCalledTimes(2);
});
it('should not dispatch on empty, null, undefined server version', async () => {

View File

@@ -13,7 +13,10 @@ function config(state: Partial<Config> = {}, action: GenericAction) {
return Object.assign({}, state, action.data);
case UserTypes.LOGIN: // Used by the mobile app
case GeneralTypes.SET_CONFIG_AND_LICENSE:
return Object.assign({}, state, action.data.config);
if (action.data.config) {
return Object.assign({}, state, action.data.config);
}
return state;
case GeneralTypes.CLIENT_CONFIG_RESET:
default:
return state;

View File

@@ -110,3 +110,17 @@ export function permalinkBadTeam(intl) {
alertErrorWithFallback(intl, {}, message);
}
export function semverFromServerVersion(value) {
if (!value || typeof value !== 'string') {
return undefined;
}
const split = value.split('.');
const major = parseInt(split[0], 10);
const minor = parseInt(split[1] || '0', 10);
const patch = parseInt(split[2] || '0', 10);
return `${major}.${minor}.${patch}`;
}