mattermost-mobile/app/store/middlewares/middleware.test.js
Anurag Shivarathri fc929891ea
MM-37485 MM-37602 MM-37610 MM-37684 MM-37719 MM-37522 MM-37720 App crash, Sync issue fixes (#5612)
Summary
Fixes app crash on opening channel header
Fixes app crash on opening permalink from permalink screen
Fixes showing duplicates channels while searching in "Jump to..."
Fixes auto following the thread on opening the thread view when there are unreads
Fixes updation of threads state (Read/Unread/Deleted) of already loaded threads
Fixes counter showing as blank due to threads sync issue
Doesn't show Follow Thread/Message when CRT is not enabled

Ticket Link
https://mattermost.atlassian.net/browse/MM-37485
https://mattermost.atlassian.net/browse/MM-37602
https://mattermost.atlassian.net/browse/MM-37610
https://mattermost.atlassian.net/browse/MM-37684
https://mattermost.atlassian.net/browse/MM-37719
https://mattermost.atlassian.net/browse/MM-37522
https://mattermost.atlassian.net/browse/MM-37720

Device Information
This PR was tested on: iOS 14.5
2021-08-11 15:56:38 +05:30

802 lines
26 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-nested-callbacks */
import assert from 'assert';
import merge from 'deepmerge';
import DeviceInfo from 'react-native-device-info';
import {REHYDRATE} from 'redux-persist';
import {ViewTypes} from '@constants';
import {Posts} from '@mm-redux/constants';
import initialState from '@store/initial_state';
import {
cleanUpPostsInChannel,
cleanUpState,
cleanUpThreadsInTeam,
getAllFromPostsInChannel,
} from './helpers';
import messageRetention from './message_retention';
describe('messageRetention', () => {
describe('should chain the same incoming action type', () => {
const actions = [
{
type: 'persist/REHYDRATE',
payload: {
views: {
team: {
},
},
},
},
{
type: ViewTypes.DATA_CLEANUP,
payload: {
entities: {
channels: {
},
posts: {
},
general: {
config: {},
},
preferences: {
myPreferences: {},
},
},
views: {
team: {
},
},
},
},
{
type: 'other',
},
];
actions.forEach((action) => {
it(`for action type ${action.type}`, () => {
const store = {};
const next = (a) => a;
const nextAction = messageRetention(store)(next)(action);
assert.equal(action.type, nextAction.type);
});
});
});
describe('should add build, version, and previousVersion to payload.app on persist/REHYDRATE', () => {
const next = (a) => a;
const store = {};
const build = 'build';
const version = 'version';
const previousBuild = 'previous-build';
const previousVersion = 'previous-version';
DeviceInfo.getBuildNumber = jest.fn().mockReturnValue('build');
DeviceInfo.getVersion = jest.fn().mockReturnValue('version');
const rehydrateAction = {
type: REHYDRATE,
payload: {
app: {
build: previousBuild,
version: previousVersion,
},
},
};
const expectedPayloadApp = {
build,
version,
previousVersion,
};
const entities = {
channels: {},
general: {
config: {},
},
posts: {},
preferences: {
myPreferences: {},
},
};
const views = {
team: {
lastChannelForTeam: {},
},
};
test('when entities is missing', () => {
const action = {...rehydrateAction};
const nextAction = messageRetention(store)(next)(action);
expect(nextAction.payload.app).toStrictEqual(expectedPayloadApp);
});
test('when views is missing', () => {
const action = {
...rehydrateAction,
payload: {
...rehydrateAction.payload,
entities,
},
};
const nextAction = messageRetention(store)(next)(action);
expect(nextAction.payload.app).toStrictEqual(expectedPayloadApp);
});
test('when previousVersion !== version', () => {
const action = {
...rehydrateAction,
payload: {
...rehydrateAction.payload,
entities,
views,
},
};
expect(action.payload.app.version).not.toEqual(DeviceInfo.getVersion());
const nextAction = messageRetention(store)(next)(action);
expect(nextAction.payload.app).toStrictEqual(expectedPayloadApp);
});
test('when previousBuild !== build', () => {
const action = {
...rehydrateAction,
payload: {
...rehydrateAction.payload,
app: {
...rehydrateAction.payload.app,
version: DeviceInfo.getVersion(),
},
entities,
views,
},
};
expect(action.payload.app.version).toEqual(DeviceInfo.getVersion());
expect(action.payload.app.build).not.toEqual(DeviceInfo.getBuildNumber());
const nextAction = messageRetention(store)(next)(action);
expect(nextAction.payload.app).toStrictEqual({
...expectedPayloadApp,
previousVersion: DeviceInfo.getVersion(),
});
});
test('when cleanUpState', () => {
const action = {
...rehydrateAction,
payload: {
...rehydrateAction.payload,
app: {
...rehydrateAction.payload.app,
version: DeviceInfo.getVersion(),
build: DeviceInfo.getBuildNumber(),
},
entities,
views,
},
};
expect(action.payload.app.version).toEqual(DeviceInfo.getVersion());
expect(action.payload.app.build).toEqual(DeviceInfo.getBuildNumber());
const nextAction = messageRetention(store)(next)(action);
expect(nextAction.payload.app).toStrictEqual({
...expectedPayloadApp,
previousVersion: DeviceInfo.getVersion(),
});
});
});
});
describe('cleanUpState', () => {
test('should remove post because of data retention', () => {
const state = {
entities: {
channels: {
currentChannelId: 'channel1',
},
files: {
fileIdsByPostId: {},
},
general: {
config: {},
dataRetention: {
policies: {
global: {
message_deletion_enabled: true,
message_retention_cutoff: 1000,
},
},
},
},
posts: {
posts: {
post1: {id: 'post1', channel_id: 'channel1', create_at: 1000},
post2: {id: 'post2', channel_id: 'channel1', create_at: 999},
},
postsInChannel: {
channel1: [
{order: ['post1', 'post2'], recent: true},
],
},
postsInThread: {},
reactions: {},
},
preferences: {
myPreferences: {},
},
search: {
results: ['post1', 'post2'],
flagged: ['post1', 'post2', 'post3'],
},
},
views: {
team: {
lastChannelForTeam: {
team1: ['channel1'],
},
},
},
};
const result = cleanUpState(state);
expect(result.entities.posts.posts.post1).toBeDefined();
expect(result.entities.posts.posts.post2).toBeUndefined();
expect(result.entities.posts.postsInChannel.channel1).toEqual([{order: ['post1'], recent: true}]);
expect(result.entities.search.results).toEqual(['post1', 'post2']);
expect(result.entities.search.flagged).toEqual(['post1', 'post2', 'post3']);
});
test('should migrate dataRetentionPolicy key to granular data retention structure', () => {
const dataRetentionPolicy = {
file_deletion_enabled: false,
file_retention_cutoff: 0,
message_deletion_enabled: false,
message_retention_cutoff: 0,
};
const state = {
entities: {
general: {
dataRetentionPolicy,
},
preferences: {
myPreferences: {},
},
},
};
const result = cleanUpState(state);
expect(result.entities.general.dataRetentionPolicy).toBeUndefined();
expect(result.entities.general.dataRetention.policies.global).toEqual(dataRetentionPolicy);
});
test('should remove post because of granular data retention', () => {
const getCreateAtBeforeDays = (days) => {
const date = new Date();
date.setDate(date.getDate() - days);
return date.getTime();
};
const state = {
entities: {
channels: {
currentChannelId: 'channel1',
channelsInTeam: {
team1: new Set(['team1_channel1', 'team1_channel2']),
team2: new Set(['team2_channel1', 'team2_channel2']),
},
},
files: {
fileIdsByPostId: {},
},
general: {
dataRetention: {
policies: {
teams: [{
post_duration: 5,
team_id: 'team1',
}, {
post_duration: 10,
team_id: 'team2',
}],
channels: [{
post_duration: 2,
channel_id: 'team1_channel1',
}],
},
},
},
posts: {
posts: {
// Team 1 - Channel 1, Channel Policy - 2 Days
post1: {id: 'post1', channel_id: 'team1_channel1', create_at: getCreateAtBeforeDays(1)},
post2: {id: 'post2', channel_id: 'team1_channel1', create_at: getCreateAtBeforeDays(3)}, // X
// Team 1 - Channel 2, Team Policy - 5 Days
post3: {id: 'post3', channel_id: 'team1_channel2', create_at: getCreateAtBeforeDays(3)},
post4: {id: 'post4', channel_id: 'team1_channel2', create_at: getCreateAtBeforeDays(6)}, // X
// Team 2, Channel 1 & 2, Team Policy - 10 Days
post5: {id: 'post5', channel_id: 'team2_channel1', create_at: getCreateAtBeforeDays(9)},
post6: {id: 'post6', channel_id: 'team2_channel2', create_at: getCreateAtBeforeDays(11)}, // X
},
postsInChannel: {
team1_channel1: [
{order: ['post1', 'post2'], recent: true},
],
team1_channel2: [
{order: ['post3', 'post4'], recent: true},
],
team2_channel1: [
{order: ['post5'], recent: true},
],
team2_channel2: [
{order: ['post6'], recent: true},
],
},
postsInThread: {},
reactions: {},
},
preferences: {
myPreferences: {},
},
search: {
results: [],
flagged: [],
},
},
views: {
team: {
lastChannelForTeam: {
team1: ['team1_channel1', 'team1_channel2'],
team2: ['team2_channel1', 'team2_channel2'],
},
},
},
};
const result = cleanUpState(state);
expect(result.entities.posts.posts.post1).toBeDefined();
expect(result.entities.posts.posts.post2).toBeUndefined();
expect(result.entities.posts.posts.post3).toBeDefined();
expect(result.entities.posts.posts.post4).toBeUndefined();
expect(result.entities.posts.posts.post5).toBeDefined();
expect(result.entities.posts.posts.post6).toBeUndefined();
});
test('should keep failed pending post', () => {
const state = {
entities: {
channels: {
currentChannelId: 'channel1',
},
files: {
fileIdsByPostId: {},
},
general: {
config: {},
},
posts: {
pendingPostIds: ['pending'],
posts: {
pending: {id: 'pending', pending_post_id: 'pending', channel_id: 'channel1', failed: true},
post1: {id: 'post1', channel_id: 'channel1'},
post2: {id: 'post2', channel_id: 'channel1'},
},
postsInChannel: {
channel1: [
{order: ['pending', 'post1', 'post2'], recent: true},
],
},
postsInThread: {},
reactions: {},
},
preferences: {
myPreferences: {},
},
},
views: {
team: {
lastChannelForTeam: {
team1: ['channel1'],
},
},
},
};
const result = cleanUpState(state);
expect(result.entities.posts.pendingPostIds).toEqual(['pending']);
expect(result.entities.posts.posts.pending).toBeDefined();
expect(result.entities.posts.postsInChannel.channel1).toEqual([{order: ['pending', 'post1', 'post2'], recent: true}]);
});
test('should remove non-failed pending post', () => {
const state = {
entities: {
channels: {
currentChannelId: 'channel1',
},
files: {
fileIdsByPostId: {},
},
general: {
config: {},
},
posts: {
pendingPostIds: ['pending'],
posts: {
pending: {id: 'pending', pending_post_id: 'pending', channel_id: 'channel1'},
post1: {id: 'post1', channel_id: 'channel1'},
post2: {id: 'post2', channel_id: 'channel1'},
},
postsInChannel: {
channel1: [
{order: ['pending', 'post1', 'post2'], recent: true},
],
},
postsInThread: {},
reactions: {},
},
preferences: {
myPreferences: {},
},
},
views: {
team: {
lastChannelForTeam: {
team1: ['channel1'],
},
},
},
};
const result = cleanUpState(state);
expect(result.entities.posts.pendingPostIds).toEqual([]);
expect(result.entities.posts.posts.pending).toBeUndefined();
expect(result.entities.posts.postsInChannel.channel1).toEqual([{order: ['post1', 'post2'], recent: true}]);
});
test('should remove non-existent pending post', () => {
const state = {
entities: {
channels: {
currentChannelId: 'channel1',
},
files: {
fileIdsByPostId: {},
},
general: {
config: {},
},
posts: {
pendingPostIds: ['pending'],
posts: {
post1: {id: 'post1', channel_id: 'channel1'},
post2: {id: 'post2', channel_id: 'channel1'},
},
postsInChannel: {
channel1: [
{order: ['post1', 'post2'], recent: true},
],
},
postsInThread: {},
reactions: {},
},
preferences: {
myPreferences: {},
},
},
views: {
team: {
lastChannelForTeam: {
team1: ['channel1'],
},
},
},
};
const result = cleanUpState(state);
expect(result.entities.posts.pendingPostIds).toEqual([]);
expect(result.entities.posts.postsInChannel.channel1).toEqual([{order: ['post1', 'post2'], recent: true}]);
});
test('should always set _persist.rehydrated to true', () => {
const persistValues = [
null,
{},
{rehydrated: false},
{rehydrated: true},
];
for (let i = 0; i < persistValues.length; i++) {
const _persist = persistValues[i]; // eslint-disable-line no-underscore-dangle
const state = merge(initialState, {
// eslint-disable-next-line no-underscore-dangle
_persist,
});
const result = cleanUpState(state);
expect(result._persist.rehydrated).toBe(true); // eslint-disable-line no-underscore-dangle
}
});
test('should set views.root.hydrationComplete to true when previous views.root.hydrationComplete is true', () => {
const state = merge(initialState, {
views: {
root: {
hydrationComplete: true,
},
},
});
const result = cleanUpState(state);
expect(result.views.root.hydrationComplete).toBe(true);
});
test('should set views.root.hydrationComplete to !_persist when previous views.root.hydrationComplete is falsy', () => {
const persistValues = [true, false];
const viewsValues = [
{},
{root: {}},
{root: {hydrationComplete: false}},
];
for (let i = 0; i < persistValues.length; i++) {
const _persist = persistValues[i]; // eslint-disable-line no-underscore-dangle
for (let j = 0; j < viewsValues.length; j++) {
const views = viewsValues[j];
const state = merge(initialState, {
_persist,
views,
});
const result = cleanUpState(state);
expect(result.views.root.hydrationComplete).toBe(!_persist); // eslint-disable-line no-underscore-dangle
}
}
});
});
describe('cleanUpPostsInChannel', () => {
test('should only keep posts for recently viewed channels', () => {
const postsInChannel = {
channel1: [
{order: ['a', 'b'], recent: true},
],
channel2: [
{order: ['c', 'd'], recent: true},
],
};
const lastChannelForTeam = {
team1: ['channel1'],
};
const nextPostsInChannel = cleanUpPostsInChannel(postsInChannel, lastChannelForTeam);
expect(nextPostsInChannel).toEqual({
channel1: [
{order: ['a', 'b'], recent: true},
],
});
});
test('should keep posts for recently viewed channels across multiple teams', () => {
const postsInChannel = {
channel1: [
{order: ['a', 'b'], recent: true},
],
channel2: [
{order: ['c', 'd'], recent: true},
],
};
const lastChannelForTeam = {
team1: ['channel1'],
team2: ['channel2'],
};
const nextPostsInChannel = cleanUpPostsInChannel(postsInChannel, lastChannelForTeam);
expect(nextPostsInChannel).toEqual({
channel1: [
{order: ['a', 'b'], recent: true},
],
channel2: [
{order: ['c', 'd'], recent: true},
],
});
});
test('should only keep the last X posts in each channel', () => {
const postsInChannel = {
channel1: [
{order: ['a', 'b', 'c', 'd'], recent: true},
],
channel2: [
{order: ['e', 'f', 'g', 'h'], recent: true},
],
};
const lastChannelForTeam = {
team1: ['channel1', 'channel2'],
};
const nextPostsInChannel = cleanUpPostsInChannel(postsInChannel, lastChannelForTeam, '', 2);
expect(nextPostsInChannel).toEqual({
channel1: [
{order: ['a', 'b'], recent: true},
],
channel2: [
{order: ['e', 'f'], recent: true},
],
});
});
test('should only keep the most recent posts in a channel', () => {
const postsInChannel = {
channel1: [
{order: ['a', 'b', 'c', 'd'], recent: true},
{order: ['e', 'f', 'g', 'h']},
],
};
const lastChannelForTeam = {
team1: ['channel1'],
};
const nextPostsInChannel = cleanUpPostsInChannel(postsInChannel, lastChannelForTeam, '');
expect(nextPostsInChannel).toEqual({
channel1: [
{order: ['a', 'b', 'c', 'd'], recent: true},
],
});
});
test('should keep all posts in the current channel, if specified', () => {
const postsInChannel = {
channel1: [
{order: ['a', 'b', 'c', 'd'], recent: true},
],
channel2: [
{order: ['e', 'f', 'g', 'h'], recent: true},
],
};
const lastChannelForTeam = {
team1: ['channel1', 'channel2'],
};
const nextPostsInChannel = cleanUpPostsInChannel(postsInChannel, lastChannelForTeam, 'channel2', 2);
expect(nextPostsInChannel).toEqual({
channel1: [
{order: ['a', 'b'], recent: true},
],
channel2: [
{order: ['e', 'f', 'g', 'h'], recent: true},
],
});
});
test('should not error when a DM/GM channel appears on multiple teams', () => {
const postsInChannel = {
dmChannel: [
{order: ['a', 'b', 'c', 'd'], recent: true},
],
};
const lastChannelForTeam = {
team1: ['dmChannel'],
team2: ['dmChannel'],
};
const nextPostsInChannel = cleanUpPostsInChannel(postsInChannel, lastChannelForTeam);
expect(nextPostsInChannel).toEqual({
dmChannel: [
{order: ['a', 'b', 'c', 'd'], recent: true},
],
});
});
test('should not error when a recent channel is missing', () => {
const postsInChannel = {
channel1: [
{order: ['a', 'b', 'c', 'd'], recent: true},
],
};
const lastChannelForTeam = {
team1: ['channel1', 'channel2'],
};
const nextPostsInChannel = cleanUpPostsInChannel(postsInChannel, lastChannelForTeam);
expect(nextPostsInChannel).toEqual({
channel1: [
{order: ['a', 'b', 'c', 'd'], recent: true},
],
});
});
});
describe('cleanUpThreadsInTeam', () => {
const threadsInTeam = {
team1: ['thread1', 'thread2', 'thread3'],
team2: ['thread3', 'thread4', 'thread5'],
team3: ['thread1', 'thread2', 'thread3', 'thread4'],
team4: ['thread7'],
};
const posts = {
thread1: {},
thread2: {},
thread3: {},
thread4: {},
thread5: {},
thread6: {},
thread7: {state: Posts.POST_DELETED},
};
const threads = {
thread1: {id: 'thread1', last_reply_at: 100},
thread2: {id: 'thread2', last_reply_at: 99},
thread3: {id: 'thread3', last_reply_at: 98},
thread4: {id: 'thread4', last_reply_at: 97},
thread5: {id: 'thread5', last_reply_at: 96},
thread6: {id: 'thread6', last_reply_at: 95},
thread7: {id: 'thread7', last_reply_at: 100},
};
const {threads: nextThreads, threadsInTeam: nextThreadsInTeam} = cleanUpThreadsInTeam(posts, threads, threadsInTeam, 'team3', 2);
test('should only keep limited threads per team', () => {
expect(nextThreadsInTeam.team1).toEqual(['thread1', 'thread2']);
expect(nextThreadsInTeam.team2).toEqual(['thread3', 'thread4']);
expect(nextThreads.thread1).toBeTruthy();
expect(nextThreads.thread5).toBeFalsy();
});
test('should not remove the thread if included in one team and not included in another', () => {
expect(nextThreadsInTeam.team1.indexOf('thread3')).toBe(-1);
expect(nextThreadsInTeam.team2.indexOf('thread3')).toBeGreaterThan(-1);
expect(nextThreads.thread3).toBeTruthy();
});
test('Should exclude passed teamId', () => {
expect(nextThreadsInTeam.team3).toEqual(threadsInTeam.team3);
});
test('Should not include deleted posts', () => {
expect(nextThreadsInTeam.team4).toEqual([]);
});
});
describe('getAllFromPostsInChannel', () => {
test('should return every post ID', () => {
const postsInChannel = {
channel1: [
{order: ['a', 'b']},
{order: ['c', 'd']},
],
channel2: [
{order: ['e', 'f']},
],
channel3: [
{order: ['g', 'h']},
],
};
const postIds = getAllFromPostsInChannel(postsInChannel);
expect(postIds).toMatchObject(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
});
});