Add tests to remaining actions/websocket files (#8495)

* test: Add comprehensive tests for websocket roles actions

* test: Add comprehensive tests for websocket category actions

* test: Add comprehensive tests for websocket preferences actions

* test: Add comprehensive tests for websocket threads actions

* test: Add comprehensive tests for websocket reactions actions

* test: Add comprehensive tests for websocket system actions

* test: Add comprehensive tests for websocket integrations actions
This commit is contained in:
Joram Wilander 2025-01-21 08:42:09 -05:00 committed by GitHub
parent e8ed761d30
commit 575a689407
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1462 additions and 1 deletions

View file

@ -0,0 +1,296 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {deleteCategory, storeCategories} from '@actions/local/category';
import {fetchCategories} from '@actions/remote/category';
import DatabaseManager from '@database/manager';
import {queryCategoriesById} from '@queries/servers/categories';
import {handleCategoryCreatedEvent, handleCategoryUpdatedEvent, handleCategoryDeletedEvent, handleCategoryOrderUpdatedEvent, type WebsocketCategoriesMessage} from './category';
import type ServerDataOperator from '@database/operator/server_data_operator';
jest.mock('@actions/local/category');
jest.mock('@actions/remote/category');
jest.mock('@database/manager');
jest.mock('@queries/servers/categories');
describe('WebSocket Category Actions', () => {
const serverUrl = 'baseHandler.test.com';
const teamId = 'team-id';
const categoryId = 'category-id';
let operator: ServerDataOperator;
let batchRecords: jest.SpyInstance;
beforeEach(async () => {
jest.clearAllMocks();
await DatabaseManager.init([serverUrl]);
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
batchRecords = jest.spyOn(operator, 'batchRecords').mockResolvedValue();
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
jest.restoreAllMocks();
});
describe('handleCategoryCreatedEvent', () => {
it('should handle valid category creation', async () => {
const mockCategory = {
id: categoryId,
team_id: teamId,
display_name: 'Test Category',
};
const msg = {
data: {
category: JSON.stringify(mockCategory),
},
broadcast: {
team_id: teamId,
},
} as WebsocketCategoriesMessage;
await handleCategoryCreatedEvent(serverUrl, msg);
expect(storeCategories).toHaveBeenCalledWith(serverUrl, [mockCategory]);
});
it('should handle invalid JSON in category data', async () => {
const msg = {
data: {
category: 'invalid-json',
},
broadcast: {
team_id: teamId,
},
} as WebsocketCategoriesMessage;
await handleCategoryCreatedEvent(serverUrl, msg);
expect(fetchCategories).toHaveBeenCalledWith(serverUrl, teamId);
});
it('should handle invalid JSON in category data - no team id', async () => {
const msg = {
data: {
category: 'invalid-json',
},
broadcast: {},
} as WebsocketCategoriesMessage;
await handleCategoryCreatedEvent(serverUrl, msg);
expect(fetchCategories).not.toHaveBeenCalled();
});
it('should handle missing category data', async () => {
const msg = {
data: {},
broadcast: {
team_id: teamId,
},
} as WebsocketCategoriesMessage;
await handleCategoryCreatedEvent(serverUrl, msg);
expect(storeCategories).not.toHaveBeenCalled();
});
});
describe('handleCategoryUpdatedEvent', () => {
it('should handle valid category updates', async () => {
const mockCategories = [{
id: categoryId,
team_id: teamId,
display_name: 'Updated Category',
}];
const msg = {
data: {
updatedCategories: JSON.stringify(mockCategories),
},
broadcast: {
team_id: teamId,
},
} as WebsocketCategoriesMessage;
await handleCategoryUpdatedEvent(serverUrl, msg);
expect(storeCategories).toHaveBeenCalledWith(serverUrl, mockCategories);
});
it('should handle invalid JSON in updated categories', async () => {
const msg = {
data: {
updatedCategories: 'invalid-json',
},
broadcast: {
team_id: teamId,
},
} as WebsocketCategoriesMessage;
await handleCategoryUpdatedEvent(serverUrl, msg);
expect(fetchCategories).toHaveBeenCalledWith(serverUrl, teamId, true);
});
it('should handle invalid JSON in updated categories - no team id', async () => {
const msg = {
data: {
updatedCategories: 'invalid-json',
},
broadcast: {},
} as WebsocketCategoriesMessage;
await handleCategoryUpdatedEvent(serverUrl, msg);
expect(fetchCategories).not.toHaveBeenCalled();
});
it('should handle missing updated categories data', async () => {
const msg = {
data: {},
broadcast: {
team_id: teamId,
},
} as WebsocketCategoriesMessage;
await handleCategoryUpdatedEvent(serverUrl, msg);
expect(storeCategories).not.toHaveBeenCalled();
});
});
describe('handleCategoryDeletedEvent', () => {
it('should handle category deletion', async () => {
const msg = {
data: {
category_id: categoryId,
},
broadcast: {
team_id: teamId,
},
} as WebsocketCategoriesMessage;
await handleCategoryDeletedEvent(serverUrl, msg);
expect(deleteCategory).toHaveBeenCalledWith(serverUrl, categoryId);
expect(fetchCategories).toHaveBeenCalledWith(serverUrl, teamId);
});
it('should handle missing category_id', async () => {
const msg = {
data: {},
broadcast: {
team_id: teamId,
},
} as WebsocketCategoriesMessage;
await handleCategoryDeletedEvent(serverUrl, msg);
expect(deleteCategory).not.toHaveBeenCalled();
expect(fetchCategories).toHaveBeenCalledWith(serverUrl, teamId);
});
});
describe('handleCategoryOrderUpdatedEvent', () => {
it('should handle empty order array', async () => {
const msg = {
data: {
order: [],
team_id: teamId,
},
broadcast: {
team_id: teamId,
},
} as unknown as WebsocketCategoriesMessage;
await handleCategoryOrderUpdatedEvent(serverUrl, msg);
expect(batchRecords).not.toHaveBeenCalled();
});
it('should update category order', async () => {
const mockCategories = [
{id: 'cat1', prepareUpdate: jest.fn()},
{id: 'cat2', prepareUpdate: jest.fn()},
];
jest.mocked(queryCategoriesById).mockReturnValue({
fetch: jest.fn().mockResolvedValue(mockCategories),
} as any);
const msg = {
data: {
order: ['cat1', 'cat2'],
team_id: teamId,
},
broadcast: {
team_id: teamId,
},
} as WebsocketCategoriesMessage;
await handleCategoryOrderUpdatedEvent(serverUrl, msg);
expect(batchRecords).toHaveBeenCalled();
mockCategories.forEach((cat) => {
expect(cat.prepareUpdate).toHaveBeenCalled();
});
});
it('should handle missing order data', async () => {
const msg = {
data: {
team_id: teamId,
},
broadcast: {
team_id: teamId,
},
} as WebsocketCategoriesMessage;
await handleCategoryOrderUpdatedEvent(serverUrl, msg);
expect(batchRecords).not.toHaveBeenCalled();
});
it('should handle database error', async () => {
jest.mocked(queryCategoriesById).mockImplementation(() => {
throw new Error('Database error');
});
const msg = {
data: {
order: ['cat1', 'cat2'],
team_id: teamId,
},
broadcast: {
team_id: teamId,
},
} as WebsocketCategoriesMessage;
await handleCategoryOrderUpdatedEvent(serverUrl, msg);
expect(fetchCategories).toHaveBeenCalledWith(serverUrl, teamId);
});
it('should handle database error - no team id', async () => {
jest.mocked(queryCategoriesById).mockImplementation(() => {
throw new Error('Database error');
});
const msg = {
data: {
order: ['cat1', 'cat2'],
team_id: teamId,
},
broadcast: {},
} as WebsocketCategoriesMessage;
await handleCategoryOrderUpdatedEvent(serverUrl, msg);
expect(fetchCategories).not.toHaveBeenCalledWith();
});
});
});

View file

@ -7,7 +7,7 @@ import DatabaseManager from '@database/manager';
import {queryCategoriesById} from '@queries/servers/categories';
import {logError} from '@utils/log';
type WebsocketCategoriesMessage = {
export type WebsocketCategoriesMessage = {
broadcast: {
team_id: string;
};

View file

@ -0,0 +1,90 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import IntegrationsManager from '@managers/integrations_manager';
import {getActiveServerUrl} from '@queries/app/servers';
import {handleOpenDialogEvent} from './integrations';
jest.mock('@managers/integrations_manager');
jest.mock('@queries/app/servers');
describe('WebSocket Integrations Actions', () => {
const serverUrl = 'baseHandler.test.com';
beforeEach(() => {
jest.clearAllMocks();
const mockManager = {
setDialog: jest.fn(),
};
jest.spyOn(IntegrationsManager, 'getManager').mockReturnValue(mockManager as any);
});
describe('handleOpenDialogEvent', () => {
const mockDialog = {
app_id: 'app1',
trigger_id: 'trigger1',
url: 'http://test.com',
dialog: {
callback_id: 'callback1',
title: 'Test Dialog',
introduction_text: 'Test Intro',
elements: [],
submit_label: 'Submit',
notify_on_cancel: false,
state: 'some-state',
},
} as InteractiveDialogConfig;
it('should handle missing dialog data', async () => {
const msg = {
data: {},
} as WebSocketMessage;
await handleOpenDialogEvent(serverUrl, msg);
expect(IntegrationsManager.getManager).not.toHaveBeenCalled();
});
it('should handle invalid JSON dialog data', async () => {
const msg = {
data: {
dialog: '{invalid json',
},
} as WebSocketMessage;
await handleOpenDialogEvent(serverUrl, msg);
expect(IntegrationsManager.getManager).not.toHaveBeenCalled();
});
it('should not set dialog when server url does not match active server', async () => {
jest.mocked(getActiveServerUrl).mockResolvedValue('different-server');
const msg = {
data: {
dialog: JSON.stringify(mockDialog),
},
} as WebSocketMessage;
await handleOpenDialogEvent(serverUrl, msg);
expect(IntegrationsManager.getManager).not.toHaveBeenCalled();
});
it('should set dialog when server url matches active server', async () => {
jest.mocked(getActiveServerUrl).mockResolvedValue(serverUrl);
const msg = {
data: {
dialog: JSON.stringify(mockDialog),
},
} as WebSocketMessage;
await handleOpenDialogEvent(serverUrl, msg);
expect(IntegrationsManager.getManager).toHaveBeenCalledWith(serverUrl);
expect(IntegrationsManager.getManager(serverUrl).setDialog).toHaveBeenCalledWith(mockDialog);
});
});
});

View file

@ -0,0 +1,206 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {updateDmGmDisplayName} from '@actions/local/channel';
import {fetchPostById} from '@actions/remote/post';
import {handleCRTToggled} from '@actions/remote/preference';
import {Preferences} from '@constants';
import DatabaseManager from '@database/manager';
import {getPostById} from '@queries/servers/post';
import {deletePreferences, differsFromLocalNameFormat, getHasCRTChanged} from '@queries/servers/preference';
import EphemeralStore from '@store/ephemeral_store';
import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences';
import type ServerDataOperator from '@database/operator/server_data_operator';
jest.mock('@actions/local/channel');
jest.mock('@actions/remote/post');
jest.mock('@actions/remote/preference');
jest.mock('@database/manager');
jest.mock('@queries/servers/post');
jest.mock('@queries/servers/preference');
jest.mock('@store/ephemeral_store');
describe('WebSocket Preferences Actions', () => {
const serverUrl = 'baseHandler.test.com';
let operator: ServerDataOperator;
beforeEach(async () => {
jest.clearAllMocks();
await DatabaseManager.init([serverUrl]);
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
jest.spyOn(operator, 'handlePreferences').mockResolvedValue([]);
jest.mocked(EphemeralStore.isEnablingCRT).mockReturnValue(false);
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
describe('handlePreferenceChangedEvent', () => {
const mockPreference = {
category: Preferences.CATEGORIES.SAVED_POST,
name: 'post1',
user_id: 'user1',
value: 'true',
};
it('should handle enabling CRT', async () => {
jest.mocked(EphemeralStore.isEnablingCRT).mockReturnValue(true);
const msg = {
data: {
preference: JSON.stringify(mockPreference),
},
} as WebSocketMessage;
await handlePreferenceChangedEvent(serverUrl, msg);
expect(operator.handlePreferences).not.toHaveBeenCalled();
});
it('should handle preference change with saved post', async () => {
const msg = {
data: {
preference: JSON.stringify(mockPreference),
},
} as WebSocketMessage;
jest.mocked(differsFromLocalNameFormat).mockResolvedValue(false);
jest.mocked(getHasCRTChanged).mockResolvedValue(false);
jest.mocked(getPostById).mockResolvedValue(undefined);
await handlePreferenceChangedEvent(serverUrl, msg);
expect(operator.handlePreferences).toHaveBeenCalledWith({
prepareRecordsOnly: false,
preferences: [mockPreference],
});
expect(fetchPostById).toHaveBeenCalledWith(serverUrl, 'post1', false);
});
it('should handle name format changes', async () => {
const msg = {
data: {
preference: JSON.stringify(mockPreference),
},
} as WebSocketMessage;
jest.mocked(differsFromLocalNameFormat).mockResolvedValue(true);
jest.mocked(getHasCRTChanged).mockResolvedValue(false);
await handlePreferenceChangedEvent(serverUrl, msg);
expect(updateDmGmDisplayName).toHaveBeenCalledWith(serverUrl);
});
it('should handle CRT changes', async () => {
const msg = {
data: {
preference: JSON.stringify(mockPreference),
},
} as WebSocketMessage;
jest.mocked(differsFromLocalNameFormat).mockResolvedValue(false);
jest.mocked(getHasCRTChanged).mockResolvedValue(true);
await handlePreferenceChangedEvent(serverUrl, msg);
expect(handleCRTToggled).toHaveBeenCalledWith(serverUrl);
});
});
describe('handlePreferencesChangedEvent', () => {
const mockPreferences = [{
category: Preferences.CATEGORIES.SAVED_POST,
name: 'post1',
user_id: 'user1',
value: 'true',
}];
it('should handle enabling CRT', async () => {
jest.mocked(EphemeralStore.isEnablingCRT).mockReturnValue(true);
const msg = {
data: {
preferences: JSON.stringify(mockPreferences),
},
} as WebSocketMessage;
await handlePreferencesChangedEvent(serverUrl, msg);
expect(operator.handlePreferences).not.toHaveBeenCalled();
});
it('should handle multiple preferences change', async () => {
const msg = {
data: {
preferences: JSON.stringify(mockPreferences),
},
} as WebSocketMessage;
jest.mocked(differsFromLocalNameFormat).mockResolvedValue(false);
jest.mocked(getHasCRTChanged).mockResolvedValue(false);
jest.mocked(getPostById).mockResolvedValue(undefined);
await handlePreferencesChangedEvent(serverUrl, msg);
expect(operator.handlePreferences).toHaveBeenCalledWith({
prepareRecordsOnly: false,
preferences: mockPreferences,
});
expect(fetchPostById).toHaveBeenCalledWith(serverUrl, 'post1', false);
});
it('should handle name format changes in bulk', async () => {
const msg = {
data: {
preferences: JSON.stringify(mockPreferences),
},
} as WebSocketMessage;
jest.mocked(differsFromLocalNameFormat).mockResolvedValue(true);
jest.mocked(getHasCRTChanged).mockResolvedValue(false);
await handlePreferencesChangedEvent(serverUrl, msg);
expect(updateDmGmDisplayName).toHaveBeenCalledWith(serverUrl);
});
});
describe('handlePreferencesDeletedEvent', () => {
const mockPreferences = [{
category: Preferences.CATEGORIES.SAVED_POST,
name: 'post1',
user_id: 'user1',
value: 'true',
}];
it('should delete preferences', async () => {
const msg = {
data: {
preferences: JSON.stringify(mockPreferences),
},
} as WebSocketMessage;
await handlePreferencesDeletedEvent(serverUrl, msg);
expect(deletePreferences).toHaveBeenCalled();
});
it('should handle invalid preferences data', async () => {
const msg = {
data: {
preferences: 'invalid-json',
},
} as WebSocketMessage;
await handlePreferencesDeletedEvent(serverUrl, msg);
expect(deletePreferences).not.toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,182 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {queryReaction} from '@queries/servers/reaction';
import {handleAddCustomEmoji, handleReactionAddedToPostEvent, handleReactionRemovedFromPostEvent} from './reactions';
jest.mock('@database/manager');
jest.mock('@queries/servers/reaction');
describe('WebSocket Reactions Actions', () => {
const serverUrl = 'baseHandler.test.com';
const postId = 'post-id';
const userId = 'user-id';
const emojiName = 'smile';
let operator: any;
let database: any;
beforeEach(async () => {
jest.clearAllMocks();
operator = {
handleCustomEmojis: jest.fn(),
handleReactions: jest.fn(),
};
database = {
write: jest.fn((callback) => callback()),
};
DatabaseManager.getServerDatabaseAndOperator = jest.fn().mockReturnValue({
database,
operator,
});
await DatabaseManager.init([serverUrl]);
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
describe('handleAddCustomEmoji', () => {
it('should handle custom emoji addition', async () => {
const emoji = {
id: 'emoji-id',
name: 'custom-emoji',
creator_id: userId,
};
const msg = {
data: {
emoji: JSON.stringify(emoji),
},
} as WebSocketMessage;
await handleAddCustomEmoji(serverUrl, msg);
expect(operator.handleCustomEmojis).toHaveBeenCalledWith({
prepareRecordsOnly: false,
emojis: [emoji],
});
});
it('should handle invalid emoji data gracefully', async () => {
const msg = {
data: {
emoji: 'invalid-json',
},
} as WebSocketMessage;
await handleAddCustomEmoji(serverUrl, msg);
expect(operator.handleCustomEmojis).not.toHaveBeenCalled();
});
});
describe('handleReactionAddedToPostEvent', () => {
it('should handle reaction addition', async () => {
const reaction = {
user_id: userId,
post_id: postId,
emoji_name: emojiName,
create_at: 123,
};
const msg = {
data: {
reaction: JSON.stringify(reaction),
},
} as WebSocketMessage;
await handleReactionAddedToPostEvent(serverUrl, msg);
expect(operator.handleReactions).toHaveBeenCalledWith({
prepareRecordsOnly: false,
skipSync: true,
postsReactions: [{
post_id: postId,
reactions: [reaction],
}],
});
});
it('should handle invalid reaction data gracefully', async () => {
const msg = {
data: {
reaction: 'invalid-json',
},
} as WebSocketMessage;
await handleReactionAddedToPostEvent(serverUrl, msg);
expect(operator.handleReactions).not.toHaveBeenCalled();
});
});
describe('handleReactionRemovedFromPostEvent', () => {
it('should handle reaction removal', async () => {
const reaction = {
user_id: userId,
post_id: postId,
emoji_name: emojiName,
};
const mockReactionModel = {
destroyPermanently: jest.fn(),
};
jest.mocked(queryReaction).mockReturnValue({
fetch: jest.fn().mockResolvedValue([mockReactionModel]),
} as any);
const msg = {
data: {
reaction: JSON.stringify(reaction),
},
} as WebSocketMessage;
await handleReactionRemovedFromPostEvent(serverUrl, msg);
expect(queryReaction).toHaveBeenCalledWith(database, emojiName, postId, userId);
expect(mockReactionModel.destroyPermanently).toHaveBeenCalled();
expect(database.write).toHaveBeenCalled();
});
it('should handle non-existent reaction gracefully', async () => {
jest.mocked(queryReaction).mockReturnValue({
fetch: jest.fn().mockResolvedValue([]),
} as any);
const msg = {
data: {
reaction: JSON.stringify({
user_id: userId,
post_id: postId,
emoji_name: emojiName,
}),
},
} as WebSocketMessage;
await handleReactionRemovedFromPostEvent(serverUrl, msg);
expect(database.write).not.toHaveBeenCalled();
});
it('should handle invalid reaction data gracefully', async () => {
const msg = {
data: {
reaction: 'invalid-json',
},
} as WebSocketMessage;
await handleReactionRemovedFromPostEvent(serverUrl, msg);
expect(queryReaction).not.toHaveBeenCalled();
expect(database.write).not.toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,309 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fetchRolesIfNeeded} from '@actions/remote/role';
import DatabaseManager from '@database/manager';
import {getRoleById} from '@queries/servers/role';
import {getCurrentUserId} from '@queries/servers/system';
import {getCurrentUser} from '@queries/servers/user';
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');
jest.mock('@queries/servers/role');
jest.mock('@queries/servers/system');
jest.mock('@queries/servers/user');
describe('WebSocket Roles Actions', () => {
const serverUrl = 'baseHandler.test.com';
const currentUserId = 'current-user-id';
const roleId = 'role-id';
const teamId = 'team-id';
let operator: ServerDataOperator;
let batchRecords: jest.SpyInstance;
let handleRole: jest.SpyInstance;
let handleMyTeam: jest.SpyInstance;
let handleTeamMemberships: jest.SpyInstance;
beforeEach(async () => {
jest.clearAllMocks();
await DatabaseManager.init([serverUrl]);
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
batchRecords = jest.spyOn(operator, 'batchRecords').mockResolvedValue();
handleRole = jest.spyOn(operator, 'handleRole').mockResolvedValue([{id: 'role1'} as RoleModel]);
handleMyTeam = jest.spyOn(operator, 'handleMyTeam').mockResolvedValue([]);
handleTeamMemberships = jest.spyOn(operator, 'handleTeamMemberships').mockResolvedValue([]);
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
jest.restoreAllMocks();
});
describe('handleRoleUpdatedEvent', () => {
it('should handle missing operator', async () => {
DatabaseManager.serverDatabases = {};
const msg = {
data: {
role: JSON.stringify({id: roleId}),
},
} as WebSocketMessage;
await handleRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).not.toHaveBeenCalled();
});
it('should handle missing role in database', async () => {
jest.mocked(getRoleById).mockResolvedValue(undefined);
const msg = {
data: {
role: JSON.stringify({id: roleId}),
},
} as WebSocketMessage;
await handleRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).not.toHaveBeenCalled();
});
it('should update existing role', async () => {
const mockRole = {
id: roleId,
name: 'test_role',
permissions: ['permission1'],
};
jest.mocked(getRoleById).mockResolvedValue({id: roleId} as RoleModel);
const msg = {
data: {
role: JSON.stringify(mockRole),
},
} as WebSocketMessage;
await handleRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).toHaveBeenCalledWith({
roles: [mockRole],
prepareRecordsOnly: false,
});
});
it('should handle invalid JSON in role data', async () => {
jest.mocked(getRoleById).mockResolvedValue({id: roleId} as RoleModel);
const msg = {
data: {
role: 'invalid json',
},
} as WebSocketMessage;
await handleRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).not.toHaveBeenCalled();
});
});
describe('handleUserRoleUpdatedEvent', () => {
it('should handle missing operator', async () => {
DatabaseManager.serverDatabases = {};
const msg = {
data: {
user_id: currentUserId,
roles: 'role1 role2',
},
} as WebSocketMessage;
await handleUserRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).not.toHaveBeenCalled();
});
it('should handle different user', async () => {
jest.mocked(getCurrentUserId).mockResolvedValue('different-user');
const msg = {
data: {
user_id: currentUserId,
roles: 'role1 role2',
},
} as WebSocketMessage;
await handleUserRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).not.toHaveBeenCalled();
});
it('should update roles and user', async () => {
jest.mocked(getCurrentUserId).mockResolvedValue(currentUserId);
jest.mocked(getCurrentUser).mockResolvedValue({
prepareUpdate: jest.fn(),
} as unknown as UserModel);
jest.mocked(fetchRolesIfNeeded).mockResolvedValue({
roles: [{id: 'role1'} as Role],
});
const msg = {
data: {
user_id: currentUserId,
roles: 'role1 role2',
},
} as WebSocketMessage;
await handleUserRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).toHaveBeenCalled();
expect(batchRecords).toHaveBeenCalled();
});
it('should handle missing current user', async () => {
jest.mocked(getCurrentUserId).mockResolvedValue(currentUserId);
jest.mocked(getCurrentUser).mockResolvedValue(undefined);
jest.mocked(fetchRolesIfNeeded).mockResolvedValue({
roles: [{id: 'role1'} as Role],
});
const msg = {
data: {
user_id: currentUserId,
roles: 'role1',
},
} as WebSocketMessage;
await handleUserRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).toHaveBeenCalled();
expect(batchRecords).toHaveBeenCalledWith(expect.arrayContaining([]), 'handleUserRoleUpdatedEvent');
});
it('should handle no new roles from fetchRolesIfNeeded', async () => {
jest.mocked(getCurrentUserId).mockResolvedValue(currentUserId);
jest.mocked(getCurrentUser).mockResolvedValue({
prepareUpdate: jest.fn(),
} as unknown as UserModel);
jest.mocked(fetchRolesIfNeeded).mockResolvedValue({
roles: [],
});
const msg = {
data: {
user_id: currentUserId,
roles: 'role1',
},
} as WebSocketMessage;
await handleUserRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).not.toHaveBeenCalled();
expect(batchRecords).toHaveBeenCalled();
});
});
describe('handleTeamMemberRoleUpdatedEvent', () => {
it('should handle missing operator', async () => {
DatabaseManager.serverDatabases = {};
const msg = {
data: {
member: JSON.stringify({
user_id: currentUserId,
team_id: teamId,
roles: 'role1',
delete_at: 0,
}),
},
} as WebSocketMessage;
await handleTeamMemberRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).not.toHaveBeenCalled();
});
it('should handle deleted member', async () => {
const msg = {
data: {
member: JSON.stringify({
user_id: currentUserId,
team_id: teamId,
roles: 'role1',
delete_at: 1234,
}),
},
} as WebSocketMessage;
await handleTeamMemberRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).not.toHaveBeenCalled();
});
it('should handle different user', async () => {
jest.mocked(getCurrentUserId).mockResolvedValue('different-user');
const msg = {
data: {
member: JSON.stringify({
user_id: currentUserId,
team_id: teamId,
roles: 'role1',
delete_at: 0,
}),
},
} as WebSocketMessage;
await handleTeamMemberRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).not.toHaveBeenCalled();
});
it('should update roles, myTeam and teamMembership', async () => {
jest.mocked(getCurrentUserId).mockResolvedValue(currentUserId);
jest.mocked(fetchRolesIfNeeded).mockResolvedValue({
roles: [{id: 'role1'} as Role],
});
const msg = {
data: {
member: JSON.stringify({
user_id: currentUserId,
team_id: teamId,
roles: 'role1',
delete_at: 0,
}),
},
} as WebSocketMessage;
await handleTeamMemberRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).toHaveBeenCalled();
expect(handleMyTeam).toHaveBeenCalled();
expect(handleTeamMemberships).toHaveBeenCalled();
expect(batchRecords).toHaveBeenCalled();
});
it('should handle invalid JSON in member data', async () => {
const msg = {
data: {
member: 'invalid json',
},
} as WebSocketMessage;
await handleTeamMemberRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).not.toHaveBeenCalled();
expect(handleMyTeam).not.toHaveBeenCalled();
expect(handleTeamMemberships).not.toHaveBeenCalled();
});
it('should handle no new roles from fetchRolesIfNeeded', async () => {
jest.mocked(getCurrentUserId).mockResolvedValue(currentUserId);
jest.mocked(fetchRolesIfNeeded).mockResolvedValue({
roles: [],
});
const msg = {
data: {
member: JSON.stringify({
user_id: currentUserId,
team_id: teamId,
roles: 'role1',
delete_at: 0,
}),
},
} as WebSocketMessage;
await handleTeamMemberRoleUpdatedEvent(serverUrl, msg);
expect(handleRole).not.toHaveBeenCalled();
expect(handleMyTeam).toHaveBeenCalled();
expect(handleTeamMemberships).toHaveBeenCalled();
expect(batchRecords).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,157 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {updateDmGmDisplayName} from '@actions/local/channel';
import {storeConfig} from '@actions/local/systems';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {getConfig, getLicense} from '@queries/servers/system';
import {handleLicenseChangedEvent, handleConfigChangedEvent} from './system';
import type ServerDataOperator from '@database/operator/server_data_operator';
jest.mock('@actions/local/channel');
jest.mock('@actions/local/systems');
jest.mock('@database/manager');
jest.mock('@queries/servers/system');
describe('WebSocket System Actions', () => {
const serverUrl = 'baseHandler.test.com';
let operator: ServerDataOperator;
let handleSystem: jest.SpyInstance;
beforeEach(async () => {
jest.clearAllMocks();
await DatabaseManager.init([serverUrl]);
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
handleSystem = jest.spyOn(operator, 'handleSystem');
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
jest.restoreAllMocks();
});
describe('handleLicenseChangedEvent', () => {
it('should handle missing operator', async () => {
DatabaseManager.serverDatabases = {};
const msg = {
data: {
license: {
LockTeammateNameDisplay: true,
},
},
} as WebSocketMessage;
await handleLicenseChangedEvent(serverUrl, msg);
expect(handleSystem).not.toHaveBeenCalled();
});
it('should handle license update with no display name change', async () => {
const mockLicense = {
LockTeammateNameDisplay: false,
};
jest.mocked(getLicense).mockResolvedValue({
LockTeammateNameDisplay: 'false',
} as ClientLicense);
const msg = {
data: {
license: mockLicense,
},
} as WebSocketMessage;
await handleLicenseChangedEvent(serverUrl, msg);
expect(handleSystem).toHaveBeenCalledWith({
systems: [{
id: SYSTEM_IDENTIFIERS.LICENSE,
value: JSON.stringify(mockLicense),
}],
prepareRecordsOnly: false,
});
expect(updateDmGmDisplayName).not.toHaveBeenCalled();
});
it('should handle license update with display name change', async () => {
const mockLicense = {
LockTeammateNameDisplay: true,
};
jest.mocked(getLicense).mockResolvedValue({
LockTeammateNameDisplay: 'false',
} as ClientLicense);
const msg = {
data: {
license: mockLicense,
},
} as WebSocketMessage;
await handleLicenseChangedEvent(serverUrl, msg);
expect(handleSystem).toHaveBeenCalled();
expect(updateDmGmDisplayName).toHaveBeenCalledWith(serverUrl);
});
});
describe('handleConfigChangedEvent', () => {
it('should handle missing operator', async () => {
DatabaseManager.serverDatabases = {};
const msg = {
data: {
config: {
LockTeammateNameDisplay: true,
},
},
} as WebSocketMessage;
await handleConfigChangedEvent(serverUrl, msg);
expect(storeConfig).not.toHaveBeenCalled();
});
it('should handle config update with no display name change', async () => {
const mockConfig = {
LockTeammateNameDisplay: false,
};
jest.mocked(getConfig).mockResolvedValue({
LockTeammateNameDisplay: 'false',
} as ClientConfig);
const msg = {
data: {
config: mockConfig,
},
} as WebSocketMessage;
await handleConfigChangedEvent(serverUrl, msg);
expect(storeConfig).toHaveBeenCalledWith(serverUrl, mockConfig);
expect(updateDmGmDisplayName).not.toHaveBeenCalled();
});
it('should handle config update with display name change', async () => {
const mockConfig = {
LockTeammateNameDisplay: true,
};
jest.mocked(getConfig).mockResolvedValue({
LockTeammateNameDisplay: 'false',
} as ClientConfig);
const msg = {
data: {
config: mockConfig,
},
} as WebSocketMessage;
await handleConfigChangedEvent(serverUrl, msg);
expect(storeConfig).toHaveBeenCalledWith(serverUrl, mockConfig);
expect(updateDmGmDisplayName).toHaveBeenCalledWith(serverUrl);
});
});
});

View file

@ -0,0 +1,221 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {markTeamThreadsAsRead, processReceivedThreads, updateThread} from '@actions/local/thread';
import DatabaseManager from '@database/manager';
import {getCurrentTeamId} from '@queries/servers/system';
import EphemeralStore from '@store/ephemeral_store';
import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads';
jest.mock('@actions/local/thread');
jest.mock('@database/manager');
jest.mock('@queries/servers/system');
jest.mock('@store/ephemeral_store');
describe('WebSocket Threads Actions', () => {
const serverUrl = 'baseHandler.test.com';
const teamId = 'team-id';
const threadId = 'thread-id';
beforeEach(async () => {
jest.clearAllMocks();
await DatabaseManager.init([serverUrl]);
DatabaseManager.getServerDatabaseAndOperator = jest.fn().mockReturnValue({
database: {},
operator: {},
});
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
describe('handleThreadUpdatedEvent', () => {
it('should process received thread', async () => {
const mockThread = {
id: threadId,
reply_count: 3,
};
const msg = {
data: {
thread: JSON.stringify(mockThread),
},
broadcast: {
team_id: teamId,
},
} as WebSocketMessage;
jest.mocked(getCurrentTeamId).mockResolvedValue(teamId);
jest.mocked(processReceivedThreads);
await handleThreadUpdatedEvent(serverUrl, msg);
expect(processReceivedThreads).toHaveBeenCalledWith(
serverUrl,
[{...mockThread, is_following: true}],
teamId,
);
});
it('should handle missing team_id', async () => {
const mockThread = {
id: threadId,
reply_count: 3,
};
const msg = {
data: {
thread: JSON.stringify(mockThread),
},
broadcast: {},
} as WebSocketMessage;
jest.mocked(getCurrentTeamId).mockResolvedValue(teamId);
await handleThreadUpdatedEvent(serverUrl, msg);
expect(processReceivedThreads).toHaveBeenCalledWith(
serverUrl,
[{...mockThread, is_following: true}],
teamId,
);
});
it('should handle error gracefully', async () => {
const msg = {
data: {
thread: 'invalid-json',
},
broadcast: {},
} as WebSocketMessage;
await handleThreadUpdatedEvent(serverUrl, msg);
expect(processReceivedThreads).not.toHaveBeenCalled();
});
});
describe('handleThreadReadChangedEvent', () => {
it('should update thread when thread_id is present', async () => {
const timestamp = 1234567890;
const msg = {
data: {
thread_id: threadId,
timestamp,
unread_mentions: 2,
unread_replies: 5,
},
broadcast: {
team_id: teamId,
},
} as WebSocketMessage<ThreadReadChangedData>;
jest.mocked(EphemeralStore.getCurrentThreadId).mockReturnValue('different-thread');
await handleThreadReadChangedEvent(serverUrl, msg);
expect(updateThread).toHaveBeenCalledWith(serverUrl, threadId, {
unread_mentions: 2,
unread_replies: 5,
last_viewed_at: timestamp,
viewed_at: timestamp,
});
});
it('should not update viewed_at when thread is currently visible', async () => {
const timestamp = 1234567890;
const msg = {
data: {
thread_id: threadId,
timestamp,
unread_mentions: 2,
unread_replies: 5,
},
broadcast: {
team_id: teamId,
},
} as WebSocketMessage<ThreadReadChangedData>;
jest.mocked(EphemeralStore.getCurrentThreadId).mockReturnValue(threadId);
await handleThreadReadChangedEvent(serverUrl, msg);
expect(updateThread).toHaveBeenCalledWith(serverUrl, threadId, {
unread_mentions: 2,
unread_replies: 5,
last_viewed_at: timestamp,
});
});
it('should mark team threads as read when thread_id is missing', async () => {
const msg = {
data: {
timestamp: 1234567890,
},
broadcast: {
team_id: teamId,
},
} as WebSocketMessage<ThreadReadChangedData>;
await handleThreadReadChangedEvent(serverUrl, msg);
expect(markTeamThreadsAsRead).toHaveBeenCalledWith(serverUrl, teamId);
});
it('should handle error gracefully', async () => {
jest.mocked(updateThread).mockRejectedValue(new Error('test error'));
const msg = {
data: {
thread_id: threadId,
timestamp: 1234567890,
},
broadcast: {
team_id: teamId,
},
} as WebSocketMessage<ThreadReadChangedData>;
await handleThreadReadChangedEvent(serverUrl, msg);
expect(updateThread).toHaveBeenCalled();
});
});
describe('handleThreadFollowChangedEvent', () => {
it('should update thread following state', async () => {
const msg = {
data: {
thread_id: threadId,
state: true,
reply_count: 5,
},
} as WebSocketMessage;
await handleThreadFollowChangedEvent(serverUrl, msg);
expect(updateThread).toHaveBeenCalledWith(serverUrl, threadId, {
is_following: true,
reply_count: 5,
});
});
it('should handle error gracefully', async () => {
jest.mocked(updateThread).mockRejectedValue(new Error('test error'));
const msg = {
data: {
thread_id: threadId,
state: false,
reply_count: 3,
},
} as WebSocketMessage;
await handleThreadFollowChangedEvent(serverUrl, msg);
expect(updateThread).toHaveBeenCalled();
});
});
});