* convert attachment fields short and disabled to bool * a more robust existence test * only perform the conversion on iOS * add tests
398 lines
16 KiB
TypeScript
398 lines
16 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
/* eslint-disable max-lines */
|
|
|
|
import {Platform} from 'react-native';
|
|
|
|
import * as PostActions from '@actions/local/post';
|
|
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
|
import DatabaseManager from '@database/manager';
|
|
import NetworkManager from '@managers/network_manager';
|
|
import TestHelper from '@test/test_helper';
|
|
|
|
import {
|
|
fetchNotificationData,
|
|
backgroundNotification,
|
|
openNotification,
|
|
sendTestNotification,
|
|
} from './notifications';
|
|
|
|
import type ServerDataOperator from '@database/operator/server_data_operator';
|
|
|
|
const serverUrl = 'baseHandler.test.com';
|
|
let operator: ServerDataOperator;
|
|
|
|
const teamId = 'teamid1';
|
|
const team: Team = {
|
|
id: teamId,
|
|
name: 'team1',
|
|
} as Team;
|
|
|
|
const channelId = 'channelid1';
|
|
const channel: Channel = {
|
|
id: channelId,
|
|
display_name: 'channelname',
|
|
team_id: '',
|
|
total_msg_count: 0,
|
|
delete_at: 0,
|
|
} as Channel;
|
|
|
|
const user1 = {id: 'userid1', username: 'user1', email: 'user1@mattermost.com', roles: ''} as UserProfile;
|
|
|
|
const post1 = TestHelper.fakePost({channel_id: channelId, id: 'postid1'});
|
|
|
|
const notificationExtraData = {
|
|
channel,
|
|
myChannel: {channel_id: channelId, user_id: user1.id, roles: ''} as ChannelMembership,
|
|
team,
|
|
myTeam: {team_id: teamId, user_id: user1.id, roles: ''} as TeamMembership,
|
|
posts: {posts: {[post1.id]: post1}, order: [post1.id], next: 0, prev: 0},
|
|
categoryChannels: [{channel_id: channelId, category_id: 'categoryid1', sort_order: 0} as CategoryChannel],
|
|
categories: {categories: [{id: 'categoryid1', team_id: teamId, display_name: 'category1', sort_order: 0, sorting: 'alpha', type: 'custom', muted: false, collapsed: false, channel_ids: [channelId]}], order: ['categoryid1']},
|
|
} as NotificationExtraData;
|
|
|
|
const notificationData = {
|
|
channel_id: channelId,
|
|
team_id: teamId,
|
|
post_id: post1.id,
|
|
type: 'type',
|
|
version: 'version',
|
|
isCRTEnabled: false,
|
|
data: notificationExtraData,
|
|
} as NotificationData;
|
|
|
|
const notificationWithData = {
|
|
payload: notificationData,
|
|
} as NotificationWithData;
|
|
|
|
const throwFunc = (e?: string) => {
|
|
throw Error(e == null ? 'error' : e);
|
|
};
|
|
|
|
let mockEmitNotificationError: jest.Mock;
|
|
jest.mock('@utils/notification', () => {
|
|
const original = jest.requireActual('@utils/notification');
|
|
mockEmitNotificationError = jest.fn();
|
|
return {
|
|
...original,
|
|
emitNotificationError: mockEmitNotificationError,
|
|
};
|
|
});
|
|
|
|
const mockClient = {
|
|
getTeam: jest.fn((id: string) => ({id, name: 'team1'})),
|
|
getTeamMember: jest.fn((id: string, userId: string) => ({id: userId + '-' + id, user_id: userId === 'me' ? user1.id : userId, team_id: id, roles: ''})),
|
|
getChannel: jest.fn((_channelId: string) => ({...channel, id: _channelId})),
|
|
getChannelMember: jest.fn((_channelId: string, userId: string) => ({id: userId + '-' + _channelId, user_id: userId === 'me' ? user1.id : userId, channel_id: _channelId, roles: ''})),
|
|
sendTestNotification: jest.fn(),
|
|
};
|
|
|
|
beforeAll(() => {
|
|
// eslint-disable-next-line
|
|
// @ts-ignore
|
|
NetworkManager.getClient = (url) => {
|
|
if (serverUrl === url) {
|
|
return mockClient;
|
|
}
|
|
throw new Error('invalid url');
|
|
};
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await DatabaseManager.init([serverUrl]);
|
|
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await DatabaseManager.destroyServerDatabase(serverUrl);
|
|
});
|
|
|
|
afterAll(() => {
|
|
// This removes the timer set to log the results of the network metrics
|
|
jest.clearAllTimers();
|
|
});
|
|
|
|
describe('notifications', () => {
|
|
it('fetchNotificationData - handle no channel id', async () => {
|
|
const result = await fetchNotificationData(serverUrl, {} as NotificationWithData);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBe('No channel Id was specified');
|
|
});
|
|
|
|
it('fetchNotificationData - handle not found database', async () => {
|
|
const result = await fetchNotificationData('foo', {payload: {channel_id: channelId}} as NotificationWithData);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeDefined();
|
|
});
|
|
|
|
it('fetchNotificationData - no team', async () => {
|
|
await operator.handleTeam({teams: [team], prepareRecordsOnly: false});
|
|
mockClient.getTeam.mockImplementationOnce(jest.fn(throwFunc));
|
|
|
|
const result = await fetchNotificationData(serverUrl, notificationWithData);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeDefined();
|
|
});
|
|
|
|
it('fetchNotificationData - no channel', async () => {
|
|
const myTeams = [
|
|
{
|
|
id: team.id,
|
|
roles: '',
|
|
},
|
|
];
|
|
|
|
await operator.handleMyTeam({
|
|
myTeams,
|
|
prepareRecordsOnly: false,
|
|
});
|
|
await operator.handleTeam({teams: [team], prepareRecordsOnly: false});
|
|
mockClient.getChannel.mockImplementationOnce(jest.fn(() => ({...channel, delete_at: 1})));
|
|
|
|
const result = await fetchNotificationData(serverUrl, notificationWithData);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBe('Channel');
|
|
});
|
|
|
|
it('fetchNotificationData - base case', async () => {
|
|
Platform.OS = 'android';
|
|
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}, {id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
|
|
|
|
const result = await fetchNotificationData(serverUrl, {payload: {...notificationData, team_id: ''}} as NotificationWithData);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('backgroundNotification - handle not found database', async () => {
|
|
const result = await backgroundNotification('foo', {payload: {channel_id: channelId}} as NotificationWithData);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeDefined();
|
|
});
|
|
|
|
it('backgroundNotification - handle no channel id', async () => {
|
|
const result = await backgroundNotification(serverUrl, {} as NotificationWithData);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toMatchObject(new Error('No channel Id was specified'));
|
|
});
|
|
|
|
it('backgroundNotification - base case', async () => {
|
|
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}, {id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
|
|
|
|
const result = await backgroundNotification(serverUrl, {payload: {...notificationData, team_id: ''}} as NotificationWithData);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.models).toBeDefined();
|
|
expect(result.models?.length).toBe(1); // team model
|
|
});
|
|
|
|
it('openNotification - handle not found database', async () => {
|
|
const result = await openNotification('foo', {payload: {channel_id: channelId}} as NotificationWithData) as {error: unknown};
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeDefined();
|
|
});
|
|
|
|
it('openNotification - base case', async () => {
|
|
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}, {id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
|
|
|
|
const result = await openNotification(serverUrl, {payload: {...notificationData, team_id: ''}} as NotificationWithData) as {error?: unknown};
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('sendTestNotification', () => {
|
|
it('calls client function and returns correctly', async () => {
|
|
mockClient.sendTestNotification.mockResolvedValueOnce({status: 'OK'});
|
|
const result = await sendTestNotification(serverUrl);
|
|
expect(result.status).toBe('OK');
|
|
expect(result.error).toBeUndefined();
|
|
expect(mockClient.sendTestNotification).toHaveBeenCalled();
|
|
});
|
|
|
|
it('calls client function and returns correctly when error value', async () => {
|
|
mockClient.sendTestNotification.mockRejectedValueOnce(new Error('some error'));
|
|
const result = await sendTestNotification(serverUrl);
|
|
expect(result.error).toBeTruthy();
|
|
expect(mockClient.sendTestNotification).toHaveBeenCalled();
|
|
});
|
|
|
|
it('calls client function and returns error on throw', async () => {
|
|
mockClient.sendTestNotification.mockImplementationOnce(() => {
|
|
throw new Error('error');
|
|
});
|
|
const result = await sendTestNotification(serverUrl);
|
|
expect(result.error).toBeTruthy();
|
|
expect(mockClient.sendTestNotification).toHaveBeenCalled();
|
|
});
|
|
|
|
it('show error when wrong server url is used', async () => {
|
|
const result = await sendTestNotification('bad server url');
|
|
expect(result.error).toBeTruthy();
|
|
expect(mockClient.sendTestNotification).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('backgroundNotification boolean conversion', () => {
|
|
const postId = 'postWithBooleans';
|
|
const samplePostData = {
|
|
posts: [
|
|
{
|
|
id: postId,
|
|
props: {
|
|
attachments: [
|
|
{
|
|
id: 1,
|
|
fields: [
|
|
{title: 'Field1', short: 1},
|
|
{title: 'Field2', short: 0},
|
|
{title: 'Field3', short: true},
|
|
{title: 'Field4', short: false},
|
|
{title: 'Field5', short: undefined},
|
|
{title: 'Field6'},
|
|
],
|
|
actions: [
|
|
{id: 'action1', disabled: 1},
|
|
{id: 'action2', disabled: 0},
|
|
{id: 'action3', disabled: true},
|
|
{id: 'action4', disabled: false},
|
|
{id: 'action5', disabled: undefined},
|
|
{id: 'action6'},
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
},
|
|
],
|
|
},
|
|
|
|
// Other necessary post props
|
|
channel_id: channelId,
|
|
create_at: 12345,
|
|
update_at: 12345,
|
|
delete_at: 0,
|
|
message: 'Test message',
|
|
user_id: user1.id,
|
|
type: '',
|
|
metadata: {},
|
|
is_pinned: false, // Add other base post props as needed
|
|
reply_count: 0,
|
|
|
|
// Add potentially missing required props for type check, even if empty
|
|
edit_at: 0,
|
|
root_id: '',
|
|
original_id: '',
|
|
hashtags: '',
|
|
pending_post_id: '',
|
|
} as any, // Use 'as any' to bypass strict type checking for test data
|
|
],
|
|
order: [postId],
|
|
previousPostId: '',
|
|
} as ProcessedPosts;
|
|
|
|
let storePostsSpy: jest.SpyInstance;
|
|
let freshNotificationWithBooleanPost: NotificationWithData;
|
|
|
|
beforeEach(async () => {
|
|
// Reset mocks and setup basic DB state for each test
|
|
jest.clearAllMocks();
|
|
|
|
// Reconstruct the notification object with deep-copied post data for each test
|
|
const currentSamplePostData = JSON.parse(JSON.stringify(samplePostData)); // Deep copy
|
|
freshNotificationWithBooleanPost = {
|
|
payload: {
|
|
...notificationData,
|
|
post_id: postId,
|
|
data: {
|
|
...notificationExtraData,
|
|
posts: {
|
|
posts: {[postId]: currentSamplePostData.posts[0]},
|
|
order: currentSamplePostData.order,
|
|
|
|
// Ensure previousPostId is handled if needed by processPostsFetched
|
|
prev_post_id: currentSamplePostData.previousPostId,
|
|
},
|
|
},
|
|
},
|
|
|
|
// Add other required NotificationWithData properties
|
|
identifier: 'test-identifier',
|
|
foreground: false,
|
|
userInteraction: false,
|
|
} as any; // Use 'as any' to bypass strict type checking for test data
|
|
|
|
// Spy on the actual function and provide a mock implementation that matches the return type
|
|
storePostsSpy = jest.spyOn(PostActions, 'storePostsForChannel').mockImplementation(async () => ({models: []}));
|
|
|
|
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}, {id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore the original implementation after each test
|
|
storePostsSpy.mockRestore();
|
|
});
|
|
|
|
it('should convert numeric booleans to true/false on iOS', async () => {
|
|
Platform.OS = 'ios';
|
|
|
|
await backgroundNotification(serverUrl, freshNotificationWithBooleanPost);
|
|
|
|
// Check the arguments passed to the spy
|
|
expect(storePostsSpy).toHaveBeenCalledTimes(1);
|
|
const storedPosts = storePostsSpy.mock.calls[0][2] as Post[]; // 3rd argument is posts
|
|
expect(storedPosts).toHaveLength(1);
|
|
|
|
const attachments = storedPosts[0].props!.attachments as MessageAttachment[];
|
|
expect(attachments).toHaveLength(2);
|
|
|
|
// Check fields conversion
|
|
const fields = attachments[0].fields!;
|
|
expect(fields[0].short).toBe(true); // 1 -> true
|
|
expect(fields[1].short).toBe(false); // 0 -> false
|
|
expect(fields[2].short).toBe(true); // true -> true
|
|
expect(fields[3].short).toBe(false); // false -> false
|
|
expect(fields[4].short).toBe(undefined);
|
|
expect(fields[5].short).toBe(undefined);
|
|
|
|
// Check actions conversion
|
|
const actions = attachments[0].actions!;
|
|
expect(actions[0].disabled).toBe(true); // 1 -> true
|
|
expect(actions[1].disabled).toBe(false); // 0 -> false
|
|
expect(actions[2].disabled).toBe(true); // true -> true
|
|
expect(actions[3].disabled).toBe(false); // false -> false
|
|
expect(actions[4].disabled).toBe(undefined);
|
|
expect(actions[5].disabled).toBe(undefined);
|
|
});
|
|
|
|
it('should NOT convert numeric booleans on Android (it wouldnt be needed)', async () => {
|
|
Platform.OS = 'android';
|
|
|
|
await backgroundNotification(serverUrl, freshNotificationWithBooleanPost);
|
|
|
|
// Check the arguments passed to the spy
|
|
expect(storePostsSpy).toHaveBeenCalledTimes(1);
|
|
const storedPosts = storePostsSpy.mock.calls[0][2] as Post[]; // 3rd argument is posts
|
|
expect(storedPosts).toHaveLength(1);
|
|
|
|
const attachments = storedPosts[0].props!.attachments as MessageAttachment[];
|
|
expect(attachments).toHaveLength(2);
|
|
|
|
// Check fields remain unchanged
|
|
const fields = attachments[0].fields!;
|
|
expect(fields[0].short).toBe(1); // 1 -> 1
|
|
expect(fields[1].short).toBe(0); // 0 -> 0
|
|
expect(fields[2].short).toBe(true);
|
|
expect(fields[3].short).toBe(false);
|
|
expect(fields[4].short).toBe(undefined);
|
|
expect(fields[5].short).toBe(undefined);
|
|
|
|
// Check actions remain unchanged
|
|
const actions = attachments[0].actions!;
|
|
expect(actions[0].disabled).toBe(1); // 1 -> 1
|
|
expect(actions[1].disabled).toBe(0); // 0 -> 0
|
|
expect(actions[2].disabled).toBe(true);
|
|
expect(actions[3].disabled).toBe(false);
|
|
expect(actions[4].disabled).toBe(undefined);
|
|
expect(actions[5].disabled).toBe(undefined);
|
|
});
|
|
});
|