From 3ef637ff9df80e18cde076f0ce24ec772e097610 Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Tue, 28 Jan 2025 08:09:43 -0500 Subject: [PATCH] MM-59426 Add tests for entry common functions (#8515) * test: Add tests for entry common functions * increase functions coverage to 80% --------- Co-authored-by: yasserfaraazkhan --- app/actions/remote/entry/common.test.ts | 957 ++++++++++++++++++++++++ app/actions/remote/entry/common.ts | 4 +- 2 files changed, 959 insertions(+), 2 deletions(-) create mode 100644 app/actions/remote/entry/common.test.ts diff --git a/app/actions/remote/entry/common.test.ts b/app/actions/remote/entry/common.test.ts new file mode 100644 index 000000000..37effb595 --- /dev/null +++ b/app/actions/remote/entry/common.test.ts @@ -0,0 +1,957 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable max-lines */ + +import {nativeApplicationVersion} from 'expo-application'; +import {RESULTS} from 'react-native-permissions'; + +import {handleKickFromChannel, fetchAllMyChannelsForAllTeams, fetchMissingDirectChannelsInfo, type MyChannelsRequest} from '@actions/remote/channel'; +import {fetchGroupsForMember} from '@actions/remote/groups'; +import {fetchPostsForUnreadChannels} from '@actions/remote/post'; +import {fetchMyPreferences} from '@actions/remote/preference'; +import {fetchConfigAndLicense} from '@actions/remote/systems'; +import {fetchMyTeams, fetchTeamsThreads, updateCanJoinTeams, handleKickFromTeam, type MyTeamsRequest} from '@actions/remote/team'; +import {fetchMe, updateAllUsersSince, autoUpdateTimezone} from '@actions/remote/user'; +import {Preferences, 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 {getCurrentChannelId, getCurrentTeamId, setCurrentTeamAndChannelId} from '@queries/servers/system'; +import NavigationStore from '@store/navigation_store'; + +import {entry, setExtraSessionProps, verifyPushProxy, entryInitialChannelId, restDeferredAppEntryActions, handleEntryAfterLoadNavigation, deferredAppEntryActions} from './common'; + +jest.mock('@actions/remote/channel'); +jest.mock('@actions/remote/preference'); +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/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).mockResolvedValue(mockUser); + + const mockChannels = {channels: [], memberships: []}; + (fetchAllMyChannelsForAllTeams as jest.Mock).mockResolvedValue(mockChannels); + + const result = await entry(serverUrl); + + expect(result).toEqual(expect.objectContaining({ + initialChannelId: '', + initialTeamId: '', + models: expect.any(Array), + prefData: mockPreferences, + teamData: mockTeams, + chData: mockChannels, + meData: mockUser, + gmConverted: false, + })); + }); + + 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, + ); + }); + + 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, + ); + }); + + 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('deferredAppEntryActions', () => { + it('should call autoUpdateTimezone', async () => { + const since = 123456789; + const currentUserId = 'user1'; + const currentUserLocale = 'en'; + const preferences = [{ + category: 'advanced_settings', + name: 'feature_enabled', + value: 'true', + user_id: 'user1', + }]; + const config = { + Version: '7.8.0', + CollapsedThreads: 'false', + FeatureFlagCollapsedThreads: 'true', + } as ClientConfig; + const license = {} as ClientLicense; + const teamData = {teams: [], memberships: []}; + const chData = undefined; + + await deferredAppEntryActions( + serverUrl, + since, + currentUserId, + currentUserLocale, + preferences, + config, + license, + teamData, + chData, + ); + + expect(autoUpdateTimezone).toHaveBeenCalledWith(serverUrl, undefined); + }); + }); + + describe('restDeferredAppEntryActions', () => { + global.setTimeout = jest.fn((cb) => cb()) as any; + + it('should execute deferred actions correctly', async () => { + const since = 123456789; + const currentUserId = 'user1'; + const currentUserLocale = 'en'; + const preferences = [{ + category: 'advanced_settings', + name: 'feature_enabled', + value: 'true', + user_id: 'user1', + }]; + const config = { + Version: '7.8.0', + CollapsedThreads: 'false', + FeatureFlagCollapsedThreads: 'true', + LockTeammateNameDisplay: 'false', + TeammateNameDisplay: 'username', + } as ClientConfig; + const license = {} as ClientLicense; + const teamData = { + teams: [{id: 'team1', display_name: 'Team 1'}], + memberships: [{team_id: 'team1', user_id: 'user1'}], + } as MyTeamsRequest; + const chData = { + channels: [ + {id: 'dm1', type: 'D', name: 'dm-channel'}, + {id: 'channel1', type: 'O', name: 'channel1', team_id: 'team1'}, + ], + memberships: [ + {channel_id: 'dm1', user_id: 'user1'}, + {channel_id: 'channel1', user_id: 'user1'}, + ], + } as MyChannelsRequest; + const initialTeamId = 'team1'; + const initialChannelId = 'channel1'; + + await restDeferredAppEntryActions( + serverUrl, + since, + currentUserId, + currentUserLocale, + preferences, + config, + license, + teamData, + chData, + initialTeamId, + initialChannelId, + ); + + expect(fetchMissingDirectChannelsInfo).toHaveBeenCalled(); + expect(updateAllUsersSince).toHaveBeenCalledWith(serverUrl, since, false, undefined); + expect(updateCanJoinTeams).toHaveBeenCalledWith(serverUrl); + expect(fetchPostsForUnreadChannels).toHaveBeenCalled(); + expect(fetchGroupsForMember).toHaveBeenCalledWith(serverUrl, currentUserId, false, undefined); + }); + + it('should handle missing data gracefully', async () => { + const since = 123456789; + const currentUserId = 'user1'; + const currentUserLocale = 'en'; + const preferences = undefined; + const config = { + Version: '7.8.0', + CollapsedThreads: 'false', + } as ClientConfig; + const license = {} as ClientLicense; + const teamData = { + teams: [], + memberships: [], + }; + const chData = undefined; + + await restDeferredAppEntryActions( + serverUrl, + since, + currentUserId, + currentUserLocale, + preferences, + config, + license, + teamData, + chData, + ); + + expect(updateAllUsersSince).toHaveBeenCalledWith(serverUrl, since, false, undefined); + expect(updateCanJoinTeams).toHaveBeenCalledWith(serverUrl); + expect(fetchGroupsForMember).toHaveBeenCalledWith(serverUrl, currentUserId, false, undefined); + expect(fetchMissingDirectChannelsInfo).not.toHaveBeenCalled(); + expect(fetchPostsForUnreadChannels).not.toHaveBeenCalled(); + expect(fetchTeamsThreads).not.toHaveBeenCalled(); + }); + + it('should handle teams order preference correctly', async () => { + const since = 123456789; + const currentUserId = 'user1'; + const currentUserLocale = 'en'; + const preferences = [{ + category: Preferences.CATEGORIES.TEAMS_ORDER, + name: '', + value: 'team2,team1', + user_id: 'user1', + }]; + const config = { + Version: '7.8.0', + CollapsedThreads: 'true', + FeatureFlagCollapsedThreads: 'true', + } as ClientConfig; + const license = {} as ClientLicense; + const teamData = { + teams: [ + {id: 'team1', display_name: 'Team 1'}, + {id: 'team2', display_name: 'Team 2'}, + ], + memberships: [ + {team_id: 'team1', user_id: 'user1'}, + {team_id: 'team2', user_id: 'user1'}, + ], + } as MyTeamsRequest; + const chData = { + channels: [ + {id: 'channel1', team_id: 'team1', type: 'O'}, + {id: 'channel2', team_id: 'team2', type: 'O'}, + ], + memberships: [ + {channel_id: 'channel1', user_id: 'user1'}, + {channel_id: 'channel2', user_id: 'user1'}, + ], + } as MyChannelsRequest; + + await restDeferredAppEntryActions( + serverUrl, + since, + currentUserId, + currentUserLocale, + preferences, + config, + license, + teamData, + chData, + 'team1', + 'channel1', + ); + + expect(fetchTeamsThreads).toHaveBeenCalled(); + expect(fetchPostsForUnreadChannels).toHaveBeenCalled(); + }); + + it('should handle direct channels info correctly', async () => { + const since = 123456789; + const currentUserId = 'user1'; + const currentUserLocale = 'en'; + const preferences = [] as PreferenceType[]; + const config = { + Version: '7.8.0', + CollapsedThreads: 'false', + LockTeammateNameDisplay: 'false', + TeammateNameDisplay: 'username', + } as ClientConfig; + const license = {} as ClientLicense; + const teamData = {teams: [], memberships: []}; + const chData = { + channels: [ + {id: 'dm1', type: 'D', name: 'user1__user2'}, + {id: 'gm1', type: 'G', name: 'group-message'}, + ], + memberships: [ + {channel_id: 'dm1', user_id: 'user1'}, + {channel_id: 'gm1', user_id: 'user1'}, + ], + } as MyChannelsRequest; + + await restDeferredAppEntryActions( + serverUrl, + since, + currentUserId, + currentUserLocale, + preferences, + config, + license, + teamData, + chData, + ); + + expect(fetchMissingDirectChannelsInfo).toHaveBeenCalledWith( + serverUrl, + expect.arrayContaining([ + expect.objectContaining({id: 'dm1'}), + expect.objectContaining({id: 'gm1'}), + ]), + currentUserLocale, + 'username', + currentUserId, + false, + undefined, + ); + }); + + it('should sort teams when preferences are missing', async () => { + const since = 123456789; + const currentUserId = 'user1'; + const currentUserLocale = 'en'; + const preferences: PreferenceType[] = []; + const config = { + Version: '7.8.0', + CollapsedThreads: 'false', + } as ClientConfig; + const license = {} as ClientLicense; + const teamData = { + teams: [ + {id: 'team1', display_name: 'Zebra'}, + {id: 'team2', display_name: 'Alpha'}, + ], + memberships: [ + {team_id: 'team1', user_id: 'user1'}, + {team_id: 'team2', user_id: 'user1'}, + ], + } as MyTeamsRequest; + const chData = { + channels: [], + memberships: [], + } as MyChannelsRequest; + + await restDeferredAppEntryActions( + serverUrl, + since, + currentUserId, + currentUserLocale, + preferences, + config, + license, + teamData, + chData, + undefined, + undefined, + ); + + expect(teamData.teams?.[0]?.display_name).toBe('Alpha'); + expect(teamData.teams?.[1]?.display_name).toBe('Zebra'); + }); + + it('should handle extra teams correctly when sorted teams are provided', async () => { + const since = 123456789; + const currentUserId = 'user1'; + const currentUserLocale = 'en'; + const preferences = [{ + category: Preferences.CATEGORIES.TEAMS_ORDER, + name: '', + value: 'team3,team1', + user_id: 'user1', + }]; + const config = { + Version: '7.8.0', + CollapsedThreads: 'true', + FeatureFlagCollapsedThreads: 'true', + } as ClientConfig; + const license = {} as ClientLicense; + const teamData = { + teams: [ + {id: 'team1', display_name: 'Team 1'}, + {id: 'team2', display_name: 'Team 2'}, + {id: 'team3', display_name: 'Team 3'}, + ], + memberships: [ + {team_id: 'team1', user_id: 'user1'}, + {team_id: 'team2', user_id: 'user1'}, + {team_id: 'team3', user_id: 'user1'}, + ], + } as MyTeamsRequest; + const chData = { + channels: [ + {id: 'channel1', team_id: 'team1', type: 'O'}, + {id: 'channel2', team_id: 'team2', type: 'O'}, + ], + memberships: [ + {channel_id: 'channel1', user_id: 'user1'}, + {channel_id: 'channel2', user_id: 'user1'}, + ], + } as MyChannelsRequest; + + await restDeferredAppEntryActions( + serverUrl, + since, + currentUserId, + currentUserLocale, + preferences, + config, + license, + teamData, + chData, + 'team1', + 'channel1', + ); + + // Check that 'team3' (which was in memberships but not sorted) is in the 'extraTeams' result + expect(fetchTeamsThreads).toHaveBeenCalled(); + expect(fetchPostsForUnreadChannels).toHaveBeenCalled(); + + // Optionally check if team3 is sorted after team1 based on display name or the intended order + expect(teamData.teams?.[0]?.display_name).toBe('Team 1'); + expect(teamData.teams?.[1]?.display_name).toBe('Team 2'); + expect(teamData.teams?.[2]?.display_name).toBe('Team 3'); + }); + + it('should sort extra teams correctly by display name', async () => { + const since = 123456789; + const currentUserId = 'user1'; + const currentUserLocale = 'en'; + const preferences = [{ + category: Preferences.CATEGORIES.TEAMS_ORDER, + name: '', + value: 'team1,team2', // Teams that are already sorted based on preferences + user_id: 'user1', + }]; + const config = { + Version: '7.8.0', + CollapsedThreads: 'true', + FeatureFlagCollapsedThreads: 'true', + } as ClientConfig; + const license = {} as ClientLicense; + const teamData = { + teams: [ + {id: 'team1', display_name: 'Team 1'}, + {id: 'team2', display_name: 'Team 2'}, + {id: 'team3', display_name: 'Alpha Team'}, + {id: 'team4', display_name: 'Zebra Team'}, + ], + memberships: [ + {team_id: 'team1', user_id: 'user1'}, + {team_id: 'team2', user_id: 'user1'}, + {team_id: 'team3', user_id: 'user1'}, + {team_id: 'team4', user_id: 'user1'}, + ], + } as MyTeamsRequest; + const chData = { + channels: [], + memberships: [], + } as MyChannelsRequest; + + // Call the function under test + await restDeferredAppEntryActions( + serverUrl, + since, + currentUserId, + currentUserLocale, + preferences, + config, + license, + teamData, + chData, + 'team1', // initialTeamId + 'channel1', // initialChannelId + ); + + // Extract the extra teams (those that are not in the preferences-sorted list) + const extraTeams = [...(teamData.memberships || [])]. + filter((membership) => !preferences[0].value.split(',').includes(membership.team_id)). // Filter teams not in sorted order + map((membership) => (teamData.teams ?? []).find((team) => team.id === membership.team_id)!). // Map to actual team data + sort((a, b) => a.display_name.toLocaleLowerCase().localeCompare(b.display_name.toLocaleLowerCase())); // Sort by display_name + + // Test that the extra teams are sorted alphabetically by display_name + expect(extraTeams[0].display_name).toBe('Alpha Team'); // Alphabetically first + expect(extraTeams[1].display_name).toBe('Zebra Team'); // Alphabetically last + }); + }); + + 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', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + 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 () => { + const consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); + (DatabaseManager.getServerDatabaseAndOperator as jest.Mock).mockImplementation(() => { + throw new Error('Test error'); + }); + + await handleEntryAfterLoadNavigation( + serverUrl, + teamMembers, + channelMembers, + 'team1', + 'channel1', + 'initialTeam1', + 'initialChannel1', + false, + ); + + expect(consoleDebugSpy).toHaveBeenCalled(); + consoleDebugSpy.mockRestore(); + }); + + 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(); + }); + }); +}); diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index c98ce3542..dfcc510ec 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -204,7 +204,7 @@ const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, } }; -async function entryInitialChannelId(database: Database, requestedChannelId = '', requestedTeamId = '', initialTeamId: string, locale: string, channels?: Channel[], memberships?: ChannelMember[]) { +export async function entryInitialChannelId(database: Database, requestedChannelId = '', requestedTeamId = '', initialTeamId: string, locale: string, channels?: Channel[], memberships?: ChannelMember[]) { const membershipIds = new Set(memberships?.map((m) => m.channel_id)); const requestedChannel = channels?.find((c) => (c.id === requestedChannelId) && membershipIds.has(c.id)); @@ -242,7 +242,7 @@ async function entryInitialChannelId(database: Database, requestedChannelId = '' return myFirstTeamChannel?.id || ''; } -async function restDeferredAppEntryActions( +export async function restDeferredAppEntryActions( serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined, config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined, initialTeamId?: string, initialChannelId?: string, groupLabel?: BaseRequestGroupLabel,