Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Websocket extension #8585

Merged
merged 8 commits into from
Feb 17, 2025
3 changes: 2 additions & 1 deletion app/actions/local/scheduled_post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
// See LICENSE.txt for license information.

import DatabaseManager from '@database/manager';
import ScheduledPostModel from '@typings/database/models/servers/scheduled_post';
import {logError} from '@utils/log';

export async function handleScheduledPosts(serverUrl: string, actionType: string, scheduledPosts: ScheduledPost[], prepareRecordsOnly = false) {
export async function handleScheduledPosts(serverUrl: string, actionType: string, scheduledPosts: ScheduledPost[], prepareRecordsOnly = false): Promise<{models?: ScheduledPostModel[]; error?: any}> {
if (!scheduledPosts.length) {
return {models: undefined};
}
Expand Down
22 changes: 17 additions & 5 deletions app/actions/remote/scheduled_post.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,28 @@ const throwFunc = () => {
throw Error('error');
};

const mockConnectionId = 'mock-connection-id';
const mockClient = {
createScheduledPost: jest.fn(() => ({...scheduledPost})),
createScheduledPost: jest.fn((post, connId) => ({...scheduledPost, connectionId: connId})),
getScheduledPostsForTeam: jest.fn(() => Promise.resolve({...scheduledPostsResponse})),
deleteScheduledPost: jest.fn((scheduledPostId) => {
return Promise.resolve(scheduledPostsResponse.bar.find((post) => post.id === scheduledPostId));
deleteScheduledPost: jest.fn((scheduledPostId, connId) => {
const post = scheduledPostsResponse.bar.find((p) => p.id === scheduledPostId);
return Promise.resolve({...post, connectionId: connId});
}),
};

const mockWebSocketClient = {
getConnectionId: jest.fn(() => mockConnectionId),
};
jest.mock('@queries/servers/system', () => ({
getConfigValue: jest.fn(),
getCurrentTeamId: jest.fn(),
}));

jest.mock('@managers/websocket_manager', () => ({
getClient: jest.fn(() => mockWebSocketClient),
}));

const mockedGetConfigValue = jest.mocked(getConfigValue);

beforeAll(() => {
Expand Down Expand Up @@ -108,6 +118,7 @@ describe('scheduled_post', () => {
expect(result.error).toBeUndefined();
expect(result.data).toBe(true);
expect(result!.response!.id).toBe(scheduledPost.id);
expect(mockClient.createScheduledPost).toHaveBeenCalledWith(scheduledPost, mockConnectionId);
});

it('createScheduledPost - request error', async () => {
Expand Down Expand Up @@ -170,11 +181,12 @@ describe('deleteScheduledPost', () => {
it('delete Schedule post - handle scheduled post enabled', async () => {
const spyHandleScheduledPosts = jest.spyOn(operator, 'handleScheduledPosts');
const result = await deleteScheduledPost(serverUrl, 'scheduled_post_id');
expect(result.scheduledPost).toEqual(scheduledPostsResponse.bar[0]);
expect(result.scheduledPost).toEqual({...scheduledPostsResponse.bar[0], connectionId: mockConnectionId});
expect(spyHandleScheduledPosts).toHaveBeenCalledWith({
actionType: ActionType.SCHEDULED_POSTS.DELETE_SCHEDULED_POST,
scheduledPosts: [scheduledPostsResponse.bar[0]],
scheduledPosts: [{...scheduledPostsResponse.bar[0], connectionId: mockConnectionId}],
prepareRecordsOnly: false,
});
expect(mockClient.deleteScheduledPost).toHaveBeenCalledWith('scheduled_post_id', mockConnectionId);
});
});
8 changes: 5 additions & 3 deletions app/actions/remote/scheduled_post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {forceLogoutIfNecessary} from './session';

import type {CreateResponse} from '@hooks/handle_send_message';

export async function createScheduledPost(serverUrl: string, schedulePost: ScheduledPost, connectionId?: string): Promise<CreateResponse> {
export async function createScheduledPost(serverUrl: string, schedulePost: ScheduledPost): Promise<CreateResponse> {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}

const connectionId = websocketManager.getClient(serverUrl)?.getConnectionId();

try {
const client = NetworkManager.getClient(serverUrl);
const response = await client.createScheduledPost(schedulePost, connectionId);
Expand Down Expand Up @@ -73,9 +75,9 @@ export async function deleteScheduledPost(serverUrl: string, scheduledPostId: st
}
try {
const client = NetworkManager.getClient(serverUrl);
const ws = websocketManager.getClient(serverUrl);
const connectionId = websocketManager.getClient(serverUrl)?.getConnectionId();

const result = await client.deleteScheduledPost(scheduledPostId, ws?.getConnectionId());
const result = await client.deleteScheduledPost(scheduledPostId, connectionId);

if (result) {
await operator.handleScheduledPosts({
Expand Down
99 changes: 55 additions & 44 deletions app/actions/websocket/scheduled_post.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,30 @@
// See LICENSE.txt for license information.

import {ActionType} from '@constants';
import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
import {ScheduledPostModel} from '@database/models/server';

import {handleCreateOrUpdateScheduledPost, handleDeleteScheduledPost} from './scheduled_post';

import type ServerDataOperator from '@database/operator/server_data_operator';
import type ScheduledPostModel from '@typings/database/models/servers/scheduled_post';

const serverUrl = 'baseHandler.test.com';
let operator: ServerDataOperator;

const scheduledPosts = [
{
channel_id: 'channel_id',
error_code: '',
files: [],
id: 'scheduled_post_id',
message: 'test scheduled post',
metadata: {},
processed_at: 0,
root_id: '',
scheduled_at: 123,
update_at: 456,
user_id: '',
},
{
id: 'scheduled_post_id_2',
channel_id: 'channel_id',
root_id: '',
message: 'test scheduled post 2',
files: [],
metadata: {},
scheduled_at: 123,
user_id: 'user_id',
processed_at: 0,
update_at: 456,
error_code: '',
},
];
const scheduledPost = {
channel_id: 'channel_id',
error_code: '',
files: [],
id: 'scheduled_post_id',
message: 'test scheduled post',
metadata: {},
processed_at: 0,
root_id: '',
scheduled_at: 123,
update_at: 456,
user_id: '',
};

beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
Expand All @@ -51,48 +37,73 @@ afterEach(async () => {
});

describe('handleCreateOrUpdateSchedulePost', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nicer to have the tests check if the posts were really created / deleted in the database, but 0/5

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

it('handleCreateOrUpdateScheduledPost - handle not found database', async () => {
const {error} = await handleCreateOrUpdateScheduledPost('foo', {data: {scheduled_post: JSON.stringify(scheduledPosts[0])}} as WebSocketMessage) as {error: Error};
it('handleCreateOrUpdateScheduledPost - handle empty payload', async () => {
const {error} = await handleCreateOrUpdateScheduledPost('foo', {data: {scheduledPost: JSON.stringify(scheduledPost)}} as WebSocketMessage);
expect(error.message).toBe('foo database not found');
});

it('handleCreateOrUpdateScheduledPost - wrong websocket scheduled post message', async () => {
const {error} = await handleCreateOrUpdateScheduledPost('foo', {} as WebSocketMessage) as {error: Error};
expect(error.message).toBe('Cannot read properties of undefined (reading \'scheduled_post\')');
const {models, error} = await handleCreateOrUpdateScheduledPost('foo', {data: {}} as WebSocketMessage);
expect(error).toBeUndefined();
expect(models).toBeUndefined();
});

it('handleCreateOrUpdateScheduledPost - no scheduled post', async () => {
const {models} = await handleCreateOrUpdateScheduledPost(serverUrl, {data: {scheduled_post: ''}} as WebSocketMessage) as {models: undefined};
const {models} = await handleCreateOrUpdateScheduledPost(serverUrl, {data: {scheduledPost: ''}} as WebSocketMessage);
expect(models).toBeUndefined();
});

it('handleCreateOrUpdateScheduledPost - success', async () => {
const {models} = await handleCreateOrUpdateScheduledPost(serverUrl, {data: {scheduled_post: JSON.stringify(scheduledPosts[0])}} as WebSocketMessage) as {models: ScheduledPostModel[]};
expect(models[0].id).toEqual(scheduledPosts[0].id);
const {models} = await handleCreateOrUpdateScheduledPost(serverUrl, {data: {scheduledPost: JSON.stringify(scheduledPost)}} as WebSocketMessage);
expect(models).toBeDefined();
expect(models![0].id).toEqual(scheduledPost.id);

// Verify post exists in database
const scheduledPosts = await operator.database.get<ScheduledPostModel>(MM_TABLES.SERVER.SCHEDULED_POST).query().fetch();
expect(scheduledPosts.length).toBe(1);
expect(scheduledPosts[0].id).toBe(scheduledPost.id);
expect(scheduledPosts[0].message).toBe(scheduledPost.message);
});

it('handleCreateOrUpdateScheduledPost - should return error for invalid JSON payload', async () => {
const {models, error} = await handleCreateOrUpdateScheduledPost(serverUrl, {data: {scheduledPost: 'invalid_json'}} as WebSocketMessage);
expect(models).toBeUndefined();
expect(error).toBeDefined();
});
});

describe('handleDeleteScheduledPost', () => {
it('handleDeleteScheduledPost - handle not found database', async () => {
const {error} = await handleDeleteScheduledPost('foo', {} as WebSocketMessage) as {error: unknown};
expect(error).toBeTruthy();
it('handleDeleteScheduledPost - handle empty payload', async () => {
const {error, models} = await handleDeleteScheduledPost('foo', {data: {}} as WebSocketMessage);
expect(error).toBeUndefined();
expect(models).toBeUndefined();
});

it('handleDeleteScheduledPost - no scheduled post', async () => {
const {models} = await handleDeleteScheduledPost(serverUrl, {data: {scheduled_post: ''}} as WebSocketMessage) as {models: undefined};
const {models} = await handleDeleteScheduledPost(serverUrl, {data: {scheduledPost: ''}} as WebSocketMessage);
expect(models).toBeUndefined();
});

it('handleDeleteScheduledPost - success', async () => {
await operator.handleScheduledPosts({
actionType: ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST,
scheduledPosts: [scheduledPosts[0]],
scheduledPosts: [scheduledPost],
prepareRecordsOnly: false,
});

const scheduledPost = scheduledPosts[0];
const deletedRecord = await handleDeleteScheduledPost(serverUrl, {data: {scheduledPost: JSON.stringify(scheduledPost)}} as WebSocketMessage);
expect(deletedRecord.models).toBeDefined();
expect(deletedRecord!.models!.length).toBe(1);
expect(deletedRecord!.models![0].id).toBe(scheduledPost.id);

// Verify post was deleted from database
const scheduledPosts = await operator.database.get<ScheduledPostModel>(MM_TABLES.SERVER.SCHEDULED_POST).query().fetch();
expect(scheduledPosts.length).toBe(0);
});

const deletedRecord = await handleDeleteScheduledPost(serverUrl, {data: {scheduled_post: JSON.stringify(scheduledPost)}} as WebSocketMessage) as {models: ScheduledPostModel[]};
expect(deletedRecord.models[0].id).toBe(scheduledPost.id);
it('handleDeleteScheduledPost - should return error for invalid JSON payload', async () => {
const {models, error} = await handleDeleteScheduledPost(serverUrl, {data: {scheduledPost: 'invalid_json'}} as WebSocketMessage);
expect(models).toBeUndefined();
expect(error).toBeDefined();
});
});
10 changes: 5 additions & 5 deletions app/actions/websocket/scheduled_post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import {ActionType} from '@constants';
import {logError} from '@utils/log';

export type ScheduledPostWebsocketEventPayload = {
scheduled_post: string;
scheduledPost: string;
}

export async function handleCreateOrUpdateScheduledPost(serverUrl: string, msg: WebSocketMessage<ScheduledPostWebsocketEventPayload>, prepareRecordsOnly = false) {
try {
const scheduledPost: ScheduledPost = JSON.parse(msg.data.scheduled_post);
return handleScheduledPosts(serverUrl, ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST, [scheduledPost], prepareRecordsOnly);
const scheduledPost: ScheduledPost[] = msg.data.scheduledPost ? [JSON.parse(msg.data.scheduledPost)] : [];
return handleScheduledPosts(serverUrl, ActionType.SCHEDULED_POSTS.CREATE_OR_UPDATED_SCHEDULED_POST, scheduledPost, prepareRecordsOnly);
} catch (error) {
logError('handleCreateOrUpdateScheduledPost cannot handle scheduled post added/update websocket event', error);
return {error};
Expand All @@ -21,8 +21,8 @@ export async function handleCreateOrUpdateScheduledPost(serverUrl: string, msg:

export async function handleDeleteScheduledPost(serverUrl: string, msg: WebSocketMessage<ScheduledPostWebsocketEventPayload>, prepareRecordsOnly = false) {
try {
const scheduledPost: ScheduledPost = JSON.parse(msg.data.scheduled_post);
return handleScheduledPosts(serverUrl, ActionType.SCHEDULED_POSTS.DELETE_SCHEDULED_POST, [scheduledPost], prepareRecordsOnly);
const scheduledPost: ScheduledPost[] = msg.data.scheduledPost ? [JSON.parse(msg.data.scheduledPost)] : [];
return handleScheduledPosts(serverUrl, ActionType.SCHEDULED_POSTS.DELETE_SCHEDULED_POST, scheduledPost, prepareRecordsOnly);
} catch (error) {
logError('handleDeleteScheduledPost cannot handle scheduled post deleted websocket event', error);
return {error};
Expand Down
Loading