test: Add tests for push notifications module (#8535)
This commit is contained in:
parent
dc45bcb4a6
commit
0565a687c5
1 changed files with 515 additions and 0 deletions
515
app/init/push_notifications.test.ts
Normal file
515
app/init/push_notifications.test.ts
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
// 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} 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 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', () => ({
|
||||
getVisibleScreen: jest.fn(() => '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('Channel');
|
||||
AppState.currentState = 'active';
|
||||
jest.spyOn(DeviceEventEmitter, 'emit').mockImplementation();
|
||||
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(DeviceEventEmitter.emit).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(DeviceEventEmitter.emit).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('Settings');
|
||||
|
||||
await pushNotifications.handleInAppNotification(serverUrl, notification as any);
|
||||
|
||||
expect(DeviceEventEmitter.emit).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(DeviceEventEmitter.emit).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('Thread');
|
||||
mockGetIsCRTEnabled.mockResolvedValue(true);
|
||||
jest.spyOn(EphemeralStore, 'getCurrentThreadId').mockReturnValue('thread1');
|
||||
|
||||
await pushNotifications.handleInAppNotification(serverUrl, notification as any);
|
||||
|
||||
expect(DeviceEventEmitter.emit).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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue