nexo/apps/mattermost/app/init/push_notifications.test.ts

521 lines
18 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {AppState, DeviceEventEmitter, Platform} from 'react-native';
import {storeDeviceToken} from '@actions/app/global';
import {markChannelAsViewed} from '@actions/local/channel';
import {updateThread} from '@actions/local/thread';
import {openNotification} from '@actions/remote/notifications';
import {Device, Events, PushNotification, Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {getCurrentChannelId} from '@queries/servers/system';
import {getIsCRTEnabled, getThreadById} from '@queries/servers/thread';
import EphemeralStore from '@store/ephemeral_store';
import InAppNotificationStore from '@store/in_app_notification_store';
import {NavigationStore} from '@store/navigation_store';
import {isMainActivity} from '@utils/helpers';
import PushNotifications from './push_notifications';
import type ThreadModel from '@typings/database/models/servers/thread';
const subscription = {remove: jest.fn()};
const events = {
registerNotificationOpened: jest.fn(() => subscription),
registerRemoteNotificationsRegistered: jest.fn(() => subscription),
registerNotificationReceivedBackground: jest.fn(() => subscription),
registerNotificationReceivedForeground: jest.fn(() => subscription),
registerRemoteNotificationsRegistrationFailed: jest.fn(() => subscription),
registerRemoteNotificationsRegistrationDenied: jest.fn(() => subscription),
} as any;
const mockNotifications = {
events: () => events,
isRegisteredForRemoteNotifications: jest.fn(),
registerRemoteNotifications: jest.fn(),
setCategories: jest.fn(),
postLocalNotification: jest.fn(),
cancelLocalNotification: jest.fn(),
};
const NotificationBackgroundFetchResult = {
NEW_DATA: 'new-data',
NO_DATA: 'no-data',
FAILED: 'failed',
};
class mockNotificationAction {
}
class mockNotificationCategory {
}
jest.mock('react-native-notifications', () => ({
Notifications: mockNotifications,
NotificationAction: mockNotificationAction,
NotificationCategory: mockNotificationCategory,
NotificationBackgroundFetchResult,
}));
jest.mock('@actions/app/global');
jest.mock('@actions/local/channel');
jest.mock('@actions/local/thread');
jest.mock('@actions/remote/notifications');
jest.mock('@utils/general', () => ({
...jest.requireActual('@utils/general'),
isBetaApp: jest.fn(),
}));
jest.mock('@utils/helpers', () => ({
...jest.requireActual('@utils/helpers'),
isMainActivity: jest.fn(),
}));
jest.mock('@store/navigation_store', () => ({
NavigationStore: {
getVisibleScreen: jest.fn(),
},
}));
jest.mock('@store/in_app_notification_store');
jest.mocked(NavigationStore.getVisibleScreen).mockReturnValue(Screens.CHANNEL);
jest.mock('@queries/servers/system', () => ({
getCurrentChannelId: jest.fn(),
}));
jest.mock('@queries/servers/thread', () => ({
getIsCRTEnabled: jest.fn(),
getThreadById: jest.fn(),
}));
describe('PushNotifications', () => {
let pushNotifications: typeof PushNotifications;
beforeEach(() => {
jest.clearAllMocks();
pushNotifications = require('./push_notifications').default;
});
describe('init', () => {
it('should register event listeners', () => {
pushNotifications.init(false);
expect(events.registerNotificationOpened).toHaveBeenCalled();
expect(events.registerRemoteNotificationsRegistered).toHaveBeenCalled();
expect(events.registerNotificationReceivedBackground).toHaveBeenCalled();
expect(events.registerNotificationReceivedForeground).toHaveBeenCalled();
expect(events.registerRemoteNotificationsRegistrationFailed).toHaveBeenCalled();
expect(events.registerRemoteNotificationsRegistrationDenied).toHaveBeenCalled();
});
it('should register for notifications when register is true', (done) => {
pushNotifications.init(true);
setTimeout(() => {
expect(mockNotifications.registerRemoteNotifications).toHaveBeenCalled();
done();
}, 0);
});
});
describe('onRemoteNotificationsRegistered', () => {
it('should store device token with correct prefix for iOS', async () => {
Platform.OS = 'ios';
const deviceToken = 'test-token';
await pushNotifications.onRemoteNotificationsRegistered({deviceToken});
expect(storeDeviceToken).toHaveBeenCalledWith(
`apple_rnbeta-v2:${deviceToken}`,
);
});
it('should store device token with correct prefix for Android', async () => {
pushNotifications.configured = false;
Platform.OS = 'android';
const deviceToken = 'test-token';
await pushNotifications.onRemoteNotificationsRegistered({deviceToken});
expect(storeDeviceToken).toHaveBeenCalledWith(
`${Device.PUSH_NOTIFY_ANDROID_REACT_NATIVE}-v2:${deviceToken}`,
);
});
});
describe('handleMessageNotification', () => {
it('should handle foreground notification', async () => {
const notification = {
payload: {
channel_id: 'channel1',
server_url: 'http://test.com',
},
foreground: true,
};
const handleInAppSpy = jest.spyOn(pushNotifications, 'handleInAppNotification');
await pushNotifications.handleMessageNotification(notification as any);
expect(handleInAppSpy).toHaveBeenCalledWith('http://test.com', notification);
});
it('should handle background notification with user interaction', async () => {
const notification = {
payload: {
channel_id: 'channel1',
server_url: 'http://test.com',
userInfo: {},
},
foreground: false,
userInteraction: true,
};
await pushNotifications.handleMessageNotification(notification as any);
expect(openNotification).toHaveBeenCalledWith('http://test.com', notification);
});
});
describe('handleInAppNotification', () => {
const mockGetCurrentChannelId = jest.mocked(getCurrentChannelId);
const mockGetIsCRTEnabled = jest.mocked(getIsCRTEnabled);
const mockDatabase = {};
const mockServerDatabases = {
'http://test.com': {
database: mockDatabase,
},
};
beforeEach(() => {
DatabaseManager.serverDatabases = mockServerDatabases as any;
(isMainActivity as jest.Mock).mockReturnValue(true);
(NavigationStore.getVisibleScreen as jest.Mock).mockReturnValue(Screens.CHANNEL);
AppState.currentState = 'active';
mockGetCurrentChannelId.mockResolvedValue('channel1');
mockGetIsCRTEnabled.mockResolvedValue(false);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should not show notification for same channel when in channel screen', async () => {
const serverUrl = 'http://test.com';
const notification = {
payload: {
channel_id: 'channel1',
},
};
await pushNotifications.handleInAppNotification(serverUrl, notification as any);
expect(InAppNotificationStore.show).not.toHaveBeenCalled();
});
it('should show notification for different channel when in channel screen', async () => {
const serverUrl = 'http://test.com';
const notification = {
payload: {
channel_id: 'channel2',
},
};
await pushNotifications.handleInAppNotification(serverUrl, notification as any);
expect(InAppNotificationStore.show).toHaveBeenCalled();
});
it('should show notification when not in channel screen', async () => {
const serverUrl = 'http://test.com';
const notification = {
payload: {
channel_id: 'channel1',
},
};
jest.mocked(NavigationStore.getVisibleScreen).mockReturnValue(Screens.SETTINGS);
await pushNotifications.handleInAppNotification(serverUrl, notification as any);
expect(InAppNotificationStore.show).toHaveBeenCalled();
});
it('should show notification for thread in CRT mode', async () => {
const serverUrl = 'http://test.com';
const notification = {
payload: {
channel_id: 'channel1',
root_id: 'thread1',
},
};
mockGetIsCRTEnabled.mockResolvedValue(true);
await pushNotifications.handleInAppNotification(serverUrl, notification as any);
expect(InAppNotificationStore.show).toHaveBeenCalled();
});
it('should not show notification for same thread in thread screen', async () => {
const serverUrl = 'http://test.com';
const notification = {
payload: {
channel_id: 'channel1',
root_id: 'thread1',
},
};
jest.mocked(NavigationStore.getVisibleScreen).mockReturnValue(Screens.THREAD);
mockGetIsCRTEnabled.mockResolvedValue(true);
jest.spyOn(EphemeralStore, 'getCurrentThreadId').mockReturnValue('thread1');
await pushNotifications.handleInAppNotification(serverUrl, notification as any);
expect(InAppNotificationStore.show).not.toHaveBeenCalled();
});
});
describe('handleClearNotification', () => {
const mockDatabase = {};
const mockServerDatabases = {
'http://test.com': {
database: mockDatabase,
},
};
const mockGetIsCRTEnabled = jest.mocked(getIsCRTEnabled);
const mockGetThreadById = jest.mocked(getThreadById);
beforeEach(() => {
DatabaseManager.serverDatabases = mockServerDatabases as any;
mockGetIsCRTEnabled.mockResolvedValue(false);
});
it('should mark channel as viewed for non-CRT notification', async () => {
const notification = {
payload: {
channel_id: 'channel1',
server_url: 'http://test.com',
},
};
await pushNotifications.handleClearNotification(notification as any);
expect(markChannelAsViewed).toHaveBeenCalledWith('http://test.com', 'channel1');
});
it('should update thread for CRT notification', async () => {
const notification = {
payload: {
channel_id: 'channel1',
root_id: 'thread1',
server_url: 'http://test.com',
},
};
mockGetIsCRTEnabled.mockResolvedValue(true);
mockGetThreadById.mockResolvedValue({isFollowing: true} as ThreadModel);
await pushNotifications.handleClearNotification(notification as any);
expect(updateThread).toHaveBeenCalledWith('http://test.com', 'thread1', expect.any(Object));
});
});
describe('onNotificationOpened', () => {
it('should process notification with user interaction', () => {
const notification = {
payload: {
channel_id: 'channel1',
},
};
const completion = jest.fn();
const processSpy = jest.spyOn(pushNotifications, 'processNotification');
pushNotifications.onNotificationOpened(notification as any, completion);
expect(processSpy).toHaveBeenCalledWith(expect.objectContaining({
userInteraction: true,
}));
expect(completion).toHaveBeenCalled();
});
});
describe('onNotificationReceivedBackground', () => {
it('should not process unverified notification', async () => {
const notification = {
payload: {
verified: 'false',
ackId: '123',
},
};
const completion = jest.fn();
const processSpy = jest.spyOn(pushNotifications, 'processNotification');
await pushNotifications.onNotificationReceivedBackground(notification as any, completion);
expect(processSpy).not.toHaveBeenCalled();
});
it('should process verified notification', async () => {
const notification = {
payload: {
verified: 'true',
channel_id: 'channel1',
},
};
const completion = jest.fn();
const processSpy = jest.spyOn(pushNotifications, 'processNotification');
await pushNotifications.onNotificationReceivedBackground(notification as any, completion);
expect(processSpy).toHaveBeenCalled();
expect(completion).toHaveBeenCalledWith('new-data');
});
});
describe('onNotificationReceivedForeground', () => {
beforeEach(() => {
(isMainActivity as jest.Mock).mockReturnValue(true);
AppState.currentState = 'active';
});
it('should not process unverified notification', () => {
const notification = {
payload: {
verified: 'false',
ackId: '123',
},
};
const completion = jest.fn();
const processSpy = jest.spyOn(pushNotifications, 'processNotification');
pushNotifications.onNotificationReceivedForeground(notification as any, completion);
expect(processSpy).not.toHaveBeenCalled();
});
it('should process verified notification with sound', () => {
const notification = {
payload: {
verified: 'true',
channel_id: 'channel1',
},
};
const completion = jest.fn();
const processSpy = jest.spyOn(pushNotifications, 'processNotification');
pushNotifications.onNotificationReceivedForeground(notification as any, completion);
expect(processSpy).toHaveBeenCalled();
expect(completion).toHaveBeenCalledWith({
alert: false,
sound: true,
badge: true,
});
});
});
describe('handleSessionNotification', () => {
beforeEach(() => {
jest.spyOn(DeviceEventEmitter, 'emit');
});
it('should emit session expired event on user interaction', async () => {
const notification = {
payload: {
server_url: 'http://test.com',
},
userInteraction: true,
};
await pushNotifications.handleSessionNotification(notification as any);
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(Events.SESSION_EXPIRED, 'http://test.com');
});
it('should emit server logout event without user interaction', async () => {
const notification = {
payload: {
server_url: 'http://test.com',
},
userInteraction: false,
};
await pushNotifications.handleSessionNotification(notification as any);
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(Events.SERVER_LOGOUT, {serverUrl: 'http://test.com'});
});
});
describe('processNotification', () => {
beforeEach(() => {
jest.spyOn(pushNotifications, 'handleClearNotification');
jest.spyOn(pushNotifications, 'handleMessageNotification');
jest.spyOn(pushNotifications, 'handleSessionNotification');
});
it('should handle clear notification', async () => {
const notification = {
payload: {
type: PushNotification.NOTIFICATION_TYPE.CLEAR,
},
};
await pushNotifications.processNotification(notification as any);
expect(pushNotifications.handleClearNotification).toHaveBeenCalledWith(notification);
});
it('should handle message notification', async () => {
const notification = {
payload: {
type: PushNotification.NOTIFICATION_TYPE.MESSAGE,
},
};
await pushNotifications.processNotification(notification as any);
expect(pushNotifications.handleMessageNotification).toHaveBeenCalledWith(notification);
});
it('should handle session notification', async () => {
const notification = {
payload: {
type: PushNotification.NOTIFICATION_TYPE.SESSION,
},
};
await pushNotifications.processNotification(notification as any);
expect(pushNotifications.handleSessionNotification).toHaveBeenCalledWith(notification);
});
it('should not process notification without payload', async () => {
const notification = {} as any;
await pushNotifications.processNotification(notification);
expect(pushNotifications.handleClearNotification).not.toHaveBeenCalled();
expect(pushNotifications.handleMessageNotification).not.toHaveBeenCalled();
expect(pushNotifications.handleSessionNotification).not.toHaveBeenCalled();
});
});
describe('notification management', () => {
it('should schedule notification with correct date format', () => {
const notification = {
fireDate: new Date('2025-01-01').getTime(),
};
Platform.OS = 'ios';
pushNotifications.scheduleNotification(notification as any);
expect(mockNotifications.postLocalNotification).toHaveBeenCalledWith(
expect.objectContaining({
fireDate: new Date('2025-01-01').toISOString(),
}),
);
});
it('should cancel scheduled notification', () => {
pushNotifications.cancelScheduleNotification(123);
expect(mockNotifications.cancelLocalNotification).toHaveBeenCalledWith(123);
});
});
});