mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-14 15:10:17 +00:00
* MM-68219 - Add redacted files placeholder and related functionality * add feature flag control and e2e tests * adjust placeholder design, polish code, fix permalink preview issue * polish code and add missing test * use import path with right path * gate post refetch behind the feature flag, refetch the open thread and avoid observeCount * restore filesInfo for the permalink files prop added upstream * fix stale redaction on background servers, consume the flag on every fetch path and share the viewport emitter * replace the in-memory redaction flag with a persisted per-post epoch so ABAC file decisions survive restarts and stale posts stay hidden * raise the redaction epoch on every resync, capture the epoch counter, only invalidate channels on role changes, and store the newer epoch when a re-verified post is otherwise unchanged * store superseded responses unverified instead of dropping them, keep the since-fetch when re-verifying, verify permalink embeds against the linked channel, and capture the websocket epoch on arrival * nvalidate file decisions when a user attribute field changes or all its values are cleared * batch channel invalidations, close file viewers only when affected, reset the epoch on unstamped redaction changes, and only re-verify the newest page's attachment posts * re-check older channel posts a page at a time instead of one request per post * skip the unstamped epoch lookup for posts with no files, redacted count or permalink * keep the attribute-view retry after later invalidations, retry linked posts unless refused for the current epoch, and stop older payloads from rolling back edits via the redaction check * MM-70905 - disable and grey out the attachment button when an ABAC pe… (#10195) * MM-70905 - disable and grey out the attachment button when an ABAC permission policy denies uploads in the channel * only back off render permission fetches when the server request fails, and log other errors without retrying * remove the disableServer helper and have removeServer do both steps * split defaultAllowed into allowWhenUnenforced and allowWhenNotEvaluated, and treat render permissions as unenforced on servers without the decisions API * MM-69627: Add playbook task requirements support on mobile (#10035) * MM-69627: Add playbook task requirements support on mobile Allow filling required task fields when checking off checklist items during a run, gated by the same Playbooks beta setting used on web. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Address PR review feedback for task requirements Propagate settings write failures, harden fill-requirements save/title handling, and add missing unit coverage. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Fix TypeScript errors in new unit tests Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Use defineMessages for fill requirements titles Keep i18n extraction able to find the header titles after moving them to the route. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Address review feedback for task requirements Update checklist items locally when saving requirement values instead of refetching the run, add missing query/navigation tests, and apply small callback/reduce cleanups from review. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Fix task requirements review follow-ups Propagate local checklist write failures, clear a stale task-requirements flag when playbooks is unsupported, and cap fill values at the desktop/ server max length. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Move requirement field into its own component Keep one component per file on the fill-requirements screen. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69122: Add file element support to interactive dialogs (#9898) * Implement Interactive Dialog - file upload * update demo plugin version for e2etests * fix and add e2etests * review fixes * Fix version bump checkout when a tag shares the branch name (#10229) * Fix Android crashes caused by animated custom emoji (#10030) * Route GIF decoding through Glide - Keep APNG4Android for APNG, animated WebP, and animated AVIF support. - Prepend Glide's GIF decoders to avoid APNG4Android's native GIF overflow. * Clear Expo image resources with Glide targets - Detach drawables when Glide clears their custom target. - Prevent recycled GIF frame resources from remaining attached to Expo image views. * Preserve foreground image during GIF target recycling --------- Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Nuno Simões <nuno.simoes@mattermost.com> Co-authored-by: MangelSpec <74370284+MangelSpec@users.noreply.github.com> * detox - fix clobbering of feature flags that cause failures in other suites. Also small fix of a incorrectly set test-id --------- Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Nuno Simões <nuno.simoes@mattermost.com> Co-authored-by: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Co-authored-by: Nate Schlossberg <nate.schlossberg@mattermost.com>
661 lines
32 KiB
TypeScript
661 lines
32 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {addChannelToDefaultCategory, handleConvertedGMCategories} from '@actions/local/category';
|
|
import {markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, updateChannelInfoFromChannel, updateMyChannelFromWebsocket, deletePostsForChannel} from '@actions/local/channel';
|
|
import {storePostsForChannel} from '@actions/local/post';
|
|
import {fetchMyChannel, fetchChannelById, fetchMissingDirectChannelsInfo} from '@actions/remote/channel';
|
|
import {fetchPostsForChannel} from '@actions/remote/post';
|
|
import {fetchRolesIfNeeded} from '@actions/remote/role';
|
|
import {fetchUsersByIds, updateUsersNoLongerVisible} from '@actions/remote/user';
|
|
import {invalidateRedactionForChannelMembership} from '@actions/websocket/access_control';
|
|
import {leaveCall} from '@calls/actions/calls';
|
|
import {getCurrentCall} from '@calls/state';
|
|
import {General} from '@constants';
|
|
import DatabaseManager from '@database/manager';
|
|
import {deleteChannelMembership, getChannelById, getCurrentChannel, getMyChannel, prepareMyChannelsForTeam} from '@queries/servers/channel';
|
|
import {setCurrentTeamId, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, canViewArchivedChannels} from '@queries/servers/system';
|
|
import {getCurrentUser, getTeammateNameDisplay, getUserById} from '@queries/servers/user';
|
|
import EphemeralStore from '@store/ephemeral_store';
|
|
import TestHelper from '@test/test_helper';
|
|
|
|
import {handleChannelCreatedEvent, handleChannelUnarchiveEvent, handleChannelConvertedEvent, handleChannelUpdatedEvent, handleChannelViewedEvent, handleMultipleChannelsViewedEvent, handleChannelMemberUpdatedEvent, handleChannelDeletedEvent, handleDirectAddedEvent, handleUserAddedToChannelEvent, handleUserRemovedFromChannelEvent} from './channel';
|
|
|
|
import type ServerDataOperator from '@database/operator/server_data_operator';
|
|
import type ChannelModel from '@typings/database/models/servers/channel';
|
|
|
|
jest.mock('@database/manager');
|
|
jest.mock('@store/ephemeral_store');
|
|
jest.mock('@actions/local/category');
|
|
jest.mock('@actions/remote/category');
|
|
jest.mock('@actions/remote/channel');
|
|
jest.mock('@actions/remote/role');
|
|
jest.mock('@actions/remote/post');
|
|
jest.mock('@actions/remote/user');
|
|
jest.mock('@actions/websocket/access_control');
|
|
jest.mock('@actions/local/channel');
|
|
jest.mock('@actions/local/post');
|
|
jest.mock('@queries/servers/channel');
|
|
jest.mock('@queries/servers/system');
|
|
jest.mock('@queries/servers/user');
|
|
jest.mock('@calls/actions');
|
|
jest.mock('@actions/local/user');
|
|
jest.mock('@utils/log');
|
|
jest.mock('@calls/state');
|
|
jest.mock('@calls/actions/calls');
|
|
|
|
const serverUrl = 'baseHandler.test.com';
|
|
|
|
describe('WebSocket Channel Actions', () => {
|
|
let msg: any;
|
|
let operator: ServerDataOperator;
|
|
|
|
const teamId = 'teamid1';
|
|
const channelId = 'channelid1';
|
|
const channel = {id: channelId, team_id: teamId} as Channel;
|
|
const channelModel = {
|
|
id: channelId,
|
|
type: General.PRIVATE_CHANNEL,
|
|
teamId,
|
|
createAt: 0,
|
|
updateAt: 0,
|
|
deleteAt: 0,
|
|
displayName: 'channeldisplayname',
|
|
name: 'channelname',
|
|
} as ChannelModel;
|
|
const userId = 'userid1';
|
|
|
|
const mockedGetChannelById = jest.mocked(getChannelById);
|
|
const mockedFetchMyChannel = jest.mocked(fetchMyChannel);
|
|
const mockedPrepareMyChannelsForTeam = jest.mocked(prepareMyChannelsForTeam);
|
|
const mockedFetchChannelById = jest.mocked(fetchChannelById);
|
|
|
|
beforeEach(async () => {
|
|
await DatabaseManager.init([serverUrl]);
|
|
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
|
|
msg = {
|
|
data: {
|
|
team_id: teamId,
|
|
channel_id: channelId,
|
|
channel: JSON.stringify({id: channelId, type: General.PRIVATE_CHANNEL, team_id: teamId}),
|
|
},
|
|
broadcast: {
|
|
channel_id: channelId,
|
|
userId,
|
|
user_id: userId,
|
|
},
|
|
};
|
|
DatabaseManager.getActiveServerUrl = jest.fn().mockResolvedValue(serverUrl);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await DatabaseManager.destroyServerDatabase(serverUrl);
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
describe('handleChannelCreatedEvent', () => {
|
|
it('should handle channel created event', async () => {
|
|
(EphemeralStore.creatingChannel as boolean) = false;
|
|
mockedGetChannelById.mockResolvedValue(undefined);
|
|
mockedFetchMyChannel.mockResolvedValue({channels: [channel], memberships: [{} as any]});
|
|
mockedPrepareMyChannelsForTeam.mockResolvedValue([[] as any]);
|
|
(addChannelToDefaultCategory as jest.Mock).mockResolvedValue({models: []});
|
|
|
|
await handleChannelCreatedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).toHaveBeenCalled();
|
|
expect(fetchMyChannel).toHaveBeenCalledWith(serverUrl, teamId, channelId, true);
|
|
expect(prepareMyChannelsForTeam).toHaveBeenCalled();
|
|
expect(addChannelToDefaultCategory).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle channel created event - no channels', async () => {
|
|
(EphemeralStore.creatingChannel as boolean) = false;
|
|
mockedGetChannelById.mockResolvedValue(undefined);
|
|
mockedFetchMyChannel.mockResolvedValue({channels: undefined, memberships: undefined});
|
|
|
|
await handleChannelCreatedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).toHaveBeenCalled();
|
|
expect(fetchMyChannel).toHaveBeenCalledWith(serverUrl, teamId, channelId, true);
|
|
expect(prepareMyChannelsForTeam).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle channel created event - no members', async () => {
|
|
(EphemeralStore.creatingChannel as boolean) = false;
|
|
mockedGetChannelById.mockResolvedValue(undefined);
|
|
mockedFetchMyChannel.mockResolvedValue({channels: [channel], memberships: undefined});
|
|
|
|
await handleChannelCreatedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).toHaveBeenCalled();
|
|
expect(fetchMyChannel).toHaveBeenCalledWith(serverUrl, teamId, channelId, true);
|
|
expect(prepareMyChannelsForTeam).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle channel created event - nothing to prepare', async () => {
|
|
(EphemeralStore.creatingChannel as boolean) = false;
|
|
mockedGetChannelById.mockResolvedValue(undefined);
|
|
mockedFetchMyChannel.mockResolvedValue({channels: [channel], memberships: [{} as any]});
|
|
mockedPrepareMyChannelsForTeam.mockResolvedValue([]);
|
|
|
|
await handleChannelCreatedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).toHaveBeenCalled();
|
|
expect(fetchMyChannel).toHaveBeenCalledWith(serverUrl, teamId, channelId, true);
|
|
expect(prepareMyChannelsForTeam).toHaveBeenCalled();
|
|
expect(addChannelToDefaultCategory).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return if creatingChannel is true', async () => {
|
|
(EphemeralStore.creatingChannel as boolean) = true;
|
|
|
|
await handleChannelCreatedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return if channel already exists', async () => {
|
|
(EphemeralStore.creatingChannel as boolean) = false;
|
|
mockedGetChannelById.mockResolvedValue(channelModel);
|
|
|
|
await handleChannelCreatedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).toHaveBeenCalled();
|
|
expect(fetchMyChannel).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('handleChannelUnarchiveEvent', () => {
|
|
it('should handle channel unarchive event', async () => {
|
|
(EphemeralStore.isArchivingChannel as jest.Mock).mockReturnValue(false);
|
|
|
|
await handleChannelUnarchiveEvent(serverUrl, msg);
|
|
|
|
expect(setChannelDeleteAt).toHaveBeenCalledWith(serverUrl, channelId, 0);
|
|
});
|
|
|
|
it('should return if channel is being archived', async () => {
|
|
(EphemeralStore.isArchivingChannel as jest.Mock).mockReturnValue(true);
|
|
|
|
await handleChannelUnarchiveEvent(serverUrl, msg);
|
|
|
|
expect(setChannelDeleteAt).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('handleChannelConvertedEvent', () => {
|
|
it('should handle channel converted event', async () => {
|
|
(EphemeralStore.isConvertingChannel as jest.Mock).mockReturnValue(false);
|
|
mockedFetchChannelById.mockResolvedValue({channel});
|
|
jest.spyOn(operator, 'handleChannel').mockResolvedValueOnce([]);
|
|
|
|
await handleChannelConvertedEvent(serverUrl, msg);
|
|
|
|
expect(EphemeralStore.isConvertingChannel).toHaveBeenCalledWith(channelId);
|
|
expect(fetchChannelById).toHaveBeenCalled();
|
|
expect(operator.handleChannel).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle channel converted event - no channel', async () => {
|
|
(EphemeralStore.isConvertingChannel as jest.Mock).mockReturnValue(false);
|
|
mockedFetchChannelById.mockResolvedValue({error: 'some error'});
|
|
jest.spyOn(operator, 'handleChannel').mockResolvedValueOnce([]);
|
|
|
|
await handleChannelConvertedEvent(serverUrl, msg);
|
|
|
|
expect(fetchChannelById).toHaveBeenCalled();
|
|
expect(operator.handleChannel).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return if channel is being converted', async () => {
|
|
(EphemeralStore.isConvertingChannel as jest.Mock).mockReturnValue(true);
|
|
|
|
await handleChannelConvertedEvent(serverUrl, msg);
|
|
expect(fetchChannelById).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('handleChannelUpdatedEvent', () => {
|
|
it('should handle channel updated event', async () => {
|
|
(EphemeralStore.isConvertingChannel as jest.Mock).mockReturnValue(false);
|
|
mockedGetChannelById.mockResolvedValue({...channelModel, type: General.GM_CHANNEL} as ChannelModel);
|
|
(updateChannelInfoFromChannel as jest.Mock).mockResolvedValue({model: []});
|
|
jest.spyOn(operator, 'handleChannel').mockResolvedValueOnce([]);
|
|
(getCurrentChannelId as jest.Mock).mockResolvedValue(channelId);
|
|
(getCurrentTeamId as jest.Mock).mockResolvedValue('otherteamid');
|
|
|
|
await handleChannelUpdatedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).toHaveBeenCalled();
|
|
expect(operator.handleChannel).toHaveBeenCalled();
|
|
expect(updateChannelInfoFromChannel).toHaveBeenCalled();
|
|
expect(handleConvertedGMCategories).toHaveBeenCalled();
|
|
expect(setCurrentTeamId).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return if channel is being converted', async () => {
|
|
(EphemeralStore.isConvertingChannel as jest.Mock).mockReturnValue(true);
|
|
|
|
await handleChannelUpdatedEvent(serverUrl, msg);
|
|
});
|
|
|
|
it('should not call handleConvertedGMCategories if channel type is not GM', async () => {
|
|
mockedGetChannelById.mockResolvedValue(channelModel);
|
|
(updateChannelInfoFromChannel as jest.Mock).mockResolvedValue({model: []});
|
|
|
|
await handleChannelUpdatedEvent(serverUrl, msg);
|
|
|
|
expect(handleConvertedGMCategories).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not call setCurrentTeamId if current team ID is the same', async () => {
|
|
mockedGetChannelById.mockResolvedValue({...channelModel, type: General.GM_CHANNEL} as ChannelModel);
|
|
(updateChannelInfoFromChannel as jest.Mock).mockResolvedValue({model: []});
|
|
(getCurrentChannelId as jest.Mock).mockResolvedValue(channelId);
|
|
(getCurrentTeamId as jest.Mock).mockResolvedValue(teamId);
|
|
|
|
await handleChannelUpdatedEvent(serverUrl, msg);
|
|
|
|
expect(setCurrentTeamId).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should call deletePostsForChannel when autotranslation is disabled', async () => {
|
|
jest.mocked(EphemeralStore.isConvertingChannel).mockReturnValue(false);
|
|
mockedGetChannelById.mockResolvedValue({...channelModel, autotranslation: true} as ChannelModel);
|
|
msg.data.channel = JSON.stringify({id: channelId, type: General.PRIVATE_CHANNEL, team_id: teamId, autotranslation: false});
|
|
jest.mocked(updateChannelInfoFromChannel).mockResolvedValue({model: []});
|
|
jest.spyOn(operator, 'handleChannel').mockResolvedValueOnce([]);
|
|
|
|
await handleChannelUpdatedEvent(serverUrl, msg);
|
|
|
|
expect(deletePostsForChannel).toHaveBeenCalledWith(serverUrl, channelId);
|
|
});
|
|
|
|
it('should not call deletePostsForChannel when autotranslation stays enabled', async () => {
|
|
jest.mocked(EphemeralStore.isConvertingChannel).mockReturnValue(false);
|
|
mockedGetChannelById.mockResolvedValue({...channelModel, autotranslation: true} as ChannelModel);
|
|
msg.data.channel = JSON.stringify({id: channelId, type: General.PRIVATE_CHANNEL, team_id: teamId, autotranslation: true});
|
|
jest.mocked(updateChannelInfoFromChannel).mockResolvedValue({model: []});
|
|
jest.spyOn(operator, 'handleChannel').mockResolvedValueOnce([]);
|
|
(deletePostsForChannel as jest.Mock).mockClear();
|
|
|
|
await handleChannelUpdatedEvent(serverUrl, msg);
|
|
|
|
expect(deletePostsForChannel).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('handleChannelViewedEvent', () => {
|
|
it('should handle channel viewed event', async () => {
|
|
(DatabaseManager.getActiveServerUrl as jest.Mock).mockResolvedValue(serverUrl);
|
|
(getCurrentChannelId as jest.Mock).mockResolvedValue('different_channel_id');
|
|
(EphemeralStore.isSwitchingToChannel as jest.Mock).mockReturnValue(false);
|
|
|
|
await handleChannelViewedEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentChannelId).toHaveBeenCalled();
|
|
expect(markChannelAsViewed).toHaveBeenCalledWith(serverUrl, channelId);
|
|
});
|
|
|
|
it('should return if current channel ID is the same and not switching to channel', async () => {
|
|
(DatabaseManager.getActiveServerUrl as jest.Mock).mockResolvedValue(serverUrl);
|
|
(getCurrentChannelId as jest.Mock).mockResolvedValue(channelId);
|
|
(EphemeralStore.isSwitchingToChannel as jest.Mock).mockReturnValue(false);
|
|
|
|
await handleChannelViewedEvent(serverUrl, msg);
|
|
|
|
expect(markChannelAsViewed).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('handleMultipleChannelsViewedEvent', () => {
|
|
it('should handle multiple channels viewed event', async () => {
|
|
msg.data.channel_times = {channel_id_1: 123, channel_id_2: 456};
|
|
(DatabaseManager.getActiveServerUrl as jest.Mock).mockResolvedValue(serverUrl);
|
|
(getCurrentChannelId as jest.Mock).mockResolvedValue('different_channel_id');
|
|
(EphemeralStore.isSwitchingToChannel as jest.Mock).mockReturnValue(false);
|
|
(markChannelAsViewed as jest.Mock).mockResolvedValue({member: {}});
|
|
|
|
await handleMultipleChannelsViewedEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentChannelId).toHaveBeenCalled();
|
|
expect(markChannelAsViewed).toHaveBeenCalledWith(serverUrl, 'channel_id_1', false, true);
|
|
expect(markChannelAsViewed).toHaveBeenCalledWith(serverUrl, 'channel_id_2', false, true);
|
|
});
|
|
|
|
it('should handle multiple channels viewed event - failed', async () => {
|
|
msg.data.channel_times = {channel_id_1: 123};
|
|
(DatabaseManager.getActiveServerUrl as jest.Mock).mockResolvedValue(serverUrl);
|
|
(getCurrentChannelId as jest.Mock).mockResolvedValue('different_channel_id');
|
|
(EphemeralStore.isSwitchingToChannel as jest.Mock).mockReturnValue(false);
|
|
(markChannelAsViewed as jest.Mock).mockRejectedValueOnce({message: 'error'});
|
|
|
|
await handleMultipleChannelsViewedEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentChannelId).toHaveBeenCalled();
|
|
expect(markChannelAsViewed).toHaveBeenCalledWith(serverUrl, 'channel_id_1', false, true);
|
|
});
|
|
|
|
it('should return if current channel ID is the same and not switching to channel', async () => {
|
|
msg.data.channel_times = {channel_id_1: 123, channel_id_2: 456};
|
|
(DatabaseManager.getActiveServerUrl as jest.Mock).mockResolvedValue(serverUrl);
|
|
(getCurrentChannelId as jest.Mock).mockResolvedValue('channel_id_1');
|
|
(EphemeralStore.isSwitchingToChannel as jest.Mock).mockReturnValue(false);
|
|
|
|
await handleMultipleChannelsViewedEvent(serverUrl, msg);
|
|
|
|
expect(markChannelAsViewed).not.toHaveBeenCalledWith(serverUrl, 'channel_id_1', false, true);
|
|
expect(markChannelAsViewed).toHaveBeenCalledWith(serverUrl, 'channel_id_2', false, true);
|
|
});
|
|
});
|
|
|
|
describe('handleChannelMemberUpdatedEvent', () => {
|
|
it('should handle channel member updated event', async () => {
|
|
const mockMember = JSON.stringify({id: 'member_id', channel_id: channelId, user_id: userId, roles: ''});
|
|
msg.data = {channelMember: mockMember};
|
|
mockedGetChannelById.mockResolvedValue(channelModel);
|
|
(updateChannelInfoFromChannel as jest.Mock).mockResolvedValue({model: []});
|
|
(updateMyChannelFromWebsocket as jest.Mock).mockResolvedValue({model: {}});
|
|
(fetchRolesIfNeeded as jest.Mock).mockResolvedValue({roles: []});
|
|
jest.spyOn(operator, 'batchRecords').mockResolvedValueOnce();
|
|
|
|
await handleChannelMemberUpdatedEvent(serverUrl, msg);
|
|
|
|
expect(fetchRolesIfNeeded).toHaveBeenCalled();
|
|
expect(operator.batchRecords).toHaveBeenCalled();
|
|
});
|
|
|
|
describe('redaction invalidation', () => {
|
|
const receive = async (previousRoles: string, roles: string) => {
|
|
msg.data = {channelMember: JSON.stringify({channel_id: channelId, user_id: userId, roles})};
|
|
jest.mocked(getMyChannel).mockResolvedValue(TestHelper.fakeMyChannelModel({id: channelId, roles: previousRoles}));
|
|
jest.mocked(getCurrentUserId).mockResolvedValue(userId);
|
|
(updateMyChannelFromWebsocket as jest.Mock).mockResolvedValue({model: {}});
|
|
(fetchRolesIfNeeded as jest.Mock).mockResolvedValue({roles: []});
|
|
jest.spyOn(operator, 'batchRecords').mockResolvedValueOnce();
|
|
|
|
await handleChannelMemberUpdatedEvent(serverUrl, msg);
|
|
};
|
|
|
|
it('should not invalidate when the roles are unchanged, as for a mute or notify props change', async () => {
|
|
await receive('channel_user channel_admin', 'channel_admin channel_user');
|
|
|
|
expect(invalidateRedactionForChannelMembership).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should invalidate the channel when the current user roles change', async () => {
|
|
await receive('channel_user', 'channel_user channel_admin');
|
|
|
|
expect(invalidateRedactionForChannelMembership).toHaveBeenCalledWith(serverUrl, channelId);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('handleDirectAddedEvent', () => {
|
|
it('should handle direct added event', async () => {
|
|
msg.data = {teammate_id: userId};
|
|
(EphemeralStore.creatingDMorGMTeammates as string[]) = [];
|
|
mockedGetChannelById.mockResolvedValue(undefined);
|
|
mockedFetchMyChannel.mockResolvedValue({channels: [channel], memberships: [{} as any]});
|
|
(getCurrentUser as jest.Mock).mockResolvedValue({id: userId, locale: 'en'});
|
|
(getTeammateNameDisplay as jest.Mock).mockResolvedValue('username');
|
|
(fetchMissingDirectChannelsInfo as jest.Mock).mockResolvedValue({directChannels: [{}], users: [{}]});
|
|
(storeMyChannelsForTeam as jest.Mock).mockResolvedValue({models: []});
|
|
(addChannelToDefaultCategory as jest.Mock).mockResolvedValue({models: []});
|
|
|
|
await handleDirectAddedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).toHaveBeenCalled();
|
|
expect(fetchMyChannel).toHaveBeenCalledWith(serverUrl, '', channelId, true);
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(getTeammateNameDisplay).toHaveBeenCalled();
|
|
expect(fetchMissingDirectChannelsInfo).toHaveBeenCalledWith(serverUrl, [channel], 'en', 'username', userId, true);
|
|
expect(storeMyChannelsForTeam).toHaveBeenCalledWith(serverUrl, '', [{}], [{}], true);
|
|
expect(addChannelToDefaultCategory).toHaveBeenCalledWith(serverUrl, channel, true);
|
|
});
|
|
|
|
it('should handle direct added event - already adding DM', async () => {
|
|
msg.data = {teammate_id: userId};
|
|
(EphemeralStore.creatingDMorGMTeammates as string[]) = [userId];
|
|
|
|
await handleDirectAddedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle direct added event - already adding GM', async () => {
|
|
const teammateIds = [userId, 'userid2'];
|
|
msg.data = {teammate_ids: JSON.stringify(teammateIds)};
|
|
(EphemeralStore.creatingDMorGMTeammates as string[]) = teammateIds;
|
|
|
|
await handleDirectAddedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return if channel already exists', async () => {
|
|
mockedGetChannelById.mockResolvedValue(channelModel);
|
|
|
|
await handleDirectAddedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).toHaveBeenCalled();
|
|
expect(fetchMyChannel).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return if no channels exist', async () => {
|
|
mockedGetChannelById.mockResolvedValue(undefined);
|
|
mockedFetchMyChannel.mockResolvedValue({channels: undefined, memberships: [{} as any]});
|
|
|
|
await handleDirectAddedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).toHaveBeenCalled();
|
|
expect(fetchMyChannel).toHaveBeenCalled();
|
|
expect(getCurrentUser).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return if no current user exists', async () => {
|
|
mockedGetChannelById.mockResolvedValue(undefined);
|
|
mockedFetchMyChannel.mockResolvedValue({channels: [channel], memberships: [{} as any]});
|
|
(getCurrentUser as jest.Mock).mockResolvedValue(null);
|
|
|
|
await handleDirectAddedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).toHaveBeenCalled();
|
|
expect(fetchMyChannel).toHaveBeenCalled();
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(fetchMissingDirectChannelsInfo).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return if no direct channels exists', async () => {
|
|
mockedGetChannelById.mockResolvedValue(undefined);
|
|
mockedFetchMyChannel.mockResolvedValue({channels: [channel], memberships: [{} as any]});
|
|
(getCurrentUser as jest.Mock).mockResolvedValue({id: userId, locale: 'en'});
|
|
(getTeammateNameDisplay as jest.Mock).mockResolvedValue('username');
|
|
(fetchMissingDirectChannelsInfo as jest.Mock).mockResolvedValue({directChannels: [], users: [{}]});
|
|
|
|
await handleDirectAddedEvent(serverUrl, msg);
|
|
|
|
expect(getChannelById).toHaveBeenCalled();
|
|
expect(fetchMyChannel).toHaveBeenCalled();
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(fetchMissingDirectChannelsInfo).toHaveBeenCalled();
|
|
expect(storeMyChannelsForTeam).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('handleUserAddedToChannelEvent', () => {
|
|
it('should handle user added to channel event for current user', async () => {
|
|
(getCurrentUser as jest.Mock).mockResolvedValue({id: userId});
|
|
mockedFetchMyChannel.mockResolvedValue({channels: [channel], memberships: [{} as any]});
|
|
mockedPrepareMyChannelsForTeam.mockResolvedValue([[] as any]);
|
|
(addChannelToDefaultCategory as jest.Mock).mockResolvedValue({models: []});
|
|
(fetchPostsForChannel as jest.Mock).mockResolvedValue({posts: [{}], order: ['order'], authors: [{}], actionType: 'action', previousPostId: 'prev_id'});
|
|
(storePostsForChannel as jest.Mock).mockResolvedValue({models: []});
|
|
|
|
await handleUserAddedToChannelEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(fetchMyChannel).toHaveBeenCalledWith(serverUrl, teamId, channelId, true);
|
|
expect(prepareMyChannelsForTeam).toHaveBeenCalled();
|
|
expect(addChannelToDefaultCategory).toHaveBeenCalledWith(serverUrl, channel, true);
|
|
expect(fetchPostsForChannel).toHaveBeenCalledWith(serverUrl, channelId, true);
|
|
expect(storePostsForChannel).toHaveBeenCalledWith(serverUrl, channelId, [{}], ['order'], 'prev_id', 'action', [{}], true, undefined);
|
|
});
|
|
|
|
it('should handle user added to channel event for current user - already joining', async () => {
|
|
(getCurrentUser as jest.Mock).mockResolvedValue({id: userId});
|
|
(EphemeralStore.isLeavingChannel as jest.Mock).mockReturnValueOnce(false);
|
|
(EphemeralStore.isJoiningChannel as jest.Mock).mockReturnValueOnce(true);
|
|
|
|
await handleUserAddedToChannelEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(fetchMyChannel).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle user added to channel event for another user', async () => {
|
|
(getCurrentUser as jest.Mock).mockResolvedValue({id: 'other_user_id'});
|
|
(getUserById as jest.Mock).mockResolvedValue(null);
|
|
(fetchUsersByIds as jest.Mock).mockResolvedValue({users: [{id: userId, roles: ''}]});
|
|
mockedGetChannelById.mockResolvedValue(channelModel);
|
|
|
|
await handleUserAddedToChannelEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(getUserById).toHaveBeenCalled();
|
|
expect(fetchUsersByIds).toHaveBeenCalledWith(serverUrl, [userId], true);
|
|
expect(getChannelById).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('handleUserRemovedFromChannelEvent', () => {
|
|
it('should handle user removed from channel event for current user', async () => {
|
|
(getCurrentUser as jest.Mock).mockResolvedValue({id: userId, isGuest: false});
|
|
(getCurrentChannelId as jest.Mock).mockResolvedValue(channelId);
|
|
(deleteChannelMembership as jest.Mock).mockResolvedValue({models: []});
|
|
|
|
await handleUserRemovedFromChannelEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(getCurrentChannelId).toHaveBeenCalled();
|
|
expect(removeCurrentUserFromChannel).toHaveBeenCalledWith(serverUrl, channelId);
|
|
});
|
|
|
|
it('should handle user removed from channel event - no current user', async () => {
|
|
(getCurrentUser as jest.Mock).mockResolvedValue(null);
|
|
|
|
await handleUserRemovedFromChannelEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(getCurrentChannelId).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle user removed from channel event - already leaving', async () => {
|
|
(EphemeralStore.isLeavingChannel as jest.Mock).mockReturnValueOnce(true);
|
|
(getCurrentCall as jest.Mock).mockReturnValueOnce({channelId});
|
|
|
|
await handleUserRemovedFromChannelEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentCall).toHaveBeenCalled();
|
|
expect(leaveCall).toHaveBeenCalled();
|
|
expect(getCurrentUser).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle user removed from channel event - already leaving, leave call', async () => {
|
|
(EphemeralStore.isLeavingChannel as jest.Mock).mockReturnValueOnce(true);
|
|
|
|
await handleUserRemovedFromChannelEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentCall).toHaveBeenCalled();
|
|
expect(leaveCall).not.toHaveBeenCalled();
|
|
expect(getCurrentUser).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle user removed from channel event for another user', async () => {
|
|
(getCurrentUser as jest.Mock).mockResolvedValue({id: 'other_user_id', isGuest: false});
|
|
(deleteChannelMembership as jest.Mock).mockResolvedValue({models: []});
|
|
|
|
await handleUserRemovedFromChannelEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(deleteChannelMembership).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle user removed from channel event for guest user', async () => {
|
|
(getCurrentUser as jest.Mock).mockResolvedValue({id: userId, isGuest: true});
|
|
(updateUsersNoLongerVisible as jest.Mock).mockResolvedValue({models: []});
|
|
|
|
await handleUserRemovedFromChannelEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(updateUsersNoLongerVisible).toHaveBeenCalledWith(serverUrl, true);
|
|
});
|
|
|
|
it('should handle user removed from channel event for guest user, leave call', async () => {
|
|
(getCurrentUser as jest.Mock).mockResolvedValue({id: userId, isGuest: true});
|
|
(updateUsersNoLongerVisible as jest.Mock).mockResolvedValue({models: []});
|
|
(getCurrentCall as jest.Mock).mockReturnValueOnce({channelId});
|
|
|
|
await handleUserRemovedFromChannelEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(updateUsersNoLongerVisible).toHaveBeenCalledWith(serverUrl, true);
|
|
expect(leaveCall).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('handleChannelDeletedEvent', () => {
|
|
it('should handle channel deleted event', async () => {
|
|
(EphemeralStore.isLeavingChannel as jest.Mock).mockReturnValueOnce(false);
|
|
(EphemeralStore.isArchivingChannel as jest.Mock).mockReturnValueOnce(false);
|
|
(getCurrentUser as jest.Mock).mockResolvedValue({id: userId, isGuest: false});
|
|
(getCurrentChannel as jest.Mock).mockResolvedValue({id: channelId});
|
|
(canViewArchivedChannels as jest.Mock).mockResolvedValue(false);
|
|
|
|
await handleChannelDeletedEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(setChannelDeleteAt).toHaveBeenCalledWith(serverUrl, channelId, undefined);
|
|
expect(removeCurrentUserFromChannel).toHaveBeenCalledWith(serverUrl, channelId);
|
|
});
|
|
|
|
it('should handle channel deleted event - leaving channel, early return', async () => {
|
|
(EphemeralStore.isLeavingChannel as jest.Mock).mockReturnValueOnce(true);
|
|
(EphemeralStore.isArchivingChannel as jest.Mock).mockReturnValueOnce(false);
|
|
|
|
await handleChannelDeletedEvent(serverUrl, msg);
|
|
expect(getCurrentUser).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle channel deleted event for guest user', async () => {
|
|
(EphemeralStore.isLeavingChannel as jest.Mock).mockReturnValueOnce(false);
|
|
(EphemeralStore.isArchivingChannel as jest.Mock).mockReturnValueOnce(false);
|
|
(getCurrentUser as jest.Mock).mockResolvedValue({id: userId, isGuest: true});
|
|
(getCurrentChannel as jest.Mock).mockResolvedValue({id: channelId});
|
|
(canViewArchivedChannels as jest.Mock).mockResolvedValue(false);
|
|
|
|
await handleChannelDeletedEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(setChannelDeleteAt).toHaveBeenCalledWith(serverUrl, channelId, undefined);
|
|
expect(updateUsersNoLongerVisible).toHaveBeenCalledWith(serverUrl);
|
|
expect(removeCurrentUserFromChannel).toHaveBeenCalledWith(serverUrl, channelId);
|
|
});
|
|
|
|
it('should keep the user in the channel when archived channels are viewable', async () => {
|
|
(EphemeralStore.isLeavingChannel as jest.Mock).mockReturnValueOnce(false);
|
|
(EphemeralStore.isArchivingChannel as jest.Mock).mockReturnValueOnce(false);
|
|
(getCurrentUser as jest.Mock).mockResolvedValue({id: userId, isGuest: false});
|
|
(getCurrentChannel as jest.Mock).mockResolvedValue({id: channelId});
|
|
(canViewArchivedChannels as jest.Mock).mockResolvedValue(true);
|
|
|
|
await handleChannelDeletedEvent(serverUrl, msg);
|
|
|
|
expect(getCurrentUser).toHaveBeenCalled();
|
|
expect(setChannelDeleteAt).toHaveBeenCalledWith(serverUrl, channelId, undefined);
|
|
expect(removeCurrentUserFromChannel).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|