import { Test, TestingModule } from '@nestjs/testing'; import { StateGateway } from './state.gateway'; import { AuthenticatedSocket } from '../../types/socket'; import { AuthService } from '../../auth/auth.service'; import { JwtVerificationService } from '../../auth/services/jwt-verification.service'; import { FriendsService } from '../../friends/friends.service'; interface MockSocket extends Partial { id: string; data: { user?: { keycloakSub: string; }; userId?: string; friends?: Set; }; handshake?: any; disconnect?: jest.Mock; } describe('StateGateway', () => { let gateway: StateGateway; let mockLoggerLog: jest.SpyInstance; let mockLoggerDebug: jest.SpyInstance; let mockLoggerWarn: jest.SpyInstance; let mockServer: { sockets: { sockets: { size: number } }; to: jest.Mock; }; let mockAuthService: Partial; let mockJwtVerificationService: Partial; let mockFriendsService: Partial; beforeEach(async () => { mockServer = { sockets: { sockets: { size: 5, }, }, to: jest.fn().mockReturnValue({ emit: jest.fn(), }), } as any; mockAuthService = { syncUserFromToken: jest.fn().mockResolvedValue({ id: 'user-id', keycloakSub: 'test-sub', }), }; mockJwtVerificationService = { extractToken: jest.fn((handshake) => handshake.auth?.token), verifyToken: jest.fn().mockResolvedValue({ sub: 'test-sub', email: 'test@example.com', }), }; mockFriendsService = { getFriends: jest.fn().mockResolvedValue([]), }; const module: TestingModule = await Test.createTestingModule({ providers: [ StateGateway, { provide: AuthService, useValue: mockAuthService }, { provide: JwtVerificationService, useValue: mockJwtVerificationService, }, { provide: FriendsService, useValue: mockFriendsService }, ], }).compile(); gateway = module.get(StateGateway); gateway.io = mockServer as any; mockLoggerLog = jest.spyOn(gateway['logger'], 'log').mockImplementation(); mockLoggerDebug = jest .spyOn(gateway['logger'], 'debug') .mockImplementation(); mockLoggerWarn = jest.spyOn(gateway['logger'], 'warn').mockImplementation(); }); afterEach(() => { jest.restoreAllMocks(); }); it('should be defined', () => { expect(gateway).toBeDefined(); }); describe('afterInit', () => { it('should log initialization message', () => { gateway.afterInit(); expect(mockLoggerLog).toHaveBeenCalledWith('Initialized'); }); }); describe('handleConnection', () => { it('should log client connection and sync user when authenticated', async () => { const mockClient: MockSocket = { id: 'client1', data: { user: { keycloakSub: 'test-sub' } }, handshake: { auth: { token: 'mock-token' }, headers: {}, }, }; await gateway.handleConnection( mockClient as unknown as AuthenticatedSocket, ); expect(mockJwtVerificationService.extractToken).toHaveBeenCalledWith( mockClient.handshake, ); expect(mockJwtVerificationService.verifyToken).toHaveBeenCalledWith( 'mock-token', ); expect(mockAuthService.syncUserFromToken).toHaveBeenCalledWith( expect.objectContaining({ keycloakSub: 'test-sub', }), ); expect(mockLoggerLog).toHaveBeenCalledWith( `Client id: ${mockClient.id} connected (user: test-sub)`, ); expect(mockLoggerDebug).toHaveBeenCalledWith( 'Number of connected clients: 5', ); }); it('should disconnect client when no token provided', async () => { const mockClient: MockSocket = { id: 'client1', data: {}, handshake: { auth: {}, headers: {}, }, disconnect: jest.fn(), }; (mockJwtVerificationService.extractToken as jest.Mock).mockReturnValue( undefined, ); await gateway.handleConnection( mockClient as unknown as AuthenticatedSocket, ); expect(mockLoggerWarn).toHaveBeenCalledWith( 'WebSocket connection attempt without token', ); expect(mockClient.disconnect).toHaveBeenCalled(); }); }); describe('handleDisconnect', () => { it('should log client disconnection', () => { const mockClient: MockSocket = { id: 'client1', data: { user: { keycloakSub: 'test-sub' } }, }; gateway.handleDisconnect(mockClient as unknown as AuthenticatedSocket); expect(mockLoggerLog).toHaveBeenCalledWith( `Client id: ${mockClient.id} disconnected (user: test-sub)`, ); }); it('should handle disconnection when no user data', () => { const mockClient: MockSocket = { id: 'client1', data: {}, }; gateway.handleDisconnect(mockClient as unknown as AuthenticatedSocket); expect(mockLoggerLog).toHaveBeenCalledWith( `Client id: ${mockClient.id} disconnected (user: unknown)`, ); }); }); describe('handleCursorReportPosition', () => { it('should emit cursor position to connected friends', () => { const mockClient: MockSocket = { id: 'client1', data: { user: { keycloakSub: 'test-sub' }, userId: 'user-1', friends: new Set(['friend-1']), }, }; // Setup the userSocketMap to simulate a connected friend // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (gateway as any).userSocketMap.set('friend-1', 'friend-socket-id'); const data = { x: 100, y: 200, isDrawing: false }; gateway.handleCursorReportPosition( mockClient as unknown as AuthenticatedSocket, data, ); // Verify that the message was emitted to the friend expect(mockServer.to).toHaveBeenCalledWith('friend-socket-id'); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const emitMock = mockServer.to().emit as jest.Mock; expect(emitMock).toHaveBeenCalledWith('friend-cursor-position', { userId: 'user-1', position: data, }); }); it('should not emit when no friends are online', () => { const mockClient: MockSocket = { id: 'client1', data: { user: { keycloakSub: 'test-sub' }, userId: 'user-1', friends: new Set(['friend-1']), }, }; // Don't set up userSocketMap - friend is not online const data = { x: 100, y: 200, isDrawing: false }; gateway.handleCursorReportPosition( mockClient as unknown as AuthenticatedSocket, data, ); // Verify that no message was emitted expect(mockServer.to).not.toHaveBeenCalled(); }); it('should log warning when userId is missing', () => { const mockClient: MockSocket = { id: 'client1', data: { user: { keycloakSub: 'test-sub' }, // userId is missing friends: new Set(['friend-1']), }, }; const data = { x: 100, y: 200, isDrawing: false }; gateway.handleCursorReportPosition( mockClient as unknown as AuthenticatedSocket, data, ); // Verify that a warning was logged expect(mockLoggerWarn).toHaveBeenCalledWith( `Could not find user ID for client ${mockClient.id}`, ); // Verify that no message was emitted expect(mockServer.to).not.toHaveBeenCalled(); }); it('should throw exception when client is not authenticated', () => { const mockClient: MockSocket = { id: 'client1', data: {}, }; const data = { x: 100, y: 200 }; expect(() => { gateway.handleCursorReportPosition( mockClient as unknown as AuthenticatedSocket, data, ); }).toThrow('Unauthorized'); }); }); });