signal-desktop/ts/test-electron/WebsocketResources_test.ts

250 lines
7.2 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2015 Signal Messenger, LLC
2021-06-09 22:28:54 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2021-06-09 22:28:54 +00:00
/* eslint-disable
no-new,
@typescript-eslint/no-empty-function,
@typescript-eslint/no-explicit-any
*/
import { assert } from 'chai';
import * as sinon from 'sinon';
import EventEmitter from 'events';
import type { connection as WebSocket } from 'websocket';
2021-07-02 19:21:24 +00:00
import Long from 'long';
2021-06-09 22:28:54 +00:00
2021-07-02 19:21:24 +00:00
import { dropNull } from '../util/dropNull';
import { SignalService as Proto } from '../protobuf';
2021-06-09 22:28:54 +00:00
import WebSocketResource from '../textsecure/WebsocketResources';
describe('WebSocket-Resource', () => {
class FakeSocket extends EventEmitter {
public sendBytes(_: Uint8Array) {}
public socket = {
localPort: 5678,
};
2021-06-09 22:28:54 +00:00
public close() {}
}
2021-07-02 19:21:24 +00:00
const NOW = Date.now();
beforeEach(function (this: Mocha.Context) {
2021-07-02 19:21:24 +00:00
this.sandbox = sinon.createSandbox();
this.clock = this.sandbox.useFakeTimers({
now: NOW,
});
this.sandbox
.stub(window.SignalContext.timers, 'setTimeout')
.callsFake(setTimeout);
this.sandbox
.stub(window.SignalContext.timers, 'clearTimeout')
.callsFake(clearTimeout);
2021-07-02 19:21:24 +00:00
});
afterEach(function (this: Mocha.Context) {
2021-07-02 19:21:24 +00:00
this.sandbox.restore();
});
2021-06-09 22:28:54 +00:00
describe('requests and responses', () => {
it('receives requests and sends responses', done => {
// mock socket
2021-07-02 19:21:24 +00:00
const requestId = new Long(0xdeadbeef, 0x7fffffff);
2021-06-09 22:28:54 +00:00
const socket = new FakeSocket();
sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => {
2021-07-02 19:21:24 +00:00
const message = Proto.WebSocketMessage.decode(data);
assert.strictEqual(message.type, Proto.WebSocketMessage.Type.RESPONSE);
2021-06-09 22:28:54 +00:00
assert.strictEqual(message.response?.message, 'OK');
assert.strictEqual(message.response?.status, 200);
2021-07-02 19:21:24 +00:00
const id = message.response?.id;
2021-11-08 21:43:37 +00:00
if (Long.isLong(id)) {
2021-07-02 19:21:24 +00:00
assert(id.equals(requestId));
} else {
assert(false, `id should be Long, got ${id}`);
}
2021-06-09 22:28:54 +00:00
done();
});
// actual test
new WebSocketResource(socket as WebSocket, {
2023-06-07 00:36:38 +00:00
name: 'test',
2021-06-09 22:28:54 +00:00
handleRequest(request: any) {
assert.strictEqual(request.verb, 'PUT');
assert.strictEqual(request.path, '/some/path');
2021-07-02 19:21:24 +00:00
assert.deepEqual(request.body, new Uint8Array([1, 2, 3]));
2021-06-09 22:28:54 +00:00
request.respond(200, 'OK');
},
});
// mock socket request
socket.emit('message', {
type: 'binary',
2021-07-02 19:21:24 +00:00
binaryData: Proto.WebSocketMessage.encode({
type: Proto.WebSocketMessage.Type.REQUEST,
request: {
id: requestId,
verb: 'PUT',
path: '/some/path',
body: new Uint8Array([1, 2, 3]),
},
}).finish(),
2021-06-09 22:28:54 +00:00
});
});
it('sends requests and receives responses', async () => {
2021-06-09 22:28:54 +00:00
// mock socket and request handler
2022-03-23 20:49:27 +00:00
let requestId: Long | undefined;
2021-06-09 22:28:54 +00:00
const socket = new FakeSocket();
sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => {
2021-07-02 19:21:24 +00:00
const message = Proto.WebSocketMessage.decode(data);
assert.strictEqual(message.type, Proto.WebSocketMessage.Type.REQUEST);
2021-06-09 22:28:54 +00:00
assert.strictEqual(message.request?.verb, 'PUT');
assert.strictEqual(message.request?.path, '/some/path');
2021-07-02 19:21:24 +00:00
assert.deepEqual(message.request?.body, new Uint8Array([1, 2, 3]));
requestId = dropNull(message.request?.id);
2021-06-09 22:28:54 +00:00
});
// actual test
2023-06-07 00:36:38 +00:00
const resource = new WebSocketResource(socket as WebSocket, {
name: 'test',
});
const promise = resource.sendRequest({
2021-06-09 22:28:54 +00:00
verb: 'PUT',
path: '/some/path',
2021-07-02 19:21:24 +00:00
body: new Uint8Array([1, 2, 3]),
2021-06-09 22:28:54 +00:00
});
// mock socket response
socket.emit('message', {
type: 'binary',
2021-07-02 19:21:24 +00:00
binaryData: Proto.WebSocketMessage.encode({
type: Proto.WebSocketMessage.Type.RESPONSE,
response: { id: requestId, message: 'OK', status: 200 },
}).finish(),
2021-06-09 22:28:54 +00:00
});
const { status, message } = await promise;
assert.strictEqual(message, 'OK');
assert.strictEqual(status, 200);
2021-06-09 22:28:54 +00:00
});
});
describe('close', () => {
it('closes the connection', done => {
const socket = new FakeSocket();
sinon.stub(socket, 'close').callsFake(() => done());
2023-06-07 00:36:38 +00:00
const resource = new WebSocketResource(socket as WebSocket, {
name: 'test',
});
2021-06-09 22:28:54 +00:00
resource.close();
});
it('force closes the connection', function (this: Mocha.Context, done) {
2021-07-02 19:21:24 +00:00
const socket = new FakeSocket();
2021-06-09 22:28:54 +00:00
2023-06-07 00:36:38 +00:00
const resource = new WebSocketResource(socket as WebSocket, {
name: 'test',
});
2021-07-02 19:21:24 +00:00
resource.close();
resource.addEventListener('close', () => done());
2021-06-09 22:28:54 +00:00
2021-07-02 19:21:24 +00:00
// Wait 5 seconds to forcefully close the connection
this.clock.next();
2021-06-09 22:28:54 +00:00
});
2021-07-02 19:21:24 +00:00
});
2021-06-09 22:28:54 +00:00
2021-07-02 19:21:24 +00:00
describe('with a keepalive config', () => {
it('sends keepalives once a minute', function (this: Mocha.Context, done) {
2021-06-09 22:28:54 +00:00
const socket = new FakeSocket();
sinon.stub(socket, 'sendBytes').callsFake(data => {
2021-07-02 19:21:24 +00:00
const message = Proto.WebSocketMessage.decode(data);
assert.strictEqual(message.type, Proto.WebSocketMessage.Type.REQUEST);
2021-06-09 22:28:54 +00:00
assert.strictEqual(message.request?.verb, 'GET');
assert.strictEqual(message.request?.path, '/v1/keepalive');
done();
});
new WebSocketResource(socket as WebSocket, {
2023-06-07 00:36:38 +00:00
name: 'test',
2021-06-09 22:28:54 +00:00
keepalive: { path: '/v1/keepalive' },
});
this.clock.next();
});
it('optionally disconnects if no response', function (this: Mocha.Context, done) {
2021-06-09 22:28:54 +00:00
const socket = new FakeSocket();
sinon.stub(socket, 'close').callsFake(() => done());
new WebSocketResource(socket as WebSocket, {
2023-06-07 00:36:38 +00:00
name: 'test',
keepalive: { path: '/' },
2021-06-09 22:28:54 +00:00
});
// One to trigger send
this.clock.next();
// Another to trigger send timeout
this.clock.next();
});
it('optionally disconnects if suspended', function (this: Mocha.Context, done) {
const socket = new FakeSocket();
sinon.stub(socket, 'close').callsFake(() => done());
new WebSocketResource(socket as WebSocket, {
2023-06-07 00:36:38 +00:00
name: 'test',
keepalive: { path: '/' },
});
// Just skip one hour immediately
this.clock.setSystemTime(NOW + 3600 * 1000);
this.clock.next();
});
it('allows resetting the keepalive timer', function (this: Mocha.Context, done) {
2021-06-09 22:28:54 +00:00
const startTime = Date.now();
const socket = new FakeSocket();
sinon.stub(socket, 'sendBytes').callsFake(data => {
2021-07-02 19:21:24 +00:00
const message = Proto.WebSocketMessage.decode(data);
assert.strictEqual(message.type, Proto.WebSocketMessage.Type.REQUEST);
2021-06-09 22:28:54 +00:00
assert.strictEqual(message.request?.verb, 'GET');
assert.strictEqual(message.request?.path, '/');
assert.strictEqual(
Date.now(),
startTime + 30000 + 5000,
'keepalive time should be 35s'
2021-06-09 22:28:54 +00:00
);
done();
});
const resource = new WebSocketResource(socket as WebSocket, {
2023-06-07 00:36:38 +00:00
name: 'test',
keepalive: { path: '/' },
2021-06-09 22:28:54 +00:00
});
setTimeout(() => {
resource.keepalive?.reset();
}, 5000);
// Trigger setTimeout above
this.clock.next();
// Trigger sendBytes
this.clock.next();
});
});
});