mattermost-mobile-upstream/app/actions/remote/entry/common.test.ts
Carlos Garcia 462514347e
fetch roles during entry to avoid UI observing inconsistent channel s… (#9873)
* fetch roles during entry to avoid UI observing inconsistent channel states

first time channels are fetched for a team, roles should also be fetched at
the same time instead of only relying in deferred fetch. That avoids having
channels observed by the UI in an inconsistent state and rendering as read
only channels.

During reconnection the non forced entry fetch roles is a no-op since force is
set to false.

* separate fetch roles promise to log errors

* returns not empty models in test
2026-09-22 14:50:35 +02:00

582 lines
21 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {nativeApplicationVersion} from 'expo-application';
import {RESULTS} from 'react-native-permissions';
import {handleKickFromChannel, fetchMyChannelsForTeam} from '@actions/remote/channel';
import {fetchMyPreferences} from '@actions/remote/preference';
import {fetchRoles} from '@actions/remote/role';
import {fetchConfigAndLicense} from '@actions/remote/systems';
import {fetchMyTeams, handleKickFromTeam} from '@actions/remote/team';
import {fetchMe} from '@actions/remote/user';
import {Screens} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import {PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {getDeviceToken} from '@queries/app/global';
import {prepareEntryModels} from '@queries/servers/entry';
import {getCurrentChannelId, getCurrentTeamId, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {NavigationStore} from '@store/navigation_store';
import {logDebug} from '@utils/log';
import {entry, setExtraSessionProps, verifyPushProxy, entryInitialChannelId, handleEntryAfterLoadNavigation} from './common';
jest.mock('@actions/remote/channel');
jest.mock('@actions/remote/scheduled_post');
jest.mock('@actions/remote/preference');
jest.mock('@actions/remote/role');
jest.mock('@actions/remote/systems');
jest.mock('@actions/remote/team');
jest.mock('@actions/remote/user');
jest.mock('@actions/remote/post');
jest.mock('@actions/remote/groups');
jest.mock('@actions/remote/thread');
jest.mock('@queries/app/global');
jest.mock('@queries/servers/system');
jest.mock('@queries/servers/entry');
jest.mock('@queries/servers/system', () => {
const original = jest.requireActual('@queries/servers/system');
return {
...original,
getCurrentTeamId: jest.fn(),
getCurrentChannelId: jest.fn(),
setCurrentTeamAndChannelId: jest.fn(original.setCurrentTeamAndChannelId),
};
});
jest.mock('react-native-permissions');
jest.mock('expo-application');
describe('actions/remote/entry/common', () => {
const serverUrl = 'https://server.example.com';
let mockClient: any;
const mockOperator = {
handleSystem: jest.fn(),
handleUsers: jest.fn(),
handlePreferences: jest.fn(),
handleTeams: jest.fn(),
handleChannels: jest.fn(),
handlePosts: jest.fn(),
handleCategories: jest.fn(),
handleGroups: jest.fn(),
handleRoles: jest.fn(),
};
const mockQuery = {
fetch: jest.fn().mockReturnValue(['true']),
};
const mockCollection = {
query: jest.fn().mockReturnValue(mockQuery),
};
const mockDatabase = {
get: jest.fn().mockReturnValue(mockCollection),
collections: {
get: jest.fn().mockReturnValue(mockCollection),
},
};
beforeEach(async () => {
jest.clearAllMocks();
mockClient = {
setExtraSessionProps: jest.fn(),
ping: jest.fn().mockResolvedValue({
data: {
CanReceiveNotifications: true,
},
}),
};
NetworkManager.init([{serverUrl} as ServerCredential]);
NetworkManager.getClient = jest.fn().mockReturnValue(mockClient);
DatabaseManager.init = jest.fn().mockResolvedValue(undefined);
DatabaseManager.getServerDatabaseAndOperator = jest.fn().mockReturnValue({
database: mockDatabase,
operator: mockOperator,
});
// Reset mock implementations
(RESULTS as any).GRANTED = 'granted';
require('react-native-permissions').checkNotifications = jest.fn().mockResolvedValue({status: RESULTS.GRANTED});
require('expo-application').nativeApplicationVersion = '0.0.0';
});
describe('entry', () => {
it('should fetch initial data successfully', async () => {
const mockConfig = {Version: '7.8.0'};
const mockLicense = {};
(fetchConfigAndLicense as jest.Mock).mockResolvedValue({error: false, config: mockConfig, license: mockLicense});
const mockPreferences = {preferences: []};
(fetchMyPreferences as jest.Mock).mockResolvedValue(mockPreferences);
const mockTeams = {teams: [], memberships: []};
(fetchMyTeams as jest.Mock).mockResolvedValue(mockTeams);
const mockUser = {user: {id: 'user1', roles: '', username: 'user1'}};
(fetchMe as jest.Mock).mockResolvedValueOnce(mockUser);
const mockChannels = {channels: [], memberships: [], categories: []};
(fetchMyChannelsForTeam as jest.Mock).mockResolvedValue(mockChannels);
(prepareEntryModels as jest.Mock).mockResolvedValue([]);
jest.mocked(fetchRoles).mockResolvedValue({roles: []});
const result = await entry(serverUrl, 'team1');
expect(prepareEntryModels).toHaveBeenCalledWith({
operator: mockOperator,
teamData: mockTeams,
chData: mockChannels,
prefData: mockPreferences,
meData: mockUser,
isCRTEnabled: false,
});
expect(fetchRoles).toHaveBeenCalled();
expect(result).toEqual(expect.objectContaining({
initialChannelId: '',
initialTeamId: '',
models: expect.any(Array),
prefData: mockPreferences,
teamData: mockTeams,
chData: mockChannels,
meData: mockUser,
gmConverted: false,
}));
});
it('should complete the entry when fetching roles fails', async () => {
(fetchConfigAndLicense as jest.Mock).mockResolvedValue({error: false, config: {Version: '7.8.0'}, license: {}});
(fetchMyPreferences as jest.Mock).mockResolvedValue({preferences: []});
(fetchMyTeams as jest.Mock).mockResolvedValue({teams: [], memberships: []});
(fetchMe as jest.Mock).mockResolvedValueOnce({user: {id: 'user1', roles: '', username: 'user1'}});
(fetchMyChannelsForTeam as jest.Mock).mockResolvedValue({channels: [], memberships: [], categories: []});
const preparedModel = {id: 'model1'};
(prepareEntryModels as jest.Mock).mockResolvedValue([Promise.resolve([preparedModel])]);
jest.mocked(fetchRoles).mockResolvedValue({error: new Error('Roles error')});
const result = await entry(serverUrl, 'team1');
expect(result).toEqual(expect.objectContaining({models: [preparedModel]}));
expect(logDebug).toHaveBeenCalledWith('entryRest: failed to fetch roles', undefined, 'Roles error');
});
it('should handle errors in data fetching', async () => {
const mockError = new Error('Network error');
(fetchConfigAndLicense as jest.Mock).mockResolvedValue({error: mockError});
const result = await entry(serverUrl);
expect(result).toEqual({
error: mockError,
});
});
});
describe('entryInitialChannelId', () => {
it('should return requested channel id for DM/GM channels', async () => {
const channels = [
{id: 'dm1', type: 'D', name: 'dm-channel'},
] as Channel[];
const memberships = [{channel_id: 'dm1'}] as ChannelMembership[];
const result = await entryInitialChannelId(
mockDatabase as any,
'dm1',
'',
'',
'en',
channels,
memberships,
);
expect(result).toBe('dm1');
});
it('should return default channel when available', async () => {
const channels = [
{id: 'town-square', name: 'town-square', team_id: 'team1', type: 'O'},
] as Channel[];
const memberships = [{channel_id: 'town-square'}] as ChannelMembership[];
const result = await entryInitialChannelId(
mockDatabase as any,
'',
'team1',
'team1',
'en',
channels,
memberships,
);
expect(result).toBe('town-square');
});
});
describe('setExtraSessionProps', () => {
it('should set extra session properties when notifications are granted', async () => {
(RESULTS as any).GRANTED = 'granted';
const mockCheckNotifications = jest.fn().mockResolvedValue({status: RESULTS.GRANTED});
require('react-native-permissions').checkNotifications = mockCheckNotifications;
jest.mocked(getDeviceToken).mockResolvedValueOnce('deviceToken');
const result = await setExtraSessionProps(serverUrl);
expect(result).toEqual({});
expect(mockClient.setExtraSessionProps).toHaveBeenCalledWith(
expect.any(String),
false,
nativeApplicationVersion,
undefined,
undefined,
);
});
it('should set extra session properties when notifications are limited', async () => {
(RESULTS as any).LIMITED = 'limited';
const mockCheckNotifications = jest.fn().mockResolvedValue({status: RESULTS.LIMITED});
require('react-native-permissions').checkNotifications = mockCheckNotifications;
jest.mocked(getDeviceToken).mockResolvedValueOnce('deviceToken');
const result = await setExtraSessionProps(serverUrl);
expect(result).toEqual({});
expect(mockClient.setExtraSessionProps).toHaveBeenCalledWith(
expect.any(String),
false,
nativeApplicationVersion,
undefined,
undefined,
);
});
it('should handle errors gracefully', async () => {
const mockError = new Error('Failed to set props');
const mockCheckNotifications = jest.fn().mockRejectedValue(mockError);
require('react-native-permissions').checkNotifications = mockCheckNotifications;
jest.mocked(getDeviceToken).mockResolvedValueOnce('deviceToken');
const result = await setExtraSessionProps(serverUrl);
expect(result).toEqual({error: mockError});
});
});
describe('verifyPushProxy', () => {
it('should verify push proxy status successfully', async () => {
jest.mocked(getDeviceToken).mockResolvedValueOnce('deviceToken');
await verifyPushProxy(serverUrl);
expect(mockClient.ping).toHaveBeenCalled();
});
it('should handle error gracefully', async () => {
jest.mocked(getDeviceToken).mockResolvedValueOnce('deviceToken');
mockClient.ping.mockRejectedValueOnce(new Error('Network error'));
await verifyPushProxy(serverUrl);
expect(mockOperator.handleSystem).not.toHaveBeenCalled();
});
it('should handle verified push proxy response', async () => {
jest.mocked(getDeviceToken).mockResolvedValueOnce('deviceToken');
mockClient.ping.mockResolvedValueOnce({
data: {
CanReceiveNotifications: true,
},
});
await verifyPushProxy(serverUrl);
expect(mockOperator.handleSystem).toHaveBeenCalledWith({
systems: [{
id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS,
value: PUSH_PROXY_STATUS_VERIFIED,
}],
prepareRecordsOnly: false,
});
});
it('should handle missing device token', async () => {
jest.mocked(getDeviceToken).mockResolvedValueOnce('');
await verifyPushProxy(serverUrl);
expect(mockClient.ping).not.toHaveBeenCalled();
});
it('should handle push proxy not available response', async () => {
jest.mocked(getDeviceToken).mockResolvedValueOnce('deviceToken');
mockClient.ping.mockResolvedValueOnce({
data: {
CanReceiveNotifications: 'false',
},
});
await verifyPushProxy(serverUrl);
expect(mockOperator.handleSystem).toHaveBeenCalledWith({
systems: [{
id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS,
value: 'not_available',
}],
prepareRecordsOnly: false,
});
});
it('should handle push proxy unknown response', async () => {
jest.mocked(getDeviceToken).mockResolvedValueOnce('deviceToken');
mockClient.ping.mockResolvedValueOnce({
data: {
CanReceiveNotifications: 'unknown',
},
});
await verifyPushProxy(serverUrl);
expect(mockOperator.handleSystem).not.toHaveBeenCalled();
});
it('should handle network error', async () => {
jest.mocked(getDeviceToken).mockResolvedValueOnce('deviceToken');
mockClient.ping.mockRejectedValueOnce(new Error('Network error'));
await verifyPushProxy(serverUrl);
expect(mockOperator.handleSystem).not.toHaveBeenCalled();
});
});
describe('handleEntryAfterLoadNavigation', () => {
const teamMembers = [{team_id: 'team1', delete_at: 0} as TeamMembership];
const channelMembers = [{channel_id: 'channel1'} as ChannelMembership];
it('should handle first load or no team scenario', async () => {
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'team1',
'channel1',
'initialTeam1',
'initialChannel1',
false,
);
expect(mockOperator.handleSystem).toHaveBeenCalled();
});
it('should handle current team switch during loading', async () => {
jest.mocked(getCurrentTeamId).mockResolvedValueOnce('team2');
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'team1',
'channel1',
'initialTeam1',
'initialChannel1',
false,
);
expect(handleKickFromTeam).toHaveBeenCalled();
});
it('should handle initial team switch during loading', async () => {
jest.mocked(getCurrentTeamId).mockResolvedValueOnce('team1');
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'team1',
'channel1',
'initialTeam1',
'initialChannel1',
false,
);
expect(handleKickFromTeam).toHaveBeenCalled();
});
it('should handle initial team switch during loading', async () => {
jest.mocked(getCurrentTeamId).mockResolvedValueOnce('team1');
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'team1',
'channel1',
'initialTeam1',
'initialChannel1',
true,
);
expect(setCurrentTeamAndChannelId).toHaveBeenCalled();
});
it('should handle current channel switch during loading', async () => {
jest.mocked(getCurrentTeamId).mockResolvedValueOnce('team1');
jest.mocked(getCurrentChannelId).mockResolvedValueOnce('channel2');
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'team1',
'channel1',
'team1',
'initialChannel1',
true,
);
expect(setCurrentTeamAndChannelId).toHaveBeenCalled();
});
it('should handle initial channel switch during loading', async () => {
jest.mocked(getCurrentTeamId).mockResolvedValueOnce('team1');
jest.mocked(getCurrentChannelId).mockResolvedValueOnce('channel1');
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'team1',
'channel1',
'team1',
'initialChannel1',
true,
);
expect(setCurrentTeamAndChannelId).toHaveBeenCalled();
});
it('should handle current channel switch during loading - thread mounted', async () => {
jest.spyOn(NavigationStore, 'getScreensInStack').mockReturnValue([Screens.THREAD]);
jest.mocked(getCurrentTeamId).mockResolvedValueOnce('team1');
jest.mocked(getCurrentChannelId).mockResolvedValueOnce('channel2');
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'team1',
'channel1',
'team1',
'initialChannel1',
true,
);
expect(handleKickFromChannel).toHaveBeenCalled();
});
it('should handle initial channel switch during loading - thread mounted', async () => {
jest.spyOn(NavigationStore, 'getScreensInStack').mockReturnValue([Screens.THREAD]);
jest.mocked(getCurrentTeamId).mockResolvedValueOnce('team1');
jest.mocked(getCurrentChannelId).mockResolvedValueOnce('channel1');
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'team1',
'channel1',
'team1',
'initialChannel1',
true,
);
expect(handleKickFromChannel).toHaveBeenCalled();
});
it('should handle error gracefully', async () => {
(DatabaseManager.getServerDatabaseAndOperator as jest.Mock).mockImplementation(() => {
throw new Error('Test error');
});
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'team1',
'channel1',
'initialTeam1',
'initialChannel1',
false,
);
expect(logDebug).toHaveBeenCalled();
});
it('should handle tablet device scenario', async () => {
jest.spyOn(NavigationStore, 'getScreensInStack').mockReturnValue([]);
jest.spyOn(require('@utils/helpers'), 'isTablet').mockReturnValue(true);
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'team1',
'channel1',
'initialTeam1',
'initialChannel1',
false,
);
expect(mockOperator.handleSystem).toHaveBeenCalled();
});
it('should handle channel screen mounted scenario', async () => {
jest.spyOn(NavigationStore, 'getScreensInStack').mockReturnValue([Screens.CHANNEL]);
jest.spyOn(require('@utils/helpers'), 'isTablet').mockReturnValue(false);
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'team1',
'differentChannel',
'team1',
'initialChannel1',
false,
);
expect(mockOperator.handleSystem).toHaveBeenCalled();
});
it('should handle threads screen mounted scenario', async () => {
jest.spyOn(NavigationStore, 'getScreensInStack').mockReturnValue([Screens.THREAD]);
jest.spyOn(require('@utils/helpers'), 'isTablet').mockReturnValue(false);
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'team1',
'differentChannel',
'team1',
'initialChannel1',
false,
);
expect(mockOperator.handleSystem).toHaveBeenCalled();
});
it('should handle kick from team scenario', async () => {
jest.spyOn(NavigationStore, 'getScreensInStack').mockReturnValue([]);
await handleEntryAfterLoadNavigation(
serverUrl,
teamMembers,
channelMembers,
'differentTeam',
'channel1',
'initialTeam1',
'initialChannel1',
false,
);
expect(mockOperator.handleSystem).toHaveBeenCalled();
});
});
});