mattermost-mobile/app/actions/remote/thread.test.ts
Daniel Espino García 058d080e9a
[MM-67507] Fix threads not being refetch when switching translations (#9512)
* [MM-67507] Fix threads not being refetch when switching translations

* Fix test
2026-02-17 16:07:10 +01:00

413 lines
17 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {waitFor} from '@testing-library/react-native';
import {ActionType} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import TestHelper from '@test/test_helper';
import {
fetchThread,
updateTeamThreadsAsRead,
markThreadAsRead,
markThreadAsUnread,
updateThreadFollowing,
fetchThreads,
syncTeamThreads,
loadEarlierThreads,
fetchAndSwitchToThread,
syncThreadsIfNeeded,
batchTeamThreadSync,
} from './thread';
import type {Client} from '@client/rest';
import type ServerDataOperator from '@database/operator/server_data_operator';
const serverUrl = 'baseHandler.test.com';
let operator: ServerDataOperator;
const channelId = 'channelid1';
const teamId = 'teamid1';
const team: Team = {
id: teamId,
name: 'team1',
} as Team;
const user1 = {id: 'userid1', username: 'user1', email: 'user1@mattermost.com', roles: ''} as UserProfile;
const post1 = TestHelper.fakePost({channel_id: channelId, id: 'postid1'});
const thread1: Thread = {id: 'postid1',
reply_count: 1,
last_reply_at: 1,
last_viewed_at: 1,
participants: [user1],
post: {id: 'postid1',
channel_id: channelId,
create_at: 1,
message: 'post1',
user_id: user1.id} as Post,
unread_replies: 0,
unread_mentions: 0,
delete_at: 0,
};
const threads = [
thread1,
] as ThreadWithLastFetchedAt[];
const throwFunc = () => {
throw Error('error');
};
const partialClient: Partial<Client> = {
getThread: jest.fn((userId: string, _teamId: string, threadId: string) => Promise.resolve({...thread1, id: threadId})),
updateTeamThreadsAsRead: jest.fn(),
markThreadAsRead: jest.fn(),
markThreadAsUnread: jest.fn(),
updateThreadFollow: jest.fn(),
getThreads: jest.fn(() => Promise.resolve({threads, total: threads.length, total_unread_mentions: 0, total_unread_threads: 0})),
};
const mockClient = partialClient as Client;
let mockGetIsCRTEnabled: jest.Mock;
jest.mock('@queries/servers/thread', () => {
const original = jest.requireActual('@queries/servers/thread');
mockGetIsCRTEnabled = jest.fn(() => true);
return {
...original,
getIsCRTEnabled: mockGetIsCRTEnabled,
};
});
beforeAll(() => {
// @ts-ignore
NetworkManager.getClient = () => mockClient;
});
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
describe('get threads', () => {
it('fetchThread - handle error', async () => {
jest.mocked(mockClient.getThread).mockImplementationOnce(jest.fn(throwFunc));
const result = await fetchThread(serverUrl, teamId, thread1.id);
expect(result).toBeDefined();
expect(result.error).toBeDefined();
});
it('fetchThread - base case', async () => {
const result = await fetchThread(serverUrl, teamId, thread1.id);
expect(result).toBeDefined();
expect(result.error).toBeUndefined();
expect(result.data).toBeDefined();
});
it('fetchThreads - handle error', async () => {
const result = await fetchThreads('foo', '', {});
expect(result).toBeDefined();
expect(result.error).toBeTruthy();
});
it('fetchThreads - no current user', async () => {
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
const result = await fetchThreads(serverUrl, team.id, {});
expect(result).toBeDefined();
expect(result.error).toBeDefined();
expect(result.error).toBe('currentUser not found');
});
it('fetchThreads - base case', async () => {
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
const result = await fetchThreads(serverUrl, team.id, {});
expect(result).toBeDefined();
expect(result.error).toBeFalsy();
expect(result.threads).toBeDefined();
expect(result.threads?.[0].id).toBe(thread1.id);
});
it('syncTeamThreads - handle error', async () => {
const result = await syncTeamThreads('foo', '');
expect(result).toBeDefined();
expect(result.error).toBeTruthy();
});
it('syncTeamThreads - base case', async () => {
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
const result = await syncTeamThreads(serverUrl, team.id);
expect(result).toBeDefined();
expect(result.error).toBeFalsy();
expect(result.models).toBeDefined();
expect(result.models?.length).toBe(4); // 1 thread, 1 thread participant, 1 latest thread in team, 1 team thread sync (the unread thread is actually the latest so it the duplicate is removed)
// Sync again after first sync
const result2 = await syncTeamThreads(serverUrl, team.id);
expect(result2).toBeDefined();
expect(result2.error).toBeFalsy();
expect(result2.models).toBeDefined();
expect(result2.models?.length).toBe(1); // 1 team thread sync
});
it('syncThreadsIfNeeded - handle error', async () => {
const result = await syncThreadsIfNeeded('foo', true, []);
expect(result).toBeDefined();
expect(result.error).toBeTruthy();
});
it('syncThreadsIfNeeded - base case', async () => {
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
const result = await syncThreadsIfNeeded(serverUrl, true, [team]);
expect(result).toBeDefined();
expect(result.error).toBeFalsy();
expect(result.models).toBeDefined();
expect(result.models?.length).toBe(4); // 1 thread, 1 thread participant, 1 latest thread in team, 1 team thread sync (the unread thread is actually the latest so it the duplicate is removed)
});
it('loadEarlierThreads - handle error', async () => {
const result = await loadEarlierThreads('foo', '', '');
expect(result).toBeDefined();
expect(result.error).toBeTruthy();
});
it('loadEarlierThreads - no current user', async () => {
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
const result = await loadEarlierThreads(serverUrl, team.id, thread1.id);
expect(result).toBeDefined();
expect(result.error).toBeDefined();
expect(result.error).toBe('currentUser not found');
});
it('loadEarlierThreads - base case', async () => {
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
const result = await loadEarlierThreads(serverUrl, team.id, thread1.id);
expect(result).toBeDefined();
expect(result.error).toBeUndefined();
expect(result.threads).toBeDefined();
expect(result.threads?.[0].id).toBe(thread1.id);
expect(result.models).toBeDefined();
expect(result.models?.length).toBe(4); // 1 thread, 1 thread participant, 1 thread in team, 1 team thread sync
});
it('fetchAndSwitchToThread - handle error', async () => {
const result = await fetchAndSwitchToThread('foo', '');
expect(result).toBeDefined();
expect(result.error).toBeTruthy();
});
it('fetchAndSwitchToThread - base case', async () => {
await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
order: [post1.id],
posts: [post1],
prepareRecordsOnly: false,
});
await operator.handleThreads({threads, prepareRecordsOnly: false, teamId: team.id});
const result = await fetchAndSwitchToThread(serverUrl, thread1.id);
expect(result).toBeDefined();
expect(result.error).toBeUndefined();
});
});
describe('update threads', () => {
it('updateTeamThreadsAsRead - handle error', async () => {
jest.mocked(mockClient.updateTeamThreadsAsRead).mockImplementationOnce(jest.fn(throwFunc));
const result = await updateTeamThreadsAsRead(serverUrl, teamId);
expect(result).toBeDefined();
expect(result.error).toBeDefined();
});
it('updateTeamThreadsAsRead - base case', async () => {
const result = await updateTeamThreadsAsRead(serverUrl, teamId);
expect(result).toBeDefined();
expect(result.error).toBeUndefined();
});
it('markThreadAsRead - handle error', async () => {
const result = await markThreadAsRead('foo', '', '');
expect(result).toBeDefined();
expect(result.error).toBeDefined();
});
it('markThreadAsRead - base case', async () => {
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
order: [post1.id],
posts: [post1],
prepareRecordsOnly: false,
});
const result = await markThreadAsRead(serverUrl, undefined, thread1.id);
expect(result).toBeDefined();
expect(result.error).toBeUndefined();
});
it('markThreadAsUnread - handle error', async () => {
const result = await markThreadAsUnread('foo', '', '', '');
expect(result).toBeDefined();
expect(result.error).toBeDefined();
});
it('markThreadAsUnread - base case', async () => {
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
order: [post1.id],
posts: [post1],
prepareRecordsOnly: false,
});
const result = await markThreadAsUnread(serverUrl, '', thread1.id, post1.id);
expect(result).toBeDefined();
expect(result.error).toBeUndefined();
});
it('updateThreadFollowing - handle error', async () => {
const result = await updateThreadFollowing('foo', '', '', false, false);
expect(result).toBeDefined();
expect(result.error).toBeDefined();
});
it('updateThreadFollowing - base case', async () => {
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
order: [post1.id],
posts: [post1],
prepareRecordsOnly: false,
});
await operator.handleThreads({threads, prepareRecordsOnly: false, teamId: team.id});
const result = await updateThreadFollowing(serverUrl, '', thread1.id, true, true);
expect(result).toBeDefined();
expect(result.error).toBeUndefined();
});
});
describe('batchTeamThreadSync', () => {
// We don't use fake timers since they break the
// database operations.
beforeEach(() => {
jest.mocked(mockClient.getThreads).mockClear();
});
const teamFilter = (id: string) => {
return (call: unknown[]) => call[1] === id && call[6] === undefined;
};
// fetchThreads calls twice the function, one for the unreads and one for
// the latests. We expect that part to work correctly and if one is called
// for unreads, the one for the latest is also called.
const allCallsFilter = (call: unknown[]) => call[6] === undefined;
it('returns early when CRT is disabled', async () => {
mockGetIsCRTEnabled.mockResolvedValueOnce(false);
await batchTeamThreadSync(serverUrl, teamId);
await waitFor(() => expect(mockClient.getThreads).not.toHaveBeenCalled(), {timeout: 600});
});
it('schedules sync after timeout when CRT is enabled', async () => {
mockGetIsCRTEnabled.mockResolvedValue(true);
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
await batchTeamThreadSync(serverUrl, teamId);
expect(mockClient.getThreads).not.toHaveBeenCalled();
await waitFor(() => expect(mockClient.getThreads).toHaveBeenCalled());
const getThreadsCalls = jest.mocked(mockClient.getThreads).mock.calls;
expect(getThreadsCalls.filter(teamFilter(teamId))).toHaveLength(1);
expect(getThreadsCalls.filter(allCallsFilter)).toHaveLength(1);
});
it('batches multiple teamIds and syncs each after timeout', async () => {
mockGetIsCRTEnabled.mockResolvedValue(true);
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
await batchTeamThreadSync(serverUrl, teamId);
await batchTeamThreadSync(serverUrl, 'teamid2');
expect(mockClient.getThreads).not.toHaveBeenCalled();
await waitFor(() => expect(mockClient.getThreads).toHaveBeenCalled());
const getThreadsCalls = jest.mocked(mockClient.getThreads).mock.calls;
expect(getThreadsCalls.filter(teamFilter(teamId))).toHaveLength(1);
expect(getThreadsCalls.filter(teamFilter('teamid2'))).toHaveLength(1);
expect(getThreadsCalls.filter(allCallsFilter)).toHaveLength(2);
});
it('calling several times for the same team id does not create multiple calls', async () => {
mockGetIsCRTEnabled.mockResolvedValue(true);
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
await batchTeamThreadSync(serverUrl, teamId);
await batchTeamThreadSync(serverUrl, teamId);
await waitFor(() => expect(mockClient.getThreads).toHaveBeenCalled());
const getThreadsCalls = jest.mocked(mockClient.getThreads).mock.calls;
expect(getThreadsCalls.filter(teamFilter(teamId))).toHaveLength(1);
expect(getThreadsCalls.filter(allCallsFilter)).toHaveLength(1);
});
it('includes direct channels when empty teamId is in batch only once', async () => {
mockGetIsCRTEnabled.mockResolvedValue(true);
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
await batchTeamThreadSync(serverUrl, '');
await batchTeamThreadSync(serverUrl, teamId);
await batchTeamThreadSync(serverUrl, 'teamid2');
await batchTeamThreadSync(serverUrl, 'teamid3');
await waitFor(() => expect(mockClient.getThreads).toHaveBeenCalled());
expect(mockClient.getThreads).toHaveBeenCalled();
const getThreadsCalls = jest.mocked(mockClient.getThreads).mock.calls;
expect(getThreadsCalls.filter(teamFilter(teamId)).length).toBe(1);
expect(getThreadsCalls.filter(teamFilter('teamid2')).length).toBe(1);
expect(getThreadsCalls.filter(teamFilter('teamid3')).length).toBe(1);
expect(getThreadsCalls.filter(allCallsFilter)).toHaveLength(3);
expect(getThreadsCalls.filter((call) => call[10] === false && call[6] === undefined).length).toBe(1);
});
it('logs error when syncTeamThreads returns error', async () => {
mockGetIsCRTEnabled.mockResolvedValue(true);
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
const logErrorSpy = jest.spyOn(require('@utils/log'), 'logError').mockImplementation(() => {});
jest.mocked(mockClient.getThreads).mockImplementationOnce(() => {
throw new Error('sync failed');
});
await batchTeamThreadSync(serverUrl, teamId);
await waitFor(() => expect(logErrorSpy).toHaveBeenCalled());
expect(logErrorSpy).toHaveBeenCalledWith('batchTeamThreadSync: Error', expect.any(Error));
logErrorSpy.mockRestore();
});
});