mattermost-mobile-upstream/app/products/calls/observers/index.test.ts

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