mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 10:50:13 +00:00
489 lines
20 KiB
TypeScript
489 lines
20 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
import {of as of$, firstValueFrom} from 'rxjs';
|
|
|
|
import {
|
|
observeCallsConfig,
|
|
observeCallsState,
|
|
observeChannelsWithCalls,
|
|
observeCurrentCall,
|
|
observeIncomingCalls,
|
|
} from '@calls/state';
|
|
import {DefaultCallsState, DefaultIncomingCalls} from '@calls/types/calls';
|
|
import {General, License} from '@constants';
|
|
import DatabaseManager from '@database/manager';
|
|
import {observeChannel} from '@queries/servers/channel';
|
|
import {observeConfigValue, observeLicense} from '@queries/servers/system';
|
|
import {observeUser, queryUsersById} from '@queries/servers/user';
|
|
|
|
import {
|
|
observeIsCallsEnabledInChannel,
|
|
observeIsCallLimitRestricted,
|
|
observeCallStateInChannel,
|
|
observeDMCallingState,
|
|
observeCallChannel,
|
|
observeEndCallDetails,
|
|
observeCurrentSessionsDict,
|
|
} from './index';
|
|
|
|
jest.mock('@calls/state');
|
|
jest.mock('@queries/servers/channel');
|
|
jest.mock('@queries/servers/system');
|
|
jest.mock('@queries/servers/user');
|
|
|
|
describe('Calls Observers', () => {
|
|
const serverUrl = 'https://server.com';
|
|
const channelId = 'channel1';
|
|
const database = {} as any;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
// Mock DatabaseManager
|
|
(DatabaseManager as any).serverDatabases = {
|
|
'test-server': {
|
|
database: {},
|
|
},
|
|
};
|
|
});
|
|
|
|
describe('observeIsCallsEnabledInChannel', () => {
|
|
beforeEach(() => {
|
|
(observeCallsConfig as jest.Mock).mockReturnValue(of$({
|
|
pluginEnabled: true,
|
|
DefaultEnabled: true,
|
|
}));
|
|
(observeCallsState as jest.Mock).mockReturnValue(of$(DefaultCallsState));
|
|
(observeConfigValue as jest.Mock).mockReturnValue(of$('7.6.0'));
|
|
});
|
|
|
|
it('should return false when plugin is disabled regardless of other settings', async () => {
|
|
(observeCallsConfig as jest.Mock).mockReturnValue(of$({
|
|
pluginEnabled: false,
|
|
DefaultEnabled: true,
|
|
}));
|
|
(observeCallsState as jest.Mock).mockReturnValue(of$(DefaultCallsState));
|
|
(observeConfigValue as jest.Mock).mockReturnValue(of$('10.5.0'));
|
|
|
|
const result = await firstValueFrom(observeIsCallsEnabledInChannel(database, serverUrl, of$(channelId)));
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return true when explicitly enabled', async () => {
|
|
(observeCallsState as jest.Mock).mockReturnValue(of$({
|
|
enabled: {[channelId]: true},
|
|
}));
|
|
|
|
const result = await firstValueFrom(observeIsCallsEnabledInChannel(database, serverUrl, of$(channelId)));
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when explicitly disabled', async () => {
|
|
(observeCallsState as jest.Mock).mockReturnValue(of$({
|
|
enabled: {[channelId]: false},
|
|
}));
|
|
|
|
const result = await firstValueFrom(observeIsCallsEnabledInChannel(database, serverUrl, of$(channelId)));
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return true for GA server when not explicitly disabled', async () => {
|
|
(observeCallsState as jest.Mock).mockReturnValue(of$({enabled: {}}));
|
|
(observeConfigValue as jest.Mock).mockReturnValue(of$('7.6.0'));
|
|
|
|
const result = await firstValueFrom(observeIsCallsEnabledInChannel(database, serverUrl, of$(channelId)));
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should use default enabled when not GA server and not explicitly set', async () => {
|
|
(observeCallsState as jest.Mock).mockReturnValue(of$({enabled: {}}));
|
|
(observeConfigValue as jest.Mock).mockReturnValue(of$('7.5.0'));
|
|
(observeCallsConfig as jest.Mock).mockReturnValue(of$({
|
|
pluginEnabled: true,
|
|
DefaultEnabled: true,
|
|
}));
|
|
|
|
const result = await firstValueFrom(observeIsCallsEnabledInChannel(database, serverUrl, of$(channelId)));
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('observeIsCallLimitRestricted', () => {
|
|
it('should detect when call limit is not restricted', async () => {
|
|
(observeCallsConfig as jest.Mock).mockReturnValue(of$({
|
|
MaxCallParticipants: 8,
|
|
sku_short_name: License.SKU_SHORT_NAME.Professional,
|
|
}));
|
|
(observeCallsState as jest.Mock).mockReturnValue(of$({
|
|
calls: {
|
|
[channelId]: {
|
|
sessions: {user1: {}, user2: {}},
|
|
},
|
|
},
|
|
}));
|
|
(observeLicense as jest.Mock).mockReturnValue(of$({Cloud: 'false'}));
|
|
|
|
const result = await firstValueFrom(observeIsCallLimitRestricted(database, serverUrl, channelId));
|
|
expect(result).toEqual({
|
|
limitRestricted: false,
|
|
maxParticipants: 8,
|
|
isCloudStarter: false,
|
|
});
|
|
});
|
|
|
|
it('should detect when call limit is restricted', async () => {
|
|
(observeCallsConfig as jest.Mock).mockReturnValue(of$({
|
|
MaxCallParticipants: 3,
|
|
sku_short_name: License.SKU_SHORT_NAME.Starter,
|
|
}));
|
|
(observeCallsState as jest.Mock).mockReturnValue(of$({
|
|
calls: {
|
|
[channelId]: {
|
|
sessions: {user1: {}, user2: {}, user3: {}},
|
|
},
|
|
},
|
|
}));
|
|
(observeLicense as jest.Mock).mockReturnValue(of$({Cloud: 'true'}));
|
|
|
|
const result = await firstValueFrom(observeIsCallLimitRestricted(database, serverUrl, channelId));
|
|
expect(result).toEqual({
|
|
limitRestricted: true,
|
|
maxParticipants: 3,
|
|
isCloudStarter: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('observeCallStateInChannel', () => {
|
|
it('should not show banner when no call in channel', async () => {
|
|
(observeChannelsWithCalls as jest.Mock).mockReturnValue(of$({}));
|
|
(observeCurrentCall as jest.Mock).mockReturnValue(of$(null));
|
|
(observeCallsState as jest.Mock).mockReturnValue(of$({
|
|
calls: {},
|
|
myUserId: 'user1',
|
|
}));
|
|
(observeIncomingCalls as jest.Mock).mockReturnValue(of$({
|
|
incomingCalls: [],
|
|
}));
|
|
|
|
const {showJoinCallBanner, isInACall, showIncomingCalls} = observeCallStateInChannel(serverUrl, database, of$(channelId));
|
|
|
|
const bannerVisible = await firstValueFrom(showJoinCallBanner);
|
|
const inCall = await firstValueFrom(isInACall);
|
|
const hasIncoming = await firstValueFrom(showIncomingCalls);
|
|
|
|
expect(bannerVisible).toBe(false);
|
|
expect(inCall).toBe(false);
|
|
expect(hasIncoming).toBe(false);
|
|
});
|
|
|
|
it('should count a call being placed as being in a call, so the call bar has somewhere to be', async () => {
|
|
// useCallsAdjustment reserves the bar's height from the moment there is a current call.
|
|
(observeCurrentCall as jest.Mock).mockReturnValue(of$({channelId, connected: false}));
|
|
(observeChannelsWithCalls as jest.Mock).mockReturnValue(of$({}));
|
|
(observeCallsState as jest.Mock).mockReturnValue(of$(DefaultCallsState));
|
|
(observeIncomingCalls as jest.Mock).mockReturnValue(of$(DefaultIncomingCalls));
|
|
|
|
const {isInACall} = observeCallStateInChannel(serverUrl, database, of$(channelId));
|
|
|
|
expect(await firstValueFrom(isInACall)).toBe(true);
|
|
});
|
|
|
|
it('should detect active call in channel', async () => {
|
|
(observeChannelsWithCalls as jest.Mock).mockReturnValue(of$({
|
|
[channelId]: true,
|
|
}));
|
|
(observeCurrentCall as jest.Mock).mockReturnValue(of$({
|
|
channelId: 'different-channel',
|
|
connected: true,
|
|
}));
|
|
(observeCallsState as jest.Mock).mockReturnValue(of$({
|
|
calls: {
|
|
[channelId]: {
|
|
dismissed: {},
|
|
},
|
|
},
|
|
myUserId: 'user1',
|
|
}));
|
|
(observeIncomingCalls as jest.Mock).mockReturnValue(of$({
|
|
incomingCalls: [],
|
|
}));
|
|
|
|
const {showJoinCallBanner, isInACall, showIncomingCalls} = observeCallStateInChannel(serverUrl, database, of$(channelId));
|
|
|
|
const bannerVisible = await firstValueFrom(showJoinCallBanner);
|
|
const inCall = await firstValueFrom(isInACall);
|
|
const hasIncoming = await firstValueFrom(showIncomingCalls);
|
|
|
|
expect(bannerVisible).toBe(true);
|
|
expect(inCall).toBe(true);
|
|
expect(hasIncoming).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('observeCurrentSessionsDict', () => {
|
|
it('should handle empty/null call state', async () => {
|
|
(observeCurrentCall as jest.Mock).mockReturnValue(of$(null));
|
|
|
|
const result = await firstValueFrom(observeCurrentSessionsDict());
|
|
expect(result).toEqual({});
|
|
});
|
|
|
|
it('should fill user models for sessions', async () => {
|
|
const sessions = {
|
|
session1: {userId: 'user1'},
|
|
session2: {userId: 'user2'},
|
|
};
|
|
(observeCurrentCall as jest.Mock).mockReturnValue(of$({
|
|
sessions,
|
|
serverUrl: 'test-server',
|
|
}));
|
|
const userModels = [
|
|
{id: 'user1', username: 'user.one'},
|
|
{id: 'user2', username: 'user.two'},
|
|
];
|
|
(queryUsersById as jest.Mock).mockReturnValue({
|
|
observeWithColumns: () => of$(userModels),
|
|
});
|
|
|
|
const result = await firstValueFrom(observeCurrentSessionsDict());
|
|
expect(result).toEqual({
|
|
session1: {...sessions.session1, userModel: userModels[0]},
|
|
session2: {...sessions.session2, userModel: userModels[1]},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('observeCallChannel', () => {
|
|
it('should resolve the channel from the call server database', async () => {
|
|
const callServerDatabase = {id: 'call-server-db'} as any;
|
|
(DatabaseManager as any).serverDatabases = {'test-server': {database: callServerDatabase}};
|
|
(observeCurrentCall as jest.Mock).mockReturnValue(of$({serverUrl: 'test-server', channelId: 'channel1'}));
|
|
(observeChannel as jest.Mock).mockReturnValue(of$({id: 'channel1', type: 'D'}));
|
|
|
|
const result = await firstValueFrom(observeCallChannel());
|
|
|
|
expect(observeChannel).toHaveBeenCalledWith(callServerDatabase, 'channel1');
|
|
expect(result).toEqual({id: 'channel1', type: 'D'});
|
|
});
|
|
|
|
it('should emit undefined when the call server has no database', async () => {
|
|
// the call is on a server other than the one whose database is loaded
|
|
(observeCurrentCall as jest.Mock).mockReturnValue(of$({serverUrl: 'other-server', channelId: 'channel1'}));
|
|
|
|
const result = await firstValueFrom(observeCallChannel());
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(observeChannel).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should emit undefined when there is no current call', async () => {
|
|
(observeCurrentCall as jest.Mock).mockReturnValue(of$(null));
|
|
|
|
const result = await firstValueFrom(observeCallChannel());
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(observeChannel).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('observeEndCallDetails', () => {
|
|
it('should handle empty call state', async () => {
|
|
(observeCurrentCall as jest.Mock).mockReturnValue(of$(null));
|
|
|
|
const {otherParticipants, isAdmin, isHost} = observeEndCallDetails();
|
|
|
|
const hasOthers = await firstValueFrom(otherParticipants);
|
|
const admin = await firstValueFrom(isAdmin);
|
|
const host = await firstValueFrom(isHost);
|
|
|
|
expect(hasOthers).toBe(false);
|
|
expect(admin).toBe(false);
|
|
expect(host).toBe(false);
|
|
});
|
|
|
|
it('should determine host and admin status', async () => {
|
|
(observeCurrentCall as jest.Mock).mockReturnValue(of$({
|
|
myUserId: 'user1',
|
|
mySessionId: 'session1',
|
|
hostId: 'user1',
|
|
sessions: {
|
|
session1: {
|
|
userModel: {
|
|
roles: 'system_admin',
|
|
},
|
|
},
|
|
session2: {},
|
|
},
|
|
}));
|
|
|
|
const {otherParticipants, isAdmin, isHost} = observeEndCallDetails();
|
|
|
|
const hasOthers = await firstValueFrom(otherParticipants);
|
|
const admin = await firstValueFrom(isAdmin);
|
|
const host = await firstValueFrom(isHost);
|
|
|
|
expect(hasOthers).toBe(true);
|
|
expect(admin).toBe(true);
|
|
expect(host).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('observeDMCallingState', () => {
|
|
const callee = {id: 'user2'};
|
|
const dmCall = {
|
|
serverUrl: 'test-server',
|
|
channelId: 'dm-channel',
|
|
connected: true,
|
|
ownerId: 'user1',
|
|
myUserId: 'user1',
|
|
mySessionId: 'session1',
|
|
startTime: 1000,
|
|
dmCalleeAnsweredAt: 0,
|
|
sessions: {session1: {sessionId: 'session1', userId: 'user1'}},
|
|
};
|
|
|
|
const getState = async (call: unknown, channel: unknown = {type: General.DM_CHANNEL, name: 'user1__user2'}) => {
|
|
(observeCurrentCall as jest.Mock).mockReturnValue(of$(call));
|
|
(observeChannel as jest.Mock).mockReturnValue(of$(channel));
|
|
(observeUser as jest.Mock).mockReturnValue(of$(callee));
|
|
|
|
const {isDMCall, isDMConnecting, isDMCalling, dmCalleeId, dmCallee, dmCalleeAnsweredAt} = observeDMCallingState();
|
|
|
|
return {
|
|
isDMCall: await firstValueFrom(isDMCall),
|
|
isDMConnecting: await firstValueFrom(isDMConnecting),
|
|
isDMCalling: await firstValueFrom(isDMCalling),
|
|
dmCalleeId: await firstValueFrom(dmCalleeId),
|
|
dmCallee: await firstValueFrom(dmCallee),
|
|
dmCalleeAnsweredAt: await firstValueFrom(dmCalleeAnsweredAt),
|
|
};
|
|
};
|
|
|
|
it('should be calling when I own a DM call nobody else has joined', async () => {
|
|
const state = await getState(dmCall);
|
|
|
|
expect(state.isDMCall).toBe(true);
|
|
expect(state.isDMCalling).toBe(true);
|
|
expect(state.dmCalleeId).toBe('user2');
|
|
expect(state.dmCallee).toBe(callee);
|
|
|
|
// Nothing to count yet, so it falls back to when the call started.
|
|
expect(state.dmCalleeAnsweredAt).toBe(1000);
|
|
});
|
|
|
|
it('should stop calling and time from the answer once the other party joins', async () => {
|
|
const state = await getState({
|
|
...dmCall,
|
|
dmCalleeAnsweredAt: 5000,
|
|
sessions: {
|
|
...dmCall.sessions,
|
|
session2: {sessionId: 'session2', userId: 'user2'},
|
|
},
|
|
});
|
|
|
|
expect(state.isDMCalling).toBe(false);
|
|
expect(state.dmCalleeAnsweredAt).toBe(5000);
|
|
});
|
|
|
|
it('should not be calling once the callee is in the call, even before the answer is stamped', async () => {
|
|
// Only user_joined stamps dmCalleeAnsweredAt, so a session list that arrives by any
|
|
// other route, such as call_start carrying one, is the only sign they are already here.
|
|
const state = await getState({
|
|
...dmCall,
|
|
sessions: {
|
|
...dmCall.sessions,
|
|
session2: {sessionId: 'session2', userId: 'user2'},
|
|
},
|
|
});
|
|
|
|
expect(state.isDMCalling).toBe(false);
|
|
});
|
|
|
|
it('should not go back to calling after the callee answers and then leaves', async () => {
|
|
// Their session is gone again, but the call was answered, so the ring phase is over.
|
|
const state = await getState({...dmCall, dmCalleeAnsweredAt: 5000});
|
|
|
|
expect(state.isDMCalling).toBe(false);
|
|
expect(state.dmCalleeAnsweredAt).toBe(5000);
|
|
});
|
|
|
|
it('should not be calling for the callee of the DM call', async () => {
|
|
const state = await getState({...dmCall, ownerId: 'user2', myUserId: 'user1'});
|
|
|
|
expect(state.isDMCall).toBe(true);
|
|
expect(state.isDMCalling).toBe(false);
|
|
});
|
|
|
|
it('should not be calling until my own session is in the call', async () => {
|
|
const state = await getState({...dmCall, mySessionId: '', sessions: {}});
|
|
|
|
expect(state.isDMCalling).toBe(false);
|
|
});
|
|
|
|
it('should be connecting while placing the call, and calling once my session is in it', async () => {
|
|
const placing = await getState({...dmCall, startedByMe: true, connected: false, mySessionId: '', sessions: {}});
|
|
|
|
expect(placing.isDMConnecting).toBe(true);
|
|
expect(placing.isDMCalling).toBe(false);
|
|
expect(placing.dmCalleeId).toBe('user2');
|
|
expect(placing.dmCallee).toBe(callee);
|
|
|
|
// ownerId only arrives with the server's call_start event, so startedByMe has to carry
|
|
// the caller through to the ringing phase on its own.
|
|
const inTheCall = await getState({...dmCall, startedByMe: true, ownerId: ''});
|
|
|
|
expect(inTheCall.isDMConnecting).toBe(false);
|
|
expect(inTheCall.isDMCalling).toBe(true);
|
|
});
|
|
|
|
it('should still be connecting when the media connection is up but my session has not arrived', async () => {
|
|
// connected goes true on the calls socket; the session only lands with user_joined on
|
|
// the main one, and in between there must be no gap in what we show.
|
|
const state = await getState({...dmCall, startedByMe: true, connected: true, mySessionId: 'session1', sessions: {}});
|
|
|
|
expect(state.isDMConnecting).toBe(true);
|
|
expect(state.isDMCalling).toBe(false);
|
|
});
|
|
|
|
it('should not be connecting when joining a call someone else started', async () => {
|
|
const state = await getState({...dmCall, connected: false, mySessionId: '', sessions: {}});
|
|
|
|
expect(state.isDMConnecting).toBe(false);
|
|
expect(state.isDMCalling).toBe(false);
|
|
});
|
|
|
|
it('should not be calling in a channel call, and should time from the call start', async () => {
|
|
const state = await getState(
|
|
{...dmCall, dmCalleeAnsweredAt: 5000},
|
|
{type: General.OPEN_CHANNEL, name: 'town-square'},
|
|
);
|
|
|
|
expect(state.isDMCall).toBe(false);
|
|
expect(state.isDMCalling).toBe(false);
|
|
expect(state.dmCalleeId).toBe('');
|
|
expect(state.dmCallee).toBeUndefined();
|
|
expect(state.dmCalleeAnsweredAt).toBe(1000);
|
|
});
|
|
|
|
it('should not be calling in a DM with myself, since nobody can join', async () => {
|
|
const state = await getState(dmCall, {type: General.DM_CHANNEL, name: 'user1__user1'});
|
|
|
|
expect(state.isDMCall).toBe(false);
|
|
expect(state.isDMCalling).toBe(false);
|
|
expect(state.dmCalleeId).toBe('');
|
|
expect(state.dmCallee).toBeUndefined();
|
|
});
|
|
|
|
it('should handle there being no call', async () => {
|
|
const state = await getState(null);
|
|
|
|
expect(state.isDMCall).toBe(false);
|
|
expect(state.isDMCalling).toBe(false);
|
|
expect(state.dmCalleeId).toBe('');
|
|
expect(state.dmCallee).toBeUndefined();
|
|
expect(state.dmCalleeAnsweredAt).toBe(0);
|
|
});
|
|
});
|
|
});
|