mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-16 07:24:53 +09:00
Refactor to a connector based concept in lib/link
Remove connection and client subclasses of Link and put the connection logic into a "connector" class. The connector deals with sending and receiving messages, handling the connection and reconnection of lost connections and giving sequence numbers to messages. This move the logic of the actual connection away from the link sub-classes, which in turn means the connection should be a lot easier to mock in tests.
This commit is contained in:
1 parent
a3eb39712c
commit
3bccbac496
12 files changed
+492
-494
No files matched your search
@@ -0,0 +1,108 @@
|
||||
const assert = require('assert').strict;
|
||||
const events = require('events');
|
||||
|
||||
const mock = require('../../mock');
|
||||
const link = require('lib/link');
|
||||
|
||||
|
||||
describe("lib/link/connectors", function() {
|
||||
describe("class SocketIOClientConnector", function() {
|
||||
let testConnector = new link.SocketIOClientConnector('source', 'url', 'token');
|
||||
testConnector._socket = new mock.MockSocket();
|
||||
describe(".disconnect()", function() {
|
||||
it("should call close on the socket", function() {
|
||||
testConnector._socket.closeCalled = false;
|
||||
testConnector.disconnect();
|
||||
assert(testConnector._socket.closeCalled, "Close was not called");
|
||||
});
|
||||
});
|
||||
|
||||
describe(".register()", function() {
|
||||
it("is abstract", function() {
|
||||
assert.throws(
|
||||
() => testConnector.register(),
|
||||
new Error("Abstract function")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".send()", function() {
|
||||
it("calls send on the socket", function() {
|
||||
testConnector._socket.sentMessages = [];
|
||||
let seq = testConnector.send('test', { test: true });
|
||||
assert.deepEqual(testConnector._socket.sentMessages, [{ seq, type: 'test', data: { test: true }}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".close()", function() {
|
||||
it("should send close and call disconnect", function() {
|
||||
testConnector._socket.sentMessages = [];
|
||||
let called = false;
|
||||
testConnector.disconnect = () => { called = true; }
|
||||
testConnector.close("test reason");
|
||||
assert.deepEqual(
|
||||
testConnector._socket.sentMessages,
|
||||
[{ seq: testConnector._seq - 1, type: 'close', data: { reason: "test reason" } }]
|
||||
)
|
||||
assert(called, ".disconnect() was not called");
|
||||
});
|
||||
});
|
||||
|
||||
describe(".processHandshake()", function() {
|
||||
it("should close on invalid message", function() {
|
||||
testConnector._socket.sentMessages = [];
|
||||
testConnector._processHandshake({ data: "invalid message" }),
|
||||
assert.deepEqual(
|
||||
testConnector._socket.sentMessages,
|
||||
[{seq: testConnector._seq - 1, type: 'close', data: {
|
||||
reason: "Invalid handshake" }
|
||||
}]
|
||||
);
|
||||
});
|
||||
it("should call register on hello", function() {
|
||||
let called = false;
|
||||
testConnector.register = () => { called = true; };
|
||||
testConnector._processHandshake({ seq: 1, type: 'hello', data: { version: "test" }}),
|
||||
assert(called, "register was not called");
|
||||
});
|
||||
it("should emit ready on ready", async function() {
|
||||
let result = events.once(testConnector, 'ready');
|
||||
testConnector._processHandshake({ seq: 1, type: 'ready', data: {}}),
|
||||
await result;
|
||||
});
|
||||
it("should emit error on close", async function() {
|
||||
let result = events.once(testConnector, 'ready');
|
||||
testConnector._processHandshake({ seq: 1, type: 'close', data: { reason: "test" }}),
|
||||
await assert.rejects(result, new Error("server closed during handshake: test"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("._attachSocketHandlers()", function() {
|
||||
it("should attach handlers", function() {
|
||||
testConnector._attachSocketHandlers();
|
||||
assert(testConnector._socket.events.size > 0, "No handlers were attached");
|
||||
});
|
||||
it("should throw on message received in invalid state", function() {
|
||||
testConnector._state = "new";
|
||||
assert.throws(
|
||||
() => testConnector._socket.events.get('message')(),
|
||||
new Error("Received message in unexpected state new")
|
||||
);
|
||||
});
|
||||
it("should call _processHandshake on message in handshake state", function() {
|
||||
testConnector._state = "handshake";
|
||||
let called = false;
|
||||
testConnector._processHandshake = () => { called = true; };
|
||||
testConnector._socket.events.get('message')();
|
||||
assert(called, "_processHandshake was not called");
|
||||
});
|
||||
it("should emit message on message in ready state", async function() {
|
||||
testConnector._socket.sentMessages = [];
|
||||
testConnector._state = "ready";
|
||||
let result = events.once(testConnector, 'message');
|
||||
testConnector._socket.events.get('message')("message");
|
||||
assert.deepEqual(await result, ["message"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+2
-179
@@ -4,35 +4,12 @@ const events = require('events');
|
||||
const link = require("lib/link");
|
||||
const errors = require("lib/errors");
|
||||
const schema = require("lib/schema");
|
||||
const mock = require("../../mock");
|
||||
|
||||
|
||||
class MockSocket {
|
||||
constructor() {
|
||||
this.sentMessages = [];
|
||||
this.events = new Map();
|
||||
this.handshake = { address: "socket.test" };
|
||||
}
|
||||
|
||||
send(message) {
|
||||
this.sentMessages.push(message);
|
||||
}
|
||||
|
||||
on(event, fn) {
|
||||
this.events.set(event, fn);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.disconnectCalled = true;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.closeCalled = true;
|
||||
}
|
||||
};
|
||||
|
||||
describe("lib/link/link", function() {
|
||||
describe("class Link", function() {
|
||||
let testLink = new link.Link('source', 'target', new MockSocket);
|
||||
let testLink = new link.Link('source', 'target', new mock.MockConnector());
|
||||
testLink.setValidator('simple', schema.compile({
|
||||
properties: {
|
||||
"data": {
|
||||
@@ -121,23 +98,6 @@ describe("lib/link/link", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe(".disconnect()", function() {
|
||||
it("is abstract", function() {
|
||||
assert.throws(
|
||||
() => testLink.disconnect(),
|
||||
new Error("Abstract function")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".send()", function() {
|
||||
it("calls send on the socket", function() {
|
||||
let seq = testLink.send('test', { test: true });
|
||||
assert.deepEqual(testLink.socket.sentMessages, [{ seq, type: 'test', data: { test: true }}]);
|
||||
testLink.socket.sentMessages = [];
|
||||
});
|
||||
});
|
||||
|
||||
describe("._processWaiters()", function() {
|
||||
it("should call waiters", async function() {
|
||||
let waiter = new Promise(resolve => {
|
||||
@@ -196,142 +156,5 @@ describe("lib/link/link", function() {
|
||||
assert.deepEqual(await result2, { type: 'waiter', seq: 5, data: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe(".close()", function() {
|
||||
it("should send close and call disconnect", function() {
|
||||
let called = false;
|
||||
testLink.disconnect = () => { called = true; }
|
||||
testLink.close("test reason");
|
||||
assert.deepEqual(
|
||||
testLink.socket.sentMessages,
|
||||
[{ seq: testLink._seq - 1, type: 'close', data: { reason: "test reason" } }]
|
||||
)
|
||||
assert(called, ".disconnect() was not called");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("class Connection", function() {
|
||||
let testConnection = new link.Connection('target', new MockSocket());
|
||||
it("should send ready after constructor", function() {
|
||||
assert.deepEqual(
|
||||
testConnection.socket.sentMessages,
|
||||
[{seq: 2, type: 'ready', data: {}}]
|
||||
);
|
||||
testConnection.socket.sentMessages = [];
|
||||
});
|
||||
it("should disconnect when receiving a close message", function() {
|
||||
testConnection.socket.disconnectCalled = false;
|
||||
testConnection.socket.events.get('message')({
|
||||
seq: 1, type: 'close', data: { reason: "test close" }
|
||||
});
|
||||
assert(testConnection.socket.disconnectCalled, "Disconnect was not called");
|
||||
});
|
||||
it("should close when receiving an invalid message", function() {
|
||||
testConnection.socket.events.get('message')({ invalid: true });
|
||||
assert.deepEqual(
|
||||
testConnection.socket.sentMessages,
|
||||
[{seq: testConnection._seq - 1, type: 'close', data: { reason: "Invalid message: Malformed message" }}]
|
||||
);
|
||||
testConnection.socket.sentMessages = [];
|
||||
});
|
||||
|
||||
describe(".disconnect()", function() {
|
||||
it("should call disconnect on the socket", function() {
|
||||
testConnection.socket.disconnectCalled = false;
|
||||
testConnection.disconnect();
|
||||
assert(testConnection.socket.disconnectCalled, "Disconnect was not called");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("class Client", function() {
|
||||
let testClient = new link.Client('source', 'url', 'token');
|
||||
testClient.socket = new MockSocket();
|
||||
describe(".disconnect()", function() {
|
||||
it("should call close on the socket", function() {
|
||||
testClient.socket.closeCalled = false;
|
||||
testClient.disconnect();
|
||||
assert(testClient.socket.closeCalled, "Close was not called");
|
||||
});
|
||||
});
|
||||
|
||||
describe(".register()", function() {
|
||||
it("is abstract", function() {
|
||||
assert.throws(
|
||||
() => testClient.register(),
|
||||
new Error("Abstract function")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".processHandshake()", function() {
|
||||
it("should close on invalid message", function() {
|
||||
testClient._processHandshake({ data: "invalid message" }),
|
||||
assert.deepEqual(
|
||||
testClient.socket.sentMessages,
|
||||
[{seq: testClient._seq - 1, type: 'close', data: {
|
||||
reason: "Invalid handshake" }
|
||||
}]
|
||||
);
|
||||
testClient.socket.sentMessages = [];
|
||||
});
|
||||
it("should call register on hello", function() {
|
||||
let called = false;
|
||||
testClient.register = () => { called = true; };
|
||||
testClient._processHandshake({ seq: 1, type: 'hello', data: { version: "test" }}),
|
||||
assert(called, "register was not called");
|
||||
});
|
||||
it("should emit ready on ready", async function() {
|
||||
let result = events.once(testClient._events, 'ready');
|
||||
testClient._processHandshake({ seq: 1, type: 'ready', data: {}}),
|
||||
await result;
|
||||
});
|
||||
it("should emit error on close", async function() {
|
||||
let result = events.once(testClient._events, 'ready');
|
||||
testClient._processHandshake({ seq: 1, type: 'close', data: { reason: "test" }}),
|
||||
await assert.rejects(result, new Error("server closed during handshake: test"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("._attachSocketHandlers()", function() {
|
||||
it("should attach handlers", function() {
|
||||
testClient._attachSocketHandlers();
|
||||
assert(testClient.socket.events.size > 0, "No handlers were attached");
|
||||
});
|
||||
it("should throw on message received in invalid state", function() {
|
||||
testClient._state = "new";
|
||||
assert.throws(
|
||||
() => testClient.socket.events.get('message')(),
|
||||
new Error("Received message in unexpected state new")
|
||||
);
|
||||
});
|
||||
it("should call _processHandshake on message in handshake state", function() {
|
||||
testClient._state = "handshake";
|
||||
let called = false;
|
||||
testClient._processHandshake = () => { called = true; };
|
||||
testClient.socket.events.get('message')();
|
||||
assert(called, "_processHandshake was not called");
|
||||
});
|
||||
it("should call processMessage on message in ready state", function() {
|
||||
testClient._state = "ready";
|
||||
let called = false;
|
||||
testClient.processMessage = () => { called = true; };
|
||||
testClient.socket.events.get('message')();
|
||||
assert(called, "processMessage was not called");
|
||||
delete testClient.processMessage;
|
||||
});
|
||||
it("should close on invalid message in ready state", function() {
|
||||
testClient._state = "ready";
|
||||
testClient.socket.events.get('message')({ data: "invalid message"});
|
||||
assert.deepEqual(
|
||||
testClient.socket.sentMessages,
|
||||
[{seq: testClient._seq - 1, type: 'close', data: {
|
||||
reason: "Invalid message: Malformed message" }
|
||||
}]
|
||||
);
|
||||
testClient.socket.sentMessages = [];
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+19
-13
@@ -2,11 +2,12 @@ const assert = require('assert').strict;
|
||||
|
||||
const link = require('lib/link');
|
||||
const errors = require('lib/errors');
|
||||
const mock = require('../../mock');
|
||||
|
||||
|
||||
describe("lib/link/messages", function() {
|
||||
let testSourceLink = new link.Link('source', 'target', null);
|
||||
let testTargetLink = new link.Link('target', 'source', null);
|
||||
let testSourceLink = new link.Link('source', 'target', new mock.MockConnector());
|
||||
let testTargetLink = new link.Link('target', 'source', new mock.MockConnector());
|
||||
|
||||
let lastTargetSent;
|
||||
testTargetLink.send = (type, data) => { lastTargetSent = { type, data }; };
|
||||
@@ -39,7 +40,7 @@ describe("lib/link/messages", function() {
|
||||
handlerResult = { seq: 3 };
|
||||
testTargetLink._handlers.get('test_request')({ seq: 2 });
|
||||
let result = await new Promise(resolve => {
|
||||
testTargetLink.send = (type, data) => resolve({ type, data });
|
||||
testTargetLink.connector.send = (type, data) => resolve({ type, data });
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
type: 'test_response',
|
||||
@@ -53,7 +54,7 @@ describe("lib/link/messages", function() {
|
||||
handlerResult = { test: "handler" };
|
||||
testTargetLink._handlers.get('test_request')({ seq: 2 });
|
||||
let result = await new Promise(resolve => {
|
||||
testTargetLink.send = (type, data) => resolve({ type, data });
|
||||
testTargetLink.connector.send = (type, data) => resolve({ type, data });
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
type: 'test_response',
|
||||
@@ -67,7 +68,7 @@ describe("lib/link/messages", function() {
|
||||
handlerResult = undefined;
|
||||
testTargetLink._handlers.get('test_request')({ seq: 2 });
|
||||
let result = await new Promise(resolve => {
|
||||
testTargetLink.send = (type, data) => resolve({ type, data });
|
||||
testTargetLink.connector.send = (type, data) => resolve({ type, data });
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
type: 'test_response',
|
||||
@@ -79,11 +80,11 @@ describe("lib/link/messages", function() {
|
||||
});
|
||||
|
||||
describe(".send()", function() {
|
||||
let request;
|
||||
testSourceLink.send = (type, data) => {
|
||||
request = { type, data };
|
||||
};
|
||||
it("should send request with send and use waitFor to get response", async function() {
|
||||
let request;
|
||||
testSourceLink.connector.send = (type, data) => {
|
||||
request = { type, data };
|
||||
};
|
||||
testSourceLink.waitFor = (type, condition) => {
|
||||
return { data: { type, request }};
|
||||
};
|
||||
@@ -91,6 +92,8 @@ describe("lib/link/messages", function() {
|
||||
await testRequest.send(testSourceLink, { test: "request" }),
|
||||
{ type: 'test_response', request: { type: 'test_request', data: {test: "request" }}}
|
||||
);
|
||||
delete testSourceLink.connector.send;
|
||||
delete testSourceLink.waitFor;
|
||||
});
|
||||
it("should throw error response", async function() {
|
||||
testSourceLink.waitFor = (type, condition) => {
|
||||
@@ -100,6 +103,7 @@ describe("lib/link/messages", function() {
|
||||
testRequest.send(testSourceLink),
|
||||
new errors.RequestError("test error")
|
||||
);
|
||||
delete testSourceLink.waitFor;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -134,11 +138,13 @@ describe("lib/link/messages", function() {
|
||||
|
||||
describe(".send()", function() {
|
||||
it("should send the event over the link", async function() {
|
||||
let sentData = [];
|
||||
testSourceLink.send = (type, data) => { sentData.push({ type, data }); };
|
||||
|
||||
testSourceLink.connector.sentMessages = [];
|
||||
testEvent.send(testSourceLink, { test: "event" });
|
||||
assert.deepEqual(sentData, [{ type: "test_event", data: { test: "event" }}]);
|
||||
let seq = testSourceLink.connector._seq - 1;
|
||||
assert.deepEqual(
|
||||
testSourceLink.connector.sentMessages,
|
||||
[{ seq, type: "test_event", data: { test: "event" }}]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ const request = require('supertest');
|
||||
const validateHTML = require('html5-validator');
|
||||
const parallel = require('mocha.parallel');
|
||||
|
||||
const mock = require('./mock');
|
||||
const master = require('../master');
|
||||
|
||||
|
||||
@@ -43,4 +44,15 @@ describe('Master testing', function() {
|
||||
it(`sends some HTML when accessing ${path}`, () => getValidate(path));
|
||||
}
|
||||
});
|
||||
|
||||
describe("class SocketIOServerConnector", function() {
|
||||
let testConnector = new master._SocketIOServerConnector(new mock.MockSocket());
|
||||
describe(".disconnect()", function() {
|
||||
it("should call disconnect on the socket", function() {
|
||||
testConnector._socket.disconnectCalled = false;
|
||||
testConnector.disconnect();
|
||||
assert(testConnector._socket.disconnectCalled, "Disconnect was not called");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
const events = require("events");
|
||||
|
||||
|
||||
class MockSocket {
|
||||
constructor() {
|
||||
this.sentMessages = [];
|
||||
this.events = new Map();
|
||||
this.handshake = { address: "socket.test" };
|
||||
}
|
||||
|
||||
send(message) {
|
||||
this.sentMessages.push(message);
|
||||
}
|
||||
|
||||
on(event, fn) {
|
||||
this.events.set(event, fn);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.disconnectCalled = true;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.closeCalled = true;
|
||||
}
|
||||
};
|
||||
|
||||
class MockConnector extends events.EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._seq = 1;
|
||||
this.sentMessages = [];
|
||||
this.events = new Map();
|
||||
this.handshake = { address: "socket.test" };
|
||||
}
|
||||
|
||||
send(type, data) {
|
||||
this.sentMessages.push({ seq: this._seq, type, data });
|
||||
return this._seq++;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.disconnectCalled = true;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
MockSocket,
|
||||
MockConnector,
|
||||
};
|
||||
Reference in new issue
Block a user