n8n/packages/cli/test/unit/collaboration/collaboration.service.test.ts
Tomi Turtiainen ac877014ed
feat(core): Initial support for two-way communication over websockets (#7570)
- Enable two-way communication with web sockets
- Enable sending push messages to specific users
- Add collaboration service for managing active users for workflow

Missing things:
- State is currently kept only in memory, making this not work in
multi-master setups
- Removing a user from active users in situations where they go inactive
or we miss the "workflow closed" message
- I think a timer based solution for this would cover most edge cases.
I.e. have FE ping every X minutes, BE removes the user unless they have
received a ping in Y minutes, where Y > X
- FE changes to be added later by @MiloradFilipovic 

Github issue / Community forum post (link here to close automatically):

---------

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
2023-11-07 17:26:45 +02:00

151 lines
4.1 KiB
TypeScript

import { CollaborationService } from '@/collaboration/collaboration.service';
import type { Logger } from '@/Logger';
import type { User } from '@/databases/entities/User';
import type { UserService } from '@/services/user.service';
import { CollaborationState } from '@/collaboration/collaboration.state';
import type { Push } from '@/push';
import type {
WorkflowClosedMessage,
WorkflowOpenedMessage,
} from '@/collaboration/collaboration.message';
describe('CollaborationService', () => {
let collaborationService: CollaborationService;
let mockLogger: Logger;
let mockUserService: jest.Mocked<UserService>;
let state: CollaborationState;
let push: Push;
beforeEach(() => {
mockLogger = {
warn: jest.fn(),
error: jest.fn(),
} as unknown as jest.Mocked<Logger>;
mockUserService = {
getByIds: jest.fn(),
getManager: jest.fn(),
} as unknown as jest.Mocked<UserService>;
push = {
on: jest.fn(),
sendToUsers: jest.fn(),
} as unknown as Push;
state = new CollaborationState();
collaborationService = new CollaborationService(mockLogger, push, state, mockUserService);
});
describe('workflow opened message', () => {
const userId = 'test-user';
const workflowId = 'test-workflow';
const message: WorkflowOpenedMessage = {
type: 'workflowOpened',
workflowId,
};
const expectActiveUsersChangedMessage = (userIds: string[]) => {
expect(push.sendToUsers).toHaveBeenCalledWith(
'activeWorkflowUsersChanged',
{
workflowId,
activeUsers: [
{
user: { id: userId },
lastSeen: expect.any(Date),
},
],
},
[userId],
);
};
describe('user is not yet active', () => {
it('updates state correctly', async () => {
mockUserService.getByIds.mockResolvedValueOnce([{ id: userId } as User]);
await collaborationService.handleUserMessage(userId, message);
expect(state.getActiveWorkflowUsers(workflowId)).toEqual([
{
lastSeen: expect.any(Date),
userId,
},
]);
});
it('sends active workflow users changed message', async () => {
mockUserService.getByIds.mockResolvedValueOnce([{ id: userId } as User]);
await collaborationService.handleUserMessage(userId, message);
expectActiveUsersChangedMessage([userId]);
});
});
describe('user is already active', () => {
beforeEach(() => {
state.addActiveWorkflowUser(workflowId, userId);
});
it('updates state correctly', async () => {
mockUserService.getByIds.mockResolvedValueOnce([{ id: userId } as User]);
await collaborationService.handleUserMessage(userId, message);
expect(state.getActiveWorkflowUsers(workflowId)).toEqual([
{
lastSeen: expect.any(Date),
userId,
},
]);
});
it('sends active workflow users changed message', async () => {
mockUserService.getByIds.mockResolvedValueOnce([{ id: userId } as User]);
await collaborationService.handleUserMessage(userId, message);
expectActiveUsersChangedMessage([userId]);
});
});
});
describe('workflow closed message', () => {
const userId = 'test-user';
const workflowId = 'test-workflow';
const message: WorkflowClosedMessage = {
type: 'workflowClosed',
workflowId,
};
describe('user is active', () => {
beforeEach(() => {
state.addActiveWorkflowUser(workflowId, userId);
});
it('updates state correctly', async () => {
await collaborationService.handleUserMessage(userId, message);
expect(state.getActiveWorkflowUsers(workflowId)).toEqual([]);
});
it('does not send active workflow users changed message', async () => {
await collaborationService.handleUserMessage(userId, message);
expect(push.sendToUsers).not.toHaveBeenCalled();
});
});
describe('user is not active', () => {
it('updates state correctly', async () => {
await collaborationService.handleUserMessage(userId, message);
expect(state.getActiveWorkflowUsers(workflowId)).toEqual([]);
});
it('does not send active workflow users changed message', async () => {
await collaborationService.handleUserMessage(userId, message);
expect(push.sendToUsers).not.toHaveBeenCalled();
});
});
});
});