Files
mattermost-mobile/app/actions/websocket/websocket.test.js
Guillermo Vayá 0ee7b60e84 [MM-30857] request postssince on reconnect (#5000)
* [MM-30857] request postssince on reconnect

* [MM-30857] add test

* Address CR, add debounce in case of unstable connection

* remove leftovers

* Address review comments

* remove previous changes

* fix tests
2020-12-10 20:49:44 -07:00

553 lines
17 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-import-assign */
import assert from 'assert';
import nock from 'nock';
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import {GeneralTypes, UserTypes} from '@mm-redux/action_types';
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
import {Client4} from '@mm-redux/client';
import {General, Posts, RequestStatus} from '@mm-redux/constants';
import * as Actions from '@actions/websocket';
import {WebsocketEvents} from '@constants';
import TestHelper from 'test/test_helper';
import configureStore from 'test/test_store';
global.WebSocket = MockWebSocket;
const mockConfigRequest = (config = {}) => {
nock(Client4.getBaseRoute()).
get('/config/client?format=old').
reply(200, config);
};
const mockChanelsRequest = (teamId, channels = []) => {
nock(Client4.getUserRoute('me')).
get(`/teams/${teamId}/channels?include_deleted=true`).
reply(200, channels);
};
const mockGetKnownUsersRequest = (userIds = []) => {
nock(Client4.getBaseRoute()).
get('/users/known').
reply(200, userIds);
};
const mockRolesRequest = (rolesToLoad = []) => {
nock(Client4.getRolesRoute()).
post('/names', JSON.stringify(rolesToLoad)).
reply(200, rolesToLoad);
};
const mockTeamMemberRequest = (tm = []) => {
nock(Client4.getUserRoute('me')).
get('/teams/members').
reply(200, tm);
};
describe('Actions.Websocket', () => {
let store;
let mockServer;
beforeAll(async () => {
store = await configureStore();
await TestHelper.initBasic(Client4);
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
mockServer = new Server(connUrl);
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
});
afterAll(async () => {
Actions.close()();
mockServer.stop();
await TestHelper.tearDown();
});
it('WebSocket Connect', () => {
const ws = store.getState().requests.general.websocket;
assert.ok(ws.status === RequestStatus.SUCCESS);
});
});
describe('Actions.Websocket doReconnect', () => {
const mockStore = configureMockStore([thunk]);
const me = TestHelper.fakeUserWithId();
const team = TestHelper.fakeTeamWithId();
const teamMember = TestHelper.fakeTeamMember(me.id, team.id);
const channel1 = TestHelper.fakeChannelWithId(team.id);
const channel2 = TestHelper.fakeChannelWithId(team.id);
const cMember1 = TestHelper.fakeChannelMember(me.id, channel1.id);
const cMember2 = TestHelper.fakeChannelMember(me.id, channel2.id);
const currentTeamId = team.id;
const currentUserId = me.id;
const currentChannelId = channel1.id;
const initialState = {
entities: {
general: {
config: {},
},
teams: {
currentTeamId,
myMembers: {
[currentTeamId]: teamMember,
},
teams: {
[currentTeamId]: team,
},
},
channels: {
currentChannelId,
channels: {
currentChannelId: channel1,
},
},
users: {
currentUserId,
profiles: {
[me.id]: me,
},
},
preferences: {
myPreferences: {},
},
posts: {
posts: {},
postsInChannel: {},
},
},
websocket: {
connected: false,
lastConnectAt: 0,
lastDisconnectAt: 0,
},
};
beforeAll(async () => {
return TestHelper.initBasic(Client4);
});
beforeEach(() => {
nock(Client4.getBaseRoute()).
get('/users/me').
reply(200, me);
nock(Client4.getUserRoute('me')).
get('/teams').
reply(200, [team]);
nock(Client4.getUserRoute('me')).
get('/teams/unread').
reply(200, [{id: team.id, msg_count: 0, mention_count: 0}]);
nock(Client4.getBaseRoute()).
get('/users/me/preferences').
reply(200, []);
nock(Client4.getUserRoute('me')).
get(`/teams/${team.id}/channels/members`).
reply(200, [cMember1, cMember2]);
nock(Client4.getChannelRoute(channel1.id)).
get(`/posts?page=0&per_page=${Posts.POST_CHUNK_SIZE}`).
reply(200, {
posts: {
post1: {id: 'post1', create_at: 0, message: 'hey'},
},
order: ['post1'],
});
});
afterAll(async () => {
Actions.close()();
await TestHelper.tearDown();
});
it('handle doReconnect', async () => {
const state = {...initialState};
const testStore = await mockStore(state);
const timestamp = 1000;
const expectedActions = [
GeneralTypes.WEBSOCKET_SUCCESS,
];
const expectedMissingActions = [
'BATCH_WS_RECONNECT',
];
mockConfigRequest();
mockTeamMemberRequest([teamMember]);
mockChanelsRequest(team.id, [channel1, channel2]);
let rolesToLoad = Array.from(new Set(me.roles.split(' ').
concat(teamMember.roles.split(' '))));
mockRolesRequest(rolesToLoad);
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
concat(cMember2.roles.split(' '))));
mockRolesRequest(rolesToLoad);
await testStore.dispatch(Actions.doReconnect(timestamp));
await TestHelper.wait(300);
const actionTypes = testStore.getActions().map((a) => a.type);
expect(actionTypes).toEqual(expectedActions);
expect(actionTypes).not.toEqual(expect.arrayContaining(expectedMissingActions));
});
it('handle doReconnect after the current channel was archived or the user left it', async () => {
const state = {
...initialState,
entities: {
...initialState.entities,
channels: {
...initialState.entities.channels,
currentChannelId: 'channel-3',
},
},
};
const testStore = await mockStore(state);
const timestamp = 1000;
const expectedActions = [
GeneralTypes.WEBSOCKET_SUCCESS,
'BATCH_WS_RECONNECT',
];
const expectedMissingActions = [
'BATCH_GET_POSTS_SINCE',
];
mockConfigRequest();
mockTeamMemberRequest([teamMember]);
mockChanelsRequest(team.id, [channel1, channel2]);
let rolesToLoad = Array.from(new Set(me.roles.split(' ').
concat(teamMember.roles.split(' '))));
mockRolesRequest(rolesToLoad);
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
concat(cMember2.roles.split(' '))));
mockRolesRequest(rolesToLoad);
await testStore.dispatch(Actions.doReconnect(timestamp));
await TestHelper.wait(300);
const actions = testStore.getActions().map((a) => a.type);
expect(actions).toEqual(expect.arrayContaining(expectedActions));
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
});
it('handle doReconnect after the current channel was archived and setting is on', async () => {
const archived = {
...channel1,
delete_at: 123,
};
const state = {
...initialState,
channels: {
currentChannelId,
channels: {
currentChannelId: archived,
},
},
};
const testStore = await mockStore(state);
const timestamp = 1000;
const expectedActions = [
GeneralTypes.WEBSOCKET_SUCCESS,
];
const expectedMissingActions = [
'BATCH_WS_RECONNECT',
];
mockConfigRequest({ExperimentalViewArchivedChannels: 'true'});
mockTeamMemberRequest([teamMember]);
mockChanelsRequest(team.id, [archived, channel2]);
let rolesToLoad = Array.from(new Set(me.roles.split(' ').
concat(teamMember.roles.split(' '))));
mockRolesRequest(rolesToLoad);
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
concat(cMember2.roles.split(' '))));
mockRolesRequest(rolesToLoad);
await testStore.dispatch(Actions.doReconnect(timestamp));
await TestHelper.wait(300);
const actions = testStore.getActions().map((a) => a.type);
expect(actions).toEqual(expect.arrayContaining(expectedActions));
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
});
it('handle doReconnect after the current channel was archived and setting is off', async () => {
const archived = {
...channel1,
delete_at: 123,
};
const state = {
...initialState,
channels: {
currentChannelId,
channels: {
currentChannelId: archived,
},
},
};
const testStore = await mockStore(state);
const timestamp = 1000;
const expectedActions = [
GeneralTypes.WEBSOCKET_SUCCESS,
'BATCH_WS_RECONNECT',
];
const expectedMissingActions = [
'BATCH_GET_POSTS_SINCE',
];
mockConfigRequest({ExperimentalViewArchivedChannels: 'false'});
mockTeamMemberRequest([teamMember]);
mockChanelsRequest(team.id, [archived, channel2]);
let rolesToLoad = Array.from(new Set(me.roles.split(' ').
concat(teamMember.roles.split(' '))));
mockRolesRequest(rolesToLoad);
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
concat(cMember2.roles.split(' '))));
mockRolesRequest(rolesToLoad);
await testStore.dispatch(Actions.doReconnect(timestamp));
await TestHelper.wait(300);
const actions = testStore.getActions().map((a) => a.type);
expect(actions).toEqual(expectedActions);
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
});
it('handle doReconnect after user left current team', async () => {
const state = {...initialState};
state.entities.teams.myMembers = {};
const testStore = await mockStore(state);
const timestamp = 1000;
const expectedActions = [
GeneralTypes.WEBSOCKET_SUCCESS,
'BATCH_WS_LEAVE_TEAM',
'BATCH_WS_RECONNECT',
];
const expectedMissingActions = [
'BATCH_GET_POSTS_SINCE',
];
mockConfigRequest();
mockTeamMemberRequest([]);
mockChanelsRequest(team.id, [channel1, channel2]);
let rolesToLoad = me.roles.split(' ');
mockRolesRequest(rolesToLoad);
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
concat(cMember2.roles.split(' '))));
mockRolesRequest(rolesToLoad);
await testStore.dispatch(Actions.doReconnect(timestamp));
await TestHelper.wait(300);
const actions = testStore.getActions().map((a) => a.type);
expect(actions).toEqual(expectedActions);
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
});
});
describe('Actions.Websocket notVisibleUsersActions', () => {
configureMockStore([thunk]);
const me = TestHelper.fakeUserWithId();
const user = TestHelper.fakeUserWithId();
const user2 = TestHelper.fakeUserWithId();
const user3 = TestHelper.fakeUserWithId();
const user4 = TestHelper.fakeUserWithId();
const user5 = TestHelper.fakeUserWithId();
it('should do nothing if the known users and the profiles list are the same', async () => {
const profiles = {
[me.id]: me,
[user.id]: user,
[user2.id]: user2,
[user3.id]: user3,
};
Client4.serverVersion = '5.23.0';
const state = {
entities: {
users: {
currentUserId: me.id,
profiles,
},
},
};
mockGetKnownUsersRequest([user.id, user2.id, user3.id]);
const actions = await notVisibleUsersActions(state);
expect(actions.length).toEqual(0);
});
it('should do nothing if there are known users in my memberships but not in the profiles list', async () => {
const profiles = {
[me.id]: me,
[user3.id]: user3,
};
Client4.serverVersion = '5.23.0';
const state = {
entities: {
users: {
currentUserId: me.id,
profiles,
},
},
};
mockGetKnownUsersRequest([user.id, user2.id, user3.id]);
const actions = await notVisibleUsersActions(state);
expect(actions.length).toEqual(0);
});
it('should remove the users if there are unknown users in the profiles list', async () => {
const profiles = {
[me.id]: me,
[user.id]: user,
[user2.id]: user2,
[user3.id]: user3,
[user4.id]: user4,
[user5.id]: user5,
};
Client4.serverVersion = '5.23.0';
const state = {
entities: {
users: {
currentUserId: me.id,
profiles,
},
},
};
mockGetKnownUsersRequest([user.id, user3.id]);
const expectedAction = [
{type: UserTypes.PROFILE_NO_LONGER_VISIBLE, data: {user_id: user2.id}},
{type: UserTypes.PROFILE_NO_LONGER_VISIBLE, data: {user_id: user4.id}},
{type: UserTypes.PROFILE_NO_LONGER_VISIBLE, data: {user_id: user5.id}},
];
const actions = await notVisibleUsersActions(state);
expect(actions.length).toEqual(3);
expect(actions).toEqual(expectedAction);
});
it('should do nothing if the server version is less than 5.23', async () => {
const profiles = {
[me.id]: me,
[user.id]: user,
[user2.id]: user2,
[user3.id]: user3,
[user4.id]: user4,
[user5.id]: user5,
};
Client4.serverVersion = '5.22.0';
const state = {
entities: {
users: {
currentUserId: me.id,
profiles,
},
},
};
mockGetKnownUsersRequest([user.id, user3.id]);
const actions = await notVisibleUsersActions(state);
expect(actions.length).toEqual(0);
});
});
describe('Actions.Websocket handleUserTypingEvent', () => {
const mockStore = configureMockStore([thunk]);
const currentUserId = 'user-id';
const otherUserId = 'other-user-id';
const currentChannelId = 'channel-id';
const otherChannelId = 'other-channel-id';
const initialState = {
entities: {
general: {
config: {},
},
channels: {
currentChannelId,
channels: {
currentChannelId: {
id: currentChannelId,
name: 'channel',
},
},
},
users: {
currentUserId,
profiles: {
[currentUserId]: {},
[otherUserId]: {},
},
statuses: {
[currentUserId]: General.ONLINE,
[otherUserId]: General.OFFLINE,
},
},
preferences: {
myPreferences: {},
},
},
};
it('dispatches actions for current channel if other user is typing', async () => {
const state = {...initialState};
const testStore = await mockStore(state);
const msg = {broadcast: {channel_id: currentChannelId}, data: {parent_id: 'parent-id', user_id: otherUserId}};
nock(Client4.getUsersRoute()).
post('/status/ids', JSON.stringify([otherUserId])).
reply(200, ['away']);
const expectedActionsTypes = [
WebsocketEvents.TYPING,
UserTypes.RECEIVED_STATUSES,
];
await testStore.dispatch(Actions.handleUserTypingEvent(msg));
await TestHelper.wait(300);
const actionTypes = testStore.getActions().map((action) => action.type);
expect(actionTypes).toEqual(expectedActionsTypes);
});
it('does not dispatch actions for non current channel', async () => {
const state = {...initialState};
const testStore = await mockStore(state);
const msg = {broadcast: {channel_id: otherChannelId}, data: {parent_id: 'parent-id', user_id: otherUserId}};
const expectedActionsTypes = [];
await testStore.dispatch(Actions.handleUserTypingEvent(msg));
const actionTypes = testStore.getActions().map((action) => action.type);
expect(actionTypes).toEqual(expectedActionsTypes);
});
});