mattermost-mobile/app/store/middleware.test.js
Amit Uttam a8147a1697
[MM-23490] Save state to file via async middleware vs store subscription (#4059)
MM-23490 Save state to file via async middleware vs store subscription

Currently for iOS, a subset of store state is saved to an on-device file, so that the Share Extension can have access to information it needs (teams and channels) to function.

This file saving would happen via a store subscription which triggers a file save for every dispatched action. By moving this logic to a middleware function, when this function gets invoked is now limited to a configurable set of action dispatches. (e.g. `LOGIN`, `CONNECTION_CHANGED`, `WEBSOCKET_SUCCESS`), etc.

MM-23493 Move app cache purge from store subscription to middleware (#4069)

* MM-23493 Move app cache purge from store subscription to middleware

This commit exposes persistence configuration as a static reference, so that cache purging can be invoked on demand anywhere else in the codebase.
While middleware still may not be the best spot for this singular "action", existing functionality (reacting to `OFFLINE_STORE_PURGE`) is maintained.

The change also removes the need for `state.views.root.purge` to exist in the state tree.

* PR feedback: Inject config dependency for purging app cache

Previously, `middleware` imported the config back from `store` (i.e. cyclic import).

* PR feedback: No need to export config, now that it's passed as argument

* Fix tests after refactoring middleware call from array -> function

* PR feedback: Let parent continue to pass down initial store state
2020-04-18 01:32:47 -03:00

414 lines
13 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 {ViewTypes} from 'app/constants';
import {
cleanUpPostsInChannel,
cleanUpState,
getAllFromPostsInChannel,
middlewares,
} from 'app/store/middleware';
describe('messageRetention', () => {
const messageRetention = middlewares()[0];
describe('should chain the same incoming action type', () => {
const actions = [
{
type: 'persist/REHYDRATE',
payload: {
views: {
team: {
},
},
},
},
{
type: ViewTypes.DATA_CLEANUP,
payload: {
entities: {
channels: {
},
posts: {
},
},
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('cleanUpState', () => {
test('should remove post because of data retention', () => {
const state = {
entities: {
channels: {
currentChannelId: 'channel1',
},
files: {
fileIdsByPostId: {},
},
general: {
dataRetentionPolicy: {
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: {},
},
search: {
results: ['post1', 'post2'],
flagged: ['post1', 'post2', 'post3'],
},
},
views: {
team: {
lastChannelForTeam: {
team1: ['channel1'],
},
},
},
};
const result = cleanUpState({payload: state});
expect(result.payload.entities.posts.posts.post1).toBeDefined();
expect(result.payload.entities.posts.posts.post2).toBeUndefined();
expect(result.payload.entities.posts.postsInChannel.channel1).toEqual([{order: ['post1'], recent: true}]);
expect(result.payload.entities.search.results).toEqual(['post1', 'post2']);
expect(result.payload.entities.search.flagged).toEqual(['post1', 'post2', 'post3']);
});
test('should keep failed pending post', () => {
const state = {
entities: {
channels: {
currentChannelId: 'channel1',
},
files: {
fileIdsByPostId: {},
},
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: {},
},
},
views: {
team: {
lastChannelForTeam: {
team1: ['channel1'],
},
},
},
};
const result = cleanUpState({payload: state});
expect(result.payload.entities.posts.pendingPostIds).toEqual(['pending']);
expect(result.payload.entities.posts.posts.pending).toBeDefined();
expect(result.payload.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: {},
},
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: {},
},
},
views: {
team: {
lastChannelForTeam: {
team1: ['channel1'],
},
},
},
};
const result = cleanUpState({payload: state});
expect(result.payload.entities.posts.pendingPostIds).toEqual([]);
expect(result.payload.entities.posts.posts.pending).toBeUndefined();
expect(result.payload.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: {},
},
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: {},
},
},
views: {
team: {
lastChannelForTeam: {
team1: ['channel1'],
},
},
},
};
const result = cleanUpState({payload: state});
expect(result.payload.entities.posts.pendingPostIds).toEqual([]);
expect(result.payload.entities.posts.postsInChannel.channel1).toEqual([{order: ['post1', 'post2'], recent: true}]);
});
});
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('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']);
});
});