mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-09-21 17:04:00 +00:00
- The registry is a Suite, created around the environment factory it runs its tests with, so the plugin's env.lua reads as: build environments, hand the test files a suite of them. The suite returned there becomes `...` in each test file, which is now said where it happens. - pass and fail are the primitives every other check goes through. eq and deep_eq assert rather than compare, failing with both values in the detail. - Stubs are extended through extend_requires, extend_script, extend_game and extend_defines, a recursive merge which raises when a value already exists, rather than by mutating the tables. The strict labels carry the factorio class names. - The stubs record registered metatables and can save and load the script data the way factorio does: functions are refused and only registered metatables survive. env.save_load() uses it, and a new on_load test shows role methods and held roles surviving the round trip, which only passes because the module registers its metatable. - The names helper moved out of the shared framework, it is role specific. - Tests are named after the function or method they cover, on both sides: the lua tests read as "role:assign applies locally and is sent", and the javascript files wrap their tests in the class they exercise with subtests per method. module.test.js is control.test.js, after the file it covers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
147 lines
7.0 KiB
JavaScript
147 lines
7.0 KiB
JavaScript
"use strict";
|
|
const t = require("tap");
|
|
const lib = require("@clusterio/lib");
|
|
const { InstancePlugin } = require("../dist/node/instance");
|
|
const messages = require("../dist/node/messages");
|
|
|
|
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);
|
|
|
|
const sampleRoles = () => [
|
|
new messages.RoleRecord(1, "Player", ["exp_scenario.gui.readme"], new messages.RoleMetaRecord(1, 2), true),
|
|
new messages.RoleRecord(5, "Moderator", ["exp_scenario.gui.readme", "core.admin"], new messages.RoleMetaRecord(5, 1)),
|
|
];
|
|
const sampleAssignments = () => [new messages.AssignmentRecord("alice", new Set([5]))];
|
|
|
|
/** Build a plugin around a fake instance, started on first use by each test. */
|
|
async function startPlugin(t2, { syncMode = "bidirectional", roles = sampleRoles() } = {}) {
|
|
const state = { sent: [], rcons: [], warnings: [], failNext: null };
|
|
const instance = {
|
|
logger,
|
|
config: { get: field => (field === "exp_roles.sync_mode" ? syncMode : undefined) },
|
|
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),
|
|
};
|
|
|
|
const plugin = new InstancePlugin({ name: "exp_roles" }, instance, {});
|
|
plugin.logger = { ...logger, warn: message => state.warnings.push(message) };
|
|
await plugin.init();
|
|
return { plugin, state };
|
|
}
|
|
|
|
/** The lua receiver and payload of a recorded rcon command. */
|
|
function decodeRcon(command) {
|
|
const match = command.match(/^\/sc exp_roles\.(\w+)\(helpers\.json_to_table\[=\[(.*)\]=\]\)$/s);
|
|
return { receiver: match[1], payload: JSON.parse(match[2]) };
|
|
}
|
|
|
|
t.test("class InstancePlugin", t2 => {
|
|
t2.test("onStart subscribes and initialises the lua module", async t2 => {
|
|
const { plugin, state } = await startPlugin(t2, { 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");
|
|
|
|
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"],
|
|
"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");
|
|
|
|
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");
|
|
});
|
|
|
|
t2.test("onStart with bidirectional emits changes back", async t2 => {
|
|
const { plugin, state } = await startPlugin(t2);
|
|
await plugin.onStart();
|
|
const emit = decodeRcon(state.rcons[state.rcons.length - 1]);
|
|
t2.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" });
|
|
await plugin.onStart();
|
|
t2.strictSame(state.sent.length, 0, "nothing is sent");
|
|
t2.strictSame(state.rcons.length, 0, "no commands are run");
|
|
});
|
|
|
|
t2.test("onStart warns when no default role is set", async t2 => {
|
|
const roles = sampleRoles().map(record => { record.isDefault = false; return record; });
|
|
const { plugin, state } = await startPlugin(t2, { roles });
|
|
await plugin.onStart();
|
|
t2.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);
|
|
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");
|
|
|
|
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
|
|
const assignments = decodeRcon(state.rcons[1]);
|
|
t2.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" });
|
|
await plugin.handleRoleUpdatedEvent(new messages.RoleUpdatedEvent(sampleRoles()));
|
|
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
|
|
t2.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);
|
|
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");
|
|
|
|
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");
|
|
});
|
|
|
|
t2.test("handleAssignmentUpdateIPC is gated unless bidirectional", async t2 => {
|
|
const { plugin, state } = await startPlugin(t2, { syncMode: "enabled" });
|
|
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
|
|
t2.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);
|
|
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");
|
|
});
|
|
t2.end();
|
|
});
|