From 85e01a7fb669741daae3d0e403e82c2204b57529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Fri, 28 Mar 2025 09:40:08 +0100 Subject: [PATCH] Add model mocks to TestHelper (#8715) * Add model mocks to TestHelper * Address feedback --- app/actions/local/post.test.ts | 17 +- app/actions/local/user.test.ts | 26 +- app/actions/local/user.ts | 17 +- app/actions/remote/user.ts | 8 +- app/actions/websocket/index.test.ts | 24 +- app/actions/websocket/posts.test.ts | 70 ++- app/actions/websocket/roles.test.ts | 23 +- app/actions/websocket/teams.test.ts | 27 +- app/actions/websocket/users.test.ts | 43 +- .../combined_user_activity.tsx | 6 +- app/constants/general.ts | 12 +- app/constants/post.ts | 4 +- .../app_data_operator/transformers/index.ts | 10 +- .../operator/base_data_operator/index.ts | 28 +- .../server_data_operator/handlers/channel.ts | 16 +- .../server_data_operator/handlers/index.ts | 6 +- .../server_data_operator/handlers/post.ts | 10 +- .../handlers/team_threads_sync.ts | 6 +- .../transformers/category.ts | 28 +- .../transformers/channel.test.ts | 1 - .../transformers/channel.ts | 78 +-- .../transformers/general.test.ts | 2 +- .../transformers/general.ts | 51 +- .../transformers/group.ts | 31 +- .../transformers/index.ts | 8 +- .../transformers/post.test.ts | 1 - .../server_data_operator/transformers/post.ts | 45 +- .../transformers/reaction.test.ts | 2 - .../transformers/reaction.ts | 13 +- .../server_data_operator/transformers/team.ts | 65 ++- .../transformers/thread.ts | 58 +- .../server_data_operator/transformers/user.ts | 20 +- app/database/operator/utils/general.ts | 17 +- app/database/operator/utils/reaction.ts | 2 +- app/database/operator/utils/thread.ts | 2 +- app/hooks/markdown.test.ts | 19 +- app/products/calls/state/actions.test.ts | 161 +++--- app/products/calls/utils.test.ts | 52 +- app/queries/servers/channel.test.ts | 434 +++++++-------- app/queries/servers/role.test.ts | 73 ++- .../edit_profile/components/form.test.tsx | 13 +- app/screens/home/tab_bar/account.test.tsx | 11 +- .../mention_settings.test.tsx | 98 ++-- app/screens/user_profile/user_info.test.tsx | 13 +- app/utils/categories.test.ts | 122 +++-- app/utils/post/index.test.ts | 158 +++--- app/utils/post/index.ts | 4 +- app/utils/post_list/index.test.ts | 8 +- app/utils/post_list/index.ts | 44 +- app/utils/types.ts | 4 + app/utils/url/test.ts | 2 +- app/utils/user/index.test.ts | 114 ++-- test/test_helper.ts | 501 ++++++++++++++++-- types/api/posts.d.ts | 13 +- types/database/database.ts | 46 +- types/database/models/servers/post.ts | 2 +- types/database/raw_values.d.ts | 1 + 57 files changed, 1611 insertions(+), 1059 deletions(-) diff --git a/app/actions/local/post.test.ts b/app/actions/local/post.test.ts index 5dde871d9..0b9be27d2 100644 --- a/app/actions/local/post.test.ts +++ b/app/actions/local/post.test.ts @@ -21,7 +21,6 @@ import { } from './post'; import type ServerDataOperator from '@database/operator/server_data_operator'; -import type UserModel from '@typings/database/models/servers/user'; const serverUrl = 'baseHandler.test.com'; let operator: ServerDataOperator; @@ -47,11 +46,11 @@ jest.mock('@queries/servers/thread', () => { }); const channelId = 'channelid1'; -const user: UserProfile = { +const user: UserProfile = TestHelper.fakeUser({ id: 'userid', username: 'username', roles: '', -} as UserProfile; +}); beforeEach(async () => { await DatabaseManager.init([serverUrl]); @@ -64,7 +63,7 @@ afterEach(async () => { describe('sendAddToChannelEphemeralPost', () => { it('handle not found database', async () => { - const {posts, error} = await sendAddToChannelEphemeralPost('foo', {} as UserModel, ['username2'], ['added username2'], channelId, ''); + const {posts, error} = await sendAddToChannelEphemeralPost('foo', TestHelper.fakeUserModel(), ['username2'], ['added username2'], channelId, ''); expect(posts).toBeUndefined(); expect(error).toBeTruthy(); }); @@ -139,7 +138,7 @@ describe('removePost', () => { }); it('base case - system message', async () => { - const systemPost = TestHelper.fakePost({channel_id: channelId, id: `${COMBINED_USER_ACTIVITY}id1_id2`, type: Post.POST_TYPES.COMBINED_USER_ACTIVITY as PostType, props: {system_post_ids: ['id1']}}); + const systemPost = TestHelper.fakePost({channel_id: channelId, id: `${COMBINED_USER_ACTIVITY}id1_id2`, type: Post.POST_TYPES.COMBINED_USER_ACTIVITY, props: {system_post_ids: ['id1']}}); await operator.handlePosts({ actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL, @@ -187,16 +186,16 @@ describe('markPostAsDeleted', () => { describe('storePostsForChannel', () => { const post = TestHelper.fakePost({channel_id: channelId, user_id: user.id}); const teamId = 'tId1'; - const channel: Channel = { + const channel: Channel = TestHelper.fakeChannel({ id: channelId, team_id: teamId, total_msg_count: 0, - } as Channel; - const channelMember: ChannelMembership = { + }); + const channelMember: ChannelMembership = TestHelper.fakeChannelMember({ id: 'id', channel_id: channelId, msg_count: 0, - } as ChannelMembership; + }); it('handle not found database', async () => { const {models, error} = await storePostsForChannel('foo', channelId, [post], [post.id], '', ActionType.POSTS.RECEIVED_IN_CHANNEL, [user], false); diff --git a/app/actions/local/user.test.ts b/app/actions/local/user.test.ts index 1fab4f8c6..1b73ac19d 100644 --- a/app/actions/local/user.test.ts +++ b/app/actions/local/user.test.ts @@ -3,6 +3,7 @@ import {SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; +import TestHelper from '@test/test_helper'; import { setCurrentUserStatus, @@ -13,8 +14,6 @@ import { } from './user'; import type ServerDataOperator from '@database/operator/server_data_operator'; -import type SystemModel from '@typings/database/models/servers/system'; -import type UserModel from '@typings/database/models/servers/user'; const serverUrl = 'baseHandler.test.com'; let operator: ServerDataOperator; @@ -27,11 +26,11 @@ jest.mock('@init/credentials', () => { }; }); -const user: UserProfile = { +const user: UserProfile = TestHelper.fakeUser({ id: 'userid', username: 'username', roles: '', -} as UserProfile; +}); beforeEach(async () => { await DatabaseManager.init([serverUrl]); @@ -44,12 +43,12 @@ afterEach(async () => { describe('setCurrentUserStatus', () => { it('handle not found database', async () => { - const result = await setCurrentUserStatus('foo', '') as {error: unknown}; + const result = await setCurrentUserStatus('foo', ''); expect(result?.error).toBeDefined(); }); it('handle no user', async () => { - const result = await setCurrentUserStatus(serverUrl, '') as {error: unknown}; + const result = await setCurrentUserStatus(serverUrl, ''); expect(result?.error).toBeDefined(); expect((result?.error as Error).message).toBe(`No current user for ${serverUrl}`); }); @@ -58,20 +57,20 @@ describe('setCurrentUserStatus', () => { await operator.handleUsers({users: [user], prepareRecordsOnly: false}); await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user.id}], prepareRecordsOnly: false}); const result = await setCurrentUserStatus(serverUrl, 'away'); - expect(result).toBeNull(); + expect(result).not.toHaveProperty('error'); }); }); describe('updateLocalCustomStatus', () => { it('handle not found database', async () => { - const result = await updateLocalCustomStatus('foo', {} as UserModel) as {error: unknown}; + const result = await updateLocalCustomStatus('foo', TestHelper.fakeUserModel()); expect(result?.error).toBeDefined(); }); it('base case', async () => { const userModels = await operator.handleUsers({users: [user], prepareRecordsOnly: false}); await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user.id}], prepareRecordsOnly: false}); - const result = await updateLocalCustomStatus(serverUrl, userModels[0], {text: 'customstatus'}) as {}; + const result = await updateLocalCustomStatus(serverUrl, userModels[0], {text: 'customstatus'}); expect(result).toBeDefined(); expect(result).not.toHaveProperty('error'); }); @@ -79,14 +78,13 @@ describe('updateLocalCustomStatus', () => { describe('updateRecentCustomStatuses', () => { it('handle not found database', async () => { - const result = await updateRecentCustomStatuses('foo', {text: 'customstatus'}) as {error: unknown}; - expect(result?.error).toBeDefined(); + const result = await updateRecentCustomStatuses('foo', {text: 'customstatus'}); + expect(result.error).toBeDefined(); }); it('base case', async () => { - const result = await updateRecentCustomStatuses(serverUrl, {text: 'customstatus'}) as SystemModel[]; - expect(result).toBeDefined(); - expect(result.length).toBe(1); // system + const result = await updateRecentCustomStatuses(serverUrl, {text: 'customstatus'}); + expect(result.models?.length).toBe(1); // system }); }); diff --git a/app/actions/local/user.ts b/app/actions/local/user.ts index 016086039..1ee5b08fc 100644 --- a/app/actions/local/user.ts +++ b/app/actions/local/user.ts @@ -12,7 +12,7 @@ import {addRecentReaction} from './reactions'; import type Model from '@nozbe/watermelondb/Model'; import type UserModel from '@typings/database/models/servers/user'; -export async function setCurrentUserStatus(serverUrl: string, status: string) { +export async function setCurrentUserStatus(serverUrl: string, status: string): Promise<{error?: unknown}> { try { const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const user = await getCurrentUser(database); @@ -25,14 +25,14 @@ export async function setCurrentUserStatus(serverUrl: string, status: string) { await operator.batchRecords([user], 'setCurrentUserStatus'); } - return null; + return {}; } catch (error) { logError('Failed setCurrentUserStatus', error); return {error}; } } -export async function updateLocalCustomStatus(serverUrl: string, user: UserModel, customStatus?: UserCustomStatus) { +export async function updateLocalCustomStatus(serverUrl: string, user: UserModel, customStatus?: UserCustomStatus): Promise<{error?: unknown}> { try { const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const models: Model[] = []; @@ -43,9 +43,9 @@ export async function updateLocalCustomStatus(serverUrl: string, user: UserModel models.push(userModel); if (customStatus) { - const recent = await updateRecentCustomStatuses(serverUrl, customStatus, true); - if (Array.isArray(recent)) { - models.push(...recent); + const {models: customStatusModels} = await updateRecentCustomStatuses(serverUrl, customStatus, true); + if (customStatusModels?.length) { + models.push(...customStatusModels); } if (customStatus.emoji) { @@ -65,7 +65,7 @@ export async function updateLocalCustomStatus(serverUrl: string, user: UserModel } } -export const updateRecentCustomStatuses = async (serverUrl: string, customStatus: UserCustomStatus, prepareRecordsOnly = false, remove = false) => { +export const updateRecentCustomStatuses = async (serverUrl: string, customStatus: UserCustomStatus, prepareRecordsOnly = false, remove = false): Promise<{models?: Model[]; error?: unknown}> => { try { const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const recentStatuses = await getRecentCustomStatuses(database); @@ -83,13 +83,14 @@ export const updateRecentCustomStatuses = async (serverUrl: string, customStatus recentStatuses.unshift(customStatus); } - return operator.handleSystem({ + const models = await operator.handleSystem({ systems: [{ id: SYSTEM_IDENTIFIERS.RECENT_CUSTOM_STATUS, value: JSON.stringify(recentStatuses), }], prepareRecordsOnly, }); + return {models}; } catch (error) { logError('Failed updateRecentCustomStatuses', error); return {error}; diff --git a/app/actions/remote/user.ts b/app/actions/remote/user.ts index efb0a1d86..6238b5472 100644 --- a/app/actions/remote/user.ts +++ b/app/actions/remote/user.ts @@ -880,7 +880,7 @@ export const getAllSupportedTimezones = async (serverUrl: string) => { } }; -export const fetchCustomAttributes = async (serverUrl: string, userId: string, filterEmpty = false): Promise<{attributes: CustomAttributeSet; error: unknown}> => { +export const fetchCustomAttributes = async (serverUrl: string, userId: string, filterEmpty = false): Promise<{attributes: CustomAttributeSet; error?: unknown}> => { try { const client = NetworkManager.getClient(serverUrl); const [fields, attrValues] = await Promise.all([ @@ -901,13 +901,13 @@ export const fetchCustomAttributes = async (serverUrl: string, userId: string, f }; } }); - return {attributes, error: undefined}; + return {attributes}; } - return {attributes: {} as Record, error: undefined}; + return {attributes: {}}; } catch (error) { logDebug('error on fetchCustomAttributes', getFullErrorMessage(error)); forceLogoutIfNecessary(serverUrl, error); - return {attributes: {} as Record, error}; + return {attributes: {}, error}; } }; diff --git a/app/actions/websocket/index.test.ts b/app/actions/websocket/index.test.ts index 25ad32de0..20d088eee 100644 --- a/app/actions/websocket/index.test.ts +++ b/app/actions/websocket/index.test.ts @@ -20,12 +20,10 @@ import {getIsCRTEnabled} from '@queries/servers/thread'; import {getCurrentUser} from '@queries/servers/user'; import EphemeralStore from '@store/ephemeral_store'; import NavigationStore from '@store/navigation_store'; +import TestHelper from '@test/test_helper'; import {handleFirstConnect, handleReconnect} from './index'; -import type PostModel from '@typings/database/models/servers/post'; -import type UserModel from '@typings/database/models/servers/user'; - jest.mock('@actions/local/channel'); jest.mock('@actions/local/systems'); jest.mock('@actions/remote/channel'); @@ -87,10 +85,10 @@ describe('WebSocket Index Actions', () => { }; jest.mocked(entry).mockResolvedValue(mockEntryData); - jest.mocked(getCurrentUser).mockResolvedValue({ + jest.mocked(getCurrentUser).mockResolvedValue(TestHelper.fakeUserModel({ id: currentUserId, locale: 'en', - } as UserModel); + })); jest.mocked(getConfig).mockResolvedValue({Version: '9.0.0'} as ClientConfig); jest.mocked(isSupportedServerCalls).mockReturnValue(true); @@ -107,10 +105,10 @@ describe('WebSocket Index Actions', () => { it('should handle error when server database not found', async () => { DatabaseManager.serverDatabases = {}; - const error = await handleFirstConnect(serverUrl) as any; + const error = await handleFirstConnect(serverUrl); expect(error).toBeInstanceOf(Error); - expect(error.message).toBe('cannot find server database'); + expect((error as Error).message).toBe('cannot find server database'); }); }); @@ -137,10 +135,10 @@ describe('WebSocket Index Actions', () => { }; jest.mocked(entry).mockResolvedValue(mockEntryData); - jest.mocked(getCurrentUser).mockResolvedValue({ + jest.mocked(getCurrentUser).mockResolvedValue(TestHelper.fakeUserModel({ id: currentUserId, locale: 'en', - } as UserModel); + })); jest.mocked(getConfig).mockResolvedValue({Version: '9.0.0'} as ClientConfig); jest.mocked(isSupportedServerCalls).mockReturnValue(true); jest.mocked(getActiveServerUrl).mockResolvedValue(serverUrl); @@ -169,7 +167,7 @@ describe('WebSocket Index Actions', () => { it('should fetch thread posts when CRT enabled', async () => { const threadId = 'thread-id'; - const lastPost = {id: 'post-id', createAt: 123} as PostModel; + const lastPost = TestHelper.fakePostModel({id: 'post-id', createAt: 123}); jest.mocked(NavigationStore.getScreensInStack).mockReturnValue(['Thread']); @@ -193,7 +191,7 @@ describe('WebSocket Index Actions', () => { }); it('should handle notification tapped state', async () => { - (NavigationStore.getScreensInStack as jest.Mock).mockReturnValue(['Channel']); + jest.mocked(NavigationStore.getScreensInStack).mockReturnValue(['Channel']); jest.mocked(EphemeralStore.wasNotificationTapped).mockReturnValue(true); @@ -206,10 +204,10 @@ describe('WebSocket Index Actions', () => { it('should handle error in entry data', async () => { jest.mocked(entry).mockResolvedValue({error: new Error('entry error')}); - const result = await handleReconnect(serverUrl) as any; + const result = await handleReconnect(serverUrl); expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('entry error'); + expect((result as Error).message).toBe('entry error'); }); }); }); diff --git a/app/actions/websocket/posts.test.ts b/app/actions/websocket/posts.test.ts index 25770d13a..48e0bb346 100644 --- a/app/actions/websocket/posts.test.ts +++ b/app/actions/websocket/posts.test.ts @@ -6,7 +6,7 @@ import {DeviceEventEmitter} from 'react-native'; import {markChannelAsViewed, markChannelAsUnread, storeMyChannelsForTeam, updateLastPostAt} from '@actions/local/channel'; import {addPostAcknowledgement, markPostAsDeleted, removePostAcknowledgement} from '@actions/local/post'; import {updateThread} from '@actions/local/thread'; -import {fetchChannelStats, fetchMyChannel, type MyChannelsRequest} from '@actions/remote/channel'; +import {fetchChannelStats, fetchMyChannel} from '@actions/remote/channel'; import {fetchPostAuthors} from '@actions/remote/post'; import {fetchThread} from '@actions/remote/thread'; import {fetchMissingProfilesByIds} from '@actions/remote/user'; @@ -19,17 +19,13 @@ import {getCurrentUserId, getCurrentChannelId} from '@queries/servers/system'; import {getIsCRTEnabled} from '@queries/servers/thread'; import EphemeralStore from '@store/ephemeral_store'; import NavigationStore from '@store/navigation_store'; -import MyChannelModel from '@typings/database/models/servers/my_channel'; +import TestHelper from '@test/test_helper'; import {isTablet} from '@utils/helpers'; import {shouldIgnorePost} from '@utils/post'; import {handleNewPostEvent, handlePostEdited, handlePostDeleted, handlePostUnread, handlePostAcknowledgementAdded, handlePostAcknowledgementRemoved} from './posts'; import type ServerDataOperator from '@database/operator/server_data_operator'; -import type ChannelModel from '@typings/database/models/servers/channel'; -import type PostModel from '@typings/database/models/servers/post'; -import type ThreadModel from '@typings/database/models/servers/thread'; -import type UserModel from '@typings/database/models/servers/user'; jest.mock('@queries/servers/post'); jest.mock('@queries/servers/channel'); @@ -53,9 +49,9 @@ const serverUrl = 'baseHandler.test.com'; describe('WebSocket Post Actions', () => { let operator: ServerDataOperator; - const post = {id: 'post1', channel_id: 'channel1', user_id: 'user1', create_at: 12345, message: 'hello'} as Post; - const postModels = [{channelId: post.channel_id, userId: post.user_id, message: post.message} as PostModel]; - const myChannelModel = {id: 'channel1', manuallyUnread: false, messageCount: 4, mentionsCount: 0, lastViewedAt: 1} as MyChannelModel; + const post = TestHelper.fakePost({id: 'post1', channel_id: 'channel1', user_id: 'user1', create_at: 12345, message: 'hello'}); + const postModels = [TestHelper.fakePostModel({channelId: post.channel_id, userId: post.user_id, message: post.message, isPinned: true})]; + const myChannelModel = TestHelper.fakeMyChannelModel({id: 'channel1', manuallyUnread: false, messageCount: 4, mentionsCount: 0, lastViewedAt: 1}); const mockedGetPostById = jest.mocked(getPostById); const mockedUpdateLastPostAt = jest.mocked(updateLastPostAt); @@ -105,13 +101,13 @@ describe('WebSocket Post Actions', () => { mockedIsTablet.mockReturnValue(false); it('should handle new post event - channel membership present', async () => { - const userModel = {id: 'user1', username: 'username1'} as UserModel; + const userModel = TestHelper.fakeUserModel({id: 'user1', username: 'username1'}); const emitSpy = jest.spyOn(DeviceEventEmitter, 'emit'); const batchRecordsSpy = jest.spyOn(operator, 'batchRecords').mockImplementation(jest.fn()); jest.spyOn(operator, 'handlePosts').mockResolvedValue(postModels); jest.spyOn(operator, 'handleUsers').mockResolvedValue([userModel]); - mockedFetchPostAuthors.mockResolvedValueOnce({authors: [{id: 'user1', username: 'username1'} as UserProfile]}); + mockedFetchPostAuthors.mockResolvedValueOnce({authors: [TestHelper.fakeUser({id: 'user1', username: 'username1'})]}); mockedGetMyChannel.mockResolvedValue(myChannelModel); mockedGetIsCRTEnabled.mockResolvedValue(false); mockedShouldIgnorePost.mockReturnValue(false); @@ -120,12 +116,22 @@ describe('WebSocket Post Actions', () => { expect(emitSpy).toHaveBeenCalledWith(Events.USER_STOP_TYPING, { channelId: 'channel1', - rootId: undefined, + rootId: '', userId: 'user1', now: expect.any(Number), }); - expect(batchRecordsSpy).toHaveBeenCalledWith([userModel, {_preparedState: null, id: 'channel1', lastViewedAt: 1, manuallyUnread: false, mentionsCount: 0, messageCount: 4}, postModels[0]], 'handleNewPostEvent'); + expect(batchRecordsSpy).toHaveBeenCalledWith([ + userModel, + expect.objectContaining({ + id: 'channel1', + lastViewedAt: 1, + manuallyUnread: false, + mentionsCount: 0, + messageCount: 4, + }), + postModels[0], + ], 'handleNewPostEvent'); }); it('should handle new post event - without channel membership present', async () => { @@ -134,7 +140,7 @@ describe('WebSocket Post Actions', () => { jest.spyOn(operator, 'handlePosts').mockResolvedValue(postModels); mockedGetMyChannel.mockResolvedValueOnce(undefined); - mockedFetchMyChannel.mockResolvedValue({teamId: 'team1', memberships: [{user_id: 'user1', channel_id: 'channel1'}]} as MyChannelsRequest); + mockedFetchMyChannel.mockResolvedValue({teamId: 'team1', memberships: [TestHelper.fakeChannelMember({user_id: 'user1', channel_id: 'channel1'})]}); mockedStoreMyChannelsForTeam.mockResolvedValue({models: [myChannelModel], error: undefined}); mockedGetMyChannel.mockResolvedValueOnce(myChannelModel); mockedGetIsCRTEnabled.mockResolvedValue(false); @@ -144,7 +150,7 @@ describe('WebSocket Post Actions', () => { expect(emitSpy).toHaveBeenCalledWith(Events.USER_STOP_TYPING, { channelId: 'channel1', - rootId: undefined, + rootId: '', userId: 'user1', now: expect.any(Number), }); @@ -157,7 +163,7 @@ describe('WebSocket Post Actions', () => { const batchRecordsSpy = jest.spyOn(operator, 'batchRecords').mockImplementation(jest.fn()); jest.spyOn(operator, 'handlePosts').mockResolvedValue(postModels); - mockedGetMyChannel.mockResolvedValue({...myChannelModel, manuallyUnread: true} as MyChannelModel); + mockedGetMyChannel.mockResolvedValue(TestHelper.fakeMyChannelModel({...myChannelModel, manuallyUnread: true})); mockedGetIsCRTEnabled.mockResolvedValue(true); mockedShouldIgnorePost.mockReturnValue(false); mockedMarkChannelAsUnread.mockResolvedValue({member: myChannelModel}); @@ -166,16 +172,22 @@ describe('WebSocket Post Actions', () => { expect(emitSpy).toHaveBeenCalledWith(Events.USER_STOP_TYPING, { channelId: 'channel1', - rootId: undefined, + rootId: '', userId: 'user1', now: expect.any(Number), }); - expect(batchRecordsSpy).toHaveBeenCalledWith([{_preparedState: null, id: 'channel1', lastViewedAt: 1, manuallyUnread: false, mentionsCount: 0, messageCount: 4}, postModels[0]], 'handleNewPostEvent'); + expect(batchRecordsSpy).toHaveBeenCalledWith([expect.objectContaining({ + id: 'channel1', + lastViewedAt: 1, + manuallyUnread: false, + mentionsCount: 0, + messageCount: 4, + }), postModels[0]], 'handleNewPostEvent'); }); it('should handle new post event - out of order ws, CRT on, root id', async () => { - const newPost = {...post, root_id: 'post2'} as Post; + const newPost = TestHelper.fakePost({...post, root_id: 'post2'}); jest.spyOn(EphemeralStore, 'getLastPostWebsocketEvent').mockReturnValueOnce({deleted: true, post: newPost}); const emitSpy = jest.spyOn(DeviceEventEmitter, 'emit'); const batchRecordsSpy = jest.spyOn(operator, 'batchRecords'); @@ -213,12 +225,18 @@ describe('WebSocket Post Actions', () => { expect(mockedGetScreensInStack).toHaveBeenCalled(); expect(emitSpy).toHaveBeenCalledWith(Events.USER_STOP_TYPING, { channelId: 'channel1', - rootId: undefined, + rootId: '', userId: 'user1', now: expect.any(Number), }); - expect(batchRecordsSpy).toHaveBeenCalledWith([{_preparedState: null, id: 'channel1', lastViewedAt: 1, manuallyUnread: false, mentionsCount: 0, messageCount: 4}, postModels[0]], 'handleNewPostEvent'); + expect(batchRecordsSpy).toHaveBeenCalledWith([expect.objectContaining({ + id: 'channel1', + lastViewedAt: 1, + manuallyUnread: false, + mentionsCount: 0, + messageCount: 4, + }), postModels[0]], 'handleNewPostEvent'); }); it('should handle new post event - no operator', async () => { @@ -302,7 +320,7 @@ describe('WebSocket Post Actions', () => { }, } as WebSocketMessage; - const threadModel = {viewedAt: 1, id: 'thread1'} as ThreadModel; + const threadModel = TestHelper.fakeThreadModel({viewedAt: 1, id: 'thread1'}); it('should handle post deleted event', async () => { const batchRecordsSpy = jest.spyOn(operator, 'batchRecords').mockImplementation(jest.fn()); @@ -311,7 +329,7 @@ describe('WebSocket Post Actions', () => { mockedGetPostById.mockResolvedValue(postModels[0]); mockedMarkPostAsDeleted.mockResolvedValue({model: postModels[0]}); mockedUpdateThread.mockResolvedValue({model: threadModel}); - mockedGetChannelById.mockResolvedValue({id: 'channel1', teamId: 'team1'} as ChannelModel); + mockedGetChannelById.mockResolvedValue(TestHelper.fakeChannelModel({id: 'channel1', teamId: 'team1'})); await handlePostDeleted(serverUrl, msg); @@ -372,7 +390,7 @@ describe('WebSocket Post Actions', () => { it('should handle post unread event', async () => { mockedGetMyChannel.mockResolvedValue(myChannelModel); mockedGetIsCRTEnabled.mockResolvedValue(false); - mockedFetchMyChannel.mockResolvedValue({teamId: 'team1', memberships: [{user_id: 'user1', channel_id: 'channel1'}]} as MyChannelsRequest); + mockedFetchMyChannel.mockResolvedValue({teamId: 'team1', memberships: [TestHelper.fakeChannelMember({user_id: 'user1', channel_id: 'channel1'})]}); mockedMarkChannelAsUnread.mockResolvedValue({member: myChannelModel}); await handlePostUnread(serverUrl, msg); @@ -381,9 +399,9 @@ describe('WebSocket Post Actions', () => { }); it('should handle post unread event - CRT enabled, manually marked read', async () => { - mockedGetMyChannel.mockResolvedValue({...myChannelModel, manuallyUnread: true} as MyChannelModel); + mockedGetMyChannel.mockResolvedValue(TestHelper.fakeMyChannelModel({...myChannelModel, manuallyUnread: true})); mockedGetIsCRTEnabled.mockResolvedValue(true); - mockedFetchMyChannel.mockResolvedValue({teamId: 'team1', memberships: [{user_id: 'user1', channel_id: 'channel1'}]} as MyChannelsRequest); + mockedFetchMyChannel.mockResolvedValue({teamId: 'team1', memberships: [TestHelper.fakeChannelMember({user_id: 'user1', channel_id: 'channel1'})]}); mockedMarkChannelAsUnread.mockResolvedValue({member: myChannelModel}); await handlePostUnread(serverUrl, msg); diff --git a/app/actions/websocket/roles.test.ts b/app/actions/websocket/roles.test.ts index 1bc45248a..add8b430c 100644 --- a/app/actions/websocket/roles.test.ts +++ b/app/actions/websocket/roles.test.ts @@ -6,12 +6,11 @@ import DatabaseManager from '@database/manager'; import {getRoleById} from '@queries/servers/role'; import {getCurrentUserId} from '@queries/servers/system'; import {getCurrentUser} from '@queries/servers/user'; +import TestHelper from '@test/test_helper'; import {handleRoleUpdatedEvent, handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent} from './roles'; import type ServerDataOperator from '@database/operator/server_data_operator'; -import type RoleModel from '@typings/database/models/servers/role'; -import type UserModel from '@typings/database/models/servers/user'; jest.mock('@actions/remote/role'); jest.mock('@database/manager'); @@ -38,7 +37,7 @@ describe('WebSocket Roles Actions', () => { operator = DatabaseManager.serverDatabases[serverUrl]!.operator; batchRecords = jest.spyOn(operator, 'batchRecords').mockResolvedValue(); - handleRole = jest.spyOn(operator, 'handleRole').mockResolvedValue([{id: 'role1'} as RoleModel]); + handleRole = jest.spyOn(operator, 'handleRole').mockResolvedValue([TestHelper.fakeRoleModel({id: 'role1'})]); handleMyTeam = jest.spyOn(operator, 'handleMyTeam').mockResolvedValue([]); handleTeamMemberships = jest.spyOn(operator, 'handleTeamMemberships').mockResolvedValue([]); }); @@ -77,7 +76,7 @@ describe('WebSocket Roles Actions', () => { name: 'test_role', permissions: ['permission1'], }; - jest.mocked(getRoleById).mockResolvedValue({id: roleId} as RoleModel); + jest.mocked(getRoleById).mockResolvedValue(TestHelper.fakeRoleModel({id: roleId})); const msg = { data: { role: JSON.stringify(mockRole), @@ -93,7 +92,7 @@ describe('WebSocket Roles Actions', () => { }); it('should handle invalid JSON in role data', async () => { - jest.mocked(getRoleById).mockResolvedValue({id: roleId} as RoleModel); + jest.mocked(getRoleById).mockResolvedValue(TestHelper.fakeRoleModel({id: roleId})); const msg = { data: { role: 'invalid json', @@ -132,11 +131,11 @@ describe('WebSocket Roles Actions', () => { it('should update roles and user', async () => { jest.mocked(getCurrentUserId).mockResolvedValue(currentUserId); - jest.mocked(getCurrentUser).mockResolvedValue({ + jest.mocked(getCurrentUser).mockResolvedValue(TestHelper.fakeUserModel({ prepareUpdate: jest.fn(), - } as unknown as UserModel); + })); jest.mocked(fetchRolesIfNeeded).mockResolvedValue({ - roles: [{id: 'role1'} as Role], + roles: [TestHelper.fakeRole({id: 'role1'})], }); const msg = { @@ -156,7 +155,7 @@ describe('WebSocket Roles Actions', () => { jest.mocked(getCurrentUserId).mockResolvedValue(currentUserId); jest.mocked(getCurrentUser).mockResolvedValue(undefined); jest.mocked(fetchRolesIfNeeded).mockResolvedValue({ - roles: [{id: 'role1'} as Role], + roles: [TestHelper.fakeRole({id: 'role1'})], }); const msg = { @@ -174,9 +173,9 @@ describe('WebSocket Roles Actions', () => { it('should handle no new roles from fetchRolesIfNeeded', async () => { jest.mocked(getCurrentUserId).mockResolvedValue(currentUserId); - jest.mocked(getCurrentUser).mockResolvedValue({ + jest.mocked(getCurrentUser).mockResolvedValue(TestHelper.fakeUserModel({ prepareUpdate: jest.fn(), - } as unknown as UserModel); + })); jest.mocked(fetchRolesIfNeeded).mockResolvedValue({ roles: [], }); @@ -246,7 +245,7 @@ describe('WebSocket Roles Actions', () => { it('should update roles, myTeam and teamMembership', async () => { jest.mocked(getCurrentUserId).mockResolvedValue(currentUserId); jest.mocked(fetchRolesIfNeeded).mockResolvedValue({ - roles: [{id: 'role1'} as Role], + roles: [TestHelper.fakeRole({id: 'role1'})], }); const msg = { diff --git a/app/actions/websocket/teams.test.ts b/app/actions/websocket/teams.test.ts index 652f115c2..59e095c60 100644 --- a/app/actions/websocket/teams.test.ts +++ b/app/actions/websocket/teams.test.ts @@ -13,11 +13,10 @@ import {getCurrentTeam, queryMyTeamsByIds} from '@queries/servers/team'; import {getCurrentUser} from '@queries/servers/user'; import EphemeralStore from '@store/ephemeral_store'; import {setTeamLoading} from '@store/team_load_store'; +import TestHelper from '@test/test_helper'; import {logDebug} from '@utils/log'; import type ServerDataOperator from '@database/operator/server_data_operator'; -import type TeamModel from '@typings/database/models/servers/team'; -import type UserModel from '@typings/database/models/servers/user'; jest.mock('@actions/local/team', () => ({ removeUserFromTeam: jest.fn(), @@ -102,10 +101,10 @@ describe('WebSocket Team Actions', () => { const mockedUpdateUsersNoLongerVisible = jest.mocked(updateUsersNoLongerVisible); const mockedUpdateCanJoinTeams = jest.mocked(updateCanJoinTeams); - mockedQueryMyTeamsByIds.mockReturnValue({fetch: jest.fn().mockResolvedValue([{id: 'team1'}])} as any); - mockedGetCurrentTeam.mockResolvedValue({id: 'team1'} as TeamModel); + mockedQueryMyTeamsByIds.mockReturnValue(TestHelper.fakeQuery([TestHelper.fakeMyTeamModel({id: 'team1'})])); + mockedGetCurrentTeam.mockResolvedValue(TestHelper.fakeTeamModel({id: 'team1'})); mockedRemoveUserFromTeam.mockResolvedValue({error: undefined}); - mockedGetCurrentUser.mockResolvedValue({id: 'user1', isGuest: true} as UserModel); + mockedGetCurrentUser.mockResolvedValue(TestHelper.fakeUserModel({id: 'user1', isGuest: true})); mockedUpdateUsersNoLongerVisible.mockResolvedValue({error: undefined}); mockedUpdateCanJoinTeams.mockResolvedValue({error: undefined}); @@ -121,7 +120,7 @@ describe('WebSocket Team Actions', () => { const mockedUpdateCanJoinTeams = jest.mocked(updateCanJoinTeams); const mockedRemoveUserFromTeam = jest.mocked(removeUserFromTeam); - mockedQueryMyTeamsByIds.mockReturnValue({fetch: jest.fn().mockResolvedValue([undefined])} as any); + mockedQueryMyTeamsByIds.mockReturnValue(TestHelper.fakeQuery([])); mockedUpdateCanJoinTeams.mockResolvedValue({error: undefined}); await handleTeamArchived(serverUrl, msg); @@ -138,10 +137,10 @@ describe('WebSocket Team Actions', () => { const mockedUpdateUsersNoLongerVisible = jest.mocked(updateUsersNoLongerVisible); const mockedUpdateCanJoinTeams = jest.mocked(updateCanJoinTeams); - mockedQueryMyTeamsByIds.mockReturnValue({fetch: jest.fn().mockResolvedValue([{id: 'team1'}])} as any); - mockedGetCurrentTeam.mockResolvedValue({id: 'team2'} as TeamModel); + mockedQueryMyTeamsByIds.mockReturnValue(TestHelper.fakeQuery([TestHelper.fakeMyTeamModel({id: 'team1'})])); + mockedGetCurrentTeam.mockResolvedValue(TestHelper.fakeTeamModel({id: 'team2'})); mockedRemoveUserFromTeam.mockResolvedValue({error: undefined}); - mockedGetCurrentUser.mockResolvedValue({id: 'user1', isGuest: false} as UserModel); + mockedGetCurrentUser.mockResolvedValue(TestHelper.fakeUserModel({id: 'user1', isGuest: false})); mockedUpdateCanJoinTeams.mockResolvedValue({error: undefined}); await handleTeamArchived(serverUrl, msg); @@ -214,8 +213,8 @@ describe('WebSocket Team Actions', () => { const mockedGetCurrentTeam = jest.mocked(getCurrentTeam); const mockedRemoveUserFromTeam = jest.mocked(removeUserFromTeam); - mockedGetCurrentUser.mockResolvedValue({id: 'user1', isGuest: true} as UserModel); - mockedGetCurrentTeam.mockResolvedValue({id: 'team1'} as TeamModel); + mockedGetCurrentUser.mockResolvedValue(TestHelper.fakeUserModel({id: 'user1', isGuest: true})); + mockedGetCurrentTeam.mockResolvedValue(TestHelper.fakeTeamModel({id: 'team1'})); await handleLeaveTeamEvent(serverUrl, msg); @@ -226,7 +225,7 @@ describe('WebSocket Team Actions', () => { const mockedGetCurrentUser = jest.mocked(getCurrentUser); const mockedRemoveUserFromTeam = jest.mocked(removeUserFromTeam); - mockedGetCurrentUser.mockResolvedValue({id: 'user2', isGuest: true} as UserModel); + mockedGetCurrentUser.mockResolvedValue(TestHelper.fakeUserModel({id: 'user2', isGuest: true})); await handleLeaveTeamEvent(serverUrl, msg); @@ -249,8 +248,8 @@ describe('WebSocket Team Actions', () => { const mockedGetCurrentTeam = jest.mocked(getCurrentTeam); const mockedRemoveUserFromTeam = jest.mocked(removeUserFromTeam); - mockedGetCurrentUser.mockResolvedValue({id: 'user1', isGuest: false} as UserModel); - mockedGetCurrentTeam.mockResolvedValue({id: 'team2'} as TeamModel); + mockedGetCurrentUser.mockResolvedValue(TestHelper.fakeUserModel({id: 'user1', isGuest: false})); + mockedGetCurrentTeam.mockResolvedValue(TestHelper.fakeTeamModel({id: 'team2'})); await handleLeaveTeamEvent(serverUrl, msg); diff --git a/app/actions/websocket/users.test.ts b/app/actions/websocket/users.test.ts index 7ee2c0a2b..4dc39858d 100644 --- a/app/actions/websocket/users.test.ts +++ b/app/actions/websocket/users.test.ts @@ -13,11 +13,11 @@ import {queryChannelsByTypes, queryUserChannelsByTypes} from '@queries/servers/c import {queryDisplayNamePreferences} from '@queries/servers/preference'; import {getConfig, getLicense} from '@queries/servers/system'; import {getCurrentUser} from '@queries/servers/user'; +import TestHelper from '@test/test_helper'; import {handleUserUpdatedEvent, handleUserTypingEvent, handleStatusChangedEvent, userTyping} from './users'; import type ServerDataOperator from '@database/operator/server_data_operator'; -import type UserModel from '@typings/database/models/servers/user'; jest.mock('@actions/local/channel'); jest.mock('@actions/local/user'); @@ -46,13 +46,11 @@ describe('WebSocket Users Actions', () => { await DatabaseManager.init([serverUrl]); operator = DatabaseManager.serverDatabases[serverUrl]!.operator; - jest.mocked(queryChannelsByTypes).mockReturnValue({ - fetch: jest.fn().mockResolvedValue([]), - } as any); + jest.mocked(queryChannelsByTypes).mockReturnValue(TestHelper.fakeQuery([])); DatabaseManager.getActiveServerUrl = jest.fn().mockResolvedValue(serverUrl); batchRecords = jest.spyOn(operator, 'batchRecords').mockResolvedValue(); - handleUsers = jest.spyOn(operator, 'handleUsers').mockResolvedValue([{id: 'user1'} as UserModel]); + handleUsers = jest.spyOn(operator, 'handleUsers').mockResolvedValue([TestHelper.fakeUserModel({id: 'user1'})]); }); afterEach(async () => { @@ -84,17 +82,17 @@ describe('WebSocket Users Actions', () => { }); it('should handle current user update', async () => { - const mockUser = { + const mockUser = TestHelper.fakeUser({ id: currentUserId, update_at: 1234, - notify_props: {email: 'true'}, - } as UserProfile; + notify_props: TestHelper.fakeUserNotifyProps({email: 'true'}), + }); - const mockCurrentUser = { + const mockCurrentUser = TestHelper.fakeUserModel({ id: currentUserId, updateAt: 1000, locale: 'en', - } as UserModel; + }); jest.mocked(getCurrentUser).mockResolvedValue(mockCurrentUser); jest.mocked(fetchMe).mockResolvedValue({user: mockUser}); @@ -112,22 +110,21 @@ describe('WebSocket Users Actions', () => { }); it('should handle other user update', async () => { - const mockUser = { + const mockUser = TestHelper.fakeUser({ id: otherUserId, update_at: 1234, - }; + }); - const mockCurrentUser = { + const mockCurrentUser = TestHelper.fakeUserModel({ id: currentUserId, updateAt: 1000, - } as UserModel; + }); jest.mocked(getCurrentUser).mockResolvedValue(mockCurrentUser); - jest.mocked(queryUserChannelsByTypes).mockReturnValue({ - fetch: jest.fn().mockResolvedValue([{id: 'channel-1'}]), - } as any); + jest.mocked(queryUserChannelsByTypes).mockReturnValue( + TestHelper.fakeQuery([TestHelper.fakeChannelModel({id: 'channel-1'})])); jest.mocked(updateChannelsDisplayName).mockResolvedValue({models: []}); - handleUsers.mockResolvedValue([{id: 'model-1'}]); + handleUsers.mockResolvedValue([TestHelper.fakeUserModel({id: 'model-1'})]); const msg = { data: { @@ -209,15 +206,13 @@ describe('WebSocket Users Actions', () => { jest.mocked(getConfig).mockResolvedValue(mockConfig as any); jest.mocked(fetchUsersByIds).mockResolvedValue({ - users: [{id: otherUserId, username: 'other-user'} as UserProfile], + users: [TestHelper.fakeUser({id: otherUserId, username: 'other-user'})], existingUsers: [], }); - jest.mocked(queryDisplayNamePreferences).mockReturnValue({ - fetch: jest.fn().mockResolvedValue([{ - value: 'full_name', - }]), - } as any); + jest.mocked(queryDisplayNamePreferences).mockReturnValue(TestHelper.fakeQuery([TestHelper.fakePreferenceModel({ + value: 'full_name', + })])); jest.mocked(getLicense).mockResolvedValue({} as ClientLicense); diff --git a/app/components/post_list/combined_user_activity/combined_user_activity.tsx b/app/components/post_list/combined_user_activity/combined_user_activity.tsx index 43eb5381a..cf6d11706 100644 --- a/app/components/post_list/combined_user_activity/combined_user_activity.tsx +++ b/app/components/post_list/combined_user_activity/combined_user_activity.tsx @@ -114,7 +114,7 @@ const CombinedUserActivity = ({ } else { showModalOverCurrentContext(Screens.POST_OPTIONS, passProps, bottomSheetModalOptions(theme)); } - }, [post, canDelete, isTablet, intl, location]); + }, [canDelete, post, location, isTablet, intl, theme]); const renderMessage = (postType: string, userIds: string[], actorId?: string) => { if (!post) { @@ -156,10 +156,10 @@ const CombinedUserActivity = ({ (userIds[0] === currentUserId || userIds[0] === currentUsername) && secureGetFromRecord(postTypeMessages, postType)?.one_you ) { - localeHolder = postTypeMessages[postType].one_you; + localeHolder = postTypeMessages[postType as keyof typeof postTypeMessages].one_you; } } else { - localeHolder = postTypeMessages[postType].two; + localeHolder = postTypeMessages[postType as keyof typeof postTypeMessages].two; } // We default to empty string, but this should never happen diff --git a/app/constants/general.ts b/app/constants/general.ts index 38f74f886..602c881d3 100644 --- a/app/constants/general.ts +++ b/app/constants/general.ts @@ -12,12 +12,12 @@ export default { CRT_CHUNK_SIZE: 60, STATUS_INTERVAL: 60000, AUTOCOMPLETE_LIMIT_DEFAULT: 25, - MENTION: 'mention', - OUT_OF_OFFICE: 'ooo', - OFFLINE: 'offline', - AWAY: 'away', - ONLINE: 'online', - DND: 'dnd', + MENTION: 'mention' as const, + OUT_OF_OFFICE: 'ooo' as const, + OFFLINE: 'offline' as const, + AWAY: 'away' as const, + ONLINE: 'online' as const, + DND: 'dnd' as const, STATUS_COMMANDS: ['offline', 'away', 'online', 'dnd'], DEFAULT_CHANNEL: 'town-square', DM_CHANNEL: 'D' as const, diff --git a/app/constants/post.ts b/app/constants/post.ts index cc09dfb6b..404ce9661 100644 --- a/app/constants/post.ts +++ b/app/constants/post.ts @@ -3,7 +3,7 @@ import {toMilliseconds} from '@utils/datetime'; -export const PostTypes: Record = { +export const PostTypes = { CHANNEL_DELETED: 'system_channel_deleted', CHANNEL_UNARCHIVED: 'system_channel_restored', DISPLAYNAME_CHANGE: 'system_displayname_change', @@ -35,7 +35,7 @@ export const PostTypes: Record = { SYSTEM_AUTO_RESPONDER: 'system_auto_responder', CUSTOM_CALLS: 'custom_calls', CUSTOM_CALLS_RECORDING: 'custom_calls_recording', -}; +} as const; export const PostPriorityColors = { URGENT: '#D24B4E', diff --git a/app/database/operator/app_data_operator/transformers/index.ts b/app/database/operator/app_data_operator/transformers/index.ts index e3f30270c..8ecbd7165 100644 --- a/app/database/operator/app_data_operator/transformers/index.ts +++ b/app/database/operator/app_data_operator/transformers/index.ts @@ -18,9 +18,9 @@ const {INFO, GLOBAL} = MM_TABLES.APP; * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformInfoRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as AppInfo; - const record = value.record as InfoModel | undefined; +export const transformInfoRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; const fieldsMapper = (app: InfoModel) => { @@ -46,8 +46,8 @@ export const transformInfoRecord = ({action, database, value}: TransformerArgs): * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformGlobalRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as IdValue; +export const transformGlobalRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; const fieldsMapper = (global: GlobalModel) => { global._raw.id = raw?.id; diff --git a/app/database/operator/base_data_operator/index.ts b/app/database/operator/base_data_operator/index.ts index 1b77b665f..7105adc6e 100644 --- a/app/database/operator/base_data_operator/index.ts +++ b/app/database/operator/base_data_operator/index.ts @@ -21,10 +21,10 @@ import type { export interface BaseDataOperatorType { database: Database; - handleRecords: ({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues, tableName, prepareRecordsOnly}: HandleRecordsArgs, description: string) => Promise; - processRecords: ({createOrUpdateRawValues, deleteRawValues, tableName, buildKeyRecordBy, fieldName}: ProcessRecordsArgs) => Promise>; + handleRecords: ({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues, tableName, prepareRecordsOnly}: HandleRecordsArgs, description: string) => Promise; + processRecords: ({createOrUpdateRawValues, deleteRawValues, tableName, buildKeyRecordBy, fieldName}: ProcessRecordsArgs) => Promise>; batchRecords: (models: Model[], description: string) => Promise; - prepareRecords: ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs) => Promise; + prepareRecords: ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs) => Promise; } export default class BaseDataOperator { @@ -45,8 +45,8 @@ export default class BaseDataOperator { * @param {(existing: Model, newElement: RawValue) => boolean} inputsArg.buildKeyRecordBy * @returns {Promise<{ProcessRecordResults}>} */ - processRecords = async ({createOrUpdateRawValues = [], deleteRawValues = [], tableName, buildKeyRecordBy, fieldName, shouldUpdate}: ProcessRecordsArgs): Promise> => { - const getRecords = async (rawValues: RawValue[]) => { + processRecords = async ({createOrUpdateRawValues = [], deleteRawValues = [], tableName, buildKeyRecordBy, fieldName, shouldUpdate}: ProcessRecordsArgs): Promise> => { + const getRecords = async (rawValues: R[]) => { // We will query a table where one of its fields can match a range of values. Hence, here we are extracting all those potential values. const columnValues: string[] = getRangeOfValues({fieldName, raws: rawValues}); @@ -69,8 +69,8 @@ export default class BaseDataOperator { return existingRecords; }; - const createRaws: RecordPair[] = []; - const updateRaws: RecordPair[] = []; + const createRaws: Array> = []; + const updateRaws: Array> = []; // for delete flow const deleteRaws = await getRecords(deleteRawValues); @@ -97,7 +97,7 @@ export default class BaseDataOperator { } // Some raw value has an update_at field. We'll proceed to update only if the update_at value is different from the record's value in database - const updateRecords = getValidRecordsForUpdate({ + const updateRecords = getValidRecordsForUpdate({ tableName, existingRecord, newValue: newElement, @@ -129,7 +129,7 @@ export default class BaseDataOperator { * @param {(TransformerArgs) => Promise;} transformer * @returns {Promise} */ - prepareRecords = async ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs): Promise => { + prepareRecords = async ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs): Promise => { if (!this.database) { logWarning('Database not defined in prepareRecords'); return []; @@ -140,7 +140,7 @@ export default class BaseDataOperator { // create operation if (createRaws?.length) { const recordPromises = createRaws.map( - (createRecord: RecordPair) => { + (createRecord: RecordPair) => { return transformer({ database: this.database, tableName, @@ -156,7 +156,7 @@ export default class BaseDataOperator { // update operation if (updateRaws?.length) { const recordPromises = updateRaws.map( - (updateRecord: RecordPair) => { + (updateRecord: RecordPair) => { return transformer({ database: this.database, tableName, @@ -209,7 +209,7 @@ export default class BaseDataOperator { * @param {string} handleRecordsArgs.tableName * @returns {Promise} */ - async handleRecords({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues = [], tableName, prepareRecordsOnly = true, shouldUpdate}: HandleRecordsArgs, description: string): Promise { + async handleRecords({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues = [], tableName, prepareRecordsOnly = true, shouldUpdate}: HandleRecordsArgs, description: string): Promise { if (!createOrUpdateRawValues.length && !deleteRawValues.length) { logWarning( `An empty "rawValues" array has been passed to the handleRecords method for tableName ${tableName}`, @@ -217,7 +217,7 @@ export default class BaseDataOperator { return []; } - const {createRaws, deleteRaws, updateRaws} = await this.processRecords({ + const {createRaws, deleteRaws, updateRaws} = await this.processRecords({ createOrUpdateRawValues, deleteRawValues, tableName, @@ -227,7 +227,7 @@ export default class BaseDataOperator { }); let models: T[] = []; - models = await this.prepareRecords({ + models = await this.prepareRecords({ tableName, createRaws, updateRaws, diff --git a/app/database/operator/server_data_operator/handlers/channel.ts b/app/database/operator/server_data_operator/handlers/channel.ts index 2bb159aff..1d81a2e2c 100644 --- a/app/database/operator/server_data_operator/handlers/channel.ts +++ b/app/database/operator/server_data_operator/handlers/channel.ts @@ -44,7 +44,7 @@ export interface ChannelHandlerMix { handleChannelMembership: ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => Promise; handleMyChannelSettings: ({settings, prepareRecordsOnly}: HandleMyChannelSettingsArgs) => Promise; handleChannelInfo: ({channelInfos, prepareRecordsOnly}: HandleChannelInfoArgs) => Promise; - handleMyChannel: ({channels, myChannels, isCRTEnabled, prepareRecordsOnly}: HandleMyChannelArgs) => Promise; + handleMyChannel: ({channels, myChannels, isCRTEnabled, prepareRecordsOnly}: HandleMyChannelArgs) => Promise; } const ChannelHandler = >(superclass: TBase) => class extends superclass { @@ -63,7 +63,7 @@ const ChannelHandler = >(super return []; } - const uniqueRaws = getUniqueRawsBy({raws: channels, key: 'id'}) as Channel[]; + const uniqueRaws = getUniqueRawsBy({raws: channels, key: 'id'}); const keys = uniqueRaws.map((c) => c.id); const db: Database = this.database; const existing = await db.get(CHANNEL).query( @@ -113,7 +113,7 @@ const ChannelHandler = >(super return []; } - const uniqueRaws = getUniqueRawsBy({raws: settings, key: 'id'}) as ChannelMembership[]; + const uniqueRaws = getUniqueRawsBy({raws: settings, key: 'id'}); const keys = uniqueRaws.map((c) => c.channel_id); const db: Database = this.database; const existing = await db.get(MY_CHANNEL_SETTINGS).query( @@ -173,7 +173,7 @@ const ChannelHandler = >(super const uniqueRaws = getUniqueRawsBy({ raws: channelInfos as ChannelInfo[], key: 'id', - }) as ChannelInfo[]; + }); const keys = uniqueRaws.map((ci) => ci.id); const db: Database = this.database; const existing = await db.get(CHANNEL_INFO).query( @@ -221,7 +221,7 @@ const ChannelHandler = >(super * @param {boolean} myChannelsArgs.prepareRecordsOnly * @returns {Promise} */ - handleMyChannel = async ({channels, myChannels, isCRTEnabled, prepareRecordsOnly = true}: HandleMyChannelArgs): Promise => { + handleMyChannel = async ({channels, myChannels, isCRTEnabled, prepareRecordsOnly = true}: HandleMyChannelArgs): Promise => { if (!myChannels?.length) { logWarning( 'An empty or undefined "myChannels" array has been passed to the handleMyChannel method', @@ -261,7 +261,7 @@ const ChannelHandler = >(super const uniqueRaws = getUniqueRawsBy({ raws: myChannels, key: 'id', - }) as ChannelMembership[]; + }); const ids = uniqueRaws.map((cm: ChannelMembership) => cm.channel_id); const db: Database = this.database; const existing = await db.get(MY_CHANNEL).query( @@ -322,7 +322,7 @@ const ChannelHandler = >(super id: `${m.channel_id}-${m.user_id}`, })); - const uniqueRaws = getUniqueRawsBy({raws: memberships, key: 'id'}) as ChannelMember[]; + const uniqueRaws = getUniqueRawsBy({raws: memberships, key: 'id'}); const ids = uniqueRaws.map((cm: ChannelMember) => `${cm.channel_id}-${cm.user_id}`); const db: Database = this.database; const existing = await db.get(CHANNEL_MEMBERSHIP).query( @@ -372,7 +372,7 @@ const ChannelHandler = >(super return []; } - const uniqueRaws = getUniqueRawsBy({raws: bookmarks, key: 'id'}) as ChannelBookmarkWithFileInfo[]; + const uniqueRaws = getUniqueRawsBy({raws: bookmarks, key: 'id'}); const keys = uniqueRaws.map((c) => c.id); const db: Database = this.database; const existing = await db.get(CHANNEL_BOOKMARK).query( diff --git a/app/database/operator/server_data_operator/handlers/index.ts b/app/database/operator/server_data_operator/handlers/index.ts index 8da5fbd39..b69b65d02 100644 --- a/app/database/operator/server_data_operator/handlers/index.ts +++ b/app/database/operator/server_data_operator/handlers/index.ts @@ -179,7 +179,7 @@ export default class ServerDataOperatorBase extends BaseDataOperator { return res; }, {createOrUpdateFiles: [], deleteFiles: []}); - const processedFiles = (await this.processRecords({ + const processedFiles = (await this.processRecords({ createOrUpdateRawValues: raws.createOrUpdateFiles, tableName: FILE, fieldName: 'id', @@ -187,7 +187,7 @@ export default class ServerDataOperatorBase extends BaseDataOperator { shouldUpdate: shouldUpdateFileRecord, })); - const preparedFiles = await this.prepareRecords({ + const preparedFiles = await this.prepareRecords({ createRaws: processedFiles.createRaws, updateRaws: processedFiles.updateRaws, deleteRaws: processedFiles.deleteRaws, @@ -215,7 +215,7 @@ export default class ServerDataOperatorBase extends BaseDataOperator { * @param {(TransformerArgs) => Promise} execute.recordOperator * @returns {Promise} */ - async execute({createRaws, transformer, tableName, updateRaws}: OperationArgs, description: string): Promise { + async execute({createRaws, transformer, tableName, updateRaws}: OperationArgs, description: string): Promise { const models = await this.prepareRecords({ tableName, createRaws, diff --git a/app/database/operator/server_data_operator/handlers/post.ts b/app/database/operator/server_data_operator/handlers/post.ts index a731a7ab1..0a4d6a0e7 100644 --- a/app/database/operator/server_data_operator/handlers/post.ts +++ b/app/database/operator/server_data_operator/handlers/post.ts @@ -601,15 +601,15 @@ const PostHandler = >(supercla return []; } - const update: RecordPair[] = []; + const update: Array> = []; const create: PostsInThread[] = []; const ids = Object.keys(postsMap); for await (const rootId of ids) { const {firstPost, lastPost} = getPostListEdges(postsMap[rootId]); - const chunks = (await this.database.get(POSTS_IN_THREAD).query( + const chunks = (await this.database.get(POSTS_IN_THREAD).query( Q.where('root_id', rootId), Q.sortBy('latest', Q.desc), - ).fetch()) as PostsInThreadModel[]; + ).fetch()); if (chunks.length) { const chunk = chunks[0]; @@ -633,12 +633,12 @@ const PostHandler = >(supercla } } - const postInThreadRecords = (await this.prepareRecords({ + const postInThreadRecords = await this.prepareRecords({ createRaws: getRawRecordPairs(create), updateRaws: update, transformer: transformPostInThreadRecord, tableName: POSTS_IN_THREAD, - })) as PostsInThreadModel[]; + }); if (postInThreadRecords?.length && !prepareRecordsOnly) { await this.batchRecords(postInThreadRecords, 'handleReceivedPostsInThread'); diff --git a/app/database/operator/server_data_operator/handlers/team_threads_sync.ts b/app/database/operator/server_data_operator/handlers/team_threads_sync.ts index e53f9702d..6180e044e 100644 --- a/app/database/operator/server_data_operator/handlers/team_threads_sync.ts +++ b/app/database/operator/server_data_operator/handlers/team_threads_sync.ts @@ -38,7 +38,7 @@ const TeamThreadsSyncHandler = > = []; for await (const item of uniqueRaws) { const {id} = item; @@ -54,12 +54,12 @@ const TeamThreadsSyncHandler = ({ createRaws: getRawRecordPairs(create), updateRaws: update, transformer: transformTeamThreadsSyncRecord, tableName: TEAM_THREADS_SYNC, - })) as TeamThreadsSyncModel[]; + }); if (models?.length && !prepareRecordsOnly) { await this.batchRecords(models, 'handleTeamThreadsSync'); diff --git a/app/database/operator/server_data_operator/transformers/category.ts b/app/database/operator/server_data_operator/transformers/category.ts index d1a0fe2fe..7865f33d4 100644 --- a/app/database/operator/server_data_operator/transformers/category.ts +++ b/app/database/operator/server_data_operator/transformers/category.ts @@ -20,19 +20,22 @@ const { * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformCategoryRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as Category; - const record = value.record as CategoryModel; +export const transformCategoryRecord = ({action, database, value}: TransformerArgs) => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // id of category comes from server response const fieldsMapper = (category: CategoryModel) => { - category._raw.id = isCreateAction ? (raw?.id ?? category.id) : record.id; + category._raw.id = isCreateAction ? (raw?.id ?? category.id) : record!.id; category.displayName = raw.display_name; category.sorting = raw.sorting || 'recent'; category.sortOrder = raw.sort_order === 0 ? 0 : raw.sort_order / 10; // Sort order from server is in multiples of 10 category.muted = raw.muted ?? false; - category.collapsed = isCreateAction ? false : record.collapsed; + category.collapsed = isCreateAction ? false : record!.collapsed; category.type = raw.type; category.teamId = raw.team_id; }; @@ -43,7 +46,7 @@ export const transformCategoryRecord = ({action, database, value}: TransformerAr tableName: CATEGORY, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -53,14 +56,17 @@ export const transformCategoryRecord = ({action, database, value}: TransformerAr * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformCategoryChannelRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as CategoryChannel; - const record = value.record as CategoryChannelModel; +export const transformCategoryChannelRecord = ({action, database, value}: TransformerArgs) => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (categoryChannel: CategoryChannelModel) => { - categoryChannel._raw.id = isCreateAction ? (raw?.id ?? categoryChannel.id) : record.id; + categoryChannel._raw.id = isCreateAction ? (raw?.id ?? categoryChannel.id) : record!.id; categoryChannel.channelId = raw.channel_id; categoryChannel.categoryId = raw.category_id; categoryChannel.sortOrder = raw.sort_order; @@ -72,5 +78,5 @@ export const transformCategoryChannelRecord = ({action, database, value}: Transf tableName: CATEGORY_CHANNEL, value, fieldsMapper, - }) as Promise; + }); }; diff --git a/app/database/operator/server_data_operator/transformers/channel.test.ts b/app/database/operator/server_data_operator/transformers/channel.test.ts index 4c4d337f2..bc593e9b3 100644 --- a/app/database/operator/server_data_operator/transformers/channel.test.ts +++ b/app/database/operator/server_data_operator/transformers/channel.test.ts @@ -98,7 +98,6 @@ describe('*** CHANNEL Prepare Records Test ***', () => { record: undefined, raw: { id: 'c', - channel_id: 'c', guest_count: 10, header: 'channel info header', member_count: 10, diff --git a/app/database/operator/server_data_operator/transformers/channel.ts b/app/database/operator/server_data_operator/transformers/channel.ts index 671d0f42c..8092b9bfe 100644 --- a/app/database/operator/server_data_operator/transformers/channel.ts +++ b/app/database/operator/server_data_operator/transformers/channel.ts @@ -29,14 +29,17 @@ const { * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformChannelRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as Channel; - const record = value.record as ChannelModel; +export const transformChannelRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (channel: ChannelModel) => { - channel._raw.id = isCreateAction ? (raw?.id ?? channel.id) : record.id; + channel._raw.id = isCreateAction ? (raw?.id ?? channel.id) : record!.id; channel.createAt = raw.create_at; channel.creatorId = raw.creator_id; channel.deleteAt = raw.delete_at; @@ -56,7 +59,7 @@ export const transformChannelRecord = ({action, database, value}: TransformerArg tableName: CHANNEL, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -66,13 +69,16 @@ export const transformChannelRecord = ({action, database, value}: TransformerArg * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformMyChannelSettingsRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as ChannelMembership; - const record = value.record as MyChannelSettingsModel; +export const transformMyChannelSettingsRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } const fieldsMapper = (myChannelSetting: MyChannelSettingsModel) => { - myChannelSetting._raw.id = isCreateAction ? (raw.channel_id || myChannelSetting.id) : record.id; + myChannelSetting._raw.id = isCreateAction ? (raw.channel_id || myChannelSetting.id) : record!.id; myChannelSetting.notifyProps = raw.notify_props; }; @@ -82,7 +88,7 @@ export const transformMyChannelSettingsRecord = ({action, database, value}: Tran tableName: MY_CHANNEL_SETTINGS, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -92,13 +98,16 @@ export const transformMyChannelSettingsRecord = ({action, database, value}: Tran * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformChannelInfoRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as Partial; - const record = value.record as ChannelInfoModel; +export const transformChannelInfoRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } const fieldsMapper = (channelInfo: ChannelInfoModel) => { - channelInfo._raw.id = isCreateAction ? (raw.id || channelInfo.id) : record.id; + channelInfo._raw.id = isCreateAction ? (raw.id || channelInfo.id) : record!.id; channelInfo.guestCount = raw.guest_count ?? channelInfo.guestCount ?? 0; channelInfo.header = raw.header ?? channelInfo.header ?? ''; channelInfo.memberCount = raw.member_count ?? channelInfo.memberCount ?? 0; @@ -113,7 +122,7 @@ export const transformChannelInfoRecord = ({action, database, value}: Transforme tableName: CHANNEL_INFO, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -123,13 +132,16 @@ export const transformChannelInfoRecord = ({action, database, value}: Transforme * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformMyChannelRecord = async ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as ChannelMembership; - const record = value.record as MyChannelModel; +export const transformMyChannelRecord = async ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } const fieldsMapper = (myChannel: MyChannelModel) => { - myChannel._raw.id = isCreateAction ? (raw.channel_id || myChannel.id) : record.id; + myChannel._raw.id = isCreateAction ? (raw.channel_id || myChannel.id) : record!.id; myChannel.roles = raw.roles; // ignoring msg_count_root because msg_count, mention_count, last_post_at is already calculated in "handleMyChannel" based on CRT is enabled or not @@ -149,7 +161,7 @@ export const transformMyChannelRecord = async ({action, database, value}: Transf tableName: MY_CHANNEL, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -159,14 +171,17 @@ export const transformMyChannelRecord = async ({action, database, value}: Transf * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformChannelMembershipRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as ChannelMembership; - const record = value.record as ChannelMembershipModel; +export const transformChannelMembershipRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (channelMember: ChannelMembershipModel) => { - channelMember._raw.id = isCreateAction ? (raw?.id ?? channelMember.id) : record.id; + channelMember._raw.id = isCreateAction ? (raw?.id ?? channelMember.id) : record!.id; channelMember.channelId = raw.channel_id; channelMember.userId = raw.user_id; channelMember.schemeAdmin = raw.scheme_admin ?? false; @@ -178,7 +193,7 @@ export const transformChannelMembershipRecord = ({action, database, value}: Tran tableName: CHANNEL_MEMBERSHIP, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -188,14 +203,17 @@ export const transformChannelMembershipRecord = ({action, database, value}: Tran * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformChannelBookmarkRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as ChannelBookmark; - const record = value.record as ChannelBookmarkModel; +export const transformChannelBookmarkRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (bookmark: ChannelBookmarkModel) => { - bookmark._raw.id = isCreateAction ? (raw?.id ?? bookmark.id) : record.id; + bookmark._raw.id = isCreateAction ? (raw?.id ?? bookmark.id) : record!.id; bookmark.createAt = raw.create_at; bookmark.deleteAt = raw.delete_at; bookmark.updateAt = raw.update_at; @@ -219,5 +237,5 @@ export const transformChannelBookmarkRecord = ({action, database, value}: Transf tableName: CHANNEL_BOOKMARK, value, fieldsMapper, - }) as Promise; + }); }; diff --git a/app/database/operator/server_data_operator/transformers/general.test.ts b/app/database/operator/server_data_operator/transformers/general.test.ts index 6121db4f4..330ff2c03 100644 --- a/app/database/operator/server_data_operator/transformers/general.test.ts +++ b/app/database/operator/server_data_operator/transformers/general.test.ts @@ -47,7 +47,7 @@ describe('*** System Prepare Records Test ***', () => { database: database!, value: { record: undefined, - raw: {id: 'system-1', name: 'system-name-1', value: 'system'}, + raw: {id: 'system-1', value: 'system'}, }, }); diff --git a/app/database/operator/server_data_operator/transformers/general.ts b/app/database/operator/server_data_operator/transformers/general.ts index 8264739b7..78fb5b270 100644 --- a/app/database/operator/server_data_operator/transformers/general.ts +++ b/app/database/operator/server_data_operator/transformers/general.ts @@ -26,14 +26,17 @@ const { * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformCustomEmojiRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as CustomEmoji; - const record = value.record as CustomEmojiModel; +export const transformCustomEmojiRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (emoji: CustomEmojiModel) => { - emoji._raw.id = isCreateAction ? (raw?.id ?? emoji.id) : record.id; + emoji._raw.id = isCreateAction ? (raw?.id ?? emoji.id) : record!.id; emoji.name = raw.name; }; @@ -43,7 +46,7 @@ export const transformCustomEmojiRecord = ({action, database, value}: Transforme tableName: CUSTOM_EMOJI, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -53,14 +56,17 @@ export const transformCustomEmojiRecord = ({action, database, value}: Transforme * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformRoleRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as Role; - const record = value.record as RoleModel; +export const transformRoleRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (role: RoleModel) => { - role._raw.id = isCreateAction ? (raw?.id ?? role.id) : record.id; + role._raw.id = isCreateAction ? (raw?.id ?? role.id) : record!.id; role.name = raw?.name; role.permissions = raw?.permissions; }; @@ -71,7 +77,7 @@ export const transformRoleRecord = ({action, database, value}: TransformerArgs): tableName: ROLE, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -81,8 +87,8 @@ export const transformRoleRecord = ({action, database, value}: TransformerArgs): * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformSystemRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as IdValue; +export const transformSystemRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (system: SystemModel) => { @@ -96,7 +102,7 @@ export const transformSystemRecord = ({action, database, value}: TransformerArgs tableName: SYSTEM, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -106,8 +112,8 @@ export const transformSystemRecord = ({action, database, value}: TransformerArgs * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformConfigRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as IdValue; +export const transformConfigRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (config: ConfigModel) => { @@ -121,7 +127,7 @@ export const transformConfigRecord = ({action, database, value}: TransformerArgs tableName: CONFIG, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -131,14 +137,17 @@ export const transformConfigRecord = ({action, database, value}: TransformerArgs * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformFileRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as FileInfo; - const record = value.record as FileModel; +export const transformFileRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (file: FileModel) => { - file._raw.id = isCreateAction ? (raw.id || file.id) : record.id; + file._raw.id = isCreateAction ? (raw.id || file.id) : record!.id; file.postId = raw.post_id || ''; file.name = raw.name; file.extension = raw.extension; @@ -156,5 +165,5 @@ export const transformFileRecord = ({action, database, value}: TransformerArgs): tableName: FILE, value, fieldsMapper, - }) as Promise; + }); }; diff --git a/app/database/operator/server_data_operator/transformers/group.ts b/app/database/operator/server_data_operator/transformers/group.ts index 0fedf7410..2c4668d2d 100644 --- a/app/database/operator/server_data_operator/transformers/group.ts +++ b/app/database/operator/server_data_operator/transformers/group.ts @@ -26,14 +26,17 @@ const { * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformGroupRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as Group; - const record = value.record as GroupModel; +export const transformGroupRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // id of group comes from server response const fieldsMapper = (group: GroupModel) => { - group._raw.id = isCreateAction ? (raw?.id ?? group.id) : record.id; + group._raw.id = isCreateAction ? (raw?.id ?? group.id) : record!.id; group.name = raw.name; group.displayName = raw.display_name; group.source = raw.source; @@ -47,7 +50,7 @@ export const transformGroupRecord = ({action, database, value}: TransformerArgs) tableName: GROUP, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -57,8 +60,8 @@ export const transformGroupRecord = ({action, database, value}: TransformerArgs) * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformGroupChannelRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as GroupChannel; +export const transformGroupChannelRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; // id of group comes from server response const fieldsMapper = (model: GroupChannelModel) => { @@ -73,7 +76,7 @@ export const transformGroupChannelRecord = ({action, database, value}: Transform tableName: GROUP_CHANNEL, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -83,8 +86,8 @@ export const transformGroupChannelRecord = ({action, database, value}: Transform * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformGroupMembershipRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as GroupMembership; +export const transformGroupMembershipRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; // id of group comes from server response const fieldsMapper = (model: GroupMembershipModel) => { @@ -99,7 +102,7 @@ export const transformGroupMembershipRecord = ({action, database, value}: Transf tableName: GROUP_MEMBERSHIP, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -109,8 +112,8 @@ export const transformGroupMembershipRecord = ({action, database, value}: Transf * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformGroupTeamRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as GroupTeam; +export const transformGroupTeamRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; // id of group comes from server response const fieldsMapper = (model: GroupTeamModel) => { @@ -125,5 +128,5 @@ export const transformGroupTeamRecord = ({action, database, value}: TransformerA tableName: GROUP_TEAM, value, fieldsMapper, - }) as Promise; + }); }; diff --git a/app/database/operator/server_data_operator/transformers/index.ts b/app/database/operator/server_data_operator/transformers/index.ts index 7e6d963c2..27516369e 100644 --- a/app/database/operator/server_data_operator/transformers/index.ts +++ b/app/database/operator/server_data_operator/transformers/index.ts @@ -17,17 +17,17 @@ import type {PrepareBaseRecordArgs} from '@typings/database/database'; * @param {((PrepareBaseRecordArgs) => void)} operatorBase.generator * @returns {Promise} */ -export const prepareBaseRecord = async ({ +export const prepareBaseRecord = async ({ action, database, tableName, value, fieldsMapper, -}: PrepareBaseRecordArgs): Promise => { +}: PrepareBaseRecordArgs) => { if (action === OperationType.UPDATE) { - const record = value.record as Model; + const record = value.record!; return record.prepareUpdate(() => fieldsMapper(record)); } - return database.collections.get(tableName!).prepareCreate(fieldsMapper); + return database.collections.get(tableName!).prepareCreate(fieldsMapper); }; diff --git a/app/database/operator/server_data_operator/transformers/post.test.ts b/app/database/operator/server_data_operator/transformers/post.test.ts index dd8a0bbd0..4a8b61441 100644 --- a/app/database/operator/server_data_operator/transformers/post.test.ts +++ b/app/database/operator/server_data_operator/transformers/post.test.ts @@ -86,7 +86,6 @@ describe('*** POST Prepare Records Test ***', () => { value: { record: undefined, raw: { - id: 'ps81i4yypoo', root_id: 'ps81iqbddesfby8jayz7owg4yypoo', message: 'draft message', channel_id: 'channel_idp23232e', diff --git a/app/database/operator/server_data_operator/transformers/post.ts b/app/database/operator/server_data_operator/transformers/post.ts index 8e3451b67..179bcd314 100644 --- a/app/database/operator/server_data_operator/transformers/post.ts +++ b/app/database/operator/server_data_operator/transformers/post.ts @@ -24,14 +24,17 @@ const { * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformPostRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as Post; - const record = value.record as PostModel; +export const transformPostRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (post: PostModel) => { - post._raw.id = isCreateAction ? (raw?.id ?? post.id) : record.id; + post._raw.id = isCreateAction ? (raw?.id ?? post.id) : record!.id; post.channelId = raw.channel_id; post.createAt = raw.create_at; post.deleteAt = raw.delete_at || raw.delete_at === 0 ? raw?.delete_at : 0; @@ -61,7 +64,7 @@ export const transformPostRecord = ({action, database, value}: TransformerArgs): tableName: POST, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -71,13 +74,16 @@ export const transformPostRecord = ({action, database, value}: TransformerArgs): * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformPostInThreadRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as PostsInThread; - const record = value.record as PostsInThreadModel; +export const transformPostInThreadRecord = ({action, database, value}: TransformerArgs) => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } const fieldsMapper = (postsInThread: PostsInThreadModel) => { - postsInThread._raw.id = isCreateAction ? (raw.id || postsInThread.id) : record.id; + postsInThread._raw.id = isCreateAction ? (raw.id || postsInThread.id) : record!.id; postsInThread.rootId = raw.root_id; postsInThread.earliest = raw.earliest; postsInThread.latest = raw.latest!; @@ -89,7 +95,7 @@ export const transformPostInThreadRecord = ({action, database, value}: Transform tableName: POSTS_IN_THREAD, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -99,10 +105,10 @@ export const transformPostInThreadRecord = ({action, database, value}: Transform * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformDraftRecord = ({action, database, value}: TransformerArgs): Promise => { +export const transformDraftRecord = ({action, database, value}: TransformerArgs): Promise => { const emptyFileInfo: FileInfo[] = []; const emptyPostMetadata: PostMetadata = {}; - const raw = value.raw as Draft; + const raw = value.raw; // We use the raw id as Draft is client side only and we would only be creating/deleting drafts const fieldsMapper = (draft: DraftModel) => { @@ -121,7 +127,7 @@ export const transformDraftRecord = ({action, database, value}: TransformerArgs) tableName: DRAFT, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -131,13 +137,16 @@ export const transformDraftRecord = ({action, database, value}: TransformerArgs) * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformPostsInChannelRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as PostsInChannel; - const record = value.record as PostsInChannelModel; +export const transformPostsInChannelRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } const fieldsMapper = (postsInChannel: PostsInChannelModel) => { - postsInChannel._raw.id = isCreateAction ? (raw.id || postsInChannel.id) : record.id; + postsInChannel._raw.id = isCreateAction ? (raw.id || postsInChannel.id) : record!.id; postsInChannel.channelId = raw.channel_id; postsInChannel.earliest = raw.earliest; postsInChannel.latest = raw.latest; @@ -149,5 +158,5 @@ export const transformPostsInChannelRecord = ({action, database, value}: Transfo tableName: POSTS_IN_CHANNEL, value, fieldsMapper, - }) as Promise; + }); }; diff --git a/app/database/operator/server_data_operator/transformers/reaction.test.ts b/app/database/operator/server_data_operator/transformers/reaction.test.ts index 7105da1a9..6587e12ff 100644 --- a/app/database/operator/server_data_operator/transformers/reaction.test.ts +++ b/app/database/operator/server_data_operator/transformers/reaction.test.ts @@ -23,8 +23,6 @@ describe('*** REACTION Prepare Records Test ***', () => { post_id: 'ps81iqbddesfby8jayz7owg4yypoo', emoji_name: 'thumbsup', create_at: 1596032651748, - update_at: 1608253011321, - delete_at: 0, }, }, }); diff --git a/app/database/operator/server_data_operator/transformers/reaction.ts b/app/database/operator/server_data_operator/transformers/reaction.ts index eb0ca1e18..d6bb755a6 100644 --- a/app/database/operator/server_data_operator/transformers/reaction.ts +++ b/app/database/operator/server_data_operator/transformers/reaction.ts @@ -16,14 +16,17 @@ const {REACTION} = MM_TABLES.SERVER; * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformReactionRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as Reaction; - const record = value.record as ReactionModel; +export const transformReactionRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // id of reaction comes from server response const fieldsMapper = (reaction: ReactionModel) => { - reaction._raw.id = isCreateAction ? (raw?.id ?? reaction.id) : record.id; + reaction._raw.id = isCreateAction ? (raw?.id ?? reaction.id) : record!.id; reaction.userId = raw.user_id; reaction.postId = raw.post_id; reaction.emojiName = raw.emoji_name; @@ -36,5 +39,5 @@ export const transformReactionRecord = ({action, database, value}: TransformerAr tableName: REACTION, value, fieldsMapper, - }) as Promise; + }); }; diff --git a/app/database/operator/server_data_operator/transformers/team.ts b/app/database/operator/server_data_operator/transformers/team.ts index 24a6be3ef..00c08a092 100644 --- a/app/database/operator/server_data_operator/transformers/team.ts +++ b/app/database/operator/server_data_operator/transformers/team.ts @@ -26,14 +26,17 @@ const { * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformTeamMembershipRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as TeamMembership; - const record = value.record as TeamMembershipModel; +export const transformTeamMembershipRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (teamMembership: TeamMembershipModel) => { - teamMembership._raw.id = isCreateAction ? (raw?.id ?? teamMembership.id) : record.id; + teamMembership._raw.id = isCreateAction ? (raw?.id ?? teamMembership.id) : record!.id; teamMembership.teamId = raw.team_id; teamMembership.userId = raw.user_id; teamMembership.schemeAdmin = raw.scheme_admin; @@ -45,7 +48,7 @@ export const transformTeamMembershipRecord = ({action, database, value}: Transfo tableName: TEAM_MEMBERSHIP, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -55,14 +58,17 @@ export const transformTeamMembershipRecord = ({action, database, value}: Transfo * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformTeamRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as Team; - const record = value.record as TeamModel; +export const transformTeamRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (team: TeamModel) => { - team._raw.id = isCreateAction ? (raw?.id ?? team.id) : record.id; + team._raw.id = isCreateAction ? (raw?.id ?? team.id) : record!.id; team.isAllowOpenInvite = raw.allow_open_invite; team.description = raw.description; team.displayName = raw.display_name; @@ -81,7 +87,7 @@ export const transformTeamRecord = ({action, database, value}: TransformerArgs): tableName: TEAM, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -91,13 +97,16 @@ export const transformTeamRecord = ({action, database, value}: TransformerArgs): * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformTeamChannelHistoryRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as TeamChannelHistory; - const record = value.record as TeamChannelHistoryModel; +export const transformTeamChannelHistoryRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } const fieldsMapper = (teamChannelHistory: TeamChannelHistoryModel) => { - teamChannelHistory._raw.id = isCreateAction ? (raw.id || teamChannelHistory.id) : record.id; + teamChannelHistory._raw.id = isCreateAction ? (raw.id || teamChannelHistory.id) : record!.id; teamChannelHistory.channelIds = raw.channel_ids; }; @@ -107,7 +116,7 @@ export const transformTeamChannelHistoryRecord = ({action, database, value}: Tra tableName: TEAM_CHANNEL_HISTORY, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -117,13 +126,16 @@ export const transformTeamChannelHistoryRecord = ({action, database, value}: Tra * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformTeamSearchHistoryRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as TeamSearchHistory; - const record = value.record as TeamSearchHistoryModel; +export const transformTeamSearchHistoryRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } const fieldsMapper = (teamSearchHistory: TeamSearchHistoryModel) => { - teamSearchHistory._raw.id = isCreateAction ? (teamSearchHistory.id) : record.id; + teamSearchHistory._raw.id = isCreateAction ? (teamSearchHistory.id) : record!.id; teamSearchHistory.createdAt = raw.created_at; teamSearchHistory.displayTerm = raw.display_term; teamSearchHistory.term = raw.term; @@ -136,7 +148,7 @@ export const transformTeamSearchHistoryRecord = ({action, database, value}: Tran tableName: TEAM_SEARCH_HISTORY, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -146,13 +158,16 @@ export const transformTeamSearchHistoryRecord = ({action, database, value}: Tran * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformMyTeamRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as MyTeam; - const record = value.record as MyTeamModel; +export const transformMyTeamRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } const fieldsMapper = (myTeam: MyTeamModel) => { - myTeam._raw.id = isCreateAction ? (raw.id || myTeam.id) : record.id; + myTeam._raw.id = isCreateAction ? (raw.id || myTeam.id) : record!.id; myTeam.roles = raw.roles; }; @@ -162,5 +177,5 @@ export const transformMyTeamRecord = ({action, database, value}: TransformerArgs tableName: MY_TEAM, value, fieldsMapper, - }) as Promise; + }); }; diff --git a/app/database/operator/server_data_operator/transformers/thread.ts b/app/database/operator/server_data_operator/transformers/thread.ts index 7846c98ce..4016c362c 100644 --- a/app/database/operator/server_data_operator/transformers/thread.ts +++ b/app/database/operator/server_data_operator/transformers/thread.ts @@ -24,14 +24,17 @@ const { * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformThreadRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as ThreadWithLastFetchedAt; - const record = value.record as ThreadModel; +export const transformThreadRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database const fieldsMapper = (thread: ThreadModel) => { - thread._raw.id = isCreateAction ? (raw?.id ?? thread.id) : record.id; + thread._raw.id = isCreateAction ? (raw?.id ?? thread.id) : record!.id; // When post is individually fetched, we get last_reply_at as 0, so we use the record's value. // If there is no reply at, we default to the post's create_at @@ -39,7 +42,7 @@ export const transformThreadRecord = ({action, database, value}: TransformerArgs thread.lastViewedAt = raw.last_viewed_at ?? record?.lastViewedAt ?? 0; thread.replyCount = raw.reply_count; - thread.isFollowing = raw.is_following ?? record?.isFollowing; + thread.isFollowing = raw.is_following ?? record?.isFollowing ?? false; thread.unreadReplies = raw.unread_replies ?? record?.unreadReplies ?? 0; thread.unreadMentions = raw.unread_mentions ?? record?.unreadMentions ?? 0; thread.viewedAt = record?.viewedAt || 0; @@ -52,7 +55,7 @@ export const transformThreadRecord = ({action, database, value}: TransformerArgs tableName: THREAD, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -62,14 +65,17 @@ export const transformThreadRecord = ({action, database, value}: TransformerArgs * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformThreadParticipantRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as ThreadParticipant; - const record = value.record as ThreadParticipantModel; +export const transformThreadParticipantRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } // id of participant comes from server response const fieldsMapper = (participant: ThreadParticipantModel) => { - participant._raw.id = isCreateAction ? `${raw.thread_id}-${raw.id}` : record.id; + participant._raw.id = isCreateAction ? `${raw.thread_id}-${raw.id}` : record!.id; participant.threadId = raw.thread_id; participant.userId = raw.id; }; @@ -80,16 +86,19 @@ export const transformThreadParticipantRecord = ({action, database, value}: Tran tableName: THREAD_PARTICIPANT, value, fieldsMapper, - }) as Promise; + }); }; -export const transformThreadInTeamRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as ThreadInTeam; - const record = value.record as ThreadInTeamModel; +export const transformThreadInTeamRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } const fieldsMapper = (threadInTeam: ThreadInTeamModel) => { - threadInTeam._raw.id = isCreateAction ? `${raw.thread_id}-${raw.team_id}` : record.id; + threadInTeam._raw.id = isCreateAction ? `${raw.thread_id}-${raw.team_id}` : record!.id; threadInTeam.threadId = raw.thread_id; threadInTeam.teamId = raw.team_id; }; @@ -100,18 +109,21 @@ export const transformThreadInTeamRecord = ({action, database, value}: Transform tableName: THREADS_IN_TEAM, value, fieldsMapper, - }) as Promise; + }); }; -export const transformTeamThreadsSyncRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as TeamThreadsSync; - const record = value.record as TeamThreadsSyncModel; +export const transformTeamThreadsSyncRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; + if (!isCreateAction && !record) { + throw new Error('Record not found for non create action'); + } const fieldsMapper = (teamThreadsSync: TeamThreadsSyncModel) => { - teamThreadsSync._raw.id = isCreateAction ? (raw?.id ?? teamThreadsSync.id) : record.id; - teamThreadsSync.earliest = raw.earliest || record?.earliest; - teamThreadsSync.latest = raw.latest || record?.latest; + teamThreadsSync._raw.id = isCreateAction ? (raw?.id ?? teamThreadsSync.id) : record!.id; + teamThreadsSync.earliest = raw.earliest || record?.earliest || 0; + teamThreadsSync.latest = raw.latest || record?.latest || 0; }; return prepareBaseRecord({ @@ -120,5 +132,5 @@ export const transformTeamThreadsSyncRecord = ({action, database, value}: Transf tableName: TEAM_THREADS_SYNC, value, fieldsMapper, - }) as Promise; + }); }; diff --git a/app/database/operator/server_data_operator/transformers/user.ts b/app/database/operator/server_data_operator/transformers/user.ts index c98afae1c..20233b38f 100644 --- a/app/database/operator/server_data_operator/transformers/user.ts +++ b/app/database/operator/server_data_operator/transformers/user.ts @@ -17,14 +17,14 @@ const {PREFERENCE, USER} = MM_TABLES.SERVER; * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformUserRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as UserProfile; - const record = value.record as UserModel; +export const transformUserRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; // id of user comes from server response const fieldsMapper = (user: UserModel) => { - user._raw.id = isCreateAction ? (raw?.id ?? user.id) : record.id; + user._raw.id = isCreateAction ? (raw?.id ?? user.id) : record?.id || ''; user.authService = raw.auth_service; user.deleteAt = raw.delete_at; user.updateAt = raw.update_at; @@ -71,7 +71,7 @@ export const transformUserRecord = ({action, database, value}: TransformerArgs): tableName: USER, value, fieldsMapper, - }) as Promise; + }); }; /** @@ -81,14 +81,14 @@ export const transformUserRecord = ({action, database, value}: TransformerArgs): * @param {RecordPair} operator.value * @returns {Promise} */ -export const transformPreferenceRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as PreferenceType; - const record = value.record as PreferenceModel; +export const transformPreferenceRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw; + const record = value.record; const isCreateAction = action === OperationType.CREATE; // id of preference comes from server response const fieldsMapper = (preference: PreferenceModel) => { - preference._raw.id = isCreateAction ? preference.id : record.id; + preference._raw.id = isCreateAction ? preference.id : record?.id || ''; preference.category = raw.category; preference.name = raw.name; preference.userId = raw.user_id; @@ -101,6 +101,6 @@ export const transformPreferenceRecord = ({action, database, value}: Transformer tableName: PREFERENCE, value, fieldsMapper, - }) as Promise; + }); }; diff --git a/app/database/operator/utils/general.ts b/app/database/operator/utils/general.ts index 5d33bd4d2..2174fb66d 100644 --- a/app/database/operator/utils/general.ts +++ b/app/database/operator/utils/general.ts @@ -5,10 +5,6 @@ import {MM_TABLES} from '@constants/database'; import type {Model} from '@nozbe/watermelondb'; import type {IdenticalRecordArgs, RangeOfValueArgs, RecordPair, RetrieveRecordsArgs} from '@typings/database/database'; -import type ChannelModel from '@typings/database/models/servers/channel'; -import type PostModel from '@typings/database/models/servers/post'; -import type TeamModel from '@typings/database/models/servers/team'; -import type UserModel from '@typings/database/models/servers/user'; const {CHANNEL, POST, TEAM, USER} = MM_TABLES.SERVER; @@ -21,13 +17,12 @@ const {CHANNEL, POST, TEAM, USER} = MM_TABLES.SERVER; * @param {Model} identicalRecord.existingRecord * @returns {boolean} */ -export const getValidRecordsForUpdate = ({tableName, newValue, existingRecord}: IdenticalRecordArgs) => { +export const getValidRecordsForUpdate = ({tableName, newValue, existingRecord}: IdenticalRecordArgs) => { const guardTables = [CHANNEL, POST, TEAM, USER]; if (guardTables.includes(tableName)) { - type Raw = Post | UserProfile | Team | SlashCommand | Channel; - type ExistingRecord = PostModel | UserModel | TeamModel | ChannelModel; - - const shouldUpdate = (newValue as Raw).update_at === (existingRecord as ExistingRecord).updateAt; + const newValueUpdateAt = ('update_at' in newValue) ? newValue.update_at : 0; + const existingRecordUpdateAt = ('updateAt' in existingRecord) ? existingRecord.updateAt : 0; + const shouldUpdate = newValueUpdateAt === existingRecordUpdateAt; if (shouldUpdate) { return { @@ -66,7 +61,7 @@ export const getRangeOfValues = ({fieldName, raws}: RangeOfValueArgs) => { * @param {any[]} raws * @returns {{record: undefined, raw: any}[]} */ -export const getRawRecordPairs = (raws: any[]): RecordPair[] => { +export const getRawRecordPairs = (raws: R[]): Array> => { return raws.map((raw) => { return {raw, record: undefined}; }); @@ -78,7 +73,7 @@ export const getRawRecordPairs = (raws: any[]): RecordPair[] => { * @param {RawValue[]} raws * @param {string} key */ -export const getUniqueRawsBy = ({raws, key}: { raws: RawValue[]; key: string}) => { +export const getUniqueRawsBy = ({raws, key}: { raws: T[]; key: string}) => { return [...new Map(raws.map((item) => { const curItemKey = item[key as keyof typeof item]; return [curItemKey, item]; diff --git a/app/database/operator/utils/reaction.ts b/app/database/operator/utils/reaction.ts index 6252ffce2..d8e4ae5b7 100644 --- a/app/database/operator/utils/reaction.ts +++ b/app/database/operator/utils/reaction.ts @@ -28,7 +28,7 @@ export const sanitizeReactions = async ({database, post_id, rawReactions, skipSy // similarObjects: Contains objects that are in both the RawReaction array and in the Reaction table const similarObjects = new Set(); - const createReactions: RecordPair[] = []; + const createReactions: Array> = []; const reactionsMap = reactions.reduce((result: Record, reaction) => { result[`${reaction.userId}-${reaction.emojiName}`] = reaction; diff --git a/app/database/operator/utils/thread.ts b/app/database/operator/utils/thread.ts index 6f4b17173..39b6d7601 100644 --- a/app/database/operator/utils/thread.ts +++ b/app/database/operator/utils/thread.ts @@ -38,7 +38,7 @@ export const sanitizeThreadParticipants = async ({database, skipSync, thread_id, // similarObjects: Contains objects that are in both the RawParticipant array and in the ThreadParticipant table const similarObjects = new Set(); - const createParticipants: RecordPair[] = []; + const createParticipants: Array> = []; const participantsMap = participants.reduce((result: Record, participant) => { result[participant.userId] = participant; return result; diff --git a/app/hooks/markdown.test.ts b/app/hooks/markdown.test.ts index a22569c0a..5450b627f 100644 --- a/app/hooks/markdown.test.ts +++ b/app/hooks/markdown.test.ts @@ -2,12 +2,11 @@ // See LICENSE.txt for license information. import {renderHook} from '@testing-library/react-hooks'; +import TestHelper from '@test/test_helper'; import {getUsersByUsername} from '@utils/user'; import {useMemoMentionedUser, useMemoMentionedGroup} from './markdown'; -import type {GroupModel, UserModel} from '@database/models/server'; - jest.mock('@utils/user', () => ({ getUsersByUsername: jest.fn(), })); @@ -16,7 +15,7 @@ const mockGetUsersByUsername = jest.mocked(getUsersByUsername); describe('useMemoMentionedUser', () => { it('should return the mentioned user if found', () => { - const users = [{username: 'john_doe'} as UserModel]; + const users = [TestHelper.fakeUserModel({username: 'john_doe'})]; const mentionName = 'john_doe'; const usersByUsername = {john_doe: users[0]}; mockGetUsersByUsername.mockReturnValue(usersByUsername); @@ -27,7 +26,7 @@ describe('useMemoMentionedUser', () => { }); it('should return undefined if the mentioned user is not found', () => { - const users = [{username: 'john_doe'} as UserModel]; + const users = [TestHelper.fakeUserModel({username: 'john_doe'})]; const mentionName = 'jane_doe'; const usersByUsername = {john_doe: users[0]}; mockGetUsersByUsername.mockReturnValue(usersByUsername); @@ -38,7 +37,7 @@ describe('useMemoMentionedUser', () => { }); it('should trim trailing punctuation from the mention name', () => { - const users = [{username: 'john_doe'} as UserModel]; + const users = [TestHelper.fakeUserModel({username: 'john_doe'})]; const mentionName = 'john_doe.'; const usersByUsername = {john_doe: users[0]}; mockGetUsersByUsername.mockReturnValue(usersByUsername); @@ -51,7 +50,7 @@ describe('useMemoMentionedUser', () => { describe('useMemoMentionedGroup', () => { it('should return the mentioned group if found', () => { - const groups = [{name: 'developers'} as GroupModel]; + const groups = [TestHelper.fakeGroupModel({name: 'developers'})]; const user = undefined; const mentionName = 'developers'; @@ -61,7 +60,7 @@ describe('useMemoMentionedGroup', () => { }); it('should return undefined if the mentioned group is not found', () => { - const groups = [{name: 'developers'} as GroupModel]; + const groups = [TestHelper.fakeGroupModel({name: 'developers'})]; const user = undefined; const mentionName = 'designers'; @@ -71,8 +70,8 @@ describe('useMemoMentionedGroup', () => { }); it('should return undefined if the user is defined', () => { - const groups = [{name: 'developers'} as GroupModel]; - const user = {username: 'john_doe'} as UserModel; + const groups = [TestHelper.fakeGroupModel({name: 'developers'})]; + const user = TestHelper.fakeUserModel({username: 'john_doe'}); const mentionName = 'developers'; const {result} = renderHook(() => useMemoMentionedGroup(groups, user, mentionName)); @@ -81,7 +80,7 @@ describe('useMemoMentionedGroup', () => { }); it('should trim trailing punctuation from the mention name', () => { - const groups = [{name: 'developers'} as GroupModel]; + const groups = [TestHelper.fakeGroupModel({name: 'developers'})]; const user = undefined; const mentionName = 'developers.'; diff --git a/app/products/calls/state/actions.test.ts b/app/products/calls/state/actions.test.ts index bcaec7d73..474a505fb 100644 --- a/app/products/calls/state/actions.test.ts +++ b/app/products/calls/state/actions.test.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +/* eslint-disable max-lines */ + import assert from 'assert'; import {act, renderHook} from '@testing-library/react-hooks'; // Use instead of react-native version due to different behavior. Consider migrating @@ -70,13 +72,15 @@ import { import {License} from '@constants'; import Calls from '@constants/calls'; import DatabaseManager from '@database/manager'; +import {getUserById} from '@queries/servers/user'; +import TestHelper from '@test/test_helper'; import type {CallJobState, LiveCaptionData} from '@mattermost/calls/lib/types'; -import type UserModel from '@typings/database/models/servers/user'; jest.mock('@calls/alerts'); jest.mock('@constants/calls', () => ({ + ...jest.requireActual('@constants/calls'), CALL_QUALITY_RESET_MS: 100, })); @@ -97,12 +101,13 @@ jest.mock('@queries/servers/channel', () => ({ })); jest.mock('@queries/servers/user', () => ({ - getUserById: jest.fn(() => Promise.resolve({ - username: 'user-5', - })), + getUserById: jest.fn(), getCurrentUser: jest.fn(), })); +const user5 = TestHelper.fakeUserModel({username: 'user-5'}); +jest.mocked(getUserById).mockResolvedValue(user5); + jest.mock('react-native-navigation', () => ({ Navigation: { pop: jest.fn(() => Promise.resolve({ @@ -305,14 +310,14 @@ describe('useCallsState', () => { }, }; const expectedChannelsWithCallsState = initialChannelsWithCallsState; - const expectedCurrentCallState = { + const expectedCurrentCallState: CurrentCall = { ...initialCurrentCallState, ...expectedCallsState['channel-1'], - } as CurrentCall; + }; // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()]; + return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()] as const; }); act(() => { setCallsState('server1', initialCallsState); @@ -325,11 +330,11 @@ describe('useCallsState', () => { // test act(() => userJoinedCall('server1', 'channel-1', 'user-3', 'session3')); - assert.deepEqual((result.current[0] as CallsState).calls, expectedCallsState); + assert.deepEqual(result.current[0].calls, expectedCallsState); assert.deepEqual(result.current[1], expectedChannelsWithCallsState); assert.deepEqual(result.current[2], expectedCurrentCallState); act(() => userJoinedCall('server1', 'invalid-channel', 'user-1', 'session1')); - assert.deepEqual((result.current[0] as CallsState).calls, expectedCallsState); + assert.deepEqual(result.current[0].calls, expectedCallsState); assert.deepEqual(result.current[1], expectedChannelsWithCallsState); assert.deepEqual(result.current[2], expectedCurrentCallState); }); @@ -365,14 +370,14 @@ describe('useCallsState', () => { }, }; const expectedChannelsWithCallsState = initialChannelsWithCallsState; - const expectedCurrentCallState = { + const expectedCurrentCallState: CurrentCall = { ...initialCurrentCallState, ...expectedCallsState['channel-1'], - } as CurrentCall; + }; // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()]; + return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()] as const; }); act(() => { setCallsState('server1', initialCallsState); @@ -385,11 +390,11 @@ describe('useCallsState', () => { // test act(() => userLeftCall('server1', 'channel-1', 'session1')); - assert.deepEqual((result.current[0] as CallsState).calls, expectedCallsState); + assert.deepEqual(result.current[0].calls, expectedCallsState); assert.deepEqual(result.current[1], expectedChannelsWithCallsState); assert.deepEqual(result.current[2], expectedCurrentCallState); act(() => userLeftCall('server1', 'invalid-channel', 'session2')); - assert.deepEqual((result.current[0] as CallsState).calls, expectedCallsState); + assert.deepEqual(result.current[0].calls, expectedCallsState); assert.deepEqual(result.current[1], expectedChannelsWithCallsState); assert.deepEqual(result.current[2], expectedCurrentCallState); }); @@ -438,7 +443,7 @@ describe('useCallsState', () => { // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()]; + return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()] as const; }); act(() => { setCallsState('server1', initialCallsState); @@ -451,7 +456,7 @@ describe('useCallsState', () => { // test act(() => userLeftCall('server1', 'channel-1', 'session1')); - assert.deepEqual((result.current[0] as CallsState).calls, expectedCallsState); + assert.deepEqual(result.current[0].calls, expectedCallsState); assert.deepEqual(result.current[1], expectedChannelsWithCallsState); assert.deepEqual(result.current[2], expectedCurrentCallState); }); @@ -469,7 +474,7 @@ describe('useCallsState', () => { await DatabaseManager.init(['server1']); const {result} = renderHook(() => { - return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()]; + return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()] as const; }); assert.deepEqual(result.current[0], DefaultCallsState); assert.deepEqual(result.current[1], {}); @@ -480,7 +485,7 @@ describe('useCallsState', () => { setCurrentCall(initialCurrentCallState); await callStarted('server1', call1); }); - assert.deepEqual((result.current[0] as CallsState).calls, {'channel-1': call1}); + assert.deepEqual(result.current[0].calls, {'channel-1': call1}); assert.deepEqual(result.current[1], {'channel-1': true}); assert.deepEqual(result.current[2], initialCurrentCallState); expect(updateThreadFollowing).toBeCalled(); @@ -533,7 +538,7 @@ describe('useCallsState', () => { // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()]; + return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()] as const; }); act(() => { setCallsState('server1', initialCallsState); @@ -546,19 +551,19 @@ describe('useCallsState', () => { // test act(() => setUserMuted('server1', 'channel-1', 'session1', true)); - assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].sessions.session1.muted, true); - assert.deepEqual((result.current[2] as CurrentCall | null)?.sessions.session1.muted, true); + assert.deepEqual((result.current[0]).calls['channel-1'].sessions.session1.muted, true); + assert.deepEqual((result.current[2])?.sessions.session1.muted, true); act(() => { setUserMuted('server1', 'channel-1', 'session1', false); setUserMuted('server1', 'channel-1', 'session2', false); }); - assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].sessions.session1.muted, false); - assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].sessions.session2.muted, false); - assert.deepEqual((result.current[2] as CurrentCall | null)?.sessions.session1.muted, false); - assert.deepEqual((result.current[2] as CurrentCall | null)?.sessions.session2.muted, false); + assert.deepEqual((result.current[0]).calls['channel-1'].sessions.session1.muted, false); + assert.deepEqual((result.current[0]).calls['channel-1'].sessions.session2.muted, false); + assert.deepEqual((result.current[2])?.sessions.session1.muted, false); + assert.deepEqual((result.current[2])?.sessions.session2.muted, false); act(() => setUserMuted('server1', 'channel-1', 'session2', true)); - assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].sessions.session2.muted, true); - assert.deepEqual((result.current[2] as CurrentCall | null)?.sessions.session2.muted, true); + assert.deepEqual((result.current[0]).calls['channel-1'].sessions.session2.muted, true); + assert.deepEqual((result.current[2])?.sessions.session2.muted, true); assert.deepEqual(result.current[0], initialCallsState); act(() => setUserMuted('server1', 'invalid-channel', 'session1', true)); assert.deepEqual(result.current[0], initialCallsState); @@ -579,7 +584,7 @@ describe('useCallsState', () => { // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()]; + return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()] as const; }); act(() => { setCallsState('server1', initialCallsState); @@ -592,9 +597,9 @@ describe('useCallsState', () => { // test act(() => setCallScreenOn('server1', 'channel-1', 'session1')); - assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].screenOn, 'session1'); + assert.deepEqual((result.current[0]).calls['channel-1'].screenOn, 'session1'); assert.deepEqual(result.current[1], initialChannelsWithCallsState); - assert.deepEqual((result.current[2] as CurrentCall).screenOn, 'session1'); + assert.deepEqual((result.current[2])?.screenOn, 'session1'); act(() => setCallScreenOff('server1', 'channel-1', 'session1')); assert.deepEqual(result.current[0], initialCallsState); assert.deepEqual(result.current[1], initialChannelsWithCallsState); @@ -644,7 +649,7 @@ describe('useCallsState', () => { // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useCurrentCall()]; + return [useCallsState('server1'), useCurrentCall()] as const; }); act(() => { setCallsState('server1', initialCallsState); @@ -655,12 +660,12 @@ describe('useCallsState', () => { // test act(() => setRaisedHand('server1', 'channel-1', 'session2', 345)); - assert.deepEqual((result.current[0] as CallsState).calls, expectedCalls); - assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState); + assert.deepEqual((result.current[0]).calls, expectedCalls); + assert.deepEqual((result.current[1]), expectedCurrentCallState); act(() => setRaisedHand('server1', 'invalid-channel', 'session1', 345)); - assert.deepEqual((result.current[0] as CallsState).calls, expectedCalls); - assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState); + assert.deepEqual((result.current[0]).calls, expectedCalls); + assert.deepEqual((result.current[1]), expectedCurrentCallState); // unraise hand: act(() => setRaisedHand('server1', 'channel-1', 'session2', 0)); @@ -772,7 +777,7 @@ describe('useCallsState', () => { // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useCurrentCall()]; + return [useCallsState('server1'), useCurrentCall()] as const; }); act(() => setCallsState('server1', initialCallsState)); assert.deepEqual(result.current[0], initialCallsState); @@ -781,9 +786,9 @@ describe('useCallsState', () => { // test joining a call and setting url: act(() => newCurrentCall('server1', 'channel-1', 'myUserId')); act(() => userJoinedCall('server1', 'channel-1', 'myUserId', 'mySessionId')); - assert.deepEqual((result.current[1] as CurrentCall | null)?.screenShareURL, ''); + assert.deepEqual((result.current[1])?.screenShareURL, ''); act(() => setScreenShareURL('testUrl')); - assert.deepEqual((result.current[1] as CurrentCall | null)?.screenShareURL, 'testUrl'); + assert.deepEqual((result.current[1])?.screenShareURL, 'testUrl'); act(() => { myselfLeftCall(); @@ -817,7 +822,7 @@ describe('useCallsState', () => { // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useCurrentCall()]; + return [useCallsState('server1'), useCurrentCall()] as const; }); act(() => setCallsState('server1', initialCallsState)); assert.deepEqual(result.current[0], initialCallsState); @@ -826,11 +831,11 @@ describe('useCallsState', () => { // test act(() => newCurrentCall('server1', 'channel-1', 'myUserId')); act(() => userJoinedCall('server1', 'channel-1', 'myUserId', 'mySessionId')); - assert.deepEqual((result.current[1] as CurrentCall | null)?.speakerphoneOn, false); + assert.deepEqual((result.current[1])?.speakerphoneOn, false); act(() => setSpeakerPhone(true)); - assert.deepEqual((result.current[1] as CurrentCall | null)?.speakerphoneOn, true); + assert.deepEqual((result.current[1])?.speakerphoneOn, true); act(() => setSpeakerPhone(false)); - assert.deepEqual((result.current[1] as CurrentCall | null)?.speakerphoneOn, false); + assert.deepEqual((result.current[1])?.speakerphoneOn, false); assert.deepEqual(result.current[0], expectedCallsState); act(() => { myselfLeftCall(); @@ -872,7 +877,7 @@ describe('useCallsState', () => { // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useCurrentCall()]; + return [useCallsState('server1'), useCurrentCall()] as const; }); act(() => setCallsState('server1', initialCallsState)); assert.deepEqual(result.current[0], initialCallsState); @@ -881,10 +886,10 @@ describe('useCallsState', () => { // test act(() => newCurrentCall('server1', 'channel-1', 'myUserId')); act(() => userJoinedCall('server1', 'channel-1', 'myUserId', 'mySessionId')); - assert.deepEqual((result.current[1] as CurrentCall | null)?.audioDeviceInfo, defaultAudioDeviceInfo); + assert.deepEqual((result.current[1])?.audioDeviceInfo, defaultAudioDeviceInfo); act(() => setAudioDeviceInfo(newAudioDeviceInfo)); - assert.deepEqual((result.current[1] as CurrentCall | null)?.audioDeviceInfo, newAudioDeviceInfo); - assert.deepEqual((result.current[1] as CurrentCall | null)?.speakerphoneOn, false); + assert.deepEqual((result.current[1])?.audioDeviceInfo, newAudioDeviceInfo); + assert.deepEqual((result.current[1])?.speakerphoneOn, false); assert.deepEqual(result.current[0], expectedCallsState); act(() => { myselfLeftCall(); @@ -1025,7 +1030,7 @@ describe('useCallsState', () => { // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useCurrentCall()]; + return [useCallsState('server1'), useCurrentCall()] as const; }); act(() => setCallsState('server1', initialCallsState)); assert.deepEqual(result.current[0], initialCallsState); @@ -1041,8 +1046,8 @@ describe('useCallsState', () => { // call quality goes bad act(() => processMeanOpinionScore(3.4999)); - assert.deepEqual((result.current[1] as CurrentCall).callQualityAlert, true); - assert.equal((result.current[1] as CurrentCall).callQualityAlertDismissed, 0); + assert.deepEqual((result.current[1])?.callQualityAlert, true); + assert.equal((result.current[1])?.callQualityAlertDismissed, 0); // call quality goes good act(() => processMeanOpinionScore(4)); @@ -1050,26 +1055,26 @@ describe('useCallsState', () => { // call quality goes bad act(() => processMeanOpinionScore(3.499)); - assert.deepEqual((result.current[1] as CurrentCall).callQualityAlert, true); - assert.equal((result.current[1] as CurrentCall).callQualityAlertDismissed, 0); + assert.deepEqual((result.current[1])?.callQualityAlert, true); + assert.equal((result.current[1])?.callQualityAlertDismissed, 0); // dismiss call quality alert const timeNow = Date.now(); act(() => setCallQualityAlertDismissed()); - assert.deepEqual((result.current[1] as CurrentCall).callQualityAlert, false); - assert.equal((result.current[1] as CurrentCall).callQualityAlertDismissed >= timeNow && - (result.current[1] as CurrentCall).callQualityAlertDismissed <= Date.now(), true); + assert.deepEqual((result.current[1])?.callQualityAlert, false); + assert.equal((result.current[1]!).callQualityAlertDismissed >= timeNow && + (result.current[1]!).callQualityAlertDismissed <= Date.now(), true); // call quality goes bad, but we're not past the dismissed limit act(() => processMeanOpinionScore(3.4999)); - assert.deepEqual((result.current[1] as CurrentCall).callQualityAlert, false); + assert.deepEqual((result.current[1])?.callQualityAlert, false); // test that the dismiss expired await act(async () => { await new Promise((r) => setTimeout(r, 101)); processMeanOpinionScore(3.499); }); - assert.deepEqual((result.current[1] as CurrentCall).callQualityAlert, true); + assert.deepEqual((result.current[1])?.callQualityAlert, true); }); it('voiceOn and Off', async () => { @@ -1265,7 +1270,7 @@ describe('useCallsState', () => { // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useCurrentCall()]; + return [useCallsState('server1'), useCurrentCall()] as const; }); act(() => { setCallsState('server1', initialCallsState); @@ -1276,11 +1281,11 @@ describe('useCallsState', () => { // test act(() => setRecordingState('server1', 'channel-1', recState)); - assert.deepEqual((result.current[0] as CallsState), expectedCallsState); - assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState); + assert.deepEqual((result.current[0]), expectedCallsState); + assert.deepEqual((result.current[1]), expectedCurrentCallState); act(() => setRecordingState('server1', 'channel-2', recState)); - assert.deepEqual((result.current[0] as CallsState).calls['channel-2'], {...call2, recState}); - assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState); + assert.deepEqual((result.current[0]).calls['channel-2'], {...call2, recState}); + assert.deepEqual((result.current[1]), expectedCurrentCallState); act(() => setRecordingState('server1', 'channel-1', {...recState, start_at: recState.start_at + 1})); expect(needsRecordingAlert).toBeCalled(); }); @@ -1318,7 +1323,7 @@ describe('useCallsState', () => { // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useCurrentCall()]; + return [useCallsState('server1'), useCurrentCall()] as const; }); act(() => { setCallsState('server1', initialCallsState); @@ -1329,11 +1334,11 @@ describe('useCallsState', () => { // test act(() => setHost('server1', 'channel-1', 'user-52')); - assert.deepEqual((result.current[0] as CallsState), expectedCallsState); - assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState); + assert.deepEqual((result.current[0]), expectedCallsState); + assert.deepEqual((result.current[1]), expectedCurrentCallState); act(() => setHost('server1', 'channel-2', 'user-1923')); - assert.deepEqual((result.current[0] as CallsState).calls['channel-2'], {...call2, hostId: 'user-1923'}); - assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState); + assert.deepEqual((result.current[0]).calls['channel-2'], {...call2, hostId: 'user-1923'}); + assert.deepEqual((result.current[1]), expectedCurrentCallState); act(() => setHost('server1', 'channel-1', 'myUserId')); expect(needsRecordingAlert).toBeCalled(); }); @@ -1352,7 +1357,7 @@ describe('useCallsState', () => { incomingCalls: [{ callID: 'callDM', callerID: 'user-5', - callerModel: {username: 'user-5'} as UserModel, + callerModel: user5, channelID: 'channel-private', myUserId: 'myId', serverUrl: 'server1', @@ -1452,7 +1457,7 @@ describe('useCallsState', () => { incomingCalls: [{ callID: 'callDM', callerID: 'user-5', - callerModel: {username: 'user-5'} as UserModel, + callerModel: TestHelper.fakeUserModel({username: 'user-5'}), channelID: 'channel-private', myUserId: 'myId', serverUrl: 'server1', @@ -1576,7 +1581,7 @@ describe('useCallsState', () => { // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()]; + return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()] as const; }); await act(async () => { await setCallsState('server1', initialCallsState); @@ -1599,7 +1604,7 @@ describe('useCallsState', () => { // test: update existing call const updatedCall1 = {...call1, screenOn: 'newScreen'}; await act(async () => setCallForChannel('server1', 'channel-1', updatedCall1)); - assert.deepEqual((result.current[0] as CallsState).calls['channel-1'], updatedCall1); + assert.deepEqual(result.current[0].calls['channel-1'], updatedCall1); assert.deepEqual(result.current[2], {...initialCurrentCallState, screenOn: 'newScreen'}); // test: remove call @@ -1613,7 +1618,7 @@ describe('useCallsState', () => { // test: just update enabled state await act(async () => setCallForChannel('server1', 'channel-3', undefined, true)); - assert.deepEqual((result.current[0] as CallsState).enabled['channel-3'], true); + assert.deepEqual(result.current[0].enabled['channel-3'], true); assert.deepEqual(result.current[1], {'channel-2': true}); }); @@ -1663,7 +1668,7 @@ describe('useCallsState', () => { // setup const {result} = renderHook(() => { - return [useCallsState('server1'), useCurrentCall()]; + return [useCallsState('server1'), useCurrentCall()] as const; }); act(() => { setCallsState('server1', initialCallsState); @@ -1679,9 +1684,9 @@ describe('useCallsState', () => { receivedCaption('server1', caption1user2Data); }); assert.deepEqual(result.current[0], initialCallsState); - let currentCall = result.current[1] as CurrentCall; - assert.equal(currentCall.captions.session1.text, 'caption 2'); - assert.equal(currentCall.captions.session2.text, 'caption 1 user 2'); + let currentCall = result.current[1]; + assert.equal(currentCall?.captions.session1.text, 'caption 2'); + assert.equal(currentCall?.captions.session2.text, 'caption 1 user 2'); // test sending the next captions for users 1 and 2 act(() => { @@ -1689,8 +1694,8 @@ describe('useCallsState', () => { receivedCaption('server1', caption2user2Data); }); assert.deepEqual(result.current[0], initialCallsState); - currentCall = result.current[1] as CurrentCall; - assert.equal(currentCall.captions.session1.text, 'caption 3'); - assert.equal(currentCall.captions.session2.text, 'caption 2 user 2'); + currentCall = result.current[1]; + assert.equal(currentCall?.captions.session1.text, 'caption 3'); + assert.equal(currentCall?.captions.session2.text, 'caption 2 user 2'); }); }); diff --git a/app/products/calls/utils.test.ts b/app/products/calls/utils.test.ts index b641d4f7d..b9f3ef02a 100644 --- a/app/products/calls/utils.test.ts +++ b/app/products/calls/utils.test.ts @@ -7,8 +7,9 @@ import {Alert} from 'react-native'; import {SelectedTrackType} from 'react-native-video'; import {type CallsConfigState, DefaultCallsConfig} from '@calls/types/calls'; -import {License, Post} from '@constants'; +import {License, Post, Preferences} from '@constants'; import {NOTIFICATION_SUB_TYPE} from '@constants/push_notification'; +import TestHelper from '@test/test_helper'; import { getICEServersConfigs, @@ -28,7 +29,6 @@ import { getTranscriptionUri, } from './utils'; -import type UserModel from '@typings/database/models/servers/user'; import type {IntlShape} from 'react-intl'; describe('getICEServersConfigs', () => { @@ -136,21 +136,21 @@ describe('sortSessions', () => { userId: 'user1', muted: true, raisedHand: 0, - userModel: {username: 'charlie'} as UserModel, + userModel: TestHelper.fakeUserModel({username: 'charlie'}), }, 2: { sessionId: '2', userId: 'user2', muted: true, raisedHand: 0, - userModel: {username: 'alice'} as UserModel, + userModel: TestHelper.fakeUserModel({username: 'alice'}), }, 3: { sessionId: '3', userId: 'user3', muted: true, raisedHand: 0, - userModel: {username: 'bob'} as UserModel, + userModel: TestHelper.fakeUserModel({username: 'bob'}), }, }; @@ -165,21 +165,21 @@ describe('sortSessions', () => { userId: 'user1', muted: true, raisedHand: 0, - userModel: {username: 'a'} as UserModel, + userModel: TestHelper.fakeUserModel({username: 'a'}), }, 2: { sessionId: '2', userId: 'user2', muted: false, raisedHand: 0, - userModel: {username: 'b'} as UserModel, + userModel: TestHelper.fakeUserModel({username: 'b'}), }, 3: { sessionId: '3', userId: 'user3', muted: true, raisedHand: 1000, - userModel: {username: 'c'} as UserModel, + userModel: TestHelper.fakeUserModel({username: 'c'}), }, }; @@ -194,14 +194,14 @@ describe('sortSessions', () => { userId: 'user1', muted: true, raisedHand: 2000, // Raised hand second - userModel: {username: 'a'} as UserModel, + userModel: TestHelper.fakeUserModel({username: 'a'}), }, 2: { sessionId: '2', userId: 'user2', muted: true, raisedHand: 1000, // Raised hand first - userModel: {username: 'b'} as UserModel, + userModel: TestHelper.fakeUserModel({username: 'b'}), }, }; @@ -252,14 +252,14 @@ describe('getHandsRaisedNames', () => { sessionId: '1', userId: 'user1', raisedHand: 2000, - userModel: {username: 'alice'} as UserModel, + userModel: TestHelper.fakeUserModel({username: 'alice'}), muted: false, }, { sessionId: '2', userId: 'user2', raisedHand: 1000, - userModel: {username: 'bob'} as UserModel, + userModel: TestHelper.fakeUserModel({username: 'bob'}), muted: false, }, ]; @@ -274,7 +274,7 @@ describe('getHandsRaisedNames', () => { sessionId: '1', userId: 'user1', raisedHand: 1000, - userModel: {username: 'alice'} as UserModel, + userModel: TestHelper.fakeUserModel({username: 'alice'}), muted: false, }, ]; @@ -318,9 +318,8 @@ describe('isHostControlsAllowed and areGroupCallsAllowed', () => { describe('isCallsCustomMessage', () => { it('identifies calls messages', () => { - expect(isCallsCustomMessage({type: Post.POST_TYPES.CUSTOM_CALLS} as Post)).toBe(true); - expect(isCallsCustomMessage({type: 'regular_post' as unknown} as Post)).toBe(false); - expect(isCallsCustomMessage({} as Post)).toBe(false); + expect(isCallsCustomMessage(TestHelper.fakePost({type: Post.POST_TYPES.CUSTOM_CALLS}))).toBe(true); + expect(isCallsCustomMessage(TestHelper.fakePost({type: ''}))).toBe(false); // Regular post }); }); @@ -358,8 +357,9 @@ describe('errorAlert', () => { describe('makeCallsTheme', () => { it('creates calls theme from base theme', () => { const theme = { + ...Preferences.THEMES.denim, sidebarBg: '#000000', - } as Theme; + }; const callsTheme = makeCallsTheme(theme); @@ -415,7 +415,7 @@ describe('getTranscriptionUri', () => { describe('getCallPropsFromPost', () => { test('undefined props', () => { - const post = {} as Post; + const post = TestHelper.fakePost({props: undefined}); const props = getCallPropsFromPost(post); @@ -429,9 +429,7 @@ describe('getCallPropsFromPost', () => { }); test('missing props', () => { - const post = { - props: {}, - } as Post; + const post = TestHelper.fakePost({props: {}}); const props = getCallPropsFromPost(post); @@ -455,9 +453,9 @@ describe('getCallPropsFromPost', () => { recording_files: 45, }; - const post = { - props: callProps as unknown, - } as Post; + const post = TestHelper.fakePost({ + props: callProps, + }); const props = getCallPropsFromPost(post); @@ -503,9 +501,9 @@ describe('getCallPropsFromPost', () => { participants: ['userA', 'userB'], }; - const post = { - props: callProps as unknown, - } as Post; + const post = TestHelper.fakePost({ + props: callProps, + }); const props = getCallPropsFromPost(post); diff --git a/app/queries/servers/channel.test.ts b/app/queries/servers/channel.test.ts index 2b98bd74e..1f3bd5a8e 100644 --- a/app/queries/servers/channel.test.ts +++ b/app/queries/servers/channel.test.ts @@ -3,7 +3,7 @@ /* eslint-disable max-lines */ -import {Database, Model, Query, Relation, Q} from '@nozbe/watermelondb'; +import {Database, Q} from '@nozbe/watermelondb'; import {of as of$} from 'rxjs'; import {General, Permissions} from '@constants'; @@ -18,7 +18,6 @@ import {prepareChannels, prepareMyChannelsForTeam, prepareDeleteChannel, prepareDeleteBookmarks, - type ChannelMembershipsExtended, queryAllChannels, queryAllChannelsForTeam, queryAllChannelsInfo, @@ -71,13 +70,10 @@ import {queryRoles} from './role'; import {getCurrentChannelId, observeCurrentChannelId, observeCurrentUserId} from './system'; import {observeTeammateNameDisplay} from './user'; -import type {UserModel} from '@database/models/server'; import type ChannelModel from '@typings/database/models/servers/channel'; import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark'; import type ChannelInfoModel from '@typings/database/models/servers/channel_info'; import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership'; -import type MyChannelModel from '@typings/database/models/servers/my_channel'; -import type MyChannelSettingsModel from '@typings/database/models/servers/my_channel_settings'; jest.mock('./role'); jest.mock('./system'); @@ -98,10 +94,22 @@ describe('prepareChannels', () => { }); it('should prepare channels, channelInfos, channelMemberships, memberships, and myChannelSettings', () => { - const channels = [{id: 'channel1'}, {id: 'channel2'}] as Channel[]; - const channelInfos = [{id: 'channel1'}, {id: 'channel2'}] as ChannelInfo[]; - const channelMemberships = [{channel_id: 'channel1', user_id: 'user1'}, {channel_id: 'channel2', user_id: 'user2'}] as ChannelMembershipsExtended[]; - const memberships = [{channel_id: 'channel1', user_id: 'user1'}, {channel_id: 'channel2', user_id: 'user2'}] as ChannelMembership[]; + const channels = [ + TestHelper.fakeChannel({id: 'channel1'}), + TestHelper.fakeChannel({id: 'channel2'}), + ]; + const channelInfos = [ + TestHelper.fakeChannelInfo({id: 'channel1'}), + TestHelper.fakeChannelInfo({id: 'channel2'}), + ]; + const channelMemberships = [ + TestHelper.fakeChannelMember({channel_id: 'channel1', user_id: 'user1'}), + TestHelper.fakeChannelMember({channel_id: 'channel2', user_id: 'user2'}), + ]; + const memberships = [ + TestHelper.fakeChannelMember({channel_id: 'channel1', user_id: 'user1'}), + TestHelper.fakeChannelMember({channel_id: 'channel2', user_id: 'user2'}), + ]; const channelRecords = Promise.resolve([]); const channelInfoRecords = Promise.resolve([]); @@ -109,11 +117,11 @@ describe('prepareChannels', () => { const myChannelRecords = Promise.resolve([]); const myChannelSettingsRecords = Promise.resolve([]); - (operator.handleChannel as jest.Mock).mockReturnValue(channelRecords); - (operator.handleChannelInfo as jest.Mock).mockReturnValue(channelInfoRecords); - (operator.handleChannelMembership as jest.Mock).mockReturnValue(membershipRecords); - (operator.handleMyChannel as jest.Mock).mockReturnValue(myChannelRecords); - (operator.handleMyChannelSettings as jest.Mock).mockReturnValue(myChannelSettingsRecords); + jest.mocked(operator.handleChannel).mockReturnValue(channelRecords); + jest.mocked(operator.handleChannelInfo).mockReturnValue(channelInfoRecords); + jest.mocked(operator.handleChannelMembership).mockReturnValue(membershipRecords); + jest.mocked(operator.handleMyChannel).mockReturnValue(myChannelRecords); + jest.mocked(operator.handleMyChannelSettings).mockReturnValue(myChannelSettingsRecords); const result = prepareChannels(operator, channels, channelInfos, channelMemberships, memberships, true); @@ -126,7 +134,7 @@ describe('prepareChannels', () => { }); it('should return an empty array if an error occurs', () => { - (operator.handleChannel as jest.Mock).mockImplementation(() => { + jest.mocked(operator.handleChannel).mockImplementation(() => { throw new Error('Test error'); }); @@ -151,13 +159,13 @@ describe('prepareMissingChannelsForAllTeams', () => { it('should prepare missing channels for all teams', () => { const channels = [ - {id: 'channel1', header: 'header1', purpose: 'purpose1', last_post_at: 123, last_root_post_at: 456}, - {id: 'channel2', header: 'header2', purpose: 'purpose2', last_post_at: 789, last_root_post_at: 101112}, - ] as Channel[]; + TestHelper.fakeChannel({id: 'channel1', header: 'header1', purpose: 'purpose1', last_post_at: 123, last_root_post_at: 456}), + TestHelper.fakeChannel({id: 'channel2', header: 'header2', purpose: 'purpose2', last_post_at: 789, last_root_post_at: 101112}), + ]; const channelMembers = [ - {channel_id: 'channel1', user_id: 'user1'}, - {channel_id: 'channel2', user_id: 'user2'}, - ] as ChannelMembership[]; + TestHelper.fakeChannelMember({channel_id: 'channel1', user_id: 'user1'}), + TestHelper.fakeChannelMember({channel_id: 'channel2', user_id: 'user2'}), + ]; const channelRecords = Promise.resolve([]); const channelInfoRecords = Promise.resolve([]); @@ -165,11 +173,11 @@ describe('prepareMissingChannelsForAllTeams', () => { const myChannelRecords = Promise.resolve([]); const myChannelSettingsRecords = Promise.resolve([]); - (operator.handleChannel as jest.Mock).mockReturnValue(channelRecords); - (operator.handleChannelInfo as jest.Mock).mockReturnValue(channelInfoRecords); - (operator.handleChannelMembership as jest.Mock).mockReturnValue(membershipRecords); - (operator.handleMyChannel as jest.Mock).mockReturnValue(myChannelRecords); - (operator.handleMyChannelSettings as jest.Mock).mockReturnValue(myChannelSettingsRecords); + jest.mocked(operator.handleChannel).mockReturnValue(channelRecords); + jest.mocked(operator.handleChannelInfo).mockReturnValue(channelInfoRecords); + jest.mocked(operator.handleChannelMembership).mockReturnValue(membershipRecords); + jest.mocked(operator.handleMyChannel).mockReturnValue(myChannelRecords); + jest.mocked(operator.handleMyChannelSettings).mockReturnValue(myChannelSettingsRecords); const result = prepareMissingChannelsForAllTeams(operator, channels, channelMembers, true); @@ -184,31 +192,31 @@ describe('prepareMissingChannelsForAllTeams', () => { }); expect(operator.handleChannelMembership).toHaveBeenCalledWith({ channelMemberships: [ - {channel_id: 'channel1', user_id: 'user1', id: 'channel1', last_post_at: 123, last_root_post_at: 456}, - {channel_id: 'channel2', user_id: 'user2', id: 'channel2', last_post_at: 789, last_root_post_at: 101112}, + expect.objectContaining({channel_id: 'channel1', user_id: 'user1', id: 'channel1', last_post_at: 123, last_root_post_at: 456}), + expect.objectContaining({channel_id: 'channel2', user_id: 'user2', id: 'channel2', last_post_at: 789, last_root_post_at: 101112}), ], prepareRecordsOnly: true, }); expect(operator.handleMyChannel).toHaveBeenCalledWith({ channels, myChannels: [ - {channel_id: 'channel1', user_id: 'user1', id: 'channel1', last_post_at: 123, last_root_post_at: 456}, - {channel_id: 'channel2', user_id: 'user2', id: 'channel2', last_post_at: 789, last_root_post_at: 101112}, + expect.objectContaining({channel_id: 'channel1', user_id: 'user1', id: 'channel1', last_post_at: 123, last_root_post_at: 456}), + expect.objectContaining({channel_id: 'channel2', user_id: 'user2', id: 'channel2', last_post_at: 789, last_root_post_at: 101112}), ], prepareRecordsOnly: true, isCRTEnabled: true, }); expect(operator.handleMyChannelSettings).toHaveBeenCalledWith({ settings: [ - {channel_id: 'channel1', user_id: 'user1', id: 'channel1', last_post_at: 123, last_root_post_at: 456}, - {channel_id: 'channel2', user_id: 'user2', id: 'channel2', last_post_at: 789, last_root_post_at: 101112}, + expect.objectContaining({channel_id: 'channel1', user_id: 'user1', id: 'channel1', last_post_at: 123, last_root_post_at: 456}), + expect.objectContaining({channel_id: 'channel2', user_id: 'user2', id: 'channel2', last_post_at: 789, last_root_post_at: 101112}), ], prepareRecordsOnly: true, }); }); it('should return an empty array if an error occurs', () => { - (operator.handleChannel as jest.Mock).mockImplementation(() => { + jest.mocked(operator.handleChannel).mockImplementation(() => { throw new Error('Test error'); }); @@ -245,13 +253,13 @@ describe('prepareMyChannelsForTeam', () => { it('should prepare my channels for a team', async () => { const teamId = 'team_id'; const channels = [ - {id: 'channel1', header: 'header1', purpose: 'purpose1', last_post_at: 123, last_root_post_at: 456}, - {id: 'channel2', header: 'header2', purpose: 'purpose2', last_post_at: 789, last_root_post_at: 101112}, - ] as Channel[]; + TestHelper.fakeChannel({id: 'channel1', header: 'header1', purpose: 'purpose1', last_post_at: 123, last_root_post_at: 456}), + TestHelper.fakeChannel({id: 'channel2', header: 'header2', purpose: 'purpose2', last_post_at: 789, last_root_post_at: 101112}), + ]; const channelMembers = [ - {channel_id: 'channel1', user_id: 'user1'}, - {channel_id: 'channel2', user_id: 'user2'}, - ] as ChannelMembership[]; + TestHelper.fakeChannelMember({channel_id: 'channel1', user_id: 'user1'}), + TestHelper.fakeChannelMember({channel_id: 'channel2', user_id: 'user2'}), + ]; const allChannelsForTeam = { channel1: {id: 'channel1'}, @@ -274,11 +282,11 @@ describe('prepareMyChannelsForTeam', () => { const myChannelRecords = Promise.resolve([]); const myChannelSettingsRecords = Promise.resolve([]); - (operator.handleChannel as jest.Mock).mockReturnValue(channelRecords); - (operator.handleChannelInfo as jest.Mock).mockReturnValue(channelInfoRecords); - (operator.handleChannelMembership as jest.Mock).mockReturnValue(membershipRecords); - (operator.handleMyChannel as jest.Mock).mockReturnValue(myChannelRecords); - (operator.handleMyChannelSettings as jest.Mock).mockReturnValue(myChannelSettingsRecords); + jest.mocked(operator.handleChannel).mockReturnValue(channelRecords); + jest.mocked(operator.handleChannelInfo).mockReturnValue(channelInfoRecords); + jest.mocked(operator.handleChannelMembership).mockReturnValue(membershipRecords); + jest.mocked(operator.handleMyChannel).mockReturnValue(myChannelRecords); + jest.mocked(operator.handleMyChannelSettings).mockReturnValue(myChannelSettingsRecords); query = jest.fn().mockResolvedValueOnce(Object.entries(allChannelsForTeam).map((c) => c[1])).mockResolvedValueOnce(Object.entries(allChannelsInfoForTeam).map((i) => i[1])); const result = await prepareMyChannelsForTeam(operator, teamId, channels, channelMembers, true); @@ -288,31 +296,31 @@ describe('prepareMyChannelsForTeam', () => { expect(operator.handleChannel).toHaveBeenCalledWith({channels, prepareRecordsOnly: true}); expect(operator.handleChannelInfo).toHaveBeenCalledWith({ channelInfos: [ - {id: 'channel1', header: 'header1', purpose: 'purpose1', guest_count: 2, member_count: 10, pinned_post_count: 1, files_count: 5}, - {id: 'channel2', header: 'header2', purpose: 'purpose2', guest_count: 3, member_count: 20, pinned_post_count: 2, files_count: 10}, + expect.objectContaining({id: 'channel1', header: 'header1', purpose: 'purpose1', guest_count: 2, member_count: 10, pinned_post_count: 1, files_count: 5}), + expect.objectContaining({id: 'channel2', header: 'header2', purpose: 'purpose2', guest_count: 3, member_count: 20, pinned_post_count: 2, files_count: 10}), ], prepareRecordsOnly: true, }); expect(operator.handleChannelMembership).toHaveBeenCalledWith({ channelMemberships: [ - {channel_id: 'channel1', user_id: 'user1'}, - {channel_id: 'channel2', user_id: 'user2'}, + expect.objectContaining({channel_id: 'channel1', user_id: 'user1'}), + expect.objectContaining({channel_id: 'channel2', user_id: 'user2'}), ], prepareRecordsOnly: true, }); expect(operator.handleMyChannel).toHaveBeenCalledWith({ channels, myChannels: [ - {channel_id: 'channel1', user_id: 'user1', id: 'channel1'}, - {channel_id: 'channel2', user_id: 'user2', id: 'channel2'}, + expect.objectContaining({channel_id: 'channel1', user_id: 'user1', id: 'channel1'}), + expect.objectContaining({channel_id: 'channel2', user_id: 'user2', id: 'channel2'}), ], prepareRecordsOnly: true, isCRTEnabled: true, }); expect(operator.handleMyChannelSettings).toHaveBeenCalledWith({ settings: [ - {channel_id: 'channel1', user_id: 'user1', id: 'channel1'}, - {channel_id: 'channel2', user_id: 'user2', id: 'channel2'}, + expect.objectContaining({channel_id: 'channel1', user_id: 'user1', id: 'channel1'}), + expect.objectContaining({channel_id: 'channel2', user_id: 'user2', id: 'channel2'}), ], prepareRecordsOnly: true, }); @@ -323,38 +331,35 @@ describe('prepareDeleteChannel', () => { let channel: ChannelModel; beforeEach(() => { - channel = { + channel = TestHelper.fakeChannelModel({ prepareDestroyPermanently: jest.fn().mockReturnValue({}), - membership: {fetch: jest.fn()} as unknown as Relation, - info: {fetch: jest.fn()} as unknown as Relation, - categoryChannel: {fetch: jest.fn()} as unknown as Relation, - members: {fetch: jest.fn()} as unknown as Query, - drafts: {fetch: jest.fn()} as unknown as Query, - postsInChannel: {fetch: jest.fn()} as unknown as Query, - posts: {fetch: jest.fn()} as unknown as Query, - bookmarks: {fetch: jest.fn()} as unknown as Query, - } as unknown as ChannelModel; + }); }); it('should prepare models for deletion', async () => { - const prepareDestroyPermanently = jest.fn().mockReturnValue({}); - const membershipModel = {prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'membership'})}; - const infoModel = {prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'info'})}; - const categoryChannelModel = {prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'category'})}; - const memberModels = [{prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'member'})}]; - const draftModels = [{prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'draft'})}]; - const postsInChannelModels = [{prepareDestroyPermanently}]; - const postModels = [{id: 'post1', prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'post'})}]; - const bookmarkModels = [{id: 'bookmark1', prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'bookmark'})}]; + const membershipModel = TestHelper.fakeMyChannelMembershipModel({prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'membership'})}); + const infoModel = TestHelper.fakeChannelInfoModel({prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'info'})}); + const categoryChannelModel = TestHelper.fakeCategoryChannelModel({prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'category'})}); + const memberModels = [TestHelper.fakeChannelMembershipModel({prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'member'})})]; + const draftModels = [TestHelper.fakeDraftModel({prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'draft'})})]; + const postsInChannelModels = [ + TestHelper.fakePostsInChannelModel({prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'postsInChannel'})}), + ]; + const postModels = [ + TestHelper.fakePostModel({id: 'post1', prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'post'})}), + ]; + const bookmarkModels = [ + TestHelper.fakeChannelBookmarkModel({id: 'bookmark1', prepareDestroyPermanently: jest.fn().mockReturnValue({id: 'bookmark'})}), + ]; - (channel.membership.fetch as jest.Mock).mockResolvedValue(membershipModel); - (channel.info.fetch as jest.Mock).mockResolvedValue(infoModel); - (channel.categoryChannel.fetch as jest.Mock).mockResolvedValue(categoryChannelModel); - (channel.members.fetch as jest.Mock).mockResolvedValue(memberModels); - (channel.drafts.fetch as jest.Mock).mockResolvedValue(draftModels); - (channel.postsInChannel.fetch as jest.Mock).mockResolvedValue(postsInChannelModels); - (channel.posts.fetch as jest.Mock).mockResolvedValue(postModels); - (channel.bookmarks.fetch as jest.Mock).mockResolvedValue(bookmarkModels); + jest.mocked(channel.membership.fetch).mockResolvedValue(membershipModel); + jest.mocked(channel.info.fetch).mockResolvedValue(infoModel); + jest.mocked(channel.categoryChannel.fetch).mockResolvedValue(categoryChannelModel); + jest.mocked(channel.members.fetch).mockResolvedValue(memberModels); + jest.mocked(channel.drafts.fetch).mockResolvedValue(draftModels); + jest.mocked(channel.postsInChannel.fetch).mockResolvedValue(postsInChannelModels); + jest.mocked(channel.posts.fetch).mockResolvedValue(postModels); + jest.mocked(channel.bookmarks.fetch).mockResolvedValue(bookmarkModels); const result = await prepareDeleteChannel(channel); @@ -365,7 +370,7 @@ describe('prepareDeleteChannel', () => { {id: 'category'}, {id: 'member'}, {id: 'draft'}, - {}, + {id: 'postsInChannel'}, {id: 'post'}, {id: 'bookmark'}, ]); @@ -381,9 +386,9 @@ describe('prepareDeleteChannel', () => { }); it('should handle errors gracefully', async () => { - (channel.membership.fetch as jest.Mock).mockRejectedValue(new Error('Test error')); - (channel.info.fetch as jest.Mock).mockRejectedValue(new Error('Test error')); - (channel.categoryChannel.fetch as jest.Mock).mockRejectedValue(new Error('Test error')); + jest.mocked(channel.membership.fetch).mockRejectedValue(new Error('Test error')); + jest.mocked(channel.info.fetch).mockRejectedValue(new Error('Test error')); + jest.mocked(channel.categoryChannel.fetch).mockRejectedValue(new Error('Test error')); const result = await prepareDeleteChannel(channel); @@ -396,17 +401,16 @@ describe('prepareDeleteBookmarks', () => { let bookmark: ChannelBookmarkModel; beforeEach(() => { - bookmark = { + bookmark = TestHelper.fakeChannelBookmarkModel({ prepareDestroyPermanently: jest.fn().mockReturnValue({}), - file: {fetch: jest.fn()} as unknown as Relation, fileId: 'file_id', - } as unknown as ChannelBookmarkModel; + }); }); it('should prepare bookmark and associated file for deletion', async () => { - const fileModel = {prepareDestroyPermanently: jest.fn().mockReturnValue({})}; + const fileModel = TestHelper.fakeFileModel({prepareDestroyPermanently: jest.fn().mockReturnValue({})}); - (bookmark.file.fetch as jest.Mock).mockResolvedValue(fileModel); + jest.mocked(bookmark.file.fetch).mockResolvedValue(fileModel); const result = await prepareDeleteBookmarks(bookmark); @@ -417,7 +421,7 @@ describe('prepareDeleteBookmarks', () => { }); it('should handle errors gracefully when fetching associated file', async () => { - (bookmark.file.fetch as jest.Mock).mockRejectedValue(new Error('Test error')); + jest.mocked(bookmark.file.fetch).mockRejectedValue(new Error('Test error')); const result = await prepareDeleteBookmarks(bookmark); @@ -692,10 +696,10 @@ describe('Channel Functions', () => { describe('getMyChannel', () => { it('should return the channel member if found', async () => { const channelId = 'channel_id'; - const mockMember = {id: channelId} as MyChannelModel; - (database.get as jest.Mock).mockReturnValue({ + const mockMember = TestHelper.fakeMyChannelModel({id: channelId}); + jest.mocked(database.get).mockReturnValue({ find: jest.fn().mockResolvedValue(mockMember), - }); + } as any); const result = await getMyChannel(database, channelId); @@ -705,9 +709,9 @@ describe('Channel Functions', () => { it('should return undefined if the channel member is not found', async () => { const channelId = 'channel_id'; - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ find: jest.fn().mockRejectedValue(new Error('Not found')), - }); + } as any); const result = await getMyChannel(database, channelId); @@ -722,9 +726,9 @@ describe('Channel Functions', () => { const mockQuery = jest.fn().mockReturnValue({ observe: jest.fn().mockReturnValue(of$([{id: channelId, observe: jest.fn().mockReturnValue(of$({id: channelId}))}])), }); - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ query: mockQuery, - }); + } as any); const result = observeMyChannel(database, channelId); @@ -740,9 +744,9 @@ describe('Channel Functions', () => { const mockQuery = jest.fn().mockReturnValue({ observe: jest.fn().mockReturnValue(of$([])), }); - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ query: mockQuery, - }); + } as any); const result = observeMyChannel(database, channelId); @@ -761,9 +765,9 @@ describe('Channel Functions', () => { const mockQuery = jest.fn().mockReturnValue({ observe: jest.fn().mockReturnValue(of$([{id: channelId, roles, observe: jest.fn().mockReturnValue(of$({roles}))}])), }); - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ query: mockQuery, - }); + } as any); const result = observeMyChannelRoles(database, channelId); @@ -777,9 +781,9 @@ describe('Channel Functions', () => { const mockQuery = jest.fn().mockReturnValue({ observe: jest.fn().mockReturnValue(of$([])), }); - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ query: mockQuery, - }); + } as any); const result = observeMyChannelRoles(database, channelId); @@ -792,10 +796,10 @@ describe('Channel Functions', () => { describe('getChannelById', () => { it('should return the channel if found', async () => { const channelId = 'channel_id'; - const mockedChannel = {id: channelId} as ChannelModel; - (database.get as jest.Mock).mockReturnValue({ + const mockedChannel = TestHelper.fakeChannelModel({id: channelId}); + jest.mocked(database.get).mockReturnValue({ find: jest.fn().mockResolvedValue(mockedChannel), - }); + } as any); const result = await getChannelById(database, channelId); @@ -805,9 +809,9 @@ describe('Channel Functions', () => { it('should return undefined if the channel is not found', async () => { const channelId = 'channel_id'; - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ find: jest.fn().mockRejectedValue(new Error('Not found')), - }); + } as any); const result = await getChannelById(database, channelId); @@ -822,9 +826,9 @@ describe('Channel Functions', () => { const mockQuery = jest.fn().mockReturnValue({ observe: jest.fn().mockReturnValue(of$([{id: channelId, observe: jest.fn().mockReturnValue(of$({id: channelId}))}])), }); - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ query: mockQuery, - }); + } as any); const result = observeChannel(database, channelId); @@ -840,9 +844,9 @@ describe('Channel Functions', () => { const mockQuery = jest.fn().mockReturnValue({ observe: jest.fn().mockReturnValue(of$([])), }); - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ query: mockQuery, - }); + } as any); const result = observeChannel(database, channelId); @@ -858,12 +862,12 @@ describe('Channel Functions', () => { it('should return the channel if found', async () => { const teamId = 'team_id'; const channelName = 'channel_name'; - const mockedChannel = {id: 'channel_id', name: channelName} as ChannelModel; - (database.get as jest.Mock).mockReturnValue({ + const mockedChannel = TestHelper.fakeChannelModel({id: 'channel_id', name: channelName}); + jest.mocked(database.get).mockReturnValue({ query: jest.fn().mockReturnValue({ fetch: jest.fn().mockResolvedValue([mockedChannel]), }), - }); + } as any); const result = await getChannelByName(database, teamId, channelName); @@ -874,11 +878,11 @@ describe('Channel Functions', () => { it('should return undefined if the channel is not found', async () => { const teamId = 'team_id'; const channelName = 'channel_name'; - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ query: jest.fn().mockReturnValue({ fetch: jest.fn().mockResolvedValue([]), }), - }); + } as any); const result = await getChannelByName(database, teamId, channelName); @@ -890,10 +894,10 @@ describe('Channel Functions', () => { describe('getChannelInfo', () => { it('should return the channel info if found', async () => { const channelId = 'channel_id'; - const mockChannelInfo = {id: channelId} as ChannelInfoModel; - (database.get as jest.Mock).mockReturnValue({ + const mockChannelInfo = TestHelper.fakeChannelInfoModel({id: channelId}); + jest.mocked(database.get).mockReturnValue({ find: jest.fn().mockResolvedValue(mockChannelInfo), - }); + } as any); const result = await getChannelInfo(database, channelId); @@ -903,9 +907,9 @@ describe('Channel Functions', () => { it('should return undefined if the channel info is not found', async () => { const channelId = 'channel_id'; - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ find: jest.fn().mockRejectedValue(new Error('Not found')), - }); + } as any); const result = await getChannelInfo(database, channelId); @@ -927,12 +931,12 @@ describe('Channel Functions', () => { it('should return the default channel for the team', async () => { const teamId = 'team_id'; - const defaultChannel = {id: 'default_channel', name: General.DEFAULT_CHANNEL} as ChannelModel; - const myFirstTeamChannel = {id: 'first_team_channel'} as ChannelModel; - const roles = [{permissions: [Permissions.JOIN_PUBLIC_CHANNELS]}]; - (queryRoles as jest.Mock).mockReturnValue({fetch: jest.fn().mockResolvedValue(roles)}); - (hasPermission as jest.Mock).mockReturnValue(true); - (mockQuery as jest.Mock).mockReturnValue({ + const defaultChannel = TestHelper.fakeChannelModel({id: 'default_channel', name: General.DEFAULT_CHANNEL}); + const myFirstTeamChannel = TestHelper.fakeChannelModel({id: 'first_team_channel'}); + const roles = [TestHelper.fakeRoleModel({permissions: [Permissions.JOIN_PUBLIC_CHANNELS]})]; + jest.mocked(queryRoles).mockReturnValue(TestHelper.fakeQuery(roles)); + jest.mocked(hasPermission).mockReturnValue(true); + jest.mocked(mockQuery).mockReturnValue({ fetch: jest.fn().mockResolvedValue([defaultChannel, myFirstTeamChannel]), }); @@ -955,11 +959,11 @@ describe('Channel Functions', () => { it('should return the default channel for the team with ignore id', async () => { const teamId = 'team_id'; - const defaultChannel = {id: 'default_channel', name: General.DEFAULT_CHANNEL} as ChannelModel; - const myFirstTeamChannel = {id: 'first_team_channel'} as ChannelModel; - (queryRoles as jest.Mock).mockReturnValue({fetch: jest.fn().mockResolvedValue([])}); - (hasPermission as jest.Mock).mockReturnValue(true); - (mockQuery as jest.Mock).mockReturnValue({ + const defaultChannel = TestHelper.fakeChannelModel({id: 'default_channel', name: General.DEFAULT_CHANNEL}); + const myFirstTeamChannel = TestHelper.fakeChannelModel({id: 'first_team_channel'}); + jest.mocked(queryRoles).mockReturnValue(TestHelper.fakeQuery([])); + jest.mocked(hasPermission).mockReturnValue(true); + jest.mocked(mockQuery).mockReturnValue({ fetch: jest.fn().mockResolvedValue([defaultChannel, myFirstTeamChannel]), }); @@ -982,11 +986,11 @@ describe('Channel Functions', () => { it('should return the first team channel if no default channel is found', async () => { const teamId = 'team_id'; - const myFirstTeamChannel = {id: 'first_team_channel'} as ChannelModel; - const roles = [{permissions: []}]; - (queryRoles as jest.Mock).mockReturnValue({fetch: jest.fn().mockResolvedValue(roles)}); - (hasPermission as jest.Mock).mockReturnValue(false); - (mockQuery as jest.Mock).mockReturnValue({ + const myFirstTeamChannel = TestHelper.fakeChannelModel({id: 'first_team_channel'}); + const roles = [TestHelper.fakeRoleModel({permissions: []})]; + jest.mocked(queryRoles).mockReturnValue(TestHelper.fakeQuery(roles)); + jest.mocked(hasPermission).mockReturnValue(false); + jest.mocked(mockQuery).mockReturnValue({ fetch: jest.fn().mockResolvedValue([myFirstTeamChannel]), }); @@ -1009,10 +1013,10 @@ describe('Channel Functions', () => { it('should return undefined if no channels are found', async () => { const teamId = 'team_id'; - const roles = [{permissions: [Permissions.JOIN_PUBLIC_CHANNELS]}]; - (queryRoles as jest.Mock).mockReturnValue({fetch: jest.fn().mockResolvedValue(roles)}); - (hasPermission as jest.Mock).mockReturnValue(true); - (mockQuery as jest.Mock).mockReturnValue({ + const roles = [TestHelper.fakeRoleModel({permissions: [Permissions.JOIN_PUBLIC_CHANNELS]})]; + jest.mocked(queryRoles).mockReturnValue(TestHelper.fakeQuery(roles)); + jest.mocked(hasPermission).mockReturnValue(true); + jest.mocked(mockQuery).mockReturnValue({ fetch: jest.fn().mockResolvedValue([]), }); @@ -1037,11 +1041,11 @@ describe('Channel Functions', () => { describe('getCurrentChannel', () => { it('should return the current channel if found', async () => { const currentChannelId = 'current_channel_id'; - const mockedChannel = {id: currentChannelId} as ChannelModel; - (getCurrentChannelId as jest.Mock).mockResolvedValue(currentChannelId); - (database.get as jest.Mock).mockReturnValue({ + const mockedChannel = TestHelper.fakeChannelModel({id: currentChannelId}); + jest.mocked(getCurrentChannelId).mockResolvedValue(currentChannelId); + jest.mocked(database.get).mockReturnValue({ find: jest.fn().mockResolvedValue(mockedChannel), - }); + } as any); const result = await getCurrentChannel(database); @@ -1051,7 +1055,7 @@ describe('Channel Functions', () => { }); it('should return undefined if no current channel is found', async () => { - (getCurrentChannelId as jest.Mock).mockResolvedValue(undefined); + jest.mocked(getCurrentChannelId).mockResolvedValue(''); const result = await getCurrentChannel(database); @@ -1063,11 +1067,11 @@ describe('Channel Functions', () => { describe('getCurrentChannelInfo', () => { it('should return the current channel info if found', async () => { const currentChannelId = 'current_channel_id'; - const mockChannelInfo = {id: currentChannelId} as ChannelInfoModel; - (getCurrentChannelId as jest.Mock).mockResolvedValue(currentChannelId); - (database.get as jest.Mock).mockReturnValue({ + const mockChannelInfo = TestHelper.fakeChannelInfoModel({id: currentChannelId}); + jest.mocked(getCurrentChannelId).mockResolvedValue(currentChannelId); + jest.mocked(database.get).mockReturnValue({ find: jest.fn().mockResolvedValue(mockChannelInfo), - }); + } as any); const result = await getCurrentChannelInfo(database); @@ -1077,7 +1081,7 @@ describe('Channel Functions', () => { }); it('should return undefined if no current channel info is found', async () => { - (getCurrentChannelId as jest.Mock).mockResolvedValue(undefined); + jest.mocked(getCurrentChannelId).mockResolvedValue(''); const result = await getCurrentChannelInfo(database); @@ -1089,14 +1093,14 @@ describe('Channel Functions', () => { describe('observeCurrentChannel', () => { it('should observe the current channel', () => { const currentChannelId = 'current_channel_id'; - const mockedChannel = {id: currentChannelId, observe: jest.fn().mockReturnValue(of$({id: currentChannelId}))} as unknown as ChannelModel; + const mockedChannel = TestHelper.fakeChannelModel({id: currentChannelId, observe: jest.fn().mockReturnValue(of$({id: currentChannelId}))}); const mockQuery = jest.fn().mockReturnValue({ observe: jest.fn().mockReturnValue(of$([mockedChannel])), }); - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ query: mockQuery, - }); - (observeCurrentChannelId as jest.Mock).mockReturnValue(of$(currentChannelId)); + } as any); + jest.mocked(observeCurrentChannelId).mockReturnValue(of$(currentChannelId)); const result = observeCurrentChannel(database); @@ -1111,10 +1115,10 @@ describe('Channel Functions', () => { const mockQuery = jest.fn().mockReturnValue({ observe: jest.fn().mockReturnValue(of$([])), }); - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ query: mockQuery, - }); - (observeCurrentChannelId as jest.Mock).mockReturnValue(of$(currentChannelId)); + } as any); + jest.mocked(observeCurrentChannelId).mockReturnValue(of$(currentChannelId)); const result = observeCurrentChannel(database); @@ -1128,10 +1132,10 @@ describe('Channel Functions', () => { describe('getChannelInfo', () => { it('should return the channel info if found', async () => { const channelId = 'channel_id'; - const mockChannelInfo = {id: channelId} as ChannelInfoModel; - (database.get as jest.Mock).mockReturnValue({ + const mockChannelInfo = TestHelper.fakeChannelInfoModel({id: channelId}); + jest.mocked(database.get).mockReturnValue({ find: jest.fn().mockResolvedValue(mockChannelInfo), - }); + } as any); const result = await getChannelInfo(database, channelId); @@ -1141,9 +1145,9 @@ describe('Channel Functions', () => { it('should return undefined if the channel info is not found', async () => { const channelId = 'channel_id'; - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ find: jest.fn().mockRejectedValue(new Error('Not found')), - }); + } as any); const result = await getChannelInfo(database, channelId); @@ -1172,14 +1176,12 @@ describe('Channel Membership Functions', () => { it('should delete channel membership and return models', async () => { const userId = 'user_id'; const channelId = 'channel_id'; - const mockMembership = { - prepareDestroyPermanently: jest.fn().mockReturnValue({}), - } as unknown as ChannelMembershipModel; - (database.get as jest.Mock).mockReturnValue({ + const mockMembership = TestHelper.fakeChannelMembershipModel({prepareDestroyPermanently: jest.fn().mockReturnValue({})}); + jest.mocked(database.get).mockReturnValue({ query: jest.fn().mockReturnValue({ fetch: jest.fn().mockResolvedValue([mockMembership]), }), - }); + } as any); const result = await deleteChannelMembership(operator, userId, channelId); @@ -1191,14 +1193,12 @@ describe('Channel Membership Functions', () => { it('should return models without batching if prepareRecordsOnly is true', async () => { const userId = 'user_id'; const channelId = 'channel_id'; - const mockMembership = { - prepareDestroyPermanently: jest.fn().mockReturnValue({}), - } as unknown as ChannelMembershipModel; - (database.get as jest.Mock).mockReturnValue({ + const mockMembership = TestHelper.fakeChannelMembershipModel({prepareDestroyPermanently: jest.fn().mockReturnValue({})}); + jest.mocked(database.get).mockReturnValue({ query: jest.fn().mockReturnValue({ fetch: jest.fn().mockResolvedValue([mockMembership]), }), - }); + } as any); const result = await deleteChannelMembership(operator, userId, channelId, true); @@ -1211,11 +1211,11 @@ describe('Channel Membership Functions', () => { const userId = 'user_id'; const channelId = 'channel_id'; const error = new Error('Test error'); - (database.get as jest.Mock).mockReturnValue({ + jest.mocked(database.get).mockReturnValue({ query: jest.fn().mockReturnValue({ fetch: jest.fn().mockRejectedValue(error), }), - }); + } as any); const result = await deleteChannelMembership(operator, userId, channelId); @@ -1241,7 +1241,7 @@ describe('Channel Membership Functions', () => { const userId = 'user_id'; const channelId = 'channel_id'; const error = new Error('Test error'); - (operator.handleChannelMembership as jest.Mock).mockRejectedValue(error); + jest.mocked(operator.handleChannelMembership).mockRejectedValue(error); const result = await addChannelMembership(operator, userId, channelId); @@ -1264,11 +1264,11 @@ describe('Channel Membership Functions', () => { it('should return the members count by channel IDs', async () => { const channelsId = ['channel1', 'channel2']; const mockMemberships = [ - {channelId: 'channel1'}, - {channelId: 'channel1'}, - {channelId: 'channel2'}, - ] as ChannelMembershipModel[]; - (mockQuery as jest.Mock).mockReturnValue({ + TestHelper.fakeChannelMembershipModel({channelId: 'channel1'}), + TestHelper.fakeChannelMembershipModel({channelId: 'channel1'}), + TestHelper.fakeChannelMembershipModel({channelId: 'channel2'}), + ]; + jest.mocked(mockQuery).mockReturnValue({ fetch: jest.fn().mockResolvedValue(mockMemberships), }); @@ -1281,8 +1281,8 @@ describe('Channel Membership Functions', () => { it('should return zero counts for channels with no members', async () => { const channelsId = ['channel1', 'channel2']; - const mockMemberships = [] as ChannelMembershipModel[]; - (mockQuery as jest.Mock).mockReturnValue({ + const mockMemberships: ChannelMembershipModel[] = []; + jest.mocked(mockQuery).mockReturnValue({ fetch: jest.fn().mockResolvedValue(mockMemberships), }); @@ -1296,7 +1296,7 @@ describe('Channel Membership Functions', () => { it('should handle errors gracefully', async () => { const channelsId = ['channel1', 'channel2']; const error = new Error('Test error'); - (mockQuery as jest.Mock).mockReturnValue({ + jest.mocked(mockQuery).mockReturnValue({ fetch: jest.fn().mockRejectedValue(error), }); @@ -1336,9 +1336,9 @@ describe('Channel Observations', () => { it('should observe all my channel notify props', () => { const mockSettings = [ - {id: 'id1', notifyProps: {mark_unread: 'all'}}, - {id: 'id2', notifyProps: {mark_unread: 'mention'}}, - ] as MyChannelSettingsModel[]; + TestHelper.fakeMyChannelSettingsModel({id: 'id1', notifyProps: {mark_unread: 'all'}}), + TestHelper.fakeMyChannelSettingsModel({id: 'id2', notifyProps: {mark_unread: 'mention'}}), + ]; mockQuery.mockReturnValue({ observeWithColumns: jest.fn().mockReturnValue(of$(mockSettings)), }); @@ -1356,13 +1356,13 @@ describe('Channel Observations', () => { it('should observe notify props by channels', () => { const channels = [ - {id: 'channel1'}, - {id: 'channel2'}, - ] as ChannelModel[]; + TestHelper.fakeChannelModel({id: 'channel1'}), + TestHelper.fakeChannelModel({id: 'channel2'}), + ]; const mockSettings = [ - {id: 'channel1', notifyProps: {mark_unread: 'all'}}, - {id: 'channel2', notifyProps: {mark_unread: 'mention'}}, - ] as MyChannelSettingsModel[]; + TestHelper.fakeMyChannelSettingsModel({id: 'channel1', notifyProps: {mark_unread: 'all'}}), + TestHelper.fakeMyChannelSettingsModel({id: 'channel2', notifyProps: {mark_unread: 'mention'}}), + ]; mockQuery.mockReturnValue({ observeWithColumns: jest.fn().mockReturnValue(of$(mockSettings)), }); @@ -1381,9 +1381,9 @@ describe('Channel Observations', () => { it('should observe my channel mention count', () => { const teamId = 'team_id'; const mockChannels = [ - {mentionsCount: 2, isUnread: true}, - {mentionsCount: 3, isUnread: false}, - ] as MyChannelModel[]; + TestHelper.fakeMyChannelModel({mentionsCount: 2, isUnread: true}), + TestHelper.fakeMyChannelModel({mentionsCount: 3, isUnread: false}), + ]; mockQuery.mockReturnValue({ observeWithColumns: jest.fn().mockReturnValue(of$(mockChannels)), }); @@ -1398,9 +1398,9 @@ describe('Channel Observations', () => { it('should observe my channel mention count with no team id', () => { const mockChannels = [ - {mentionsCount: 2, isUnread: true}, - {mentionsCount: 3, isUnread: false}, - ] as MyChannelModel[]; + TestHelper.fakeMyChannelModel({mentionsCount: 2, isUnread: true}), + TestHelper.fakeMyChannelModel({mentionsCount: 3, isUnread: false}), + ]; mockQuery.mockReturnValue({ observeWithColumns: jest.fn().mockReturnValue(of$(mockChannels)), }); @@ -1416,8 +1416,8 @@ describe('Channel Observations', () => { it('should observe direct channels by term', () => { const term = '@test'; const mockCurrentUserId = 'user_id'; - const mockChannels = [{id: 'channel1'}] as MyChannelModel[]; - (observeCurrentUserId as jest.Mock).mockReturnValue(of$(mockCurrentUserId)); + const mockChannels = [TestHelper.fakeMyChannelModel({id: 'channel1'})]; + jest.mocked(observeCurrentUserId).mockReturnValue(of$(mockCurrentUserId)); mockQuery.mockReturnValue({ observe: jest.fn().mockReturnValue(of$(mockChannels)), }); @@ -1432,8 +1432,8 @@ describe('Channel Observations', () => { it('should observe not direct channels by term', () => { const term = 'test'; const mockTeammateNameSetting = General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME; - const mockUsers = [{id: 'user1'}] as UserModel[]; - (observeTeammateNameDisplay as jest.Mock).mockReturnValue(of$(mockTeammateNameSetting)); + const mockUsers = [TestHelper.fakeUserModel({id: 'user1'})]; + jest.mocked(observeTeammateNameDisplay).mockReturnValue(of$(mockTeammateNameSetting)); mockQuery.mockReturnValue({ observe: jest.fn().mockReturnValue(of$(mockUsers)), }); @@ -1447,7 +1447,7 @@ describe('Channel Observations', () => { it('should observe joined channels by term', () => { const term = 'test'; - const mockChannels = [{id: 'channel1'}] as MyChannelModel[]; + const mockChannels = [TestHelper.fakeMyChannelModel({id: 'channel1'})]; mockQuery.mockReturnValue({ observe: jest.fn().mockReturnValue(of$(mockChannels)), }); @@ -1472,7 +1472,7 @@ describe('Channel Observations', () => { it('should observe archive channels by term', () => { const term = 'test'; - const mockChannels = [{id: 'channel1'}] as MyChannelModel[]; + const mockChannels = [TestHelper.fakeMyChannelModel({id: 'channel1'})]; mockQuery.mockReturnValue({ observe: jest.fn().mockReturnValue(of$(mockChannels)), }); @@ -1497,7 +1497,9 @@ describe('Channel Observations', () => { it('should observe channel settings', () => { const channelId = 'channel_id'; - const mockSettings = {id: channelId, observe: jest.fn().mockReturnValue(of$({id: channelId}))} as unknown as MyChannelSettingsModel; + const mockSettings = TestHelper.fakeMyChannelSettingsModel({id: channelId}); + jest.mocked(mockSettings.observe).mockReturnValue(of$(mockSettings)); + mockQuery.mockReturnValue({ observe: jest.fn().mockReturnValue(of$([mockSettings])), }); @@ -1506,13 +1508,14 @@ describe('Channel Observations', () => { expect(database.get).toHaveBeenCalledWith(MM_TABLES.SERVER.MY_CHANNEL_SETTINGS); result.subscribe((value) => { - expect(value).toEqual({id: channelId}); + expect(value).toMatchObject({id: channelId}); }); }); it('should observe is muted setting', () => { const channelId = 'channel_id'; - const mockSettings = {id: channelId, notifyProps: {mark_unread: General.MENTION}, observe: jest.fn().mockReturnValue(of$({id: channelId, notifyProps: {mark_unread: General.MENTION}}))} as unknown as MyChannelSettingsModel; + const mockSettings = TestHelper.fakeMyChannelSettingsModel({id: channelId, notifyProps: {mark_unread: General.MENTION}}); + jest.mocked(mockSettings.observe).mockReturnValue(of$(mockSettings)); mockQuery.mockReturnValue({ observe: jest.fn().mockReturnValue(of$([mockSettings])), }); @@ -1526,8 +1529,8 @@ describe('Channel Observations', () => { }); it('should observe channels by last post at', () => { - const myChannels = [{id: 'channel1'}] as MyChannelModel[]; - const mockChannels = [{id: 'channel1'}] as ChannelModel[]; + const myChannels = [TestHelper.fakeMyChannelModel({id: 'channel1'})]; + const mockChannels = [TestHelper.fakeChannelModel({id: 'channel1'})]; mockQuery.mockReturnValue({ observe: jest.fn().mockReturnValue(of$(mockChannels)), }); @@ -1542,7 +1545,10 @@ describe('Channel Observations', () => { it('should observe channel members', () => { const channelId = 'channel_id'; - const mockMembers = [{id: 'member1'}, {id: 'member2'}] as ChannelMembershipModel[]; + const mockMembers = [ + TestHelper.fakeChannelMembershipModel({id: 'member1'}), + TestHelper.fakeChannelMembershipModel({id: 'member2'}), + ]; mockQuery.mockReturnValue({ observe: jest.fn().mockReturnValue(of$(mockMembers)), }); @@ -1580,7 +1586,7 @@ describe('observeMyChannelUnreads', () => { it('should return true when there are unread channels that are not muted', async () => { const subscriptionNext = jest.fn(); - const notify_props = {mark_unread: 'all' as const}; + const notify_props = TestHelper.fakeChannelNotifyProps({mark_unread: 'all'}); const result = observeMyChannelUnreads(database, teamId); result.subscribe({next: subscriptionNext}); @@ -1628,7 +1634,7 @@ describe('observeMyChannelUnreads', () => { it('should return false when all unread channels are muted', async () => { const subscriptionNext = jest.fn(); - const notify_props = {mark_unread: 'mention' as const}; + const notify_props = TestHelper.fakeChannelNotifyProps({mark_unread: General.MENTION}); const result = observeMyChannelUnreads(database, teamId); result.subscribe({next: subscriptionNext}); @@ -1724,7 +1730,7 @@ describe('observeMyChannelUnreads', () => { it('should not retrigger the subscription when there are changes in other teams', async () => { const subscriptionNext = jest.fn(); const otherTeamId = 'other_team_id'; - const notify_props = {mark_unread: 'all' as const}; + const notify_props = TestHelper.fakeChannelNotifyProps({mark_unread: General.MENTION}); const result = observeMyChannelUnreads(database, teamId); result.subscribe({next: subscriptionNext}); diff --git a/app/queries/servers/role.test.ts b/app/queries/servers/role.test.ts index c39c14fa2..4bb102f0b 100644 --- a/app/queries/servers/role.test.ts +++ b/app/queries/servers/role.test.ts @@ -3,6 +3,7 @@ import {General, Permissions} from '@constants'; import DatabaseManager from '@database/manager'; +import TestHelper from '@test/test_helper'; import { queryRoles, @@ -17,10 +18,6 @@ import { import type ServerDataOperator from '@database/operator/server_data_operator'; import type {Database} from '@nozbe/watermelondb'; -import type ChannelModel from '@typings/database/models/servers/channel'; -import type PostModel from '@typings/database/models/servers/post'; -import type TeamModel from '@typings/database/models/servers/team'; -import type UserModel from '@typings/database/models/servers/user'; describe('Role Queries', () => { const serverUrl = 'baseHandler.test.com'; @@ -108,16 +105,16 @@ describe('Role Queries', () => { describe('observePermissionForChannel', () => { it('should observe channel permissions', (done) => { - const mockUser = { + const mockUser = TestHelper.fakeUserModel({ id: 'user1', roles: 'system_user', - } as UserModel; + }); - const mockChannel = { + const mockChannel = TestHelper.fakeChannelModel({ id: 'channel1', type: General.OPEN_CHANNEL, teamId: 'team1', - } as ChannelModel; + }); operator.handleRole({ roles: [{ @@ -146,7 +143,7 @@ describe('Role Queries', () => { it('should return default value when no user', (done) => { observePermissionForChannel( database, - {} as any, + TestHelper.fakeChannelModel(), undefined, Permissions.CREATE_POST, true, @@ -162,14 +159,14 @@ describe('Role Queries', () => { describe('observePermissionForTeam', () => { it('should observe team permissions', (done) => { - const mockUser = { + const mockUser = TestHelper.fakeUserModel({ id: 'user1', roles: 'system_user', - } as UserModel; + }); - const mockTeam = { + const mockTeam = TestHelper.fakeTeamModel({ id: 'team1', - } as TeamModel; + }); const myTeams: MyTeam[] = [{ id: mockTeam.id, @@ -214,7 +211,7 @@ describe('Role Queries', () => { observePermissionForTeam( database, undefined, - {} as any, + TestHelper.fakeUserModel(), Permissions.MANAGE_TEAM, true, ).subscribe({ @@ -229,18 +226,18 @@ describe('Role Queries', () => { describe('observeCanManageChannelMembers', () => { it('should observe manage members permission for public channel', (done) => { - const mockUser = { + const mockUser = TestHelper.fakeUserModel({ id: 'user1', roles: 'system_admin', - } as UserModel; + }); Promise.all([ operator.handleChannel({ - channels: [{ + channels: [TestHelper.fakeChannel({ id: 'channel1', type: General.OPEN_CHANNEL, delete_at: 0, - }] as Channel[], + })], prepareRecordsOnly: false, }), operator.handleRole({ @@ -266,18 +263,18 @@ describe('Role Queries', () => { }); it('should not allow managing default channel members', (done) => { - const mockUser = { + const mockUser = TestHelper.fakeUserModel({ id: 'user1', roles: 'system_admin', - } as UserModel; + }); operator.handleChannel({ - channels: [{ + channels: [TestHelper.fakeChannel({ id: 'channel1', name: General.DEFAULT_CHANNEL, type: General.OPEN_CHANNEL, delete_at: 0, - }] as Channel[], + })], prepareRecordsOnly: false, }).then(() => { observeCanManageChannelMembers( @@ -297,23 +294,23 @@ describe('Role Queries', () => { describe('observePermissionForPost', () => { it('should observe post permissions', (done) => { - const mockUser = { + const mockUser = TestHelper.fakeUserModel({ id: 'user1', roles: 'system_user', - } as UserModel; + }); - const mockPost = { + const mockPost = TestHelper.fakePostModel({ id: 'post1', channelId: 'channel1', - } as PostModel; + }); Promise.all([ operator.handleChannel({ - channels: [{ + channels: [TestHelper.fakeChannel({ id: 'channel1', type: General.OPEN_CHANNEL, team_id: 'team1', - }] as Channel[], + })], prepareRecordsOnly: false, }), operator.handleRole({ @@ -342,10 +339,10 @@ describe('Role Queries', () => { }); it('should return default value when no channel exists', (done) => { - const mockPost = { + const mockPost = TestHelper.fakePostModel({ id: 'post1', channelId: 'nonexistent', - } as PostModel; + }); observePermissionForPost( database, @@ -365,18 +362,18 @@ describe('Role Queries', () => { describe('observeCanManageChannelSettings', () => { it('should observe manage settings permission', (done) => { - const mockUser = { + const mockUser = TestHelper.fakeUserModel({ id: 'user1', roles: 'system_admin', - } as UserModel; + }); Promise.all([ operator.handleChannel({ - channels: [{ + channels: [TestHelper.fakeChannel({ id: 'channel1', type: General.OPEN_CHANNEL, delete_at: 0, - }] as Channel[], + })], prepareRecordsOnly: false, }), operator.handleRole({ @@ -403,17 +400,17 @@ describe('Role Queries', () => { }); it('should not allow managing deleted channel settings', (done) => { - const mockUser = { + const mockUser = TestHelper.fakeUserModel({ id: 'user1', roles: 'system_admin', - } as UserModel; + }); operator.handleChannel({ - channels: [{ + channels: [TestHelper.fakeChannel({ id: 'channel1', type: General.OPEN_CHANNEL, delete_at: 123, - }] as Channel[], + })], prepareRecordsOnly: false, }).then(() => { observeCanManageChannelSettings( diff --git a/app/screens/edit_profile/components/form.test.tsx b/app/screens/edit_profile/components/form.test.tsx index 9c4ebd5e7..8221dd802 100644 --- a/app/screens/edit_profile/components/form.test.tsx +++ b/app/screens/edit_profile/components/form.test.tsx @@ -2,20 +2,19 @@ // See LICENSE.txt for license information. import {fireEvent} from '@testing-library/react-native'; -import React from 'react'; +import React, {type ComponentProps} from 'react'; import {renderWithIntl} from '@test/intl-test-helper'; +import TestHelper from '@test/test_helper'; import ProfileForm from './form'; import type {CustomAttributeSet} from '@typings/api/custom_profile_attributes'; -import type UserModel from '@typings/database/models/servers/user'; -import type {UserInfo} from '@typings/screens/edit_profile'; describe('ProfileForm', () => { - const baseProps = { + const baseProps: ComponentProps = { canSave: false, - currentUser: { + currentUser: TestHelper.fakeUserModel({ id: 'user1', firstName: 'First', lastName: 'Last', @@ -24,7 +23,7 @@ describe('ProfileForm', () => { nickname: 'nick', position: 'position', authService: '', - } as UserModel, + }), isTablet: false, lockedFirstName: false, lockedLastName: false, @@ -40,7 +39,7 @@ describe('ProfileForm', () => { nickname: 'nick', position: 'position', customAttributes: {}, - } as UserInfo, + }, enableCustomAttributes: false, }; diff --git a/app/screens/home/tab_bar/account.test.tsx b/app/screens/home/tab_bar/account.test.tsx index e73514dcc..5c3a06c58 100644 --- a/app/screens/home/tab_bar/account.test.tsx +++ b/app/screens/home/tab_bar/account.test.tsx @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; +import {Preferences} from '@constants'; import {BOTTOM_TAB_PROFILE_PHOTO_SIZE, BOTTOM_TAB_STATUS_SIZE} from '@constants/view'; import {renderWithEverything, renderWithIntl} from '@test/intl-test-helper'; import TestHelper from '@test/test_helper'; @@ -9,20 +10,20 @@ import {changeOpacity} from '@utils/theme'; import {Account, default as AccountWithDatabaseObservable} from './account'; -import type {UserModel} from '@database/models/server'; import type Database from '@nozbe/watermelondb/Database'; jest.mock('@components/profile_picture', () => 'ProfilePicture'); -const currentUser = { +const currentUser = TestHelper.fakeUserModel({ id: 'user1', username: 'testuser', -} as UserModel; +}); const theme = { + ...Preferences.THEMES.denim, buttonBg: '#000', centerChannelColor: '#fff', -} as Theme; +}; describe('Account', () => { const centerChannelColorWithOpacity = changeOpacity(theme.centerChannelColor, 0.48); @@ -64,7 +65,7 @@ describe('Account', () => { const profilePicture = getByTestId('account-profile-picture'); - expect(profilePicture.props.author).toEqual({id: 'user1', username: 'testuser'}); + expect(profilePicture.props.author).toEqual(expect.objectContaining({id: 'user1', username: 'testuser'})); expect(profilePicture.props.showStatus).toBe(true); expect(profilePicture.props.size).toBe(BOTTOM_TAB_PROFILE_PHOTO_SIZE); expect(profilePicture.props.statusSize).toBe(BOTTOM_TAB_STATUS_SIZE); diff --git a/app/screens/settings/notification_mention/mention_settings.test.tsx b/app/screens/settings/notification_mention/mention_settings.test.tsx index 3ba4f42c0..330e2f976 100644 --- a/app/screens/settings/notification_mention/mention_settings.test.tsx +++ b/app/screens/settings/notification_mention/mention_settings.test.tsx @@ -1,34 +1,37 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {getMentionProps, canSaveSettings, getUniqueKeywordsFromInput, type CanSaveSettings} from './mention_settings'; +import TestHelper from '@test/test_helper'; -import type UserModel from '@typings/database/models/servers/user'; +import {getMentionProps, canSaveSettings, getUniqueKeywordsFromInput, type CanSaveSettings} from './mention_settings'; describe('getMentionProps', () => { test('Should have correct return type when input is empty', () => { - const mentionProps = getMentionProps({notifyProps: {} as UserNotifyProps} as UserModel); + const user = TestHelper.fakeUserModel({notifyProps: null}); + const mentionProps = getMentionProps(user); expect(mentionProps).toEqual({ - mentionKeywords: [], - usernameMention: false, - channel: false, - first_name: false, - comments: '', - notifyProps: {}, + mentionKeywords: [ + `@${user.username}`, + ], + usernameMention: true, + channel: true, + first_name: true, + comments: 'any', + notifyProps: expect.any(Object), }); }); test('Should have correct return type for when channel, first_name, currentUser.username are provided', () => { - const mentionProps = getMentionProps({ + const mentionProps = getMentionProps(TestHelper.fakeUserModel({ username: 'testUser', - notifyProps: { + notifyProps: TestHelper.fakeUserNotifyProps({ comments: 'any', channel: 'true', first_name: 'true', mention_keys: 'testUser', - } as UserNotifyProps, - } as UserModel); + }), + })); expect(mentionProps.mentionKeywords).toEqual([]); expect(mentionProps.usernameMention).toEqual(true); @@ -37,12 +40,12 @@ describe('getMentionProps', () => { }); test('Should have correct return type for mention_keys input', () => { - const mentionProps = getMentionProps({ + const mentionProps = getMentionProps(TestHelper.fakeUserModel({ username: 'testUser', - notifyProps: { + notifyProps: TestHelper.fakeUserNotifyProps({ mention_keys: 'testUser,testUser2,testKey1,testKey2', - } as UserNotifyProps, - } as UserModel); + }), + })); expect(mentionProps.mentionKeywords).toHaveLength(3); expect(mentionProps.mentionKeywords).toEqual(['testUser2', 'testKey1', 'testKey2']); @@ -50,44 +53,51 @@ describe('getMentionProps', () => { }); describe('canSaveSettings', () => { + const getBaseCanSaveSettingParams = (): CanSaveSettings => ({ + mentionKeywords: [], + mentionProps: { + mentionKeywords: [], + usernameMention: true, + channel: true, + first_name: true, + comments: 'any', + notifyProps: TestHelper.fakeUserNotifyProps(), + }, + channelMentionOn: true, + replyNotificationType: 'any', + firstNameMentionOn: true, + usernameMentionOn: true, + }); + test('Should return true when mentionKeywords have changed', () => { - const canSaveSettingParams = { - mentionKeywords: ['test1', 'test2'], - mentionProps: { - mentionKeywords: ['test1', 'test2', 'test3'], - }, - } as CanSaveSettings; + const canSaveSettingParams = getBaseCanSaveSettingParams(); + canSaveSettingParams.mentionKeywords = ['test1', 'test2']; + canSaveSettingParams.mentionProps.mentionKeywords = ['test1', 'test2', 'test3']; expect(canSaveSettings(canSaveSettingParams)).toEqual(true); }); test('Should return false when mentionKeywords have not changed', () => { - const canSaveSettingParams = { - mentionKeywords: ['test1', 'test2'], - mentionProps: { - mentionKeywords: ['test2', 'test1'], - }, - } as CanSaveSettings; + const canSaveSettingParams = getBaseCanSaveSettingParams(); + canSaveSettingParams.mentionKeywords = ['test1', 'test2']; + canSaveSettingParams.mentionProps.mentionKeywords = ['test2', 'test1']; expect(canSaveSettings(canSaveSettingParams)).toEqual(false); }); test('Should return true when only userName has changed', () => { - const canSaveSettingParams = { - channelMentionOn: true, - replyNotificationType: 'any', - firstNameMentionOn: true, - usernameMentionOn: true, - mentionKeywords: ['test1', 'test2'], - mentionProps: { - channel: true, - comments: 'any' as UserNotifyProps['comments'], - first_name: true, - usernameMention: false, - mentionKeywords: ['test1', 'test2'], - notifyProps: {} as UserNotifyProps, - }, - }; + const canSaveSettingParams = getBaseCanSaveSettingParams(); + canSaveSettingParams.channelMentionOn = true; + canSaveSettingParams.replyNotificationType = 'any'; + canSaveSettingParams.firstNameMentionOn = true; + canSaveSettingParams.usernameMentionOn = true; + canSaveSettingParams.mentionKeywords = ['test1', 'test2']; + canSaveSettingParams.mentionProps.channel = true; + canSaveSettingParams.mentionProps.comments = 'any'; + canSaveSettingParams.mentionProps.first_name = true; + canSaveSettingParams.mentionProps.usernameMention = false; + canSaveSettingParams.mentionProps.mentionKeywords = ['test1', 'test2']; + canSaveSettingParams.mentionProps.notifyProps = TestHelper.fakeUserNotifyProps(); expect(canSaveSettings(canSaveSettingParams)).toEqual(true); }); diff --git a/app/screens/user_profile/user_info.test.tsx b/app/screens/user_profile/user_info.test.tsx index c4b772a6b..4dfc53de2 100644 --- a/app/screens/user_profile/user_info.test.tsx +++ b/app/screens/user_profile/user_info.test.tsx @@ -5,11 +5,10 @@ import React from 'react'; import {fetchCustomAttributes} from '@actions/remote/user'; import {renderWithIntlAndTheme, screen, waitFor} from '@test/intl-test-helper'; +import TestHelper from '@test/test_helper'; import UserInfo from './user_info'; -import type UserModel from '@database/models/server/user'; - const localhost = 'http://localhost:8065'; jest.mock('@actions/remote/user', () => ({ @@ -23,18 +22,18 @@ jest.mock('@context/server', () => ({ describe('screens/user_profile/UserInfo', () => { beforeEach(() => { // Reset mock before each test - (fetchCustomAttributes as jest.Mock).mockReset(); + jest.mocked(fetchCustomAttributes).mockReset(); }); const baseProps = { - user: { + user: TestHelper.fakeUserModel({ id: 'user1', firstName: 'First', lastName: 'Last', username: 'username', nickname: 'nick', position: 'Developer', - } as UserModel, + }), isMyUser: false, showCustomStatus: false, showLocalTime: true, @@ -65,7 +64,7 @@ describe('screens/user_profile/UserInfo', () => { }; test('should display custom attributes in sort order', async () => { - (fetchCustomAttributes as jest.Mock).mockResolvedValue({attributes: baseCustomAttributes}); + jest.mocked(fetchCustomAttributes).mockResolvedValue({attributes: baseCustomAttributes}); renderWithIntlAndTheme( , @@ -136,7 +135,7 @@ describe('screens/user_profile/UserInfo', () => { sort_order: 2, }, }; - (fetchCustomAttributes as jest.Mock).mockResolvedValue({attributes: withEmptyCustomAttributes}); + jest.mocked(fetchCustomAttributes).mockResolvedValue({attributes: withEmptyCustomAttributes}); renderWithIntlAndTheme( , diff --git a/app/utils/categories.test.ts b/app/utils/categories.test.ts index 3f109c1dd..780d6f360 100644 --- a/app/utils/categories.test.ts +++ b/app/utils/categories.test.ts @@ -5,6 +5,7 @@ import {Preferences} from '@constants'; import DatabaseManager from '@database/manager'; +import TestHelper from '@test/test_helper'; import { makeCategoryChannelId, @@ -18,8 +19,6 @@ import { } from './categories'; import type {ServerDatabase} from '@typings/database/database'; -import type MyChannelModel from '@typings/database/models/servers/my_channel'; -import type PreferenceModel from '@typings/database/models/servers/preference'; import type UserModel from '@typings/database/models/servers/user'; describe('Categories utils', () => { @@ -184,7 +183,7 @@ describe('Categories utils', () => { const channelModels = await db.operator.handleChannel({channels, prepareRecordsOnly: false}); const myChannelModels = await db.operator.handleMyChannel({channels, myChannels, prepareRecordsOnly: false}); return channelModels.map((channel, index) => { - const myChannel = myChannelModels[index] as MyChannelModel; + const myChannel = myChannelModels[index]; return { channel, myChannel, @@ -253,7 +252,7 @@ describe('Categories utils', () => { const models = await db.operator.handleMyChannel({channels, myChannels, isCRTEnabled: false, prepareRecordsOnly: false}); expect(models).toHaveLength(1); - const myChannel = models[0] as MyChannelModel; + const myChannel = models[0]; let isUnread = isUnreadChannel(myChannel); expect(isUnread).toBe(true); await db.database.write(async () => { @@ -355,11 +354,11 @@ describe('Categories utils', () => { const myChannelModels = await db.operator.handleMyChannel({channels, myChannels, prepareRecordsOnly: false}); const data: ChannelWithMyChannel[] = [{ channel: channelModels[0], - myChannel: myChannelModels[0] as MyChannelModel, + myChannel: myChannelModels[0], sortOrder: 0, }, { channel: channelModels[1], - myChannel: myChannelModels[1] as MyChannelModel, + myChannel: myChannelModels[1], sortOrder: 1, }]; let archived = filterArchivedChannels(data, ''); @@ -378,19 +377,22 @@ describe('Categories utils', () => { const now = Date.now(); const dt_chan1 = now - (3 * 24 * 60 * 60 * 1000); const dt_chan2 = now - (2 * 24 * 60 * 60 * 1000); - const autoclosePrefs = [{ - id: 'pref1', - userId: 'me', - category: Preferences.CATEGORIES.CHANNEL_APPROXIMATE_VIEW_TIME, - name: 'dm1', - value: `${dt_chan1}`, - }, { - id: 'pref2', - userId: 'me', - category: Preferences.CATEGORIES.CHANNEL_APPROXIMATE_VIEW_TIME, - name: 'dm2', - value: `${dt_chan2}`, - }] as PreferenceModel[]; + const autoclosePrefs = [ + TestHelper.fakePreferenceModel({ + id: 'pref1', + userId: 'me', + category: Preferences.CATEGORIES.CHANNEL_APPROXIMATE_VIEW_TIME, + name: 'dm1', + value: `${dt_chan1}`, + }), + TestHelper.fakePreferenceModel({ + id: 'pref2', + userId: 'me', + category: Preferences.CATEGORIES.CHANNEL_APPROXIMATE_VIEW_TIME, + name: 'dm2', + value: `${dt_chan2}`, + }), + ]; const channels: Channel[] = [{ id: 'dm1', @@ -472,11 +474,11 @@ describe('Categories utils', () => { const myChannelModels = await db.operator.handleMyChannel({channels, myChannels, prepareRecordsOnly: false}); const data: ChannelWithMyChannel[] = [{ channel: channelModels[0], - myChannel: myChannelModels[0] as MyChannelModel, + myChannel: myChannelModels[0], sortOrder: 0, }, { channel: channelModels[1], - myChannel: myChannelModels[1] as MyChannelModel, + myChannel: myChannelModels[1], sortOrder: 0, }]; @@ -491,10 +493,10 @@ describe('Categories utils', () => { const lastViewedData: ChannelWithMyChannel[] = [{ ...data[0], - myChannel: { + myChannel: TestHelper.fakeMyChannelModel({ ...data[0].myChannel, lastViewedAt: 0, - } as MyChannelModel, + }), }, data[1]]; const autoclosePrefsNotViewed = [autoclosePrefs[1]]; @@ -502,10 +504,10 @@ describe('Categories utils', () => { expect(result.length).toEqual(1); const deactivated = new Map(); - deactivated.set('other1', { + deactivated.set('other1', TestHelper.fakeUserModel({ id: 'other1', deleteAt: dt_chan1 + 1, - } as UserModel); + })); result = filterAutoclosedDMs('direct_messages', 3, 'me', '', data, autoclosePrefs, {}, deactivated); expect(result.length).toEqual(1); @@ -515,29 +517,29 @@ describe('Categories utils', () => { const mockNotUnread: ChannelWithMyChannel[] = [data[0], { ...data[1], - myChannel: { + myChannel: TestHelper.fakeMyChannelModel({ id: data[1].myChannel.id, lastViewedAt: 0, isUnread: false, - } as MyChannelModel, + }), }]; result = filterAutoclosedDMs('direct_messages', 3, 'me', '', mockNotUnread, autoclosePrefs, {}, undefined, 'dm1'); expect(result[0].channel.id).toEqual('dm1'); const mockRecent1: ChannelWithMyChannel[] = [{ ...data[0], - myChannel: { + myChannel: TestHelper.fakeMyChannelModel({ id: data[0].myChannel.id, lastViewedAt: 2, isUnread: false, - } as MyChannelModel, + }), }, { ...data[1], - myChannel: { + myChannel: TestHelper.fakeMyChannelModel({ id: data[1].myChannel.id, lastViewedAt: 1, isUnread: false, - } as MyChannelModel, + }), }]; result = filterAutoclosedDMs('direct_messages', 3, 'me', '', mockRecent1, [], {}); @@ -545,18 +547,18 @@ describe('Categories utils', () => { const mockRecent2: ChannelWithMyChannel[] = [{ ...data[0], - myChannel: { + myChannel: TestHelper.fakeMyChannelModel({ id: data[0].myChannel.id, lastViewedAt: 1, isUnread: false, - } as MyChannelModel, + }), }, { ...data[1], - myChannel: { + myChannel: TestHelper.fakeMyChannelModel({ id: data[1].myChannel.id, lastViewedAt: 2, isUnread: false, - } as MyChannelModel, + }), }]; result = filterAutoclosedDMs('direct_messages', 3, 'me', '', mockRecent2, [], {}); @@ -564,18 +566,18 @@ describe('Categories utils', () => { const mockSameLastViewed: ChannelWithMyChannel[] = [{ ...data[0], - myChannel: { + myChannel: TestHelper.fakeMyChannelModel({ id: data[0].myChannel.id, lastViewedAt: 1, isUnread: false, - } as MyChannelModel, + }), }, { ...data[1], - myChannel: { + myChannel: TestHelper.fakeMyChannelModel({ id: data[1].myChannel.id, lastViewedAt: 1, isUnread: false, - } as MyChannelModel, + }), }]; result = filterAutoclosedDMs('direct_messages', 3, 'me', '', mockSameLastViewed, [], {}); @@ -585,25 +587,29 @@ describe('Categories utils', () => { test('filterManuallyClosedDms', async () => { const db = DatabaseManager.getServerDatabaseAndOperator(serverUrl); - const closedDMsPrefs = [{ - id: 'pref1', - userId: 'me', - category: Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, - name: 'dm1', - value: 'true', - }, { - id: 'pref2', - userId: 'me', - category: Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, - name: 'dm2', - value: 'true', - }, { - id: 'pref3', - userId: 'me', - category: Preferences.CATEGORIES.GROUP_CHANNEL_SHOW, - name: 'gm1', - value: 'false', - }] as PreferenceModel[]; + const closedDMsPrefs = [ + TestHelper.fakePreferenceModel({ + id: 'pref1', + userId: 'me', + category: Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, + name: 'dm1', + value: 'true', + }), + TestHelper.fakePreferenceModel({ + id: 'pref2', + userId: 'me', + category: Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, + name: 'dm2', + value: 'true', + }), + TestHelper.fakePreferenceModel({ + id: 'pref3', + userId: 'me', + category: Preferences.CATEGORIES.GROUP_CHANNEL_SHOW, + name: 'gm1', + value: 'false', + }), + ]; const {channels, myChannels} = buildChannelsData(); const data = await createChannelsDbRecords(db, channels, myChannels); diff --git a/app/utils/post/index.test.ts b/app/utils/post/index.test.ts index b6620ea2b..697c25ef9 100644 --- a/app/utils/post/index.test.ts +++ b/app/utils/post/index.test.ts @@ -8,6 +8,7 @@ import {getUsersCountFromMentions} from '@actions/local/post'; import {General, Post} from '@constants'; import {DEFAULT_LOCALE, getTranslations} from '@i18n'; import {getUserById} from '@queries/servers/user'; +import TestHelper from '@test/test_helper'; import {toMilliseconds} from '@utils/datetime'; import { @@ -28,9 +29,6 @@ import { persistentNotificationsConfirmation, } from '.'; -import type PostModel from '@typings/database/models/servers/post'; -import type UserModel from '@typings/database/models/servers/user'; - jest.mock('@actions/local/post', () => ({ getUsersCountFromMentions: jest.fn(), })); @@ -48,32 +46,32 @@ jest.mock('@database/manager', () => ({ describe('post utils', () => { describe('areConsecutivePosts', () => { it('should return true for consecutive posts from the same user within the collapse timeout', () => { - const post = { + const post = TestHelper.fakePostModel({ userId: 'user1', createAt: 1000, props: {}, - } as PostModel; - const previousPost = { + }); + const previousPost = TestHelper.fakePostModel({ userId: 'user1', createAt: 500, props: {}, - } as PostModel; + }); const result = areConsecutivePosts(post, previousPost); expect(result).toBe(true); }); it('should return false for posts from different users', () => { - const post = { + const post = TestHelper.fakePostModel({ userId: 'user1', createAt: 1000, props: {}, - } as PostModel; - const previousPost = { + }); + const previousPost = TestHelper.fakePostModel({ userId: 'user2', createAt: 500, props: {}, - } as PostModel; + }); const result = areConsecutivePosts(post, previousPost); expect(result).toBe(false); @@ -82,22 +80,22 @@ describe('post utils', () => { describe('isFromWebhook', () => { it('should return true for posts from a webhook', () => { - const post = { + const post = TestHelper.fakePostModel({ props: { from_webhook: 'true', - } as Record, - } as PostModel; + }, + }); const result = isFromWebhook(post); expect(result).toBe(true); }); it('should return false for posts not from a webhook', () => { - const post = { + const post = TestHelper.fakePostModel({ props: { from_webhook: 'false', - } as Record, - } as PostModel; + }, + }); const result = isFromWebhook(post); expect(result).toBe(false); @@ -106,18 +104,18 @@ describe('post utils', () => { describe('isEdited', () => { it('should return true if the post is edited', () => { - const post = { + const post = TestHelper.fakePostModel({ editAt: 1000, - } as PostModel; + }); const result = isEdited(post); expect(result).toBe(true); }); it('should return false if the post is not edited', () => { - const post = { + const post = TestHelper.fakePostModel({ editAt: 0, - } as PostModel; + }); const result = isEdited(post); expect(result).toBe(false); @@ -126,18 +124,18 @@ describe('post utils', () => { describe('isPostEphemeral', () => { it('should return true for an ephemeral post', () => { - const post = { + const post = TestHelper.fakePostModel({ type: Post.POST_TYPES.EPHEMERAL, - } as PostModel; + }); const result = isPostEphemeral(post); expect(result).toBe(true); }); it('should return false for a non-ephemeral post', () => { - const post = { - type: 'normal', - } as PostModel; + const post = TestHelper.fakePostModel({ + type: '', + }); const result = isPostEphemeral(post); expect(result).toBe(false); @@ -146,38 +144,38 @@ describe('post utils', () => { describe('isPostFailed', () => { it('should return true if the post has failed prop', () => { - const post = { + const post = TestHelper.fakePostModel({ props: { failed: true, - } as Record, + }, pendingPostId: 'id', id: 'id', updateAt: Date.now() - Post.POST_TIME_TO_FAIL - 1000, - } as PostModel; + }); const result = isPostFailed(post); expect(result).toBe(true); }); it('should return true if the post is pending and the update time has exceeded the failure time', () => { - const post = { + const post = TestHelper.fakePostModel({ props: {}, pendingPostId: 'id', id: 'id', updateAt: Date.now() - Post.POST_TIME_TO_FAIL - 1000, - } as PostModel; + }); const result = isPostFailed(post); expect(result).toBe(true); }); it('should return false if the post is not failed', () => { - const post = { + const post = TestHelper.fakePostModel({ props: {}, pendingPostId: 'id', id: 'id', updateAt: Date.now(), - } as PostModel; + }); const result = isPostFailed(post); expect(result).toBe(false); @@ -186,34 +184,34 @@ describe('post utils', () => { describe('isPostPendingOrFailed', () => { it('should return true if the post is pending', () => { - const post = { + const post = TestHelper.fakePostModel({ pendingPostId: 'id', id: 'id', props: {}, - } as PostModel; + }); const result = isPostPendingOrFailed(post); expect(result).toBe(true); }); it('should return true if the post has failed', () => { - const post = { + const post = TestHelper.fakePostModel({ pendingPostId: 'id', id: 'id', updateAt: Date.now() - Post.POST_TIME_TO_FAIL - 1000, props: {}, - } as PostModel; + }); const result = isPostPendingOrFailed(post); expect(result).toBe(true); }); it('should return false if the post is neither pending nor failed', () => { - const post = { + const post = TestHelper.fakePostModel({ pendingPostId: 'differentId', id: 'id', props: {}, - } as PostModel; + }); const result = isPostPendingOrFailed(post); expect(result).toBe(false); @@ -222,18 +220,18 @@ describe('post utils', () => { describe('isSystemMessage', () => { it('should return true if the post is a system message', () => { - const post = { - type: `${Post.POST_TYPES.SYSTEM_MESSAGE_PREFIX}any_type`, - } as PostModel; + const post = TestHelper.fakePostModel({ + type: `${Post.POST_TYPES.SYSTEM_MESSAGE_PREFIX}any_type` as PostType, + }); const result = isSystemMessage(post); expect(result).toBe(true); }); it('should return false if the post is not a system message', () => { - const post = { - type: 'normal_type', - } as PostModel; + const post = TestHelper.fakePostModel({ + type: 'add_bot_teams_channels', + }); const result = isSystemMessage(post); expect(result).toBe(false); @@ -290,18 +288,18 @@ describe('post utils', () => { describe('fromAutoResponder', () => { it('should return true if the post is from an auto responder', () => { - const post = { + const post = TestHelper.fakePostModel({ type: Post.POST_TYPES.SYSTEM_AUTO_RESPONDER, - } as PostModel; + }); const result = fromAutoResponder(post); expect(result).toBe(true); }); it('should return false if the post is not from an auto responder', () => { - const post = { - type: 'normal_type', - } as PostModel; + const post = TestHelper.fakePostModel({ + type: 'add_bot_teams_channels', + }); const result = fromAutoResponder(post); expect(result).toBe(false); @@ -320,8 +318,8 @@ describe('post utils', () => { const intl = createIntl({locale: DEFAULT_LOCALE, messages: getTranslations(DEFAULT_LOCALE)}); it('should show alert with DM channel description when channelType is DM_CHANNEL', async () => { - const mockUser = {username: 'teammate'}; - (getUserById as jest.Mock).mockResolvedValue(mockUser); + const mockUser = TestHelper.fakeUserModel({username: 'teammate'}); + jest.mocked(getUserById).mockResolvedValue(mockUser); await persistentNotificationsConfirmation( serverUrl, @@ -376,7 +374,7 @@ describe('post utils', () => { }); it('should show alert when no mentions found', async () => { - (getUsersCountFromMentions as jest.Mock).mockResolvedValue(0); + jest.mocked(getUsersCountFromMentions).mockResolvedValue(0); await persistentNotificationsConfirmation( serverUrl, @@ -404,7 +402,7 @@ describe('post utils', () => { }); it('should show alert when mentions exceed max recipients', async () => { - (getUsersCountFromMentions as jest.Mock).mockResolvedValue(15); + jest.mocked(getUsersCountFromMentions).mockResolvedValue(15); await persistentNotificationsConfirmation( serverUrl, @@ -435,7 +433,7 @@ describe('post utils', () => { }); it('should show confirmation alert for valid mentions within limit', async () => { - (getUsersCountFromMentions as jest.Mock).mockResolvedValue(5); + jest.mocked(getUsersCountFromMentions).mockResolvedValue(5); await persistentNotificationsConfirmation( serverUrl, @@ -467,42 +465,42 @@ describe('post utils', () => { describe('postUserDisplayName', () => { it('should return the override username if from webhook and override is enabled', () => { - const post = { + const post = TestHelper.fakePostModel({ props: { from_webhook: 'true', override_username: 'webhook_user', - } as Record, - } as PostModel; + }, + }); const result = postUserDisplayName(post, undefined, undefined, true); expect(result).toBe('webhook_user'); }); it('should return the author’s display name if not from webhook or override is disabled', () => { - const post = { + const post = TestHelper.fakePostModel({ props: { from_webhook: 'false', - } as Record, - } as PostModel; - const author = { + }, + }); + const author = TestHelper.fakeUserModel({ username: 'user1', locale: 'en', - } as UserModel; + }); const result = postUserDisplayName(post, author, undefined, false); expect(result).toBe('user1'); }); it('should return the author’s display name using the teammate name display', () => { - const post = { + const post = TestHelper.fakePostModel({ props: { from_webhook: 'false', - } as Record, - } as PostModel; - const author = { + }, + }); + const author = TestHelper.fakeUserModel({ username: 'user1', locale: 'en', - } as UserModel; + }); const result = postUserDisplayName(post, author, 'nickname', false); expect(result).toBe('user1'); @@ -511,18 +509,18 @@ describe('post utils', () => { describe('shouldIgnorePost', () => { it('should return true if the post type is in the ignore list', () => { - const post = { + const post = TestHelper.fakePost({ type: Post.POST_TYPES.CHANNEL_DELETED, - } as Post; + }); const result = shouldIgnorePost(post); expect(result).toBe(true); }); it('should return false if the post type is not in the ignore list', () => { - const post = { + const post = TestHelper.fakePost({ type: Post.POST_TYPES.EPHEMERAL, - } as Post; + }); const result = shouldIgnorePost(post); expect(result).toBe(false); @@ -534,17 +532,17 @@ describe('post utils', () => { const data = { order: ['post1', 'post2'], posts: { - post1: {id: 'post1', message: 'First post'}, - post2: {id: 'post2', message: 'Second post'}, + post1: TestHelper.fakePost({id: 'post1', message: 'First post'}), + post2: TestHelper.fakePost({id: 'post2', message: 'Second post'}), }, prev_post_id: 'post0', - } as unknown as PostResponse; + }; const result = processPostsFetched(data); expect(result).toEqual({ posts: [ - {id: 'post1', message: 'First post'}, - {id: 'post2', message: 'Second post'}, + expect.objectContaining({id: 'post1', message: 'First post'}), + expect.objectContaining({id: 'post2', message: 'Second post'}), ], order: ['post1', 'post2'], previousPostId: 'post0', @@ -555,9 +553,9 @@ describe('post utils', () => { describe('getLastFetchedAtFromPosts', () => { it('should return the maximum timestamp from the posts', () => { const posts = [ - {create_at: 1000, update_at: 2000, delete_at: 0}, - {create_at: 1500, update_at: 2500, delete_at: 3000}, - ] as Post[]; + TestHelper.fakePost({create_at: 1000, update_at: 2000, delete_at: 0}), + TestHelper.fakePost({create_at: 1500, update_at: 2500, delete_at: 3000}), + ]; const result = getLastFetchedAtFromPosts(posts); expect(result).toBe(3000); diff --git a/app/utils/post/index.ts b/app/utils/post/index.ts index 202fb08ca..3da32078d 100644 --- a/app/utils/post/index.ts +++ b/app/utils/post/index.ts @@ -11,7 +11,7 @@ import DatabaseManager from '@database/manager'; import {DEFAULT_LOCALE} from '@i18n'; import {getUserById} from '@queries/servers/user'; import {toMilliseconds} from '@utils/datetime'; -import {ensureString} from '@utils/types'; +import {ensureString, includes} from '@utils/types'; import {displayUsername, getUserIdFromChannelName} from '@utils/user'; import type PostModel from '@typings/database/models/servers/post'; @@ -78,7 +78,7 @@ export function postUserDisplayName(post: PostModel, author?: UserModel, teammat } export function shouldIgnorePost(post: Post): boolean { - return Post.IGNORE_POST_TYPES.includes(post.type); + return includes(Post.IGNORE_POST_TYPES, post.type); } export const processPostsFetched = (data: PostResponse) => { diff --git a/app/utils/post_list/index.test.ts b/app/utils/post_list/index.test.ts index 64cff6488..28ce5fe0f 100644 --- a/app/utils/post_list/index.test.ts +++ b/app/utils/post_list/index.test.ts @@ -593,8 +593,8 @@ describe('preparePostList', () => { it('should combine user activity posts correctly', () => { const posts = [ - mockPostModel({id: 'post1', type: 'user-activity', props: {user_activity_posts: []}}), - mockPostModel({id: 'post2', type: 'user-activity', props: {user_activity_posts: []}}), + mockPostModel({id: 'post1', type: 'system_combined_user_activity', props: {user_activity_posts: []}}), + mockPostModel({id: 'post2', type: 'system_combined_user_activity', props: {user_activity_posts: []}}), ]; const result = preparePostList(posts, lastViewedAt, true, currentUserId, currentUsername, showJoinLeave, currentTimezone, isThreadScreen, savedPostIds); @@ -613,7 +613,7 @@ describe('preparePostList', () => { id: 'deleted-post', deleteAt: Date.now(), // Set deleteAt to a non-zero value createAt: Date.now(), - type: 'post', // Ensure it's a 'post' type + type: '', userId: 'user-id', message: 'This is a deleted post', props: {}, @@ -732,7 +732,7 @@ describe('shouldFilterJoinLeavePost', () => { const currentUsername = 'current-username'; it('should not filter non-join/leave posts', () => { - const post = mockPostModel({type: 'custom_post_type'}); + const post = mockPostModel({type: 'custom_post_type' as PostType}); const result = shouldFilterJoinLeavePost(post, false, currentUsername); // Non-join/leave posts should not be filtered regardless of showJoinLeave value diff --git a/app/utils/post_list/index.ts b/app/utils/post_list/index.ts index d2a8cb514..c5a484f7d 100644 --- a/app/utils/post_list/index.ts +++ b/app/utils/post_list/index.ts @@ -6,7 +6,7 @@ import moment from 'moment-timezone'; import {Post} from '@constants'; import {toMilliseconds} from '@utils/datetime'; import {isFromWebhook} from '@utils/post'; -import {ensureString, isArrayOf, isStringArray} from '@utils/types'; +import {ensureString, includes, isArrayOf, isStringArray, secureGetFromRecord} from '@utils/types'; import type {PostList, PostWithPrevAndNext} from '@typings/components/post_list'; import type PostModel from '@typings/database/models/servers/post'; @@ -73,7 +73,7 @@ function combineUserActivityPosts(orderedPosts: PostList) { lastPostIsUserActivity = false; combinedCount = 0; } else { - const postIsUserActivity = item.type === 'post' && Post.USER_ACTIVITY_POST_TYPES.includes(item.value.currentPost.type); + const postIsUserActivity = item.type === 'post' && includes(Post.USER_ACTIVITY_POST_TYPES, item.value.currentPost.type); if (postIsUserActivity && lastPostIsUserActivity && combinedCount < MAX_COMBINED_SYSTEM_POSTS) { out[out.length - 1].value += '_' + item.value.currentPost.id; } else if (postIsUserActivity) { @@ -96,21 +96,26 @@ function combineUserActivityPosts(orderedPosts: PostList) { return out; } -function comparePostTypes(a: typeof postTypePriority, b: typeof postTypePriority) { - return postTypePriority[a.postType] - postTypePriority[b.postType]; +function comparePostTypes(a: MessageData, b: MessageData) { + const aPriority = secureGetFromRecord(postTypePriority, a.postType) ?? 99; + const bPriority = secureGetFromRecord(postTypePriority, b.postType) ?? 99; + return aPriority - bPriority; } -function extractUserActivityData(userActivities: any) { - const messageData: any[] = []; +function extractUserActivityData(userActivities: Record): UserActivityProp { + const messageData: MessageData[] = []; const allUserIds: string[] = []; const allUsernames: string[] = []; - Object.entries(userActivities).forEach(([postType, values]: [string, any]) => { + Object.entries(userActivities).forEach(([postType, values]: [PostType, UserActivityValue]) => { if ( postType === Post.POST_TYPES.ADD_TO_TEAM || postType === Post.POST_TYPES.ADD_TO_CHANNEL || postType === Post.POST_TYPES.REMOVE_FROM_CHANNEL ) { - Object.keys(values).map((key) => [key, values[key]]).forEach(([actorId, users]) => { + if (Array.isArray(values)) { + throw new Error('Invalid Post activity data'); + } + Object.entries(values).forEach(([actorId, users]) => { if (Array.isArray(users)) { throw new Error('Invalid Post activity data'); } @@ -256,10 +261,15 @@ export function selectOrderedPosts( return out.reverse(); } +type ActivityData = { + ids: string[]; + usernames: string[]; +} +type UserActivityValue = string[] | Record function combineUserActivitySystemPost(systemPosts: PostModel[]) { - const userActivities = systemPosts.reduce((acc: any, post: PostModel) => { + const userActivities = systemPosts.reduce((acc: Record, post: PostModel) => { const postType = post.type; - let userActivityProps = acc; + const userActivityProps = acc; const combinedPostType = userActivityProps[postType]; if ( @@ -270,10 +280,12 @@ function combineUserActivitySystemPost(systemPosts: PostModel[]) { const userId = ensureString(post.props?.addedUserId) || ensureString(post.props?.removedUserId); const username = ensureString(post.props?.addedUsername) || ensureString(post.props?.removedUsername); if (combinedPostType) { - if (Array.isArray(combinedPostType[post.userId])) { + if (Array.isArray(combinedPostType)) { throw new Error('Invalid Post activity data'); } - const users = combinedPostType[post.userId] || {ids: [], usernames: []}; + + const userCombinedPostType = combinedPostType[post.userId]; + const users = userCombinedPostType || {ids: [], usernames: []}; if (userId) { if (!users.ids.includes(userId)) { users.ids.push(userId); @@ -308,7 +320,7 @@ function combineUserActivitySystemPost(systemPosts: PostModel[]) { userActivityProps[postType] = [...combinedPostType, propsUserId]; } } else { - userActivityProps = {...userActivityProps, [postType]: [propsUserId]}; + userActivityProps[postType] = [propsUserId]; } } @@ -348,7 +360,7 @@ export function generateCombinedPost(combinedId: string, systemPosts: PostModel[ user_activity_posts: systemPosts, system_post_ids: systemPosts.map((post) => post.id), }, - type: Post.POST_TYPES.COMBINED_USER_ACTIVITY as PostType, + type: Post.POST_TYPES.COMBINED_USER_ACTIVITY, user_id: '', metadata: {}, }; @@ -380,7 +392,7 @@ export function shouldFilterJoinLeavePost(post: PostModel, showJoinLeave: boolea } // Don't filter out non-join/leave messages - if (joinLeavePostTypes.indexOf(post.type) === -1) { + if (!includes(joinLeavePostTypes, post.type)) { return false; } @@ -390,7 +402,7 @@ export function shouldFilterJoinLeavePost(post: PostModel, showJoinLeave: boolea export type MessageData = { actorId?: string; - postType: string; + postType: PostType; userIds: string[]; } diff --git a/app/utils/types.ts b/app/utils/types.ts index a78a3b8bf..d559a68d7 100644 --- a/app/utils/types.ts +++ b/app/utils/types.ts @@ -40,3 +40,7 @@ export function ensureNumber(v: unknown): number { export function secureGetFromRecord(v: Record | undefined, key: string) { return typeof v === 'object' && v && Object.prototype.hasOwnProperty.call(v, key) ? v[key] : undefined; } + +export function includes(array: T[], value: U) { + return array.includes(value as T); +} diff --git a/app/utils/url/test.ts b/app/utils/url/test.ts index bdb16205a..9d90de551 100644 --- a/app/utils/url/test.ts +++ b/app/utils/url/test.ts @@ -135,7 +135,7 @@ describe('UrlUtils', () => { const SERVER_WITH_SUBPATH = `http://${URL_PATH_NO_PROTOCOL}`; const DEEPLINK_URL_ROOT = `mattermost://${URL_NO_PROTOCOL}`; - const DM_USER = TestHelper.fakeUserWithId(); + const DM_USER = TestHelper.fakeUser(); const GM_CHANNEL_NAME = '4862db64e76a321d167fe6677f16e96e9275dabe'; const tests = [ diff --git a/app/utils/user/index.test.ts b/app/utils/user/index.test.ts index b2593254f..ef7bbf434 100644 --- a/app/utils/user/index.test.ts +++ b/app/utils/user/index.test.ts @@ -7,6 +7,7 @@ import {Alert} from 'react-native'; import {Preferences} from '@constants'; import {Ringtone} from '@constants/calls'; +import TestHelper from '@test/test_helper'; import { confirmOutOfOfficeDisabled, @@ -38,16 +39,15 @@ import { removeUserFromList, } from './index'; -import type {UserModel} from '@database/models/server'; import type {IntlShape} from 'react-intl'; describe('displayUsername', () => { - const user = { + const user = TestHelper.fakeUser({ username: 'johndoe', first_name: 'John', last_name: 'Doe', nickname: 'Johnny', - } as UserProfile; + }); it('should return the nickname if teammateDisplayNameSetting is DISPLAY_PREFER_NICKNAME', () => { const result = displayUsername(user, 'en', Preferences.DISPLAY_PREFER_NICKNAME); @@ -80,32 +80,32 @@ describe('displayUsername', () => { }); it('should return the username if nickname and full name are not available', () => { - const userWithoutNicknameAndFullName = { + const userWithoutNicknameAndFullName = TestHelper.fakeUser({ username: 'johndoe', first_name: '', last_name: '', nickname: '', - } as UserProfile; + }); const result = displayUsername(userWithoutNicknameAndFullName, 'en', Preferences.DISPLAY_PREFER_NICKNAME); expect(result).toBe('johndoe'); }); it('should return the username if name is empty or whitespace', () => { - const userWithEmptyName = { + const userWithEmptyName = TestHelper.fakeUser({ username: 'johndoe', first_name: '', last_name: '', nickname: '', - } as UserProfile; + }); const result = displayUsername(userWithEmptyName, 'en', Preferences.DISPLAY_PREFER_FULL_NAME); expect(result).toBe('johndoe'); }); }); describe('displayGroupMessageName', () => { - const user1 = {id: 'user1', username: 'john_doe', first_name: 'John', last_name: 'Doe'} as UserProfile; - const user2 = {id: 'user2', username: 'jane_doe', first_name: 'Jane', last_name: 'Doe'} as UserProfile; - const user3 = {id: 'user3', username: 'alice_smith', first_name: 'Alice', last_name: 'Smith'} as UserProfile; + const user1 = TestHelper.fakeUser({id: 'user1', username: 'john_doe', first_name: 'John', last_name: 'Doe'}); + const user2 = TestHelper.fakeUser({id: 'user2', username: 'jane_doe', first_name: 'Jane', last_name: 'Doe'}); + const user3 = TestHelper.fakeUser({id: 'user3', username: 'alice_smith', first_name: 'Alice', last_name: 'Smith'}); it('should return a comma-separated list of usernames', () => { const users = [user1, user2, user3]; @@ -133,7 +133,7 @@ describe('displayGroupMessageName', () => { }); it('should fallback to username if full name or nickname is not available', () => { - const userWithEmptyName = {id: 'user4', username: 'empty_name', first_name: '', last_name: ''} as UserProfile; + const userWithEmptyName = TestHelper.fakeUser({id: 'user4', username: 'empty_name', first_name: '', last_name: ''}); const users = [userWithEmptyName, user2, user3]; const result = displayGroupMessageName(users, undefined, Preferences.DISPLAY_PREFER_FULL_NAME); expect(result).toBe('Alice Smith, empty_name, Jane Doe'); @@ -148,31 +148,31 @@ describe('displayGroupMessageName', () => { describe('getFullName', () => { it('should return the full name if both first and last names are provided - UserProfile', () => { - const user = {first_name: 'John', last_name: 'Doe'} as UserProfile; + const user = TestHelper.fakeUser({first_name: 'John', last_name: 'Doe'}); const result = getFullName(user); expect(result).toBe('John Doe'); }); it('should return the full name if both first and last names are provided - UserModel', () => { - const user = {firstName: 'John', lastName: 'Doe'} as UserModel; + const user = TestHelper.fakeUserModel({firstName: 'John', lastName: 'Doe'}); const result = getFullName(user); expect(result).toBe('John Doe'); }); it('should return the first name if only the first name is provided', () => { - const user = {first_name: 'John', last_name: ''} as UserProfile; + const user = TestHelper.fakeUser({first_name: 'John', last_name: ''}); const result = getFullName(user); expect(result).toBe('John'); }); it('should return the last name if only the last name is provided', () => { - const user = {first_name: '', last_name: 'Doe'} as UserProfile; + const user = TestHelper.fakeUser({first_name: '', last_name: 'Doe'}); const result = getFullName(user); expect(result).toBe('Doe'); }); it('should return an empty string if neither first nor last name is provided', () => { - const user = {first_name: '', last_name: ''} as UserProfile; + const user = TestHelper.fakeUser({first_name: '', last_name: ''}); const result = getFullName(user); expect(result).toBe(''); }); @@ -255,8 +255,8 @@ describe('isChannelAdmin', () => { describe('getUsersByUsername', () => { it('should return a dictionary of users by username', () => { const users = [ - {username: 'john_doe'} as UserModel, - {username: 'jane_doe'} as UserModel, + TestHelper.fakeUserModel({username: 'john_doe'}), + TestHelper.fakeUserModel({username: 'jane_doe'}), ]; const result = getUsersByUsername(users); expect(result).toEqual({ @@ -268,7 +268,7 @@ describe('getUsersByUsername', () => { describe('getUserTimezoneProps', () => { it('should return the user timezone props if they exist', () => { - const user = {timezone: {useAutomaticTimezone: 'true', automaticTimezone: 'America/New_York', manualTimezone: 'America/Los_Angeles'}} as UserModel; + const user = TestHelper.fakeUserModel({timezone: {useAutomaticTimezone: 'true', automaticTimezone: 'America/New_York', manualTimezone: 'America/Los_Angeles'}}); const result = getUserTimezoneProps(user); expect(result).toEqual({ useAutomaticTimezone: true, @@ -278,7 +278,7 @@ describe('getUserTimezoneProps', () => { }); it('should return default timezone props if they do not exist', () => { - const user = {} as UserModel; + const user = TestHelper.fakeUserModel({timezone: null}); const result = getUserTimezoneProps(user); expect(result).toEqual({ useAutomaticTimezone: true, @@ -290,13 +290,13 @@ describe('getUserTimezoneProps', () => { describe('getUserTimezone', () => { it('should return the user timezone', () => { - const user = {timezone: {useAutomaticTimezone: 'true', automaticTimezone: 'America/New_York', manualTimezone: 'America/Los_Angeles'}} as UserModel; + const user = TestHelper.fakeUserModel({timezone: {useAutomaticTimezone: 'true', automaticTimezone: 'America/New_York', manualTimezone: 'America/Los_Angeles'}}); const result = getUserTimezone(user); expect(result).toBe('America/New_York'); }); it('should return an empty string if the user timezone does not exist', () => { - const user = {} as UserModel; + const user = TestHelper.fakeUserModel({timezone: null}); const result = getUserTimezone(user); expect(result).toBe(''); }); @@ -304,13 +304,13 @@ describe('getUserTimezone', () => { describe('getTimezone', () => { it('should return the automatic timezone if useAutomaticTimezone is true', () => { - const timezone = {useAutomaticTimezone: 'true', automaticTimezone: 'America/New_York', manualTimezone: 'America/Los_Angeles'} as UserTimezone; + const timezone: UserTimezone = {useAutomaticTimezone: 'true', automaticTimezone: 'America/New_York', manualTimezone: 'America/Los_Angeles'}; const result = getTimezone(timezone); expect(result).toBe('America/New_York'); }); it('should return the manual timezone if useAutomaticTimezone is false', () => { - const timezone = {useAutomaticTimezone: 'false', automaticTimezone: 'America/New_York', manualTimezone: 'America/Los_Angeles'} as UserTimezone; + const timezone: UserTimezone = {useAutomaticTimezone: 'false', automaticTimezone: 'America/New_York', manualTimezone: 'America/Los_Angeles'}; const result = getTimezone(timezone); expect(result).toBe('America/Los_Angeles'); }); @@ -337,10 +337,10 @@ describe('getTimezoneRegion', () => { describe('getUserCustomStatus', () => { it('should return the custom status if it exists', () => { - const user = { + const user = TestHelper.fakeUser({ username: 'johndoe', - props: {customStatus: '{"emoji": "smile", "text": "Happy", "duration": "today", "expires_at": "2023-12-31T23:59:59Z"}'} as UserProps, - } as UserProfile; + props: {customStatus: '{"emoji": "smile", "text": "Happy", "duration": "today", "expires_at": "2023-12-31T23:59:59Z"}'}, + }); const result = getUserCustomStatus(user); expect(result).toEqual({ emoji: 'smile', @@ -351,7 +351,7 @@ describe('getUserCustomStatus', () => { }); it('should return undefined if the custom status does not exist', () => { - const user = {} as UserModel; + const user = TestHelper.fakeUserModel(); const result = getUserCustomStatus(user); expect(result).toBeUndefined(); }); @@ -359,23 +359,25 @@ describe('getUserCustomStatus', () => { describe('isCustomStatusExpired', () => { it('should return true if the custom status is expired', () => { - const user = {username: 'john_doe', - props: {customStatus: '{"emoji": "smile", "text": "Happy", "duration": "today", "expires_at": "2020-12-31T23:59:59Z"}'} as UserProps, - } as UserProfile; + const user = TestHelper.fakeUser({ + username: 'john_doe', + props: {customStatus: '{"emoji": "smile", "text": "Happy", "duration": "today", "expires_at": "2020-12-31T23:59:59Z"}'}, + }); const result = isCustomStatusExpired(user); expect(result).toBe(true); }); it('should return false if the custom status is not expired', () => { - const user = {username: 'john_doe', - props: {customStatus: '{"emoji": "smile", "text": "Happy", "duration": "today", "expires_at": "2099-12-31T23:59:59Z"}'} as UserProps, - } as UserProfile; + const user = TestHelper.fakeUser({ + username: 'john_doe', + props: {customStatus: '{"emoji": "smile", "text": "Happy", "duration": "today", "expires_at": "2099-12-31T23:59:59Z"}'}, + }); const result = isCustomStatusExpired(user); expect(result).toBe(false); }); it('should return true if the custom status does not exist', () => { - const user = {} as UserModel; + const user = TestHelper.fakeUserModel(); const result = isCustomStatusExpired(user); expect(result).toBe(true); }); @@ -383,13 +385,13 @@ describe('isCustomStatusExpired', () => { describe('isBot', () => { it('should return true if the user is a bot', () => { - const user = {isBot: true} as UserModel; + const user = TestHelper.fakeUserModel({isBot: true}); const result = isBot(user); expect(result).toBe(true); }); it('should return false if the user is not a bot', () => { - const user = {isBot: false} as UserModel; + const user = TestHelper.fakeUserModel({isBot: false}); const result = isBot(user); expect(result).toBe(false); }); @@ -397,13 +399,13 @@ describe('isBot', () => { describe('isShared', () => { it('should return true if the user is shared', () => { - const user = {remoteId: 'remote_id'} as UserModel; + const user = TestHelper.fakeUserModel({remoteId: 'remote_id'}); const result = isShared(user); expect(result).toBe(true); }); it('should return false if the user is not shared', () => { - const user = {remoteId: ''} as UserModel; + const user = TestHelper.fakeUserModel({remoteId: ''}); const result = isShared(user); expect(result).toBe(false); }); @@ -437,8 +439,8 @@ describe('getSuggestionsSplitByMultiple', () => { describe('filterProfilesMatchingTerm', () => { const users = [ - {username: 'john_doe', first_name: 'John', last_name: 'Doe', nickname: 'Johnny', email: 'john@example.com'} as UserProfile, - {username: 'jane_doe', first_name: 'Jane', last_name: 'Doe', nickname: 'Janey', email: 'jane@example.com'} as UserProfile, + TestHelper.fakeUser({username: 'john_doe', first_name: 'John', last_name: 'Doe', nickname: 'Johnny', email: 'john@example.com'}), + TestHelper.fakeUser({username: 'jane_doe', first_name: 'Jane', last_name: 'Doe', nickname: 'Janey', email: 'jane@example.com'}), ]; it('should filter users matching the term', () => { @@ -468,13 +470,13 @@ describe('filterProfilesMatchingTerm', () => { describe('getNotificationProps', () => { it('should return the user notification props if they exist', () => { - const user = {notifyProps: {channel: 'false', comments: 'never'}} as UserModel; + const user = TestHelper.fakeUserModel({notifyProps: TestHelper.fakeUserNotifyProps({channel: 'false', comments: 'never'})}); const result = getNotificationProps(user); expect(result).toEqual(user.notifyProps); }); it('should return default notification props if they do not exist', () => { - const user = {} as UserModel; + const user = TestHelper.fakeUserModel({firstName: '', notifyProps: null}); const result = getNotificationProps(user); expect(result).toEqual({ channel: 'true', @@ -484,7 +486,7 @@ describe('getNotificationProps', () => { email: 'true', first_name: 'false', mark_unread: 'all', - mention_keys: '', + mention_keys: `${user.username},@${user.username}`, highlight_keys: '', push: 'mention', push_status: 'online', @@ -531,19 +533,19 @@ describe('getEmailIntervalTexts', () => { describe('getLastPictureUpdate', () => { it('should return bot_last_icon_update if the user is a bot', () => { - const user = {isBot: true, props: {bot_last_icon_update: 12345} as UserProps} as UserModel; + const user = TestHelper.fakeUserModel({isBot: true, props: {bot_last_icon_update: 12345}}); const result = getLastPictureUpdate(user); expect(result).toBe(12345); }); it('should return lastPictureUpdate if the user is not a bot', () => { - const user = {isBot: false, lastPictureUpdate: 67890} as UserModel; + const user = TestHelper.fakeUserModel({isBot: false, lastPictureUpdate: 67890}); const result = getLastPictureUpdate(user); expect(result).toBe(67890); }); it('should return 0 if lastPictureUpdate is not available', () => { - const user = {isBot: false} as UserModel; + const user = TestHelper.fakeUserModel({isBot: false}); const result = getLastPictureUpdate(user); expect(result).toBe(0); }); @@ -551,34 +553,34 @@ describe('getLastPictureUpdate', () => { describe('isDeactivated', () => { it('should return true if the user is deactivated', () => { - const user = {delete_at: 12345} as UserProfile; + const user = TestHelper.fakeUser({delete_at: 12345}); const result = isDeactivated(user); expect(result).toBe(true); }); it('should return false if the user is not deactivated', () => { - const user = {delete_at: 0} as UserProfile; + const user = TestHelper.fakeUser({delete_at: 0}); const result = isDeactivated(user); expect(result).toBe(false); }); it('should return true if the user is deactivated using deleteAt', () => { - const user = {deleteAt: 12345} as UserModel; + const user = TestHelper.fakeUserModel({deleteAt: 12345}); const result = isDeactivated(user); expect(result).toBe(true); }); it('should return false if the user is not deactivated using deleteAt', () => { - const user = {deleteAt: 0} as UserModel; + const user = TestHelper.fakeUserModel({deleteAt: 0}); const result = isDeactivated(user); expect(result).toBe(false); }); }); describe('removeUserFromList', () => { - const user1 = {id: 'user1'} as UserProfile; - const user2 = {id: 'user2'} as UserProfile; - const user3 = {id: 'user3'} as UserProfile; + const user1 = TestHelper.fakeUser({id: 'user1'}); + const user2 = TestHelper.fakeUser({id: 'user2'}); + const user3 = TestHelper.fakeUser({id: 'user3'}); it('should remove the user from the list', () => { const originalList = [user1, user2, user3]; @@ -669,8 +671,8 @@ describe('confirmOutOfOfficeDisabled', () => { it('should call updateStatus with the correct status when OK is pressed', () => { confirmOutOfOfficeDisabled(intl, 'online', updateStatus); - const okButton = (Alert.alert as jest.Mock).mock.calls[0][2][1]; - okButton.onPress(); + const okButton = jest.mocked(Alert.alert).mock.calls[0][2]?.[1]; + okButton?.onPress?.(); expect(updateStatus).toHaveBeenCalledWith('online'); }); diff --git a/test/test_helper.ts b/test/test_helper.ts index 5ec2735ea..5f7c35678 100644 --- a/test/test_helper.ts +++ b/test/test_helper.ts @@ -19,7 +19,25 @@ import DatabaseManager from '@database/manager'; import {prepareCommonSystemValues} from '@queries/servers/system'; import type {APIClientInterface} from '@mattermost/react-native-network-client'; -import type {Model, Query} from '@nozbe/watermelondb'; +import type {Model, Query, Relation} from '@nozbe/watermelondb'; +import type CategoryChannelModel from '@typings/database/models/servers/category_channel'; +import type ChannelModel from '@typings/database/models/servers/channel'; +import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark'; +import type ChannelInfoModel from '@typings/database/models/servers/channel_info'; +import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership'; +import type DraftModel from '@typings/database/models/servers/draft'; +import type FileModel from '@typings/database/models/servers/file'; +import type GroupModel from '@typings/database/models/servers/group'; +import type MyChannelModel from '@typings/database/models/servers/my_channel'; +import type MyChannelSettingsModel from '@typings/database/models/servers/my_channel_settings'; +import type MyTeamModel from '@typings/database/models/servers/my_team'; +import type PostModel from '@typings/database/models/servers/post'; +import type PostsInChannelModel from '@typings/database/models/servers/posts_in_channel'; +import type PreferenceModel from '@typings/database/models/servers/preference'; +import type RoleModel from '@typings/database/models/servers/role'; +import type TeamModel from '@typings/database/models/servers/team'; +import type ThreadModel from '@typings/database/models/servers/thread'; +import type UserModel from '@typings/database/models/servers/user'; const DEFAULT_LOCALE = 'en'; @@ -247,6 +265,19 @@ class TestHelperSingleton { }; }; + fakeChannelInfo = (overwrite: Partial): ChannelInfo => { + return { + id: this.generateId(), + guest_count: 0, + header: '', + member_count: 0, + purpose: '', + pinned_post_count: 0, + files_count: 0, + ...overwrite, + }; + }; + fakeChannelWithId = (teamId: string): Channel => { return { ...this.fakeChannel({team_id: teamId}), @@ -319,7 +350,7 @@ class TestHelperSingleton { return 'success' + this.generateId() + '@simulator.amazonses.com'; }; - fakePost = (overwrite: Partial): Post => { + fakePost = (overwrite?: Partial): Post => { const time = Date.now(); return { @@ -424,7 +455,424 @@ class TestHelperSingleton { }; }; - fakeUser = (): UserProfile => { + fakeQuery = (returnValue: T[]): Query => { + return { + fetch: jest.fn().mockImplementation(async () => returnValue), + observe: jest.fn().mockImplementation(() => of$(returnValue)), + } as unknown as Query; + }; + + fakeRelation = (returnValue?: T): Relation => { + return { + fetch: jest.fn().mockImplementation(async () => returnValue!), + _model: {} as T, + _columnName: '', + _relationTableName: '', + _isImmutable: false, + _cachedObservable: of$(returnValue!), + id: this.generateId(), + then: jest.fn(), + set: jest.fn(), + observe: jest.fn().mockImplementation(() => of$(returnValue!)), + }; + }; + + fakeModel = () => { + return { + id: this.generateId(), + _raw: {} as any, + collection: {} as any, + collections: {} as any, + database: {} as any, + db: {} as any, + asModel: {} as any, + _isEditing: false, + _preparedState: 'create' as const, + syncStatus: 'synced' as const, + table: '', + _subscribers: [], + _getChanges: jest.fn(), + update: jest.fn(), + prepareUpdate: jest.fn(), + cancelPrepareUpdate: jest.fn(), + prepareMarkAsDeleted: jest.fn(), + prepareDestroyPermanently: jest.fn(), + markAsDeleted: jest.fn(), + destroyPermanently: jest.fn(), + experimentalMarkAsDeleted: jest.fn(), + experimentalDestroyPermanently: jest.fn(), + observe: jest.fn(), + batch: jest.fn(), + callWriter: jest.fn(), + callReader: jest.fn(), + subAction: jest.fn(), + experimentalSubscribe: jest.fn(), + _notifyChanged: jest.fn(), + _notifyDestroyed: jest.fn(), + _getRaw: jest.fn(), + _setRaw: jest.fn(), + _dangerouslySetRawWithoutMarkingColumnChange: jest.fn(), + __ensureCanSetRaw: jest.fn(), + __ensureNotDisposable: jest.fn(), + }; + }; + + fakeUserModel = (overwrite?: Partial): UserModel => { + const modelBase = this.fakeModel(); + return { + ...modelBase, + email: this.fakeEmail(), + locale: DEFAULT_LOCALE, + username: this.generateId(), + firstName: this.generateId(), + lastName: this.generateId(), + deleteAt: 0, + roles: 'system_user', + authService: '', + id: this.generateId(), + nickname: '', + notifyProps: this.fakeUserNotifyProps(), + position: '', + updateAt: 0, + isBot: false, + isGuest: false, + lastPictureUpdate: 0, + status: 'offline', + remoteId: null, + props: {}, + timezone: {automaticTimezone: 'UTC', manualTimezone: '', useAutomaticTimezone: true}, + mentionKeys: [], + userMentionKeys: [], + highlightKeys: [], + termsOfServiceId: '', + termsOfServiceCreateAt: 0, + + channelsCreated: this.fakeQuery([]), + channels: this.fakeQuery([]), + posts: this.fakeQuery([]), + preferences: this.fakeQuery([]), + reactions: this.fakeQuery([]), + teams: this.fakeQuery([]), + threadParticipations: this.fakeQuery([]), + + prepareStatus: () => null, + + ...overwrite, + }; + }; + + fakeChannelModel = (overwrite?: Partial): ChannelModel => { + return { + ...this.fakeModel(), + createAt: 0, + creatorId: this.generateId(), + deleteAt: 0, + updateAt: 0, + displayName: this.generateId(), + isGroupConstrained: false, + name: this.generateId(), + shared: false, + teamId: this.generateId(), + type: 'O' as const, + members: this.fakeQuery([]), + drafts: this.fakeQuery([]), + bookmarks: this.fakeQuery([]), + posts: this.fakeQuery([]), + postsInChannel: this.fakeQuery([]), + team: this.fakeRelation(), + creator: this.fakeRelation(), + info: this.fakeRelation(), + membership: this.fakeRelation(), + categoryChannel: this.fakeRelation(), + toApi: jest.fn(), + ...overwrite, + }; + }; + + fakeChannelMembershipModel = (overwrite?: Partial): ChannelMembershipModel => { + return { + ...this.fakeModel(), + channelId: this.generateId(), + userId: this.generateId(), + schemeAdmin: false, + memberChannel: this.fakeRelation(), + memberUser: this.fakeRelation(), + getAllChannelsForUser: this.fakeQuery([]), + getAllUsersInChannel: this.fakeQuery([]), + ...overwrite, + }; + }; + fakeMyChannelMembershipModel = (overwrite?: Partial): MyChannelModel => { + return { + ...this.fakeModel(), + channel: this.fakeRelation(), + lastPostAt: 0, + lastFetchedAt: 0, + lastViewedAt: 0, + manuallyUnread: false, + mentionsCount: 0, + messageCount: 0, + isUnread: false, + roles: '', + viewedAt: 0, + settings: this.fakeRelation(), + resetPreparedState: jest.fn(), + ...overwrite, + }; + }; + + fakeCategoryChannelModel = (overwrite?: Partial): CategoryChannelModel => { + return { + ...this.fakeModel(), + channel: this.fakeRelation(), + categoryId: this.generateId(), + channelId: this.generateId(), + sortOrder: 0, + category: this.fakeRelation(), + myChannel: this.fakeRelation(), + ...overwrite, + }; + }; + + fakeDraftModel = (overwrite?: Partial): DraftModel => { + return { + ...this.fakeModel(), + channelId: this.generateId(), + message: '', + rootId: '', + files: [], + metadata: {}, + updateAt: 0, + ...overwrite, + }; + }; + + fakePostsInChannelModel = (overwrite?: Partial): PostsInChannelModel => { + return { + ...this.fakeModel(), + channelId: this.generateId(), + earliest: 0, + latest: 0, + channel: this.fakeRelation(), + ...overwrite, + }; + }; + + fakeChannelBookmarkModel = (overwrite?: Partial): ChannelBookmarkModel => { + return { + ...this.fakeModel(), + channelId: this.generateId(), + ownerId: this.generateId(), + fileId: this.generateId(), + displayName: this.generateId(), + createAt: 0, + updateAt: 0, + deleteAt: 0, + sortOrder: 0, + type: 'file', + channel: this.fakeRelation(), + owner: this.fakeRelation(), + file: this.fakeRelation(), + toApi: jest.fn(), + ...overwrite, + }; + }; + + fakeFileModel = (overwrite?: Partial): FileModel => { + return { + ...this.fakeModel(), + id: this.generateId(), + extension: 'png', + height: 100, + width: 100, + mimeType: 'image/png', + name: 'image1', + size: 100, + imageThumbnail: '', + localPath: 'path/to/image1', + postId: this.generateId(), + post: this.fakeRelation(), + toFileInfo: jest.fn(), + ...overwrite, + }; + }; + + fakeMyChannelSettingsModel = (overwrite?: Partial): MyChannelSettingsModel => { + return { + ...this.fakeModel(), + notifyProps: this.fakeChannelNotifyProps(), + myChannel: this.fakeRelation(), + ...overwrite, + }; + }; + + fakeTeamModel = (overwrite?: Partial): TeamModel => { + return { + ...this.fakeModel(), + id: this.generateId(), + name: this.generateId(), + displayName: this.generateId(), + type: 'O' as const, + allowedDomains: '', + inviteId: this.generateId(), + description: '', + updateAt: 0, + isAllowOpenInvite: true, + isGroupConstrained: false, + lastTeamIconUpdatedAt: 0, + categories: this.fakeQuery([]), + channels: this.fakeQuery([]), + myTeam: this.fakeRelation(), + teamChannelHistory: this.fakeRelation(), + members: this.fakeQuery([]), + teamSearchHistories: this.fakeQuery([]), + ...overwrite, + }; + }; + + fakeChannelInfoModel = (overwrite?: Partial): ChannelInfoModel => { + return { + ...this.fakeModel(), + guestCount: 0, + header: '', + memberCount: 0, + pinnedPostCount: 0, + filesCount: 0, + purpose: '', + channel: this.fakeRelation(), + ...overwrite, + }; + }; + + fakePostModel = (overwrite?: Partial): PostModel => { + return { + ...this.fakeModel(), + channelId: this.generateId(), + createAt: 0, + deleteAt: 0, + editAt: 0, + isPinned: false, + message: '', + metadata: {}, + originalId: '', + pendingPostId: '', + props: {}, + rootId: '', + type: '', + updateAt: 0, + messageSource: '', + previousPostId: '', + userId: this.generateId(), + root: this.fakeQuery([]), + drafts: this.fakeQuery([]), + files: this.fakeQuery([]), + postsInThread: this.fakeQuery([]), + reactions: this.fakeQuery([]), + author: this.fakeRelation(), + channel: this.fakeRelation(), + thread: this.fakeRelation(), + hasReplies: async () => false, + toApi: jest.fn(), + ...overwrite, + }; + }; + + fakeGroupModel = (overwrite?: Partial): GroupModel => { + return { + ...this.fakeModel(), + name: this.generateId(), + displayName: this.generateId(), + description: this.generateId(), + source: this.generateId(), + remoteId: this.generateId(), + createdAt: 0, + updatedAt: 0, + deletedAt: 0, + memberCount: 0, + channels: this.fakeQuery([]), + teams: this.fakeQuery([]), + members: this.fakeQuery([]), + ...overwrite, + }; + }; + + fakeMyChannelModel = (overwrite?: Partial): MyChannelModel => { + return { + ...this.fakeModel(), + lastPostAt: 0, + lastFetchedAt: 0, + lastViewedAt: 0, + manuallyUnread: false, + mentionsCount: 0, + messageCount: 0, + isUnread: false, + roles: '', + viewedAt: 0, + channel: this.fakeRelation(), + settings: this.fakeRelation(), + resetPreparedState: jest.fn(), + ...overwrite, + }; + }; + + fakeRoleModel = (overwrite?: Partial): RoleModel => { + return { + ...this.fakeModel(), + name: this.generateId(), + permissions: [], + ...overwrite, + }; + }; + + fakeThreadModel = (overwrite?: Partial): ThreadModel => { + return { + ...this.fakeModel(), + lastReplyAt: 0, + lastFetchedAt: 0, + lastViewedAt: 0, + replyCount: 0, + isFollowing: false, + unreadReplies: 0, + unreadMentions: 0, + viewedAt: 0, + participants: this.fakeQuery([]), + threadsInTeam: this.fakeQuery([]), + post: this.fakeRelation(), + ...overwrite, + }; + }; + + fakeMyTeamModel = (overwrite?: Partial): MyTeamModel => { + return { + ...this.fakeModel(), + roles: '', + team: this.fakeRelation(), + ...overwrite, + }; + }; + + fakePreferenceModel = (overwrite?: Partial): PreferenceModel => { + return { + ...this.fakeModel(), + value: '', + category: '', + name: '', + userId: '', + user: this.fakeRelation(), + ...overwrite, + }; + }; + + fakeRole = (overwrite?: Partial): Role => { + return { + id: this.generateId(), + name: this.generateId(), + permissions: [], + ...overwrite, + }; + }; + + fakeUser = (overwrite?: Partial): UserProfile => { return { email: this.fakeEmail(), locale: DEFAULT_LOCALE, @@ -437,13 +885,14 @@ class TestHelperSingleton { auth_service: '', id: this.generateId(), nickname: '', - notify_props: this.fakeNotifyProps(), + notify_props: this.fakeUserNotifyProps(), position: '', update_at: 0, + ...overwrite, }; }; - fakeNotifyProps = (): UserNotifyProps => { + fakeUserNotifyProps = (overwrite?: Partial): UserNotifyProps => { return { channel: 'false', comments: 'root', @@ -456,41 +905,23 @@ class TestHelperSingleton { push: 'default', push_status: 'away', calls_desktop_sound: 'true', - calls_mobile_notification_sound: '', + calls_mobile_notification_sound: Ringtone.Calm, calls_mobile_sound: 'true', calls_notification_sound: '', + ...overwrite, }; }; - fakeUserWithId = (id = this.generateId()): UserProfile => { + fakeChannelNotifyProps = (overwrite?: Partial): ChannelNotifyProps => { return { - ...this.fakeUser(), - id, - create_at: 1507840900004, - update_at: 1507840900004, - delete_at: 0, - auth_service: '', - nickname: '', - notify_props: { - desktop: 'default', - desktop_sound: 'true', - email: 'true', - mark_unread: 'all', - push: 'default', - push_status: 'away', - comments: 'any', - first_name: 'true', - channel: 'true', - mention_keys: '', - highlight_keys: '', - calls_desktop_sound: 'true', - calls_mobile_sound: '', - calls_notification_sound: Ringtone.Calm, - calls_mobile_notification_sound: '', - }, - last_picture_update: 0, - position: '', - is_bot: false, + channel_auto_follow_threads: 'on', + desktop: 'default', + email: 'default', + mark_unread: 'all', + push: 'default', + ignore_channel_mentions: 'off', + push_threads: 'mention', + ...overwrite, }; }; @@ -589,7 +1020,7 @@ class TestHelperSingleton { }; initMockEntities = () => { - this.basicUser = this.fakeUserWithId(); + this.basicUser = this.fakeUser(); this.basicUser.roles = 'system_user system_admin'; this.basicTeam = this.fakeTeamWithId(); this.basicTeamMember = this.fakeTeamMember(this.basicUser.id, this.basicTeam.id); diff --git a/types/api/posts.d.ts b/types/api/posts.d.ts index 52b83592d..bedbbb090 100644 --- a/types/api/posts.d.ts +++ b/types/api/posts.d.ts @@ -16,7 +16,18 @@ type PostType = | 'system_join_leave' | 'system_leave_channel' | 'system_purpose_change' - | 'system_remove_from_channel'; + | 'system_remove_from_channel' + | 'system_ephemeral_add_to_channel' + | 'system_guest_join_channel' + | 'system_add_guest_to_chan' + | 'system_join_team' + | 'system_leave_team' + | 'system_remove_from_team' + | 'system_combined_user_activity' + | 'add_bot_teams_channels' + | 'system_auto_responder' + | 'custom_calls' + | 'custom_calls_recording'; type PostEmbedType = 'image' | 'message_attachment' | 'opengraph'; diff --git a/types/database/database.ts b/types/database/database.ts index 3771a87e7..648ef54ed 100644 --- a/types/database/database.ts +++ b/types/database/database.ts @@ -43,24 +43,24 @@ export type ServerDatabases = { [x: string]: ServerDatabase | undefined; }; -export type TransformerArgs = { +export type TransformerArgs = { action: string; database: Database; fieldsMapper?: (model: Model) => void; tableName?: string; - value: RecordPair; + value: RecordPair; }; -export type PrepareBaseRecordArgs = TransformerArgs & { +export type PrepareBaseRecordArgs = TransformerArgs & { fieldsMapper: (model: Model) => void; } -export type OperationArgs = { +export type OperationArgs = { tableName: string; - createRaws?: RecordPair[]; - updateRaws?: RecordPair[]; + createRaws?: Array>; + updateRaws?: Array>; deleteRaws?: T[]; - transformer: (args: TransformerArgs) => Promise; + transformer: (args: TransformerArgs) => Promise; }; export type Models = Array>; @@ -137,9 +137,9 @@ export type SanitizePostsArgs = { posts: Post[]; }; -export type IdenticalRecordArgs = { - existingRecord: Model; - newValue: RawValue; +export type IdenticalRecordArgs = { + existingRecord: T; + newValue: R; tableName: string; }; @@ -149,21 +149,21 @@ export type RetrieveRecordsArgs = { condition: Clause; }; -export type ProcessRecordsArgs = { - createOrUpdateRawValues: RawValue[]; - deleteRawValues: RawValue[]; +export type ProcessRecordsArgs = { + createOrUpdateRawValues: R[]; + deleteRawValues: R[]; tableName: string; fieldName: string; buildKeyRecordBy?: (obj: Record) => string; shouldUpdate?: (existing: Record, newRaw: Record) => boolean; }; -export type HandleRecordsArgs = { +export type HandleRecordsArgs = { buildKeyRecordBy?: (obj: Record) => string; fieldName: string; - transformer: (args: TransformerArgs) => Promise; - createOrUpdateRawValues: RawValue[]; - deleteRawValues?: RawValue[]; + transformer: (args: TransformerArgs) => Promise; + createOrUpdateRawValues: R[]; + deleteRawValues?: R[]; tableName: string; prepareRecordsOnly: boolean; shouldUpdate?: (existingRecord: T, newRaw: RawValue) => boolean; @@ -174,9 +174,9 @@ export type RangeOfValueArgs = { fieldName: string; }; -export type RecordPair = { - record?: Model; - raw: RawValue; +export type RecordPair = { + record?: T; + raw: R; }; type PrepareOnly = { @@ -317,8 +317,8 @@ export type GetDatabaseConnectionArgs = { setAsActiveDatabase: boolean; } -export type ProcessRecordResults = { - createRaws: RecordPair[]; - updateRaws: RecordPair[]; +export type ProcessRecordResults = { + createRaws: Array>; + updateRaws: Array>; deleteRaws: T[]; } diff --git a/types/database/models/servers/post.ts b/types/database/models/servers/post.ts index e3a9be36a..2d30a950d 100644 --- a/types/database/models/servers/post.ts +++ b/types/database/models/servers/post.ts @@ -65,7 +65,7 @@ declare class PostModel extends Model { rootId: string; /** type : Type of props (e.g. system message) */ - type: string; + type: PostType; /** user_id : The foreign key of the User who authored this post. */ userId: string; diff --git a/types/database/raw_values.d.ts b/types/database/raw_values.d.ts index 920b06b8d..332f76a05 100644 --- a/types/database/raw_values.d.ts +++ b/types/database/raw_values.d.ts @@ -136,4 +136,5 @@ type RawValue = | ThreadParticipant | TeamThreadsSync | UserProfile + | SlashCommand | Pick