mattermost-mobile-upstream/app/actions/websocket/channel.test.ts
Pablo Vélez 7df4a0ab97
Some checks are pending
ci / lint-typecheck (push) Waiting to run
ci / detox-lint-typecheck (push) Waiting to run
ci / test (push) Waiting to run
Mm 68219 show placeholder for redacted files (#9719)
* 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>
2026-10-15 15:59:13 -07:00

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();
});
});
});