mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-09-21 17:04:00 +00:00
Address review on the suite and instance tests
- The instance tests run against a real Instance with a real InstanceConfig, matching the controller tests: the spies are sendTo and the server, which record what leaves the instance, so sendRcon passes through the real script command gate. The plugin's config fields are registered so the sync mode is a real field. - Shallow eq is gone and eq compares recursively; the suite is created with Framework.suite(extend_env), which hands the extension a fresh set of stubs per test, so the plugin's env.lua is only the extension. - add_player assigns continuous indexes itself. - The lua fixtures are referenced by role name: assignments take names, and ids in expectations come from the role object. The name lookup in env.R uses a fixture index rather than searching the roles. - Test names format functions as name() and methods as .name(). - Branch coverage filled in: sort tie breaking, the highest role assertion when a player has no roles, has all for the server, the by player default from game.player, deleted assignment records on initialise, set_emit_events with no argument, printing to a role with no holders, role property update validation, the assignment subscription replay, unassignment over the ipc, and a refused removal having nothing to roll back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e6552a39be
commit
3218351db6
+103
-64
@@ -1,14 +1,32 @@
|
||||
"use strict";
|
||||
const t = require("tap");
|
||||
const lib = require("@clusterio/lib");
|
||||
const { Instance } = require("@clusterio/host");
|
||||
const { InstancePlugin } = require("../dist/node/instance");
|
||||
const { plugin: pluginDeclaration } = require("../dist/node/index");
|
||||
const messages = require("../dist/node/messages");
|
||||
|
||||
// The instance validates message classes against the link registry, and the
|
||||
// plugin's config fields must be defined before an InstanceConfig can set them
|
||||
lib.Link.register(messages.RoleUpdatedEvent);
|
||||
lib.Link.register(messages.AssignmentUpdatedEvent);
|
||||
lib.Link.register(messages.RoleListRequest);
|
||||
lib.Link.register(messages.AssignmentListRequest);
|
||||
lib.Link.register(messages.AssignmentUpdateRequest);
|
||||
lib.addPluginConfigFields([pluginDeclaration]);
|
||||
|
||||
const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} };
|
||||
|
||||
// Subscribing validates event names against the link registry
|
||||
lib.Link.register(messages.RoleUpdatedEvent);
|
||||
lib.Link.register(messages.AssignmentUpdatedEvent);
|
||||
class TestConnector extends lib.BaseConnector {
|
||||
constructor() {
|
||||
super(lib.Address.fromShorthand({ instanceId: 1 }), lib.Address.fromShorthand({ hostId: 1 }));
|
||||
this.valid = true;
|
||||
this.connected = true;
|
||||
this.hasSession = true;
|
||||
}
|
||||
|
||||
send() {}
|
||||
}
|
||||
|
||||
const sampleRoles = () => [
|
||||
new messages.RoleRecord(1, "Player", ["exp_scenario.gui.readme"], new messages.RoleMetaRecord(1, 2), true),
|
||||
@@ -16,30 +34,40 @@ const sampleRoles = () => [
|
||||
];
|
||||
const sampleAssignments = () => [new messages.AssignmentRecord("alice", new Set([5]))];
|
||||
|
||||
/** Build a plugin around a fake instance, started on first use by each test. */
|
||||
/** Build a plugin around a real instance with spies on what leaves it, started on first use by each test. */
|
||||
async function startPlugin(t2, { syncMode = "bidirectional", roles = sampleRoles() } = {}) {
|
||||
const instanceConfig = new lib.InstanceConfig("host");
|
||||
instanceConfig.set("instance.id", 1);
|
||||
instanceConfig.set("instance.name", "test");
|
||||
instanceConfig.set("exp_roles.sync_mode", syncMode);
|
||||
|
||||
const instance = new Instance(
|
||||
{ assignGamePort: () => 1 }, new TestConnector(), t2.testdir(), "factorioDir", instanceConfig
|
||||
);
|
||||
|
||||
// Spies which record the messages and commands leaving the instance
|
||||
const state = { sent: [], rcons: [], warnings: [], failNext: null };
|
||||
const instance = {
|
||||
logger,
|
||||
config: { get: field => (field === "exp_roles.sync_mode" ? syncMode : undefined) },
|
||||
instance.server = {
|
||||
handle: () => {},
|
||||
server: { handle: () => {} },
|
||||
sendTo: async (dst, request) => {
|
||||
state.sent.push(request);
|
||||
if (state.failNext) {
|
||||
const error = state.failNext;
|
||||
state.failNext = null;
|
||||
throw error;
|
||||
}
|
||||
if (request instanceof messages.RoleListRequest) {
|
||||
return roles;
|
||||
}
|
||||
if (request instanceof messages.AssignmentListRequest) {
|
||||
return sampleAssignments();
|
||||
}
|
||||
return undefined;
|
||||
sendRcon: async command => {
|
||||
state.rcons.push(command);
|
||||
return "";
|
||||
},
|
||||
sendRcon: async command => state.rcons.push(command),
|
||||
};
|
||||
instance.sendTo = async (dst, request) => {
|
||||
state.sent.push(request);
|
||||
if (state.failNext) {
|
||||
const error = state.failNext;
|
||||
state.failNext = null;
|
||||
throw error;
|
||||
}
|
||||
if (request instanceof messages.RoleListRequest) {
|
||||
return roles;
|
||||
}
|
||||
if (request instanceof messages.AssignmentListRequest) {
|
||||
return sampleAssignments();
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const plugin = new InstancePlugin({ name: "exp_roles" }, instance, {});
|
||||
@@ -55,92 +83,103 @@ function decodeRcon(command) {
|
||||
}
|
||||
|
||||
t.test("class InstancePlugin", t2 => {
|
||||
t2.test("onStart subscribes and initialises the lua module", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2, { syncMode: "enabled" });
|
||||
t2.test(".onStart() subscribes and initialises the lua module", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3, { syncMode: "enabled" });
|
||||
await plugin.onStart();
|
||||
|
||||
const subscribed = state.sent.filter(request => request instanceof lib.SubscriptionRequest);
|
||||
t2.strictSame(subscribed.length, 2, "roles and assignments are subscribed to");
|
||||
t2.ok(state.sent.some(request => request instanceof messages.RoleListRequest), "the roles are requested");
|
||||
t2.ok(state.sent.some(request => request instanceof messages.AssignmentListRequest), "the assignments are requested");
|
||||
t3.strictSame(subscribed.length, 2, "roles and assignments are subscribed to");
|
||||
t3.ok(state.sent.some(request => request instanceof messages.RoleListRequest), "the roles are requested");
|
||||
t3.ok(
|
||||
state.sent.some(request => request instanceof messages.AssignmentListRequest),
|
||||
"the assignments are requested",
|
||||
);
|
||||
|
||||
const initialise = decodeRcon(state.rcons[0]);
|
||||
t2.strictSame(initialise.receiver, "initialise", "the lua module is initialised");
|
||||
t2.strictSame(initialise.payload.permission_names, ["exp_scenario.gui.readme", "core.admin"],
|
||||
t3.strictSame(initialise.receiver, "initialise", "the lua module is initialised");
|
||||
t3.strictSame(initialise.payload.permission_names, ["exp_scenario.gui.readme", "core.admin"],
|
||||
"each permission name is sent once");
|
||||
t2.strictSame(initialise.payload.roles[1].permissions, [0, 1], "roles reference permissions by index");
|
||||
t2.strictSame(initialise.payload.assignments, [{ name: "alice", role_ids: [5] }], "the assignments are sent");
|
||||
t3.strictSame(initialise.payload.roles[1].permissions, [0, 1], "roles reference permissions by index");
|
||||
t3.strictSame(initialise.payload.assignments, [{ name: "alice", role_ids: [5] }], "the assignments are sent");
|
||||
|
||||
const emit = decodeRcon(state.rcons[1]);
|
||||
t2.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "enabled does not emit changes back");
|
||||
t2.strictSame(state.warnings.length, 0, "no warnings with a default role");
|
||||
t3.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "enabled does not emit changes back");
|
||||
t3.strictSame(state.warnings.length, 0, "no warnings with a default role");
|
||||
});
|
||||
|
||||
t2.test("onStart with bidirectional emits changes back", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2);
|
||||
t2.test(".onStart() with bidirectional emits changes back", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3);
|
||||
await plugin.onStart();
|
||||
const emit = decodeRcon(state.rcons[state.rcons.length - 1]);
|
||||
t2.strictSame([emit.receiver, emit.payload], ["set_emit_events", true]);
|
||||
t3.strictSame([emit.receiver, emit.payload], ["set_emit_events", true]);
|
||||
});
|
||||
|
||||
t2.test("onStart with disabled does nothing", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2, { syncMode: "disabled" });
|
||||
t2.test(".onStart() with disabled does nothing", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3, { syncMode: "disabled" });
|
||||
await plugin.onStart();
|
||||
t2.strictSame(state.sent.length, 0, "nothing is sent");
|
||||
t2.strictSame(state.rcons.length, 0, "no commands are run");
|
||||
t3.strictSame(state.sent.length, 0, "nothing is sent");
|
||||
t3.strictSame(state.rcons.length, 0, "no commands are run");
|
||||
});
|
||||
|
||||
t2.test("onStart warns when no default role is set", async t2 => {
|
||||
t2.test(".onStart() warns when no default role is set", async t3 => {
|
||||
const roles = sampleRoles().map(record => { record.isDefault = false; return record; });
|
||||
const { plugin, state } = await startPlugin(t2, { roles });
|
||||
const { plugin, state } = await startPlugin(t3, { roles });
|
||||
await plugin.onStart();
|
||||
t2.ok(state.warnings.some(message => /default role/.test(message)), "the warning names the default role");
|
||||
t3.ok(state.warnings.some(message => /default role/.test(message)), "the warning names the default role");
|
||||
});
|
||||
|
||||
t2.test("handleRoleUpdatedEvent and handleAssignmentUpdatedEvent forward to lua", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2);
|
||||
t2.test(".handleRoleUpdatedEvent() and .handleAssignmentUpdatedEvent() forward to lua", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3);
|
||||
await plugin.handleRoleUpdatedEvent(new messages.RoleUpdatedEvent(sampleRoles()));
|
||||
const roles = decodeRcon(state.rcons[0]);
|
||||
t2.strictSame(roles.receiver, "receive_role_updates", "role updates are forwarded");
|
||||
t2.strictSame(roles.payload[0].name, "Player", "the records are sent in plain form");
|
||||
t3.strictSame(roles.receiver, "receive_role_updates", "role updates are forwarded");
|
||||
t3.strictSame(roles.payload[0].name, "Player", "the records are sent in plain form");
|
||||
|
||||
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
|
||||
const assignments = decodeRcon(state.rcons[1]);
|
||||
t2.strictSame(assignments.receiver, "receive_assignment_updates", "assignment updates are forwarded");
|
||||
t3.strictSame(assignments.receiver, "receive_assignment_updates", "assignment updates are forwarded");
|
||||
});
|
||||
|
||||
t2.test("handleRoleUpdatedEvent and handleAssignmentUpdatedEvent are gated while disabled", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2, { syncMode: "disabled" });
|
||||
t2.test(".handleRoleUpdatedEvent() and .handleAssignmentUpdatedEvent() are gated while disabled", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3, { syncMode: "disabled" });
|
||||
await plugin.handleRoleUpdatedEvent(new messages.RoleUpdatedEvent(sampleRoles()));
|
||||
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
|
||||
t2.strictSame(state.rcons.length, 0, "no commands are run");
|
||||
t3.strictSame(state.rcons.length, 0, "no commands are run");
|
||||
});
|
||||
|
||||
t2.test("handleAssignmentUpdateIPC sends the change and rolls back a refusal", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2);
|
||||
t2.test(".handleAssignmentUpdateIPC() sends the change and rolls back a refusal", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3);
|
||||
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
|
||||
const request = state.sent[0];
|
||||
t2.ok(request instanceof messages.AssignmentUpdateRequest, "the change is sent as a request");
|
||||
t2.strictSame([request.name, request.assign, request.unassign], ["alice", [5], []], "the request names the change");
|
||||
t3.ok(request instanceof messages.AssignmentUpdateRequest, "the change is sent as a request");
|
||||
t3.strictSame([request.name, request.assign, request.unassign], ["alice", [5], []], "the request names the change");
|
||||
|
||||
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: undefined, unassign: [5] });
|
||||
t3.strictSame(state.sent[1].unassign, [5], "removals are sent the same way");
|
||||
|
||||
state.failNext = new Error("User 'alice' does not exist");
|
||||
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
|
||||
const rollback = decodeRcon(state.rcons[0]);
|
||||
t2.strictSame(rollback.receiver, "reject_assignment", "the refusal is rolled back in lua");
|
||||
t2.strictSame(rollback.payload, { name: "alice", role_ids: [5] }, "the rollback names the refused roles");
|
||||
t3.strictSame(rollback.receiver, "reject_assignment", "the refusal is rolled back in lua");
|
||||
t3.strictSame(rollback.payload, { name: "alice", role_ids: [5] }, "the rollback names the refused roles");
|
||||
|
||||
state.failNext = new Error("User 'alice' does not exist");
|
||||
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: undefined, unassign: [5] });
|
||||
t3.strictSame(state.rcons.length, 1, "a refused removal has nothing to roll back");
|
||||
});
|
||||
|
||||
t2.test("handleAssignmentUpdateIPC is gated unless bidirectional", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2, { syncMode: "enabled" });
|
||||
t2.test(".handleAssignmentUpdateIPC() is gated unless bidirectional", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3, { syncMode: "enabled" });
|
||||
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
|
||||
t2.strictSame(state.sent.length, 0, "nothing is sent");
|
||||
t3.strictSame(state.sent.length, 0, "nothing is sent");
|
||||
});
|
||||
|
||||
t2.test("onInstanceConfigFieldChanged toggles emit with the sync mode", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2);
|
||||
t2.test(".onInstanceConfigFieldChanged() toggles emit with the sync mode", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3);
|
||||
await plugin.onInstanceConfigFieldChanged("exp_roles.sync_mode", "enabled", "bidirectional");
|
||||
const emit = decodeRcon(state.rcons[0]);
|
||||
t2.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "leaving bidirectional stops emitting");
|
||||
t3.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "leaving bidirectional stops emitting");
|
||||
});
|
||||
|
||||
t2.end();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user