farmcontrol-ws/src/socket/__tests__/socketuser.test.js
Tom Butcher 15132fefa0
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good
Add user settings management functionality
- Introduced a new UserSettingsManager class to handle user-specific settings, including retrieval and updates.
- Implemented user settings schema to define defaults and manage user-specific configurations.
- Enhanced SocketUser class to integrate user settings event handlers for getting and updating settings.
- Added comprehensive tests for UserSettingsManager to ensure correct functionality and error handling.
2026-07-18 14:48:46 +01:00

337 lines
10 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('../../lock/lockmanager.js', () => ({
LockManager: jest.fn().mockImplementation(() => ({
lockObject: jest.fn(),
unlockObject: jest.fn(),
getObjectLock: jest.fn()
}))
}));
jest.unstable_mockModule('../../updates/updatemanager.js', () => ({
UpdateManager: jest.fn().mockImplementation(() => ({
subscribeToObjectNew: jest.fn(),
subscribeToObjectDelete: jest.fn(),
subscribeToObjectUpdate: jest.fn(),
removeObjectNewListener: jest.fn(),
removeObjectDeleteListener: jest.fn(),
removeObjectUpdateListener: 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 = {
templateManager: {
renderTemplate: jest.fn(),
renderPDF: jest.fn()
}
};
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('lock', 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('lock event handlers', () => {
beforeEach(() => {
socketUser.user = { _id: 'user-id' };
});
it('handleLockEvent should call lockManager.lockObject', async () => {
const data = { _id: 'obj-1', type: 'printer' };
await socketUser.handleLockEvent(data);
expect(socketUser.lockManager.lockObject).toHaveBeenCalledWith({
...data,
user: 'user-id'
});
});
it('handleUnlockEvent should call lockManager.unlockObject', async () => {
const data = { _id: 'obj-1' };
await socketUser.handleUnlockEvent(data);
expect(socketUser.lockManager.unlockObject).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();
});
});
});