mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Drop the special case that deferred terminating the connection issuing the delete, so deleting yourself behaves the same as revoke-token and every session of the deleted user is closed before the handler returns. Set tokenValidAfter to the current time in createUser instead of 0. Deleting a user and recreating it under the same name left tokens issued before the deletion valid, as the check is iat < tokenValidAfter. Doing it in createUser covers every path that makes a user. A token signed in the same second as creation is still accepted, so create-admin and the other production paths sign after the user exists and are unaffected. The integration harness signed the control token at import time, before bootstrap create-admin created the user. Sign it in the root before hook instead and reuse it in link.js. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
146 lines
4.6 KiB
JavaScript
146 lines
4.6 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import events from "node:events";
|
|
import * as lib from "@clusterio/lib";
|
|
import { ControlConnection, ControllerHooks } from "@clusterio/controller";
|
|
import { MockConnector, MockController, MockLogger } from "../mock.js";
|
|
|
|
const addr = lib.Address.fromShorthand;
|
|
|
|
describe("controller/src/ControlConnection", function() {
|
|
describe(".handleControllerRestartRequest()", function() {
|
|
let mockController;
|
|
|
|
beforeEach(function() {
|
|
mockController = {
|
|
canRestart: true,
|
|
shouldRestart: false,
|
|
stopped: false,
|
|
async checkRestartDowngrade() { return null; },
|
|
stop() { this.stopped = true; },
|
|
};
|
|
});
|
|
|
|
async function restart() {
|
|
await ControlConnection.prototype.handleControllerRestartRequest.call({
|
|
_controller: mockController,
|
|
});
|
|
}
|
|
|
|
it("restarts when the installed version is not older", async function() {
|
|
await restart();
|
|
assert.equal(mockController.shouldRestart, true);
|
|
assert.equal(mockController.stopped, true);
|
|
});
|
|
|
|
it("rejects a downgrade without stopping the controller", async function() {
|
|
mockController.checkRestartDowngrade = async () => ({
|
|
installedVersion: "1.0.0",
|
|
runningVersion: "2.0.0",
|
|
});
|
|
|
|
await assert.rejects(restart, /Stop the controller before starting the older version manually/);
|
|
assert.equal(mockController.shouldRestart, false);
|
|
assert.equal(mockController.stopped, false);
|
|
});
|
|
});
|
|
|
|
describe("close cleanup", function() {
|
|
let connector;
|
|
let mockController;
|
|
let connection;
|
|
|
|
beforeEach(function() {
|
|
connector = new MockConnector(addr("controller"), addr({ controlId: 1 }));
|
|
connector._socket = {};
|
|
const transports = new Set();
|
|
mockController = {
|
|
hooks: new ControllerHooks(new MockLogger()),
|
|
router: null,
|
|
_registeredRequests: new Map(),
|
|
_fallbackedRequests: new Map(),
|
|
_registeredEvents: new Map(),
|
|
_snoopedEvents: new Map(),
|
|
subscriptions: { unsubscribeLink() {} },
|
|
clusterLogger: {
|
|
transports,
|
|
add(transport) { transports.add(transport); },
|
|
remove(transport) { transports.delete(transport); },
|
|
},
|
|
debugEvents: new events.EventEmitter(),
|
|
sendRequestToHostByInstanceId() {},
|
|
};
|
|
connection = new ControlConnection({ version: "test" }, connector, mockController, {}, 1);
|
|
});
|
|
|
|
it("removes the log transport from clusterLogger", async function() {
|
|
await connection.handleLogSetSubscriptionsRequest(
|
|
new lib.LogSetSubscriptionsRequest(true, false, [], [])
|
|
);
|
|
assert.equal(mockController.clusterLogger.transports.size, 1);
|
|
connector.emit("close");
|
|
assert.equal(mockController.clusterLogger.transports.size, 0);
|
|
assert.equal(connection.logTransport, null);
|
|
});
|
|
|
|
it("removes the debug dump listener after repeated requests", async function() {
|
|
await connection.handleDebugDumpWsRequest(new lib.DebugDumpWsRequest());
|
|
await connection.handleDebugDumpWsRequest(new lib.DebugDumpWsRequest());
|
|
assert.equal(mockController.debugEvents.listenerCount("message"), 1);
|
|
assert.equal(connector._socket.clusterio_ignore_dump, true);
|
|
connector.emit("close");
|
|
assert.equal(mockController.debugEvents.listenerCount("message"), 0);
|
|
});
|
|
});
|
|
|
|
describe(".handleUserDeleteRequest()", function() {
|
|
let mockController;
|
|
let nextId;
|
|
|
|
beforeEach(function() {
|
|
mockController = new MockController();
|
|
mockController.sendTo = () => {};
|
|
mockController.wsServer.controlConnections = new Map();
|
|
nextId = 1;
|
|
});
|
|
|
|
function connect(name) {
|
|
const connection = {
|
|
_controller: mockController,
|
|
user: mockController.users.getByName(name),
|
|
connector: {
|
|
terminated: false,
|
|
terminate() { this.terminated = true; },
|
|
},
|
|
};
|
|
mockController.wsServer.controlConnections.set(nextId, connection);
|
|
nextId += 1;
|
|
return connection;
|
|
}
|
|
|
|
async function deleteUser(connection, name) {
|
|
await ControlConnection.prototype.handleUserDeleteRequest.call(
|
|
connection, new lib.UserDeleteRequest(name)
|
|
);
|
|
}
|
|
|
|
it("terminates connections of the deleted user", async function() {
|
|
const admin = connect("test");
|
|
const player1 = connect("player");
|
|
const player2 = connect("player");
|
|
|
|
await deleteUser(admin, "player");
|
|
assert.equal(mockController.users.getByName("player"), undefined);
|
|
assert.equal(player1.connector.terminated, true);
|
|
assert.equal(player2.connector.terminated, true);
|
|
assert.equal(admin.connector.terminated, false);
|
|
});
|
|
|
|
it("terminates its own connection when deleting itself", async function() {
|
|
const player = connect("player");
|
|
|
|
await deleteUser(player, "player");
|
|
assert.equal(player.connector.terminated, true);
|
|
});
|
|
});
|
|
});
|