farmcontrol-ws/src/socket/__tests__/socketuser.test.js
Tom Butcher 940c05078e
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good
Add scan session management to SocketUser class
- Introduced a `scanSessions` map to track active scan sessions.
- Implemented logic in `handleObjectActionEvent` to add and remove sessions based on scan actions.
- Added `cancelScanSessions` method to clear active sessions upon user disconnect, ensuring proper cleanup.
- Updated tests to verify that scan sessions are correctly canceled during disconnection.
2026-09-14 22:55:43 +01:00

359 lines
11 KiB
JavaScript

import { jest } from '@jest/globals';
// Mock dependencies
jest.unstable_mockModule('../../config.js', () => ({
loadConfig: jest.fn(() => ({
server: { logLevel: 'info' }
}))
}));
jest.unstable_mockModule('../../auth/auth.js', () => ({
KeycloakAuth: jest.fn().mockImplementation(() => ({
verifyToken: jest.fn()
})),
createAuthMiddleware: jest.fn(() => (socket, next) => next())
}));
jest.unstable_mockModule('../../utils.js', () => ({
generateHostOTP: jest.fn()
}));
jest.unstable_mockModule('../../activity/activitymanager.js', () => ({
ActivityManager: jest.fn().mockImplementation(() => ({
setActivity: jest.fn(),
clearActivity: jest.fn(),
getObjectActivities: jest.fn(),
subscribeToObjectActivity: jest.fn(),
removeObjectActivityListener: jest.fn(),
removeAllListeners: jest.fn()
}))
}));
jest.unstable_mockModule('../../updates/updatemanager.js', () => ({
UpdateManager: jest.fn().mockImplementation(() => ({
subscribeToObjectNew: jest.fn(),
subscribeToObjectDelete: jest.fn(),
subscribeToObjectUpdate: jest.fn(),
subscribeToAllObjectUpdates: jest.fn(),
removeObjectNewListener: jest.fn(),
removeObjectDeleteListener: jest.fn(),
removeObjectUpdateListener: jest.fn(),
removeAllObjectUpdatesListener: jest.fn(),
removeAllListeners: jest.fn()
}))
}));
jest.unstable_mockModule('../../actions/actionmanager.js', () => ({
ActionManager: jest.fn().mockImplementation(() => ({
sendObjectAction: jest.fn(),
removeAllListeners: jest.fn()
}))
}));
jest.unstable_mockModule('../../events/eventmanager.js', () => ({
EventManager: jest.fn().mockImplementation(() => ({
subscribeToObjectEvent: jest.fn(),
removeObjectEventsListener: jest.fn(),
removeAllListeners: jest.fn()
}))
}));
jest.unstable_mockModule('../../stats/statsmanager.js', () => ({
StatsManager: jest.fn().mockImplementation(() => ({
subscribeToStats: jest.fn(),
removeStatsListener: jest.fn(),
removeAllListeners: jest.fn()
}))
}));
jest.unstable_mockModule('../../notification/notificationmanager.js', () => ({
NotificationManager: jest.fn().mockImplementation(() => ({
subscribe: jest.fn().mockResolvedValue({ success: true }),
removeAllListeners: jest.fn().mockResolvedValue({ success: true })
}))
}));
jest.unstable_mockModule('../../server/servermanager.js', () => ({
ServerManager: jest.fn().mockImplementation(() => ({
getServerVersion: jest.fn(() => ({
version: '1.0.0',
buildNumber: 'dev'
}))
}))
}));
jest.unstable_mockModule('../../usersettings/usersettingsmanager.js', () => ({
UserSettingsManager: jest.fn().mockImplementation(() => ({
getUserSettings: jest.fn(),
updateUserSettings: jest.fn()
}))
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
info: jest.fn()
})
}
}));
const { SocketUser } = await import('../socketuser.js');
const { generateHostOTP } = await import('../../utils.js');
describe('SocketUser', () => {
let mockSocket;
let mockSocketManager;
let socketUser;
beforeEach(() => {
jest.clearAllMocks();
mockSocket = {
id: 'test-user-socket-id',
use: jest.fn(),
on: jest.fn(),
emit: jest.fn()
};
mockSocketManager = {};
socketUser = new SocketUser(mockSocket, mockSocketManager);
});
it('should initialize correctly and setup event handlers', () => {
expect(mockSocket.use).toHaveBeenCalled();
expect(mockSocket.on).toHaveBeenCalledWith(
'authenticate',
expect.any(Function)
);
expect(mockSocket.on).toHaveBeenCalledWith(
'setActivity',
expect.any(Function)
);
expect(mockSocket.on).toHaveBeenCalledWith(
'disconnect',
expect.any(Function)
);
expect(mockSocket.on).toHaveBeenCalledWith(
'getServerVersion',
expect.any(Function)
);
expect(mockSocket.on).toHaveBeenCalledWith(
'getUserSettings',
expect.any(Function)
);
expect(mockSocket.on).toHaveBeenCalledWith(
'updateUserSettings',
expect.any(Function)
);
});
describe('handleAuthenticateEvent', () => {
it('should authenticate user with valid token', async () => {
const data = { token: 'valid-token' };
const callback = jest.fn();
const mockUser = { _id: 'user-id-obj', username: 'testuser' };
socketUser.keycloakAuth.verifyToken.mockResolvedValue({
valid: true,
user: mockUser
});
await socketUser.handleAuthenticateEvent(data, callback);
expect(socketUser.keycloakAuth.verifyToken).toHaveBeenCalledWith(
'valid-token'
);
expect(socketUser.authenticated).toBe(true);
expect(socketUser.user).toEqual(mockUser);
expect(socketUser.id).toBe('user-id-obj');
expect(socketUser.notificationManager.subscribe).toHaveBeenCalled();
expect(callback).toHaveBeenCalledWith({ valid: true, user: mockUser });
});
it('should not authenticate user with invalid token', async () => {
const data = { token: 'invalid-token' };
const callback = jest.fn();
socketUser.keycloakAuth.verifyToken.mockResolvedValue({ valid: false });
await socketUser.handleAuthenticateEvent(data, callback);
expect(socketUser.authenticated).toBe(false);
expect(callback).toHaveBeenCalledWith({ valid: false });
});
});
describe('activity event handlers', () => {
beforeEach(() => {
socketUser.user = { _id: 'user-id' };
});
it('handleSetActivityEvent should call activityManager.setActivity', async () => {
const data = { _id: 'obj-1', type: 'printer', mode: 'viewing' };
await socketUser.handleSetActivityEvent(data);
expect(socketUser.activityManager.setActivity).toHaveBeenCalledWith({
...data,
user: 'user-id'
});
});
it('handleClearActivityEvent should call activityManager.clearActivity', async () => {
const data = { _id: 'obj-1', type: 'printer' };
await socketUser.handleClearActivityEvent(data);
expect(socketUser.activityManager.clearActivity).toHaveBeenCalledWith({
...data,
user: 'user-id'
});
});
});
describe('object type subscription handlers', () => {
it('should pass filters to type subscription methods', async () => {
const data = {
objectType: 'note',
filter: { 'parent._id': 'parent-id' }
};
const callback = jest.fn();
await socketUser.handleSubscribeToObjectTypeUpdateEvent(data, callback);
expect(
socketUser.updateManager.subscribeToObjectNew
).toHaveBeenCalledWith('note', data.filter);
expect(
socketUser.updateManager.subscribeToObjectDelete
).toHaveBeenCalledWith('note', data.filter);
expect(callback).toHaveBeenCalledWith({ success: true });
});
it('should pass filters to type unsubscribe methods', async () => {
const data = {
objectType: 'note',
filter: { 'parent._id': 'parent-id' }
};
await socketUser.handleUnsubscribeToObjectTypeUpdateEvent(data);
expect(
socketUser.updateManager.removeObjectNewListener
).toHaveBeenCalledWith('note', data.filter);
expect(
socketUser.updateManager.removeObjectDeleteListener
).toHaveBeenCalledWith('note', data.filter);
});
});
describe('handleGenerateHostOtpEvent', () => {
it('should call generateHostOTP and callback', async () => {
const data = { _id: 'host-id' };
const callback = jest.fn();
generateHostOTP.mockResolvedValue('otp-123');
await socketUser.handleGenerateHostOtpEvent(data, callback);
expect(generateHostOTP).toHaveBeenCalledWith('host-id');
expect(callback).toHaveBeenCalledWith('otp-123');
});
});
describe('handleGetServerVersionEvent', () => {
it('should return server version details', async () => {
const callback = jest.fn();
await socketUser.handleGetServerVersionEvent({}, callback);
expect(socketUser.serverManager.getServerVersion).toHaveBeenCalled();
expect(callback).toHaveBeenCalledWith({
version: '1.0.0',
buildNumber: 'dev'
});
});
});
describe('user settings event handlers', () => {
it('returns user settings through the acknowledgement callback', async () => {
const callback = jest.fn();
const settings = { viewMode: { payments: 'cards' } };
socketUser.userSettingsManager.getUserSettings.mockResolvedValue(
settings
);
await socketUser.handleGetUserSettingsEvent({}, callback);
expect(callback).toHaveBeenCalledWith({ success: true, settings });
});
it('updates user settings through the acknowledgement callback', async () => {
const callback = jest.fn();
const data = {
category: 'viewMode',
key: 'payments',
value: 'cards'
};
const settings = { viewMode: { payments: 'cards' } };
socketUser.userSettingsManager.updateUserSettings.mockResolvedValue(
settings
);
await socketUser.handleUpdateUserSettingsEvent(data, callback);
expect(
socketUser.userSettingsManager.updateUserSettings
).toHaveBeenCalledWith(data);
expect(callback).toHaveBeenCalledWith({ success: true, settings });
});
it('returns user settings errors through the acknowledgement callback', async () => {
const callback = jest.fn();
socketUser.userSettingsManager.getUserSettings.mockRejectedValue(
new Error('database unavailable')
);
await socketUser.handleGetUserSettingsEvent({}, callback);
expect(callback).toHaveBeenCalledWith({
success: false,
error: 'database unavailable'
});
});
});
describe('handleDisconnect', () => {
it('should remove all listeners', async () => {
await socketUser.handleDisconnect();
expect(socketUser.actionManager.removeAllListeners).toHaveBeenCalled();
expect(socketUser.updateManager.removeAllListeners).toHaveBeenCalled();
expect(socketUser.eventManager.removeAllListeners).toHaveBeenCalled();
expect(socketUser.statsManager.removeAllListeners).toHaveBeenCalled();
expect(
socketUser.notificationManager.removeAllListeners
).toHaveBeenCalled();
});
it('should cancel tracked scan sessions', async () => {
socketUser.scanSessions.set('session-1', 'host-1');
socketUser.scanSessions.set('session-2', 'host-2');
await socketUser.handleDisconnect();
expect(socketUser.actionManager.sendObjectAction).toHaveBeenCalledWith(
'host-1',
'host',
{ type: 'scanNetworkStop', data: { sessionId: 'session-1' } }
);
expect(socketUser.actionManager.sendObjectAction).toHaveBeenCalledWith(
'host-2',
'host',
{ type: 'scanNetworkStop', data: { sessionId: 'session-2' } }
);
expect(socketUser.scanSessions.size).toBe(0);
});
});
});