CommunitySolidServer/test/unit/http/UnsecureWebSocketsProtocol.test.ts
Joachim Van Herwegen 4223dcf8a4 feat: Split up server creation and request handling
This allows us to decouple the WebSocket listening from the HTTP configs,
making these features completely orthogonal.
2022-11-09 09:10:00 +01:00

214 lines
8.2 KiB
TypeScript

import { EventEmitter } from 'events';
import type { Server } from 'http';
import { UnsecureWebSocketsProtocol } from '../../../src/http/UnsecureWebSocketsProtocol';
import type { HttpRequest } from '../../../src/server/HttpRequest';
import { BaseActivityEmitter } from '../../../src/server/notifications/ActivityEmitter';
import { AS } from '../../../src/util/Vocabularies';
jest.mock('ws', (): any => ({
// eslint-disable-next-line @typescript-eslint/naming-convention
WebSocketServer: jest.fn().mockImplementation((): any => ({
handleUpgrade(upgradeRequest: any, socket: any, head: any, callback: any): void {
callback(socket, upgradeRequest);
},
})),
}));
class DummySocket extends EventEmitter {
public readonly messages = new Array<string>();
public readonly close = jest.fn();
public send(message: string): void {
this.messages.push(message);
}
}
describe('An UnsecureWebSocketsProtocol', (): void => {
let server: Server;
let webSocket: DummySocket;
const source = new BaseActivityEmitter();
let protocol: UnsecureWebSocketsProtocol;
describe('after registering a socket', (): void => {
beforeAll(async(): Promise<void> => {
server = new EventEmitter() as any;
webSocket = new DummySocket();
protocol = new UnsecureWebSocketsProtocol(source);
await protocol.handle(server);
const upgradeRequest = {
headers: {
host: 'mypod.example',
'sec-websocket-protocol': 'solid-0.1, other/1.0.0',
},
socket: {
encrypted: true,
},
} as any as HttpRequest;
server.emit('upgrade', upgradeRequest, webSocket);
});
it('sends a protocol message.', (): void => {
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('protocol solid-0.1');
});
it('warns when receiving an unexpected message.', (): void => {
webSocket.emit('message', 'unexpected');
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('warning Unrecognized message format: unexpected');
});
it('warns when receiving an unexpected message type.', (): void => {
webSocket.emit('message', 'unknown 1 2 3');
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('warning Unrecognized message type: unknown');
});
describe('before subscribing to resources', (): void => {
it('does not emit pub messages.', (): void => {
source.emit('changed', { path: 'https://mypod.example/foo/bar' }, AS.terms.Update);
expect(webSocket.messages).toHaveLength(0);
});
});
describe('after subscribing to a resource', (): void => {
beforeAll((): void => {
webSocket.emit('message', 'sub https://mypod.example/foo/bar');
});
it('sends an ack message.', (): void => {
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('ack https://mypod.example/foo/bar');
});
it('emits pub messages for that resource.', (): void => {
source.emit('changed', { path: 'https://mypod.example/foo/bar' }, AS.terms.Update);
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('pub https://mypod.example/foo/bar');
});
});
describe('after subscribing to a resource via a relative URL', (): void => {
beforeAll((): void => {
webSocket.emit('message', 'sub /relative/foo');
});
it('sends an ack message.', (): void => {
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('ack https://mypod.example/relative/foo');
});
it('emits pub messages for that resource.', (): void => {
source.emit('changed', { path: 'https://mypod.example/relative/foo' }, AS.terms.Update);
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('pub https://mypod.example/relative/foo');
});
});
describe('after subscribing to a resource with the wrong host name', (): void => {
beforeAll((): void => {
webSocket.emit('message', 'sub https://wrong.example/host/foo');
});
it('send an error message.', (): void => {
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift())
.toBe('error Mismatched host: expected mypod.example but got wrong.example');
});
});
describe('after subscribing to a resource with the wrong protocol', (): void => {
beforeAll((): void => {
webSocket.emit('message', 'sub http://mypod.example/protocol/foo');
});
it('send an error message.', (): void => {
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift())
.toBe('error Mismatched protocol: expected https: but got http:');
});
});
});
describe('handling other situations', (): void => {
beforeEach(async(): Promise<void> => {
server = new EventEmitter() as any;
webSocket = new DummySocket();
protocol = new UnsecureWebSocketsProtocol(source);
await protocol.handle(server);
});
it('unsubscribes when a socket closes.', async(): Promise<void> => {
server.emit('upgrade', { headers: {}, socket: {}} as any, webSocket);
expect(webSocket.listenerCount('message')).toBe(1);
webSocket.emit('close');
expect(webSocket.listenerCount('message')).toBe(0);
expect(webSocket.listenerCount('close')).toBe(0);
expect(webSocket.listenerCount('error')).toBe(0);
});
it('unsubscribes when a socket errors.', async(): Promise<void> => {
server.emit('upgrade', { headers: {}, socket: {}} as any, webSocket);
expect(webSocket.listenerCount('message')).toBe(1);
webSocket.emit('error');
expect(webSocket.listenerCount('message')).toBe(0);
expect(webSocket.listenerCount('close')).toBe(0);
expect(webSocket.listenerCount('error')).toBe(0);
});
it('emits a warning when no Sec-WebSocket-Protocol is supplied.', async(): Promise<void> => {
server.emit('upgrade', { headers: {}, socket: {}} as any, webSocket);
expect(webSocket.messages).toHaveLength(2);
expect(webSocket.messages.pop())
.toBe('warning Missing Sec-WebSocket-Protocol header, expected value \'solid-0.1\'');
expect(webSocket.close).toHaveBeenCalledTimes(0);
});
it('emits an error and closes the connection with the wrong Sec-WebSocket-Protocol.', async(): Promise<void> => {
const upgradeRequest = {
headers: {
'sec-websocket-protocol': 'solid/1.0.0, other',
},
socket: {},
} as any as HttpRequest;
server.emit('upgrade', upgradeRequest, webSocket);
expect(webSocket.messages).toHaveLength(2);
expect(webSocket.messages.pop()).toBe('error Client does not support protocol solid-0.1');
expect(webSocket.close).toHaveBeenCalledTimes(1);
expect(webSocket.listenerCount('message')).toBe(0);
expect(webSocket.listenerCount('close')).toBe(0);
expect(webSocket.listenerCount('error')).toBe(0);
});
it('respects the Forwarded header.', async(): Promise<void> => {
const upgradeRequest = {
headers: {
forwarded: 'proto=https;host=other.example',
'sec-websocket-protocol': 'solid-0.1',
},
socket: {},
} as any as HttpRequest;
server.emit('upgrade', upgradeRequest, webSocket);
webSocket.emit('message', 'sub https://other.example/protocol/foo');
expect(webSocket.messages).toHaveLength(2);
expect(webSocket.messages.pop()).toBe('ack https://other.example/protocol/foo');
});
it('respects the X-Forwarded-* headers if Forwarded header is not present.', async(): Promise<void> => {
const upgradeRequest = {
headers: {
'x-forwarded-host': 'other.example',
'x-forwarded-proto': 'https',
'sec-websocket-protocol': 'solid-0.1',
},
socket: {},
} as any as HttpRequest;
server.emit('upgrade', upgradeRequest, webSocket);
webSocket.emit('message', 'sub https://other.example/protocol/foo');
expect(webSocket.messages).toHaveLength(2);
expect(webSocket.messages.pop()).toBe('ack https://other.example/protocol/foo');
});
});
});