feat: Security
This commit is contained in:
120
server/src/websocket/handler-auth.spec.ts
Normal file
120
server/src/websocket/handler-auth.spec.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeEach,
|
||||
vi
|
||||
} from 'vitest';
|
||||
import { WebSocket } from 'ws';
|
||||
import { connectedUsers } from './state';
|
||||
import { ConnectedUser } from './types';
|
||||
import { handleWebSocketMessage } from './handler';
|
||||
|
||||
vi.mock('../services/server-access.service', () => ({
|
||||
authorizeWebSocketJoin: vi.fn(async () => ({ allowed: true as const })),
|
||||
findServerMembership: vi.fn(async () => ({ id: 'membership-1' })),
|
||||
usersShareServerMembership: vi.fn(async () => false)
|
||||
}));
|
||||
|
||||
vi.mock('../services/session-auth.service', () => ({
|
||||
consumeSessionToken: vi.fn(async (token: string) => {
|
||||
if (token !== 'valid-token') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
user: {
|
||||
id: 'user-1',
|
||||
username: 'alice',
|
||||
displayName: 'Alice',
|
||||
passwordHash: 'hash',
|
||||
createdAt: Date.now()
|
||||
},
|
||||
issuedAt: Date.now(),
|
||||
expiresAt: Date.now() + 60_000
|
||||
};
|
||||
})
|
||||
}));
|
||||
|
||||
function createMockWs(): WebSocket & { sentMessages: string[] } {
|
||||
const sent: string[] = [];
|
||||
const ws = {
|
||||
readyState: WebSocket.OPEN,
|
||||
send: (data: string) => { sent.push(data); },
|
||||
close: () => {},
|
||||
sentMessages: sent
|
||||
} as unknown as WebSocket & { sentMessages: string[] };
|
||||
|
||||
return ws;
|
||||
}
|
||||
|
||||
function createConnectedUser(connectionId: string): ConnectedUser {
|
||||
const ws = createMockWs();
|
||||
const user: ConnectedUser = {
|
||||
oderId: connectionId,
|
||||
ws,
|
||||
authenticated: false,
|
||||
serverIds: new Set(),
|
||||
displayName: 'Test User',
|
||||
lastPong: Date.now()
|
||||
};
|
||||
|
||||
connectedUsers.set(connectionId, user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
describe('server websocket handler - authentication', () => {
|
||||
beforeEach(() => {
|
||||
connectedUsers.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('rejects non-identify messages until the connection is authenticated', async () => {
|
||||
createConnectedUser('conn-1');
|
||||
|
||||
await handleWebSocketMessage('conn-1', { type: 'typing', serverId: 'server-1' });
|
||||
|
||||
const user = connectedUsers.get('conn-1');
|
||||
const sentMessages = (user?.ws as WebSocket & { sentMessages: string[] }).sentMessages;
|
||||
const response = JSON.parse(sentMessages[0]) as { type: string };
|
||||
|
||||
expect(response.type).toBe('auth_required');
|
||||
expect(user?.authenticated).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects identify without a session token', async () => {
|
||||
createConnectedUser('conn-1');
|
||||
|
||||
await handleWebSocketMessage('conn-1', {
|
||||
type: 'identify',
|
||||
oderId: 'user-1',
|
||||
displayName: 'Alice'
|
||||
});
|
||||
|
||||
const user = connectedUsers.get('conn-1');
|
||||
const sentMessages = (user?.ws as WebSocket & { sentMessages: string[] }).sentMessages;
|
||||
const response = JSON.parse(sentMessages[0]) as { type: string; code: string };
|
||||
|
||||
expect(response.type).toBe('auth_error');
|
||||
expect(response.code).toBe('MISSING_TOKEN');
|
||||
expect(user?.authenticated).toBe(false);
|
||||
});
|
||||
|
||||
it('binds identify to the authenticated user id from the token', async () => {
|
||||
createConnectedUser('conn-1');
|
||||
|
||||
await handleWebSocketMessage('conn-1', {
|
||||
type: 'identify',
|
||||
token: 'valid-token',
|
||||
oderId: 'user-1',
|
||||
displayName: 'Alice'
|
||||
});
|
||||
|
||||
const user = connectedUsers.get('conn-1');
|
||||
|
||||
expect(user?.authenticated).toBe(true);
|
||||
expect(user?.oderId).toBe('user-1');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user