From 8b865cc864bbd137767dd7202107a46979888882 Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Tue, 27 Jan 2026 11:25:50 +0100 Subject: [PATCH 1/6] fix(usage): update usage using the socket event --- src/App.tsx | 28 ++- src/app/store/slices/plan/index.ts | 13 +- src/services/errors/socket.errors.ts | 6 + src/services/socket.service.test.ts | 176 ++++++++++++------ src/services/socket.service.ts | 57 +++--- src/services/types/socket.types.ts | 27 +++ .../Checkout/views/CheckoutSuccessView.tsx | 9 - .../DriveExplorer/DriveExplorer.tsx | 39 ++-- 8 files changed, 251 insertions(+), 104 deletions(-) create mode 100644 src/services/errors/socket.errors.ts create mode 100644 src/services/types/socket.types.ts diff --git a/src/App.tsx b/src/App.tsx index baea30f474..9199ed0b04 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { DndProvider } from 'react-dnd'; import { Toaster } from 'react-hot-toast'; import { connect } from 'react-redux'; @@ -36,6 +36,7 @@ import { getRoutes } from './app/routes/routes'; import { domainManager } from './app/share/services/DomainManager'; import { PreviewFileItem } from './app/share/types'; import { AppDispatch, RootState } from './app/store'; +import { planActions } from './app/store/slices/plan'; import { sessionActions } from './app/store/slices/session'; import { uiActions } from './app/store/slices/ui'; import { initializeUserThunk } from './app/store/slices/user'; @@ -43,6 +44,7 @@ import { workspaceThunks } from './app/store/slices/workspaces/workspacesStore'; import { manager } from 'utils/dnd-utils'; import useBeforeUnload from './hooks/useBeforeUnload'; import useVpnAuth from './hooks/useVpnAuth'; +import { EventData, SOCKET_EVENTS } from 'services/types/socket.types'; import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?raw'; const blob = new Blob([workerUrl], { type: 'application/javascript' }); @@ -88,6 +90,30 @@ const App = (props: AppProps): JSX.Element => { i18next.changeLanguage(); }, []); + const handlePlanUpdatedEvent = useCallback( + (data: EventData) => { + if (data.event === SOCKET_EVENTS.PLAN_UPDATED) { + const newLimit = data.payload?.maxSpaceBytes; + + if (newLimit) { + dispatch(planActions.updatePlanLimitFromSocket(Number(newLimit))); + } + } + }, + [dispatch], + ); + + useEffect(() => { + try { + const realtimeService = RealtimeService.getInstance(); + const cleanup = realtimeService.onEvent(handlePlanUpdatedEvent); + + return cleanup; + } catch (err) { + errorService.reportError(err); + } + }, [handlePlanUpdatedEvent]); + useEffect(() => { if (!isWorkspaceIdParam) { navigationService.resetB2BWorkspaceCredentials(dispatch); diff --git a/src/app/store/slices/plan/index.ts b/src/app/store/slices/plan/index.ts index a22e5e1890..b23d162398 100644 --- a/src/app/store/slices/plan/index.ts +++ b/src/app/store/slices/plan/index.ts @@ -123,7 +123,14 @@ export const fetchBusinessLimitUsageThunk = createAsyncThunk { + if (action.payload) { + state.planLimit = action.payload; + state.isLoadingPlanLimit = false; + } + }, + }, extraReducers: (builder) => { builder .addCase(initializeThunk.pending, (state) => { @@ -217,7 +224,7 @@ export const planSelectors = { isCurrentPlanLifetime: (state: RootState): boolean => { const currentPlan = currentPlanSelector(state); - return currentPlan !== null && currentPlan.isLifetime; + return currentPlan?.isLifetime ?? false; }, planLimitToShow: (state: RootState): number => { const { selectedWorkspace } = state.workspaces; @@ -255,4 +262,6 @@ export const planThunks = { fetchBusinessLimitUsageThunk, }; +export const planActions = planSlice.actions; + export default planSlice.reducer; diff --git a/src/services/errors/socket.errors.ts b/src/services/errors/socket.errors.ts new file mode 100644 index 0000000000..395f2dbbd0 --- /dev/null +++ b/src/services/errors/socket.errors.ts @@ -0,0 +1,6 @@ +export class SocketNotConnectedError extends Error { + constructor() { + super('Realtime service is not connected'); + Object.setPrototypeOf(this, SocketNotConnectedError.prototype); + } +} diff --git a/src/services/socket.service.test.ts b/src/services/socket.service.test.ts index 43543f030a..e5e98e8960 100644 --- a/src/services/socket.service.test.ts +++ b/src/services/socket.service.test.ts @@ -1,7 +1,9 @@ -import { beforeEach, describe, expect, it, vi, afterEach } from 'vitest'; -import RealtimeService, { SOCKET_EVENTS } from './socket.service'; +import { beforeEach, describe, expect, vi, afterEach, test } from 'vitest'; +import RealtimeService from './socket.service'; import localStorageService from './local-storage.service'; import envService from './env.service'; +import { SocketNotConnectedError } from './errors/socket.errors'; +import { SOCKET_EVENTS } from './types/socket.types'; const { mockSocket, ioMock } = vi.hoisted(() => { const mockSocket = { @@ -9,6 +11,7 @@ const { mockSocket, ioMock } = vi.hoisted(() => { connected: true, disconnected: false, on: vi.fn(), + off: vi.fn(), removeAllListeners: vi.fn(), close: vi.fn(), }; @@ -22,12 +25,6 @@ vi.mock('socket.io-client', () => ({ default: ioMock, })); -vi.mock('./local-storage.service', () => ({ - default: { - get: vi.fn(), - }, -})); - vi.mock('./env.service', () => ({ default: { getVariable: vi.fn(), @@ -51,12 +48,13 @@ describe('RealtimeService', () => { return ''; }); - vi.mocked(localStorageService.get).mockReturnValue('mock-token-123'); + vi.spyOn(localStorageService, 'get').mockReturnValue('mock-token-123'); mockSocket.id = 'mock-socket-id'; mockSocket.connected = true; mockSocket.disconnected = false; mockSocket.on.mockClear(); + mockSocket.off.mockClear(); mockSocket.removeAllListeners.mockClear(); mockSocket.close.mockClear(); }); @@ -67,7 +65,7 @@ describe('RealtimeService', () => { }); describe('Service instance management', () => { - it('returns the same service instance across multiple requests', () => { + test('When getInstance is called multiple times, then it returns the same service instance', () => { const instance1 = RealtimeService.getInstance(); const instance2 = RealtimeService.getInstance(); @@ -76,32 +74,32 @@ describe('RealtimeService', () => { }); describe('Event constants', () => { - it('provides predefined event types for subscription', () => { - expect(SOCKET_EVENTS).toHaveProperty('FILE_CREATED'); - expect(SOCKET_EVENTS.FILE_CREATED).toBe('FILE_CREATED'); + test('When accessing SOCKET_EVENTS, then it provides predefined event types', () => { + expect(SOCKET_EVENTS.FILE_CREATED).toStrictEqual('FILE_CREATED'); + expect(SOCKET_EVENTS.PLAN_UPDATED).toStrictEqual('PLAN_UPDATED'); }); }); describe('Establishing realtime connection', () => { - it('establishes a secure connection with authentication when initialized', () => { + test('When init is called, then it establishes a secure connection with authentication', () => { service.init(); expect(ioMock).toHaveBeenCalledWith('https://notifications.example.com', { auth: { token: 'mock-token-123' }, - reconnection: false, - withCredentials: true, + reconnection: true, + withCredentials: false, }); }); - it.each(['connect', 'disconnect', 'connect_error'])( - 'monitors connection lifecycle through %s events', + test.each(['connect', 'event', 'disconnect', 'connect_error'])( + 'When init is called, then it monitors connection lifecycle through %s events', (eventName) => { service.init(); expect(mockSocket.on).toHaveBeenCalledWith(eventName, expect.any(Function)); }, ); - it('notifies the application when connection is successfully established', () => { + test('When connection is successfully established, then it notifies the application via callback', () => { const onConnectedCallback = vi.fn(); service.init(onConnectedCallback); @@ -111,12 +109,12 @@ describe('RealtimeService', () => { expect(onConnectedCallback).toHaveBeenCalledTimes(1); }); - it.each([ - { env: 'production', reconnection: true, logs: false }, - { env: 'development', reconnection: false, logs: true }, + test.each([ + { env: 'production', reconnection: true, withCredentials: true, logs: false }, + { env: 'development', reconnection: true, withCredentials: false, logs: true }, ])( - 'adjusts behavior for $env environment with reconnection=$reconnection and logging=$logs', - ({ env, reconnection, logs }) => { + 'When running in $env environment, then it adjusts reconnection=$reconnection, withCredentials=$withCredentials and logging=$logs', + ({ env, reconnection, withCredentials, logs }) => { vi.mocked(envService.getVariable).mockImplementation((key: string) => { if (key === 'nodeEnv') return env; if (key === 'notifications') return 'https://notifications.example.com'; @@ -128,7 +126,7 @@ describe('RealtimeService', () => { expect(ioMock).toHaveBeenCalledWith('https://notifications.example.com', { auth: { token: 'mock-token-123' }, reconnection, - withCredentials: true, + withCredentials, }); if (logs) { @@ -141,7 +139,7 @@ describe('RealtimeService', () => { }); describe('Retrieving connection identifier', () => { - it('provides a unique identifier for the connected client', () => { + test('When getting the client Id after initialization, then it provides a unique identifier', () => { service.init(); const clientId = service.getClientId(); @@ -149,21 +147,20 @@ describe('RealtimeService', () => { expect(clientId).toBe('mock-socket-id'); }); - it('reports an error when requesting the client ID before connecting', () => { - expect(() => service.getClientId()).toThrow('Realtime service is not connected'); + test('When getting the client id before connecting, then an error indicating so is thrown', () => { + expect(() => service.getClientId()).toThrow(SocketNotConnectedError); }); }); describe('Receiving realtime notifications', () => { - it('delivers realtime notifications to subscribed listeners', () => { + test('When an event is received, then it delivers the notification to subscribed listeners', () => { service.init(); const callback = vi.fn(); - const eventData = { type: 'FILE_CREATED', payload: { fileId: '123' } }; + const eventData = { event: 'FILE_CREATED', payload: { fileId: '123' } }; - const result = service.onEvent(callback); + const cleanup = service.onEvent(callback); - expect(result).toBe(true); - expect(mockSocket.on).toHaveBeenCalledWith('event', expect.any(Function)); + expect(cleanup).toBeInstanceOf(Function); const eventHandler = mockSocket.on.mock.calls.find((call) => call[0] === 'event')?.[1]; eventHandler?.(eventData); @@ -171,50 +168,121 @@ describe('RealtimeService', () => { expect(callback).toHaveBeenCalledWith(eventData); }); - it('prevents event subscriptions when the connection is lost', () => { + test('When an event is received, then it distributes to all registered handlers', () => { + service.init(); + const callback1 = vi.fn(); + const callback2 = vi.fn(); + const eventData = { event: 'PLAN_UPDATED', payload: { maxSpaceBytes: 1000 } }; + + service.onEvent(callback1); + service.onEvent(callback2); + + const eventHandler = mockSocket.on.mock.calls.find((call) => call[0] === 'event')?.[1]; + eventHandler?.(eventData); + + expect(callback1).toHaveBeenCalledWith(eventData); + expect(callback2).toHaveBeenCalledWith(eventData); + }); + + test('When one handler throws an error, then it does not affect other handlers', () => { service.init(); - mockSocket.disconnected = true; + const errorCallback = vi.fn(() => { + throw new Error('Handler error'); + }); + const successCallback = vi.fn(); + const eventData = { event: 'TEST', payload: {} }; - const result = service.onEvent(vi.fn()); + service.onEvent(errorCallback); + service.onEvent(successCallback); - expect(result).toBe(false); - expect(consoleLogSpy).toHaveBeenCalledWith('[REALTIME] SOCKET IS DISCONNECTED'); + const eventHandler = mockSocket.on.mock.calls.find((call) => call[0] === 'event')?.[1]; + eventHandler?.(eventData); + + expect(errorCallback).toHaveBeenCalledWith(eventData); + expect(successCallback).toHaveBeenCalledWith(eventData); + expect(consoleErrorSpy).toHaveBeenCalledWith('[REALTIME] Error in event handler:', expect.any(Error)); + }); + + test('When a handler is registered before init, then it receives events after initialization', () => { + const callback = vi.fn(); + const eventData = { event: 'TEST', payload: {} }; + + service.onEvent(callback); + + service.init(); + + const eventHandler = mockSocket.on.mock.calls.find((call) => call[0] === 'event')?.[1]; + eventHandler?.(eventData); + + expect(callback).toHaveBeenCalledWith(eventData); }); }); describe('Cleaning up event subscriptions', () => { - it('clears all active event subscriptions when requested', () => { + test('When the cleanup function is called, then it removes a specific handler', () => { service.init(); + const callback = vi.fn(); + const eventData = { event: 'TEST', payload: {} }; + + const cleanup = service.onEvent(callback); + + const eventHandler = mockSocket.on.mock.calls.find((call) => call[0] === 'event')?.[1]; + + eventHandler?.(eventData); + expect(callback).toHaveBeenCalledTimes(1); + + cleanup(); + + eventHandler?.(eventData); + expect(callback).toHaveBeenCalledTimes(1); + }); + + test('When removing all listeners function is called, then it clears all active event subscriptions', () => { + service.init(); + const callback1 = vi.fn(); + const callback2 = vi.fn(); + const eventData = { event: 'TEST', payload: {} }; + + service.onEvent(callback1); + service.onEvent(callback2); + service.removeAllListeners(); - expect(mockSocket.removeAllListeners).toHaveBeenCalledTimes(1); + const eventHandler = mockSocket.on.mock.calls.find((call) => call[0] === 'event')?.[1]; + eventHandler?.(eventData); + + expect(callback1).not.toHaveBeenCalled(); + expect(callback2).not.toHaveBeenCalled(); }); - it('handles cleanup safely even when not initialized', () => { + test('When cleanup is called on uninitialized service, then it handles it safely', () => { expect(() => service.removeAllListeners()).not.toThrow(); }); }); describe('Closing the connection', () => { - it.each([ + test.each([ { connected: true, closes: true }, { connected: false, closes: false }, - ])('closes the socket only when connected (connected=$connected, closes=$closes)', ({ connected, closes }) => { - service.init(); - mockSocket.connected = connected; + ])( + 'When the socket is connected, then closes it (connected=$connected, closes=$closes)', + ({ connected, closes }) => { + service.init(); + mockSocket.connected = connected; - service.stop(); + service.stop(); - if (closes) { - expect(mockSocket.close).toHaveBeenCalledTimes(1); - } else { - expect(mockSocket.close).not.toHaveBeenCalled(); - } - }); + if (closes) { + expect(mockSocket.close).toHaveBeenCalledTimes(1); + } else { + expect(mockSocket.close).not.toHaveBeenCalled(); + } + }, + ); }); describe('Complete workflow', () => { - it('connects, receives notifications, and disconnects successfully in a complete flow', () => { + test('When the socket is connected, then receives notifications and disconnects successfully', () => { const onConnected = vi.fn(); const eventCallback = vi.fn(); @@ -224,7 +292,7 @@ describe('RealtimeService', () => { service.onEvent(eventCallback); const eventHandler = mockSocket.on.mock.calls.find((call) => call[0] === 'event')?.[1]; - eventHandler?.({ type: 'FILE_CREATED' }); + eventHandler?.({ event: 'FILE_CREATED', payload: { fileId: '123' } }); service.stop(); diff --git a/src/services/socket.service.ts b/src/services/socket.service.ts index 28b5aa1955..67c02e902e 100644 --- a/src/services/socket.service.ts +++ b/src/services/socket.service.ts @@ -1,14 +1,13 @@ import io, { Socket } from 'socket.io-client'; import localStorageService from './local-storage.service'; import envService from './env.service'; - -export const SOCKET_EVENTS = { - FILE_CREATED: 'FILE_CREATED', -}; +import { SocketNotConnectedError } from './errors/socket.errors'; +import { EventData } from './types/socket.types'; export default class RealtimeService { private socket?: Socket; private static instance: RealtimeService; + private readonly eventHandlers: Set<(data: EventData) => void> = new Set(); static getInstance(): RealtimeService { if (!this.instance) { @@ -27,18 +26,31 @@ export default class RealtimeService { auth: { token: getToken(), }, - reconnection: isProduction(), - withCredentials: true, + reconnection: true, + withCredentials: isProduction(), }); this.socket.on('connect', () => { if (!isProduction()) { console.log('[REALTIME]: CONNECTED WITH ID', this.socket?.id); } - onConnected?.(); }); + this.socket.on('event', (data) => { + if (!isProduction()) { + console.log('[REALTIME] EVENT RECEIVED:', JSON.stringify(data, null, 2)); + } + + this.eventHandlers.forEach((handler) => { + try { + handler(data); + } catch (error) { + console.error('[REALTIME] Error in event handler:', error); + } + }); + }); + this.socket.on('disconnect', (reason) => { if (!isProduction()) { console.log('[REALTIME] DISCONNECTED:', reason); @@ -52,37 +64,40 @@ export default class RealtimeService { getClientId(): string | undefined { if (!this.socket) { - throw new Error('Realtime service is not connected'); + throw new SocketNotConnectedError(); } return this.socket.id; } - onEvent(cb: (data: any) => void): boolean { - if (this.socket?.disconnected) { - console.log('[REALTIME] SOCKET IS DISCONNECTED'); - return false; + onEvent(cb: (data: any) => void): () => void { + if (!isProduction()) { + console.log('[REALTIME] Registering event handler. Total handlers:', this.eventHandlers.size + 1); } - this.socket?.on('event', (data) => { + this.eventHandlers.add(cb); + + // Return cleanup function + return () => { if (!isProduction()) { - console.log('[REALTIME] EVENT RECEIVED:', JSON.stringify(data, null, 2)); + console.log('[REALTIME] Removing event handler. Remaining handlers:', this.eventHandlers.size - 1); } - - cb(data); - }); - return true; + this.eventHandlers.delete(cb); + }; } removeAllListeners() { - this.socket?.removeAllListeners(); + if (!isProduction()) { + console.log('[REALTIME] Clearing all event handlers'); + } + this.eventHandlers.clear(); } stop(): void { console.log('[REALTIME] STOPING...'); - if (this.socket && this.socket?.connected) { + if (this.socket?.connected) { console.log('[REALTIME] SOCKET CLOSED.'); - this.socket?.close(); + this.socket.close(); } } } diff --git a/src/services/types/socket.types.ts b/src/services/types/socket.types.ts new file mode 100644 index 0000000000..8cc3dc52c2 --- /dev/null +++ b/src/services/types/socket.types.ts @@ -0,0 +1,27 @@ +import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; +import { DriveItemData } from 'app/drive/types'; + +export const SOCKET_EVENTS = { + FILE_CREATED: 'FILE_CREATED', + PLAN_UPDATED: 'PLAN_UPDATED', +} as const; + +interface BaseEventData { + email: UserSettings['email']; + clientId: string; + userId: UserSettings['userId']; +} + +interface FileCreatedEvent extends BaseEventData { + event: 'FILE_CREATED'; + payload: DriveItemData; +} + +interface PlanUpdatedEvent extends BaseEventData { + event: 'PLAN_UPDATED'; + payload: { + maxSpaceBytes: number; + }; +} + +export type EventData = FileCreatedEvent | PlanUpdatedEvent; diff --git a/src/views/Checkout/views/CheckoutSuccessView.tsx b/src/views/Checkout/views/CheckoutSuccessView.tsx index ef932f9be7..7db0d70afd 100644 --- a/src/views/Checkout/views/CheckoutSuccessView.tsx +++ b/src/views/Checkout/views/CheckoutSuccessView.tsx @@ -2,11 +2,8 @@ import useEffectAsync from 'hooks/useEffectAsync'; import navigationService from 'services/navigation.service'; import { AppView } from 'app/core/types'; import { useAppDispatch } from 'app/store/hooks'; -import { planThunks } from 'app/store/slices/plan'; -import { userThunks } from 'app/store/slices/user'; import { useCallback, useRef } from 'react'; import localStorageService from 'services/local-storage.service'; -import { workspaceThunks } from 'app/store/slices/workspaces/workspacesStore'; import { trackPaymentConversion } from 'app/analytics/impact.service'; import gaService from 'app/analytics/ga.service'; import metaService from 'app/analytics/meta.service'; @@ -35,12 +32,6 @@ const CheckoutSuccessView = (): JSX.Element => { hasTrackedRef.current = true; - setTimeout(async () => { - await dispatch(userThunks.initializeUserThunk()); - await dispatch(planThunks.initializeThunk()); - await dispatch(workspaceThunks.fetchWorkspaces()); - }, 3000); - try { metaService.trackPurchase(); gaService.trackPurchase(); diff --git a/src/views/Drive/components/DriveExplorer/DriveExplorer.tsx b/src/views/Drive/components/DriveExplorer/DriveExplorer.tsx index 3a7c4b07cf..df19096078 100644 --- a/src/views/Drive/components/DriveExplorer/DriveExplorer.tsx +++ b/src/views/Drive/components/DriveExplorer/DriveExplorer.tsx @@ -24,7 +24,7 @@ import BannerWrapper from 'app/banners/BannerWrapper'; import deviceService from 'services/device.service'; import errorService from 'services/error.service'; import navigationService from 'services/navigation.service'; -import RealtimeService, { SOCKET_EVENTS } from 'services/socket.service'; +import RealtimeService from 'services/socket.service'; import { ClearTrashDialog } from 'views/Trash/components'; import { CreateFolderDialog } from 'views/Drive/components'; import DeleteItemsDialog from 'views/Trash/components/DeleteItemsDialog'; @@ -62,6 +62,7 @@ import UploadItemsFailsDialog from 'app/drive/components/UploadItemsFailsDialog/ import WarningMessageWrapper from 'views/Home/components/WarningMessageWrapper'; import './DriveExplorer.scss'; import { DriveTopBarItems } from './DriveTopBarItems'; +import { EventData, SOCKET_EVENTS } from 'services/types/socket.types'; const MenuItemToGetSize = ({ isTrash, @@ -308,20 +309,23 @@ const DriveExplorer = (props: DriveExplorerProps): JSX.Element => { ); const realtimeService = RealtimeService.getInstance(); - const handleFileCreatedEvent = (data) => { - if (data.event === SOCKET_EVENTS.FILE_CREATED) { - const folderId = data.payload.folderId; - if (folderId === currentFolderId) { - dispatch( - storageActions.pushItems({ - updateRecents: true, - folderIds: [folderId], - items: [data.payload as DriveItemData], - }), - ); + const handleFileCreatedEvent = useCallback( + (data: EventData) => { + if (data.event === SOCKET_EVENTS.FILE_CREATED) { + const item = data.payload; + if (item.folderUuid === currentFolderId) { + dispatch( + storageActions.pushItems({ + updateRecents: true, + folderIds: [item.folderUuid], + items: [item], + }), + ); + } } - } - }; + }, + [currentFolderId, dispatch], + ); useEffect(() => { if (itemToRename) { @@ -331,12 +335,13 @@ const DriveExplorer = (props: DriveExplorerProps): JSX.Element => { useEffect(() => { try { - realtimeService.removeAllListeners(); - realtimeService.onEvent(handleFileCreatedEvent); + const cleanup = realtimeService.onEvent(handleFileCreatedEvent); + + return cleanup; } catch (err) { errorService.reportError(err); } - }, [currentFolderId]); + }, [handleFileCreatedEvent]); useEffect(() => { deviceService.redirectForMobile(); From 35a3eee27ff33b4e4025bcd09691f5b5a0dfaffe Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Tue, 27 Jan 2026 11:58:07 +0100 Subject: [PATCH 2/6] tests: add coverage to the plan reducer --- src/App.tsx | 2 +- src/app/store/slices/plan/index.ts | 2 +- src/app/store/slices/plan/plan.test.ts | 55 +++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 9199ed0b04..04f65dde46 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -96,7 +96,7 @@ const App = (props: AppProps): JSX.Element => { const newLimit = data.payload?.maxSpaceBytes; if (newLimit) { - dispatch(planActions.updatePlanLimitFromSocket(Number(newLimit))); + dispatch(planActions.updatePlanLimit(Number(newLimit))); } } }, diff --git a/src/app/store/slices/plan/index.ts b/src/app/store/slices/plan/index.ts index b23d162398..602492f368 100644 --- a/src/app/store/slices/plan/index.ts +++ b/src/app/store/slices/plan/index.ts @@ -124,7 +124,7 @@ export const planSlice = createSlice({ name: 'plan', initialState, reducers: { - updatePlanLimitFromSocket: (state, action) => { + updatePlanLimit: (state, action) => { if (action.payload) { state.planLimit = action.payload; state.isLoadingPlanLimit = false; diff --git a/src/app/store/slices/plan/plan.test.ts b/src/app/store/slices/plan/plan.test.ts index 9bbab6e5eb..59f026fe56 100644 --- a/src/app/store/slices/plan/plan.test.ts +++ b/src/app/store/slices/plan/plan.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect } from 'vitest'; -import { planSelectors, PlanState } from './index'; +import { describe, it, expect, test } from 'vitest'; +import { planSelectors, PlanState, planSlice, planActions } from './index'; import { RootState } from '../..'; import { StoragePlan, RenewalPeriod } from '@internxt/sdk/dist/drive/payments/types/types'; @@ -139,3 +139,54 @@ describe('Plan Selectors', () => { }); }); }); + +describe('Plan Reducers', () => { + describe('Update Limit plan', () => { + test('When providing a max space bytes, then should update the plan limit and stop loading the plan limit', () => { + const initialState: PlanState = { + isLoadingPlanLimit: true, + isLoadingPlanUsage: false, + isLoadingBusinessLimitAndUsage: false, + individualPlan: null, + businessPlan: null, + planLimit: 0, + planUsage: 0, + usageDetails: null, + individualSubscription: null, + businessSubscription: null, + businessPlanLimit: 0, + businessPlanUsage: 0, + businessPlanUsageDetails: null, + }; + + const newLimit = 5000000000; + const result = planSlice.reducer(initialState, planActions.updatePlanLimit(newLimit)); + + expect(result.planLimit).toStrictEqual(newLimit); + expect(result.isLoadingPlanLimit).toStrictEqual(false); + }); + + test('When there is no max space bytes, then should not update the user plan limit', () => { + const initialState: PlanState = { + isLoadingPlanLimit: true, + isLoadingPlanUsage: false, + isLoadingBusinessLimitAndUsage: false, + individualPlan: null, + businessPlan: null, + planLimit: 1000000000, + planUsage: 0, + usageDetails: null, + individualSubscription: null, + businessSubscription: null, + businessPlanLimit: 0, + businessPlanUsage: 0, + businessPlanUsageDetails: null, + }; + + const result = planSlice.reducer(initialState, planActions.updatePlanLimit(null)); + + expect(result.planLimit).toStrictEqual(initialState.planLimit); + expect(result.isLoadingPlanLimit).toStrictEqual(true); + }); + }); +}); From de7b45e3db0a4d5b2242e6cab0be39ff346afd9f Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Tue, 27 Jan 2026 14:03:43 +0100 Subject: [PATCH 3/6] refactor: create event handler class --- src/App.tsx | 25 +-- src/services/auth.service.test.ts | 2 +- src/services/auth.service.ts | 2 +- src/services/index.ts | 4 +- .../{ => sockets}/errors/socket.errors.ts | 0 .../sockets/event-handler.service.test.ts | 163 ++++++++++++++++++ src/services/sockets/event-handler.service.ts | 39 +++++ .../{ => sockets}/socket.service.test.ts | 14 +- src/services/{ => sockets}/socket.service.ts | 5 +- .../{ => sockets}/types/socket.types.ts | 0 .../DriveExplorer/DriveExplorer.tsx | 25 +-- 11 files changed, 222 insertions(+), 57 deletions(-) rename src/services/{ => sockets}/errors/socket.errors.ts (100%) create mode 100644 src/services/sockets/event-handler.service.test.ts create mode 100644 src/services/sockets/event-handler.service.ts rename src/services/{ => sockets}/socket.service.test.ts (96%) rename src/services/{ => sockets}/socket.service.ts (95%) rename src/services/{ => sockets}/types/socket.types.ts (100%) diff --git a/src/App.tsx b/src/App.tsx index 04f65dde46..7cc43e85b3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react'; +import { useEffect } from 'react'; import { DndProvider } from 'react-dnd'; import { Toaster } from 'react-hot-toast'; import { connect } from 'react-redux'; @@ -22,7 +22,7 @@ import envService from 'services/env.service'; import errorService from 'services/error.service'; import localStorageService from 'services/local-storage.service'; import navigationService from 'services/navigation.service'; -import RealtimeService from 'services/socket.service'; + import { AppViewConfig } from './app/core/types'; import { LRUFilesCacheManager } from './app/database/services/database.service/LRUFilesCacheManager'; import { LRUFilesPreviewCacheManager } from './app/database/services/database.service/LRUFilesPreviewCacheManager'; @@ -36,7 +36,6 @@ import { getRoutes } from './app/routes/routes'; import { domainManager } from './app/share/services/DomainManager'; import { PreviewFileItem } from './app/share/types'; import { AppDispatch, RootState } from './app/store'; -import { planActions } from './app/store/slices/plan'; import { sessionActions } from './app/store/slices/session'; import { uiActions } from './app/store/slices/ui'; import { initializeUserThunk } from './app/store/slices/user'; @@ -44,9 +43,10 @@ import { workspaceThunks } from './app/store/slices/workspaces/workspacesStore'; import { manager } from 'utils/dnd-utils'; import useBeforeUnload from './hooks/useBeforeUnload'; import useVpnAuth from './hooks/useVpnAuth'; -import { EventData, SOCKET_EVENTS } from 'services/types/socket.types'; import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?raw'; +import { eventHandler } from 'services/sockets/event-handler.service'; +import RealtimeService from 'services/sockets/socket.service'; const blob = new Blob([workerUrl], { type: 'application/javascript' }); pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(blob); @@ -90,29 +90,16 @@ const App = (props: AppProps): JSX.Element => { i18next.changeLanguage(); }, []); - const handlePlanUpdatedEvent = useCallback( - (data: EventData) => { - if (data.event === SOCKET_EVENTS.PLAN_UPDATED) { - const newLimit = data.payload?.maxSpaceBytes; - - if (newLimit) { - dispatch(planActions.updatePlanLimit(Number(newLimit))); - } - } - }, - [dispatch], - ); - useEffect(() => { try { const realtimeService = RealtimeService.getInstance(); - const cleanup = realtimeService.onEvent(handlePlanUpdatedEvent); + const cleanup = realtimeService.onEvent(eventHandler.onPlanUpdated); return cleanup; } catch (err) { errorService.reportError(err); } - }, [handlePlanUpdatedEvent]); + }, []); useEffect(() => { if (!isWorkspaceIdParam) { diff --git a/src/services/auth.service.test.ts b/src/services/auth.service.test.ts index 26f6e0b0cf..6f7898fea7 100644 --- a/src/services/auth.service.test.ts +++ b/src/services/auth.service.test.ts @@ -86,7 +86,7 @@ beforeAll(() => { workspaceThunks: vi.fn(), })); - vi.mock('services/socket.service', () => ({ + vi.mock('services/sockets/socket.service', () => ({ default: { getInstance: vi.fn().mockReturnValue({ stop: vi.fn(), diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index a5023d097e..ffa887aae7 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -15,7 +15,7 @@ import { trackLead } from 'app/analytics/meta.service'; import { getCookie, setCookie } from 'app/analytics/utils'; import localStorageService from 'services/local-storage.service'; import navigationService from 'services/navigation.service'; -import RealtimeService from 'services/socket.service'; +import RealtimeService from 'services/sockets/socket.service'; import AppError, { AppView } from 'app/core/types'; import { assertPrivateKeyIsValid, diff --git a/src/services/index.ts b/src/services/index.ts index f0479d9e06..0ebf8dedc2 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -20,8 +20,8 @@ export * from './navigation.service'; export { default as navigationService } from './navigation.service'; export * from './operating-system.service'; export { default as operatingSystemService } from './operating-system.service'; -export * from './socket.service'; -export { default as RealtimeService } from './socket.service'; +export * from './sockets/socket.service'; +export { default as RealtimeService } from './sockets/socket.service'; export * from './storage-keys'; export * from './stream.service'; export * from './validation.service'; diff --git a/src/services/errors/socket.errors.ts b/src/services/sockets/errors/socket.errors.ts similarity index 100% rename from src/services/errors/socket.errors.ts rename to src/services/sockets/errors/socket.errors.ts diff --git a/src/services/sockets/event-handler.service.test.ts b/src/services/sockets/event-handler.service.test.ts new file mode 100644 index 0000000000..b9b525126a --- /dev/null +++ b/src/services/sockets/event-handler.service.test.ts @@ -0,0 +1,163 @@ +import { beforeEach, describe, expect, vi, test } from 'vitest'; +import { EventHandler } from './event-handler.service'; +import { SOCKET_EVENTS, EventData } from './types/socket.types'; +import { store } from 'app/store'; +import { planActions } from 'app/store/slices/plan'; +import { storageActions } from 'app/store/slices/storage'; + +vi.mock('app/store', () => ({ + store: { + dispatch: vi.fn(), + }, +})); + +vi.mock('app/store/slices/plan', () => ({ + planActions: { + updatePlanLimit: vi.fn((limit: number) => ({ type: 'plan/updatePlanLimit', payload: limit })), + }, +})); + +vi.mock('app/store/slices/storage', () => ({ + storageActions: { + pushItems: vi.fn((payload) => ({ type: 'storage/pushItems', payload })), + }, +})); + +describe('Event Handler', () => { + let eventHandler: EventHandler; + let consoleLogSpy: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + eventHandler = new EventHandler(); + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + describe('Plan updated', () => { + test('When the event contains the new plan limit, then it should update plan limit', () => { + const eventData: EventData = { + event: SOCKET_EVENTS.PLAN_UPDATED, + email: 'test@example.com', + clientId: 'client-123', + userId: 'user-123', + payload: { + maxSpaceBytes: 1000000, + }, + }; + + eventHandler.onPlanUpdated(eventData); + + expect(planActions.updatePlanLimit).toHaveBeenCalledWith(eventData.payload.maxSpaceBytes); + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'plan/updatePlanLimit', + payload: 1000000, + }); + }); + + test('should log the plan limit update', () => { + const eventData: EventData = { + event: SOCKET_EVENTS.PLAN_UPDATED, + email: 'test@example.com', + clientId: 'client-123', + userId: 'user-123', + payload: { + maxSpaceBytes: 2000000, + }, + }; + + eventHandler.onPlanUpdated(eventData); + + expect(consoleLogSpy).toHaveBeenCalledWith('[Event Handler] Updating plan limit: ', 2000000); + }); + + test('When there is no new limit, then it should not dispatch ', () => { + const eventData: EventData = { + event: SOCKET_EVENTS.PLAN_UPDATED, + email: 'test@example.com', + clientId: 'client-123', + userId: 'user-123', + payload: { + maxSpaceBytes: 0, + }, + }; + + eventHandler.onPlanUpdated(eventData); + + expect(store.dispatch).not.toHaveBeenCalled(); + }); + }); + + describe('Create File', () => { + const mockFileItem = { + id: 1, + fileId: 'file-123', + type: 'file' as const, + name: 'test.txt', + size: 1024, + folderUuid: 'folder-123', + uuid: 'uuid-123', + bucket: 'bucket-1', + createdAt: '2024-01-01', + created_at: '2024-01-01', + deleted: false, + deletedAt: null, + encryptVersion: '03-aes', + encrypt_version: '03-aes', + folderId: 1, + folder_id: 1, + modificationTime: '2024-01-01', + updatedAt: '2024-01-01', + updated_at: '2024-01-01', + userId: 1, + plainName: 'test.txt', + removed: false, + removed_at: null, + status: 'EXISTS' as const, + }; + + test('When a file is created, then it should push item to storage', () => { + const eventData: EventData = { + event: SOCKET_EVENTS.FILE_CREATED, + email: 'test@example.com', + clientId: 'client-123', + userId: 'user-123', + payload: mockFileItem, + }; + + eventHandler.onFileCreated(eventData, 'folder-123'); + + expect(storageActions.pushItems).toHaveBeenCalledWith({ + updateRecents: true, + folderIds: ['folder-123'], + items: [mockFileItem], + }); + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'storage/pushItems', + payload: { + updateRecents: true, + folderIds: ['folder-123'], + items: [mockFileItem], + }, + }); + }); + + test('When a file is created but the folder id does not match, then should not push the item', () => { + const eventData: EventData = { + event: SOCKET_EVENTS.FILE_CREATED, + email: 'test@example.com', + clientId: 'client-123', + userId: 'user-123', + payload: mockFileItem, + }; + + eventHandler.onFileCreated(eventData, 'different-folder-123'); + + expect(consoleLogSpy).toHaveBeenCalledWith('[Event Handler] Handling created file:', { + itemFolderId: 'folder-123', + currentFolderId: 'different-folder-123', + match: false, + }); + expect(store.dispatch).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/services/sockets/event-handler.service.ts b/src/services/sockets/event-handler.service.ts new file mode 100644 index 0000000000..1c610d39bc --- /dev/null +++ b/src/services/sockets/event-handler.service.ts @@ -0,0 +1,39 @@ +import { planActions } from 'app/store/slices/plan'; +import { EventData, SOCKET_EVENTS } from './types/socket.types'; +import { store } from 'app/store'; +import { storageActions } from 'app/store/slices/storage'; + +export class EventHandler { + public onPlanUpdated(data: EventData) { + if (data.event !== SOCKET_EVENTS.PLAN_UPDATED) return; + const newLimit = data.payload?.maxSpaceBytes; + console.log('[Event Handler] Updating plan limit: ', newLimit); + + if (newLimit) { + store.dispatch(planActions.updatePlanLimit(Number(newLimit))); + } + } + + public onFileCreated(data: EventData, currentFolderId: string) { + if (data.event !== SOCKET_EVENTS.FILE_CREATED) return; + const item = data.payload; + + console.log('[Event Handler] Handling created file:', { + itemFolderId: item.folderUuid, + currentFolderId, + match: item.folderUuid === currentFolderId, + }); + + if (item.folderUuid !== currentFolderId) return; + + store.dispatch( + storageActions.pushItems({ + updateRecents: true, + folderIds: [item.folderUuid], + items: [item], + }), + ); + } +} + +export const eventHandler = new EventHandler(); diff --git a/src/services/socket.service.test.ts b/src/services/sockets/socket.service.test.ts similarity index 96% rename from src/services/socket.service.test.ts rename to src/services/sockets/socket.service.test.ts index e5e98e8960..9dff0777a0 100644 --- a/src/services/socket.service.test.ts +++ b/src/services/sockets/socket.service.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, vi, afterEach, test } from 'vitest'; import RealtimeService from './socket.service'; -import localStorageService from './local-storage.service'; -import envService from './env.service'; +import localStorageService from '../local-storage.service'; +import envService from '../env.service'; import { SocketNotConnectedError } from './errors/socket.errors'; import { SOCKET_EVENTS } from './types/socket.types'; @@ -25,12 +25,6 @@ vi.mock('socket.io-client', () => ({ default: ioMock, })); -vi.mock('./env.service', () => ({ - default: { - getVariable: vi.fn(), - }, -})); - describe('RealtimeService', () => { let service: RealtimeService; let consoleLogSpy: ReturnType; @@ -42,7 +36,7 @@ describe('RealtimeService', () => { service = RealtimeService.getInstance(); consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - vi.mocked(envService.getVariable).mockImplementation((key: string) => { + vi.spyOn(envService, 'getVariable').mockImplementation((key: string) => { if (key === 'nodeEnv') return 'test'; if (key === 'notifications') return 'https://notifications.example.com'; return ''; @@ -115,7 +109,7 @@ describe('RealtimeService', () => { ])( 'When running in $env environment, then it adjusts reconnection=$reconnection, withCredentials=$withCredentials and logging=$logs', ({ env, reconnection, withCredentials, logs }) => { - vi.mocked(envService.getVariable).mockImplementation((key: string) => { + vi.spyOn(envService, 'getVariable').mockImplementation((key: string) => { if (key === 'nodeEnv') return env; if (key === 'notifications') return 'https://notifications.example.com'; return ''; diff --git a/src/services/socket.service.ts b/src/services/sockets/socket.service.ts similarity index 95% rename from src/services/socket.service.ts rename to src/services/sockets/socket.service.ts index 67c02e902e..20ed76bb83 100644 --- a/src/services/socket.service.ts +++ b/src/services/sockets/socket.service.ts @@ -1,6 +1,6 @@ import io, { Socket } from 'socket.io-client'; -import localStorageService from './local-storage.service'; -import envService from './env.service'; +import localStorageService from '../local-storage.service'; +import envService from '../env.service'; import { SocketNotConnectedError } from './errors/socket.errors'; import { EventData } from './types/socket.types'; @@ -76,7 +76,6 @@ export default class RealtimeService { this.eventHandlers.add(cb); - // Return cleanup function return () => { if (!isProduction()) { console.log('[REALTIME] Removing event handler. Remaining handlers:', this.eventHandlers.size - 1); diff --git a/src/services/types/socket.types.ts b/src/services/sockets/types/socket.types.ts similarity index 100% rename from src/services/types/socket.types.ts rename to src/services/sockets/types/socket.types.ts diff --git a/src/views/Drive/components/DriveExplorer/DriveExplorer.tsx b/src/views/Drive/components/DriveExplorer/DriveExplorer.tsx index d325e2bc82..d93a3c9264 100644 --- a/src/views/Drive/components/DriveExplorer/DriveExplorer.tsx +++ b/src/views/Drive/components/DriveExplorer/DriveExplorer.tsx @@ -24,7 +24,6 @@ import BannerWrapper from 'app/banners/BannerWrapper'; import deviceService from 'services/device.service'; import errorService from 'services/error.service'; import navigationService from 'services/navigation.service'; -import RealtimeService from 'services/socket.service'; import { ClearTrashDialog } from 'views/Trash/components'; import { CreateFolderDialog } from 'views/Drive/components'; import DeleteItemsDialog from 'views/Trash/components/DeleteItemsDialog'; @@ -62,7 +61,8 @@ import WarningMessageWrapper from 'views/Home/components/WarningMessageWrapper'; import './DriveExplorer.scss'; import { DriveTopBarItems } from './DriveTopBarItems'; import { ShareDialogWrapper } from 'app/drive/components/ShareDialog/ShareDialogWrapper'; -import { EventData, SOCKET_EVENTS } from 'services/types/socket.types'; +import RealtimeService from 'services/sockets/socket.service'; +import { eventHandler } from 'services/sockets/event-handler.service'; const MenuItemToGetSize = ({ isTrash, @@ -309,23 +309,6 @@ const DriveExplorer = (props: DriveExplorerProps): JSX.Element => { ); const realtimeService = RealtimeService.getInstance(); - const handleFileCreatedEvent = useCallback( - (data: EventData) => { - if (data.event === SOCKET_EVENTS.FILE_CREATED) { - const item = data.payload; - if (item.folderUuid === currentFolderId) { - dispatch( - storageActions.pushItems({ - updateRecents: true, - folderIds: [item.folderUuid], - items: [item], - }), - ); - } - } - }, - [currentFolderId, dispatch], - ); useEffect(() => { if (itemToRename) { @@ -335,13 +318,13 @@ const DriveExplorer = (props: DriveExplorerProps): JSX.Element => { useEffect(() => { try { - const cleanup = realtimeService.onEvent(handleFileCreatedEvent); + const cleanup = realtimeService.onEvent((data) => eventHandler.onFileCreated(data, currentFolderId)); return cleanup; } catch (err) { errorService.reportError(err); } - }, [handleFileCreatedEvent]); + }, [currentFolderId]); useEffect(() => { deviceService.redirectForMobile(); From 3a93b63dd72f9ad0246a2cf0b7318425e73e4fbb Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Tue, 27 Jan 2026 14:10:11 +0100 Subject: [PATCH 4/6] feat: add a fallback when the limit does not comes in the event --- .../sockets/event-handler.service.test.ts | 28 ++++++------------- src/services/sockets/event-handler.service.ts | 4 ++- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/services/sockets/event-handler.service.test.ts b/src/services/sockets/event-handler.service.test.ts index b9b525126a..49bd21d7eb 100644 --- a/src/services/sockets/event-handler.service.test.ts +++ b/src/services/sockets/event-handler.service.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, vi, test } from 'vitest'; import { EventHandler } from './event-handler.service'; import { SOCKET_EVENTS, EventData } from './types/socket.types'; import { store } from 'app/store'; -import { planActions } from 'app/store/slices/plan'; +import { planActions, planThunks } from 'app/store/slices/plan'; import { storageActions } from 'app/store/slices/storage'; vi.mock('app/store', () => ({ @@ -15,6 +15,12 @@ vi.mock('app/store/slices/plan', () => ({ planActions: { updatePlanLimit: vi.fn((limit: number) => ({ type: 'plan/updatePlanLimit', payload: limit })), }, + planThunks: { + fetchLimitThunk: vi.fn(() => ({ type: 'plan/fetchLimitThunk' })), + fetchUsageThunk: vi.fn(() => ({ type: 'plan/fetchUsageThunk' })), + fetchSubscriptionThunk: vi.fn(() => ({ type: 'plan/fetchSubscriptionThunk' })), + fetchBusinessLimitUsageThunk: vi.fn(() => ({ type: 'plan/fetchBusinessLimitUsageThunk' })), + }, })); vi.mock('app/store/slices/storage', () => ({ @@ -54,23 +60,7 @@ describe('Event Handler', () => { }); }); - test('should log the plan limit update', () => { - const eventData: EventData = { - event: SOCKET_EVENTS.PLAN_UPDATED, - email: 'test@example.com', - clientId: 'client-123', - userId: 'user-123', - payload: { - maxSpaceBytes: 2000000, - }, - }; - - eventHandler.onPlanUpdated(eventData); - - expect(consoleLogSpy).toHaveBeenCalledWith('[Event Handler] Updating plan limit: ', 2000000); - }); - - test('When there is no new limit, then it should not dispatch ', () => { + test('When there is no new limit, then it should fetch the limit', () => { const eventData: EventData = { event: SOCKET_EVENTS.PLAN_UPDATED, email: 'test@example.com', @@ -83,7 +73,7 @@ describe('Event Handler', () => { eventHandler.onPlanUpdated(eventData); - expect(store.dispatch).not.toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(planThunks.fetchLimitThunk()); }); }); diff --git a/src/services/sockets/event-handler.service.ts b/src/services/sockets/event-handler.service.ts index 1c610d39bc..94a81d1f35 100644 --- a/src/services/sockets/event-handler.service.ts +++ b/src/services/sockets/event-handler.service.ts @@ -1,4 +1,4 @@ -import { planActions } from 'app/store/slices/plan'; +import { planActions, planThunks } from 'app/store/slices/plan'; import { EventData, SOCKET_EVENTS } from './types/socket.types'; import { store } from 'app/store'; import { storageActions } from 'app/store/slices/storage'; @@ -11,6 +11,8 @@ export class EventHandler { if (newLimit) { store.dispatch(planActions.updatePlanLimit(Number(newLimit))); + } else { + store.dispatch(planThunks.fetchLimitThunk()); } } From ed96dde32eb64d48b51fbb4fdefdfa2336e00bb2 Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Tue, 27 Jan 2026 14:17:19 +0100 Subject: [PATCH 5/6] test: remove useless test --- src/services/sockets/socket.service.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/services/sockets/socket.service.test.ts b/src/services/sockets/socket.service.test.ts index 9dff0777a0..d1163c1fd8 100644 --- a/src/services/sockets/socket.service.test.ts +++ b/src/services/sockets/socket.service.test.ts @@ -75,16 +75,6 @@ describe('RealtimeService', () => { }); describe('Establishing realtime connection', () => { - test('When init is called, then it establishes a secure connection with authentication', () => { - service.init(); - - expect(ioMock).toHaveBeenCalledWith('https://notifications.example.com', { - auth: { token: 'mock-token-123' }, - reconnection: true, - withCredentials: false, - }); - }); - test.each(['connect', 'event', 'disconnect', 'connect_error'])( 'When init is called, then it monitors connection lifecycle through %s events', (eventName) => { From baa345bd037f9379e7ee374cdc8a0775070808e8 Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Thu, 29 Jan 2026 14:46:40 +0100 Subject: [PATCH 6/6] feat: use constant for socket events --- src/services/sockets/types/socket.types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/sockets/types/socket.types.ts b/src/services/sockets/types/socket.types.ts index 8cc3dc52c2..5c3da8b8b3 100644 --- a/src/services/sockets/types/socket.types.ts +++ b/src/services/sockets/types/socket.types.ts @@ -13,12 +13,12 @@ interface BaseEventData { } interface FileCreatedEvent extends BaseEventData { - event: 'FILE_CREATED'; + event: typeof SOCKET_EVENTS.FILE_CREATED; payload: DriveItemData; } interface PlanUpdatedEvent extends BaseEventData { - event: 'PLAN_UPDATED'; + event: typeof SOCKET_EVENTS.PLAN_UPDATED; payload: { maxSpaceBytes: number; };