diff --git a/app/actions/remote/session.test.ts b/app/actions/remote/session.test.ts new file mode 100644 index 000000000..ae92fb0eb --- /dev/null +++ b/app/actions/remote/session.test.ts @@ -0,0 +1,284 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable max-lines */ + +import {GLOBAL_IDENTIFIERS, SYSTEM_IDENTIFIERS} from '@constants/database'; +import DatabaseManager from '@database/manager'; +import NetworkManager from '@managers/network_manager'; + +import { + addPushProxyVerificationStateFromLogin, + forceLogoutIfNecessary, + fetchSessions, + login, + cancelSessionNotification, + scheduleSessionNotification, + sendPasswordResetEmail, + ssoLogin, + findSession, +} from './session'; + +import type ServerDataOperator from '@database/operator/server_data_operator'; +import type {LoginArgs} from '@typings/database/database'; + +const serverUrl = 'baseHandler.test.com'; +let operator: ServerDataOperator; + +const user1 = {id: 'userid1', username: 'user1', email: 'user1@mattermost.com', roles: ''} as UserProfile; + +const session1 = {id: 'sessionid1', user_id: user1.id, device_id: 'deviceid', props: {csrf: 'csrfid'}} as Session; + +const throwFunc = () => { + throw Error('error'); +}; + +const mockClient = { + login: jest.fn(() => user1), + setCSRFToken: jest.fn(), + setBearerToken: jest.fn(), + getClientConfigOld: jest.fn(() => ({})), + getClientLicenseOld: jest.fn(() => ({})), + getSessions: jest.fn(() => [session1]), + sendPasswordResetEmail: jest.fn(() => ({status: 200})), + getMe: jest.fn(() => user1), +}; + +let mockGetPushProxyVerificationState: jest.Mock; +jest.mock('@store/ephemeral_store', () => { + const original = jest.requireActual('@store/ephemeral_store'); + mockGetPushProxyVerificationState = jest.fn(() => 'verified'); + return { + ...original, + getPushProxyVerificationState: mockGetPushProxyVerificationState, + }; +}); + +let mockFetch: jest.Mock; +jest.mock('@react-native-community/netinfo', () => { + const original = jest.requireActual('@react-native-community/netinfo'); + mockFetch = jest.fn(() => ({isInternetReachable: true})); + return { + ...original, + fetch: mockFetch, + }; +}); + +let mockCancelLocalNotification: jest.Mock; +jest.mock('react-native-notifications', () => { + const original = jest.requireActual('react-native-notifications'); + mockCancelLocalNotification = jest.fn(); + return { + ...original, + Notifications: { + ...original.Notifications, + cancelLocalNotification: mockCancelLocalNotification, + }, + }; +}); + +let mockGetCSRFFromCookie: jest.Mock; +jest.mock('@utils/security', () => { + const original = jest.requireActual('@utils/security'); + mockGetCSRFFromCookie = jest.fn(() => 'csrfid'); + return { + ...original, + getCSRFFromCookie: mockGetCSRFFromCookie, + }; +}); + +beforeAll(() => { + // eslint-disable-next-line + // @ts-ignore + NetworkManager.getClient = () => mockClient; +}); + +beforeEach(async () => { + await DatabaseManager.init([serverUrl]); + operator = DatabaseManager.serverDatabases[serverUrl]!.operator; +}); + +afterEach(async () => { + await DatabaseManager.destroyServerDatabase(serverUrl); +}); + +describe('sessions', () => { + it('addPushProxyVerificationStateFromLogin - handle not found database', async () => { + const result = await addPushProxyVerificationStateFromLogin('foo'); + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + }); + + it('addPushProxyVerificationStateFromLogin - base case', async () => { + const result = await addPushProxyVerificationStateFromLogin(serverUrl); + expect(result).toBeDefined(); + expect(result.error).toBeUndefined(); + }); + + it('forceLogoutIfNecessary - handle not found database', async () => { + const result = await forceLogoutIfNecessary('foo', {}); + expect(result).toBeDefined(); + expect(result.error).toBeTruthy(); + expect(result.logout).toBe(false); + }); + + it('forceLogoutIfNecessary - logout expected from 401', async () => { + await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false}); + + const result = await forceLogoutIfNecessary(serverUrl, {status_code: 401, url: '/api/v4/users/me'}); + expect(result).toBeDefined(); + expect(result.error).toBeNull(); + expect(result.logout).toBe(true); + }); + + it('forceLogoutIfNecessary - logout not expected', async () => { + await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false}); + + const result = await forceLogoutIfNecessary(serverUrl, {status_code: 500, url: '/api/v4/users/me'}); + expect(result).toBeDefined(); + expect(result.error).toBeNull(); + expect(result.logout).toBe(false); + }); + + it('fetchSessions - handle error', async () => { + mockClient.getSessions.mockImplementationOnce(jest.fn(throwFunc)); + const result = await fetchSessions('foo', ''); + expect(result).toBeUndefined(); + }); + + it('fetchSessions - base case', async () => { + const result = await fetchSessions(serverUrl, user1.id); + expect(result).toBeDefined(); + expect(result?.length).toBe(1); + }); + + it('login - base case', async () => { + const result = await login(serverUrl, {config: {DiagnosticId: 'diagnosticid'}} as LoginArgs); + expect(result).toBeDefined(); + expect(result.error).toBeUndefined(); + expect(result.failed).toBe(false); + }); + + it('login - handle throw on login request', async () => { + mockClient.login.mockImplementationOnce(jest.fn(throwFunc)); + + const result = await login(serverUrl, {config: {DiagnosticId: 'diagnosticid'}} as LoginArgs); + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + expect(result.failed).toBe(true); + }); + + it('cancelSessionNotification - handle not found database', async () => { + const result = await cancelSessionNotification('foo'); + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + }); + + it('cancelSessionNotification - base case', async () => { + await operator.handleSystem({ + systems: [{ + id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION, + value: { + id: 'sessionid1', + notificationId: 'notificationid', + expiresAt: 123, + }, + }], + prepareRecordsOnly: false, + }); + + const result = await cancelSessionNotification(serverUrl); + expect(result).toBeDefined(); + expect(result.error).toBeUndefined(); + }); + + it('scheduleSessionNotification - handle not found database', async () => { + const result = await scheduleSessionNotification('foo'); + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + }); + + it('scheduleSessionNotification - base case', async () => { + await operator.handleSystem({ + systems: [{ + id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION, + value: { + id: 'sessionid1', + notificationId: 'notificationid', + expiresAt: 123, + }, + }], + prepareRecordsOnly: false, + }); + + const result = await scheduleSessionNotification(serverUrl); + expect(result).toBeDefined(); + expect(result.error).toBeUndefined(); + }); + + it('sendPasswordResetEmail - handle error', async () => { + mockClient.sendPasswordResetEmail.mockImplementationOnce(jest.fn(throwFunc)); + const result = await sendPasswordResetEmail('foo', ''); + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + }); + + it('sendPasswordResetEmail - base case', async () => { + const result = await sendPasswordResetEmail(serverUrl, user1.email); + expect(result).toBeDefined(); + expect(result.error).toBeUndefined(); + expect(result.status).toBe(200); + }); + + it('ssoLogin - handle error', async () => { + mockClient.getMe.mockImplementationOnce(jest.fn(throwFunc)); + const result = await ssoLogin('foo', '', '', '', ''); + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + expect(result.failed).toBe(true); + }); + + it('ssoLogin - base case', async () => { + const result = await ssoLogin(serverUrl, 'servername', 'diagnosticid', 'authtoken', 'csrftoken'); + expect(result).toBeDefined(); + expect(result.error).toBeUndefined(); + expect(result.failed).toBe(false); + }); + + it('findSession - handle not found database', async () => { + const result = await findSession('foo', []); + expect(result).toBeUndefined(); + }); + + it('findSession - by id', async () => { + await operator.handleSystem({ + systems: [{ + id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION, + value: { + id: 'sessionid1', + notificationId: 'notificationid', + expiresAt: 123, + }, + }], + prepareRecordsOnly: false, + }); + + const session = await findSession(serverUrl, [session1]); + expect(session).toBeDefined(); + }); + + it('findSession - by device', async () => { + await DatabaseManager.appDatabase?.operator.handleGlobal({ + globals: [{id: GLOBAL_IDENTIFIERS.DEVICE_TOKEN, value: 'deviceid'}], + prepareRecordsOnly: false, + }); + + const session = await findSession(serverUrl, [session1]); + expect(session).toBeDefined(); + }); + + it('findSession - by csrf', async () => { + const session = await findSession(serverUrl, [session1]); + expect(session).toBeDefined(); + }); +}); diff --git a/app/actions/remote/session.ts b/app/actions/remote/session.ts index 467e2b5d6..60c8845ce 100644 --- a/app/actions/remote/session.ts +++ b/app/actions/remote/session.ts @@ -40,23 +40,27 @@ export const addPushProxyVerificationStateFromLogin = async (serverUrl: string) if (systems.length) { await operator.handleSystem({systems, prepareRecordsOnly: false}); } + + return {}; } catch (error) { logDebug('error setting the push proxy verification state on login', error); + return {error}; } }; export const forceLogoutIfNecessary = async (serverUrl: string, err: unknown) => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { - return {error: `${serverUrl} database not found`}; + return {error: `${serverUrl} database not found`, logout: false}; } const currentUserId = await getCurrentUserId(database); if (isErrorWithStatusCode(err) && err.status_code === HTTP_UNAUTHORIZED && isErrorWithUrl(err) && err.url?.indexOf('/login') === -1 && currentUserId) { await logout(serverUrl); + return {error: null, logout: true}; } - return {error: null}; + return {error: null, logout: false}; }; export const fetchSessions = async (serverUrl: string, currentUserId: string) => { @@ -163,8 +167,11 @@ export const cancelSessionNotification = async (serverUrl: string) => { prepareRecordsOnly: false, }); } + + return {}; } catch (e) { logError('cancelSessionNotification', e); + return {error: e}; } }; @@ -196,9 +203,11 @@ export const scheduleSessionNotification = async (serverUrl: string) => { }); } } + return {}; } catch (e) { logError('scheduleExpiredNotification', e); await forceLogoutIfNecessary(serverUrl, e); + return {error: e}; } }; @@ -258,7 +267,7 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser } }; -async function findSession(serverUrl: string, sessions: Session[]) { +export async function findSession(serverUrl: string, sessions: Session[]) { try { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const expiredSession = await getExpiredSession(database); diff --git a/app/managers/session_manager.ts b/app/managers/session_manager.ts index e9642ebf0..86712bab1 100644 --- a/app/managers/session_manager.ts +++ b/app/managers/session_manager.ts @@ -90,7 +90,7 @@ class SessionManager { if (!this.scheduling) { this.scheduling = true; const serverCredentials = await getAllServerCredentials(); - const promises: Array> = []; + const promises: Array> = []; for (const {serverUrl} of serverCredentials) { promises.push(scheduleSessionNotification(serverUrl)); }