wsmini
Version:
Minimalist WebSocket client and server for real-time applications with RPC, PubSub, Rooms and Game state synchronization.
328 lines (265 loc) • 12.5 kB
JavaScript
import { expect } from 'chai';
import sinon from 'sinon';
import WebSocketServerOrigin from '../../src/websocket/WebSocketServerOrigin.mjs';
import { createMockRequest } from '../helpers/testUtils.mjs';
describe('WebSocketServerOrigin', () => {
let server;
let mockSocket;
let mockCallback;
beforeEach(() => {
mockSocket = {
write: sinon.spy(),
destroy: sinon.spy()
};
mockCallback = sinon.spy();
});
afterEach(() => {
if (server) {
server.close();
}
sinon.restore();
});
describe('Constructor', () => {
it('should create server with valid origins array', () => {
const options = {
origins: ['http://localhost:3000', 'https://example.com'],
maxNbOfClients: 10,
noServer: true // Prevent actual server startup
};
server = new WebSocketServerOrigin(options);
expect(server.options.origins).to.deep.equal(['http://localhost:3000', 'https://example.com']);
expect(server.options.maxNbOfClients).to.equal(10);
});
it('should create server with single origin string', () => {
const options = {
origins: 'http://localhost:3000',
maxNbOfClients: 10,
noServer: true // Prevent actual server startup
};
server = new WebSocketServerOrigin(options);
expect(server.options.origins).to.deep.equal(['http://localhost:3000']);
});
it('should throw error when origins option is missing', () => {
const options = {
maxNbOfClients: 10,
noServer: true
};
expect(() => new WebSocketServerOrigin(options)).to.throw('Missing origins option');
});
it('should throw error when origins option is invalid type', () => {
const options = {
origins: 123,
maxNbOfClients: 10,
noServer: true
};
expect(() => new WebSocketServerOrigin(options)).to.throw('Invalid origins option');
});
it('should throw error when origins option is null', () => {
const options = {
origins: null,
maxNbOfClients: 10,
noServer: true
};
expect(() => new WebSocketServerOrigin(options)).to.throw('Missing origins option');
});
it('should throw error when maxNbOfClients option is missing', () => {
const options = {
origins: ['http://localhost:3000'],
noServer: true
};
expect(() => new WebSocketServerOrigin(options)).to.throw('Missing maxNbOfClients option');
});
it('should throw error when maxNbOfClients is not an integer', () => {
const options = {
origins: ['http://localhost:3000'],
maxNbOfClients: 'invalid',
noServer: true
};
expect(() => new WebSocketServerOrigin(options)).to.throw('Invalid maxNbOfClients option');
});
it('should throw error when maxNbOfClients is zero', () => {
const options = {
origins: ['http://localhost:3000'],
maxNbOfClients: 0,
noServer: true
};
expect(() => new WebSocketServerOrigin(options)).to.throw('Missing maxNbOfClients option');
});
it('should throw error when maxNbOfClients is negative', () => {
const options = {
origins: ['http://localhost:3000'],
maxNbOfClients: -1,
noServer: true
};
expect(() => new WebSocketServerOrigin(options)).to.throw('Invalid maxNbOfClients option');
});
it('should throw error when maxNbOfClients is float', () => {
const options = {
origins: ['http://localhost:3000'],
maxNbOfClients: 10.5,
noServer: true
};
expect(() => new WebSocketServerOrigin(options)).to.throw('Invalid maxNbOfClients option');
});
});
describe('handleUpgrade', () => {
beforeEach(() => {
server = new WebSocketServerOrigin({
origins: ['http://localhost:3000', 'https://example.com'],
maxNbOfClients: 2,
noServer: true // Prevent actual server startup
});
// Mock the parent handleUpgrade method
sinon.stub(Object.getPrototypeOf(WebSocketServerOrigin.prototype), 'handleUpgrade').returns('parent-result');
});
it('should reject request with invalid origin', () => {
const request = createMockRequest({
headers: { origin: 'http://malicious.com' }
});
const result = server.handleUpgrade(request, mockSocket, Buffer.from(''), mockCallback);
expect(mockSocket.write).to.have.been.calledWith('HTTP/1.1 403 Forbidden\r\n\r\n');
expect(mockSocket.destroy).to.have.been.called;
expect(result).to.be.undefined;
});
it('should reject request with no origin header', () => {
const request = createMockRequest({
headers: {}
});
// Set up server to not allow empty origins
server.options.origins = ['http://localhost:3000']; // No wildcard
const result = server.handleUpgrade(request, mockSocket, Buffer.from(''), mockCallback);
expect(mockSocket.write).to.have.been.calledWith('HTTP/1.1 403 Forbidden\r\n\r\n');
expect(mockSocket.destroy).to.have.been.called;
expect(result).to.be.undefined;
});
it('should reject request when server is full', () => {
const request = createMockRequest({
headers: { origin: 'http://localhost:3000' }
});
// Mock clients to be at capacity
server.clients = new Set(['client1', 'client2']);
const result = server.handleUpgrade(request, mockSocket, Buffer.from(''), mockCallback);
expect(mockSocket.write).to.have.been.calledWith('HTTP/1.1 503 Service Unavailable\r\n\r\n');
expect(mockSocket.destroy).to.have.been.called;
expect(result).to.be.undefined;
});
it('should call parent handleUpgrade with valid origin and available capacity', () => {
const request = createMockRequest({
headers: { origin: 'http://localhost:3000' }
});
// Mock clients to be under capacity
server.clients = new Set(['client1']);
const result = server.handleUpgrade(request, mockSocket, Buffer.from(''), mockCallback);
expect(mockSocket.write).to.not.have.been.called;
expect(mockSocket.destroy).to.not.have.been.called;
expect(Object.getPrototypeOf(WebSocketServerOrigin.prototype).handleUpgrade)
.to.have.been.calledWith(request, mockSocket, Buffer.from(''), mockCallback);
expect(result).to.equal('parent-result');
});
it('should handle undefined origin header', () => {
const request = createMockRequest({
headers: { origin: undefined }
});
// Set up server to allow empty origins
server.options.origins = ['*'];
const result = server.handleUpgrade(request, mockSocket, Buffer.from(''), mockCallback);
expect(mockSocket.write).to.not.have.been.called;
expect(mockSocket.destroy).to.not.have.been.called;
expect(Object.getPrototypeOf(WebSocketServerOrigin.prototype).handleUpgrade)
.to.have.been.calledWith(request, mockSocket, Buffer.from(''), mockCallback);
});
});
describe('checkOrigin', () => {
beforeEach(() => {
server = new WebSocketServerOrigin({
origins: ['http://localhost:3000', 'https://example.com', 'http://localhost:8080'],
maxNbOfClients: 10,
noServer: true // Prevent actual server startup
});
});
it('should return true for exact origin match', () => {
expect(server.checkOrigin('http://localhost:3000')).to.be.true;
expect(server.checkOrigin('https://example.com')).to.be.true;
expect(server.checkOrigin('http://localhost:8080')).to.be.true;
});
it('should return false for non-matching origin', () => {
expect(server.checkOrigin('http://malicious.com')).to.be.false;
expect(server.checkOrigin('https://different.com')).to.be.false;
});
it('should return true for wildcard origin', () => {
server.options.origins = ['*'];
expect(server.checkOrigin('http://any.com')).to.be.true;
expect(server.checkOrigin('https://any.com')).to.be.true;
expect(server.checkOrigin('')).to.be.true;
});
it('should handle empty origin string', () => {
expect(server.checkOrigin('')).to.be.false;
});
it('should handle undefined origin', () => {
expect(server.checkOrigin(undefined)).to.be.false;
});
it('should handle null origin', () => {
expect(server.checkOrigin(null)).to.be.false;
});
it('should match origin without protocol when allowed origin has no protocol', () => {
server.options.origins = ['localhost:3000'];
expect(server.checkOrigin('http://localhost:3000')).to.be.true;
expect(server.checkOrigin('https://localhost:3000')).to.be.true;
// ws:// protocol is not stripped, so it won't match
expect(server.checkOrigin('ws://localhost:3000')).to.be.false;
});
it('should match origin without port when allowed origin has no port', () => {
server.options.origins = ['http://localhost'];
expect(server.checkOrigin('http://localhost:3000')).to.be.true;
expect(server.checkOrigin('http://localhost:8080')).to.be.true;
expect(server.checkOrigin('http://localhost:80')).to.be.true;
});
it('should match origin without protocol and port when allowed origin has neither', () => {
server.options.origins = ['localhost'];
expect(server.checkOrigin('http://localhost:3000')).to.be.true;
expect(server.checkOrigin('https://localhost:8080')).to.be.true;
// ws:// protocol is not stripped, so it won't match
expect(server.checkOrigin('ws://localhost')).to.be.false;
});
it('should not match different hostname even without protocol/port', () => {
server.options.origins = ['localhost'];
expect(server.checkOrigin('http://different.com:3000')).to.be.false;
expect(server.checkOrigin('https://example.com')).to.be.false;
});
it('should match exact origin when allowed origin has protocol but no port', () => {
server.options.origins = ['http://localhost'];
expect(server.checkOrigin('http://localhost:3000')).to.be.true;
expect(server.checkOrigin('https://localhost:3000')).to.be.false; // Different protocol
});
it('should handle multiple allowed origins with different formats', () => {
server.options.origins = ['*', 'http://localhost:3000', 'example.com', 'ws://secure.com:8080'];
expect(server.checkOrigin('http://any.com')).to.be.true; // Wildcard
expect(server.checkOrigin('http://localhost:3000')).to.be.true; // Exact match
expect(server.checkOrigin('https://example.com:8080')).to.be.true; // Domain without protocol/port
expect(server.checkOrigin('ws://secure.com:8080')).to.be.true; // Exact match with protocol and port
});
it('should handle origin with trailing slash', () => {
server.options.origins = ['http://localhost:3000'];
expect(server.checkOrigin('http://localhost:3000/')).to.be.false; // Should not match with trailing slash
});
it('should handle origin with path', () => {
server.options.origins = ['http://localhost:3000'];
expect(server.checkOrigin('http://localhost:3000/path')).to.be.false; // Should not match with path
});
it('should handle complex port matching scenarios', () => {
server.options.origins = ['localhost:3000'];
expect(server.checkOrigin('http://localhost:3000')).to.be.true;
expect(server.checkOrigin('https://localhost:3000')).to.be.true;
// ws:// protocol is not stripped, so it won't match
expect(server.checkOrigin('ws://localhost:3000')).to.be.false;
expect(server.checkOrigin('wss://localhost:3000')).to.be.false;
expect(server.checkOrigin('localhost:3000')).to.be.true;
});
it('should handle protocol-specific matching', () => {
server.options.origins = ['http://localhost:3000'];
expect(server.checkOrigin('http://localhost:3000')).to.be.true;
expect(server.checkOrigin('https://localhost:3000')).to.be.false; // Different protocol
expect(server.checkOrigin('ws://localhost:3000')).to.be.false; // Different protocol
});
});
});