mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-09-22 09:14:01 +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>
199 lines
8.6 KiB
JavaScript
199 lines
8.6 KiB
JavaScript
"use strict";
|
|
const t = require("tap");
|
|
const lib = require("@clusterio/lib");
|
|
const { ControllerPlugin } = require("../dist/node/controller");
|
|
const messages = require("../dist/node/messages");
|
|
const { seedRoles } = require("../dist/node/seed");
|
|
|
|
// Importing this defines the exp_scenario permissions the seed grants
|
|
require("@expcluster/scenario/dist/node/permissions");
|
|
|
|
const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} };
|
|
|
|
class FakeUser {
|
|
constructor(name, roleIds = []) {
|
|
this.name = name;
|
|
this.roleIds = new Set(roleIds);
|
|
this.playerStats = { onlineTimeMs: 0 };
|
|
this.updatedAtMs = 0;
|
|
this.isDeleted = false;
|
|
}
|
|
|
|
set(field, value) {
|
|
this[field] = value;
|
|
this.updatedAtMs += 1;
|
|
}
|
|
}
|
|
|
|
/** Build a plugin around fake controller internals, started on first use by each test. */
|
|
async function startPlugin(t2, { roles = [], users = [], config = {} } = {}) {
|
|
const state = { broadcasts: [], permissionUpdates: [] };
|
|
const configValues = {
|
|
"controller.database_directory": t2.testdir(),
|
|
"controller.default_role_id": lib.Role.DefaultPlayerRoleId,
|
|
...config,
|
|
};
|
|
|
|
const roleStore = new lib.SubscribableDatastore(
|
|
...await new lib.MemoryDatastoreProvider(new Map(roles.map(role => [role.id, role]))).bootstrap()
|
|
);
|
|
const userStore = new Map(users.map(user => [user.name, user]));
|
|
|
|
const controller = {
|
|
config: { get: key => configValues[key] },
|
|
roles: roleStore,
|
|
users: {
|
|
records: { on: () => {}, values: () => userStore.values() },
|
|
getByNameMutable: name => userStore.get(name),
|
|
valuesMutable: () => userStore.values(),
|
|
getOrCreateUser: name => {
|
|
if (!userStore.has(name)) {
|
|
userStore.set(name, new FakeUser(name));
|
|
}
|
|
return userStore.get(name);
|
|
},
|
|
},
|
|
subscriptions: { handle: () => {}, broadcast: event => state.broadcasts.push(event) },
|
|
handle: () => {},
|
|
userPermissionsUpdated: user => state.permissionUpdates.push(user.name),
|
|
};
|
|
|
|
const plugin = new ControllerPlugin({ name: "exp_roles" }, controller, undefined, logger);
|
|
await plugin.init();
|
|
return { plugin, controller, state, users: userStore };
|
|
}
|
|
|
|
const role = (id, name, permissions = []) => new lib.Role(id, name, "", new Set(permissions));
|
|
|
|
t.test("class ControllerPlugin", t2 => {
|
|
t2.test("init and sweepRoleMeta manage the role properties", async t2 => {
|
|
const { plugin } = await startPlugin(t2, { roles: [role(0, "Cluster Admin"), role(5, "Moderator")] });
|
|
|
|
t2.strictSame(plugin.roleMeta.get(0).order, 1, "the first role is ordered first");
|
|
t2.strictSame(plugin.roleMeta.get(5).order, 2, "the next role is ordered after it");
|
|
|
|
plugin.roleMeta.set(new messages.RoleMetaRecord(99, 3));
|
|
plugin.sweepRoleMeta();
|
|
t2.notOk(plugin.roleMeta.get(99), "properties without a role are removed");
|
|
t2.ok(plugin.roleMeta.get(5), "properties with a role are kept");
|
|
});
|
|
|
|
t2.test("buildRoleRecord combines the role with its properties", async t2 => {
|
|
const { plugin } = await startPlugin(t2, {
|
|
roles: [role(1, "Player"), role(5, "Moderator", ["exp_scenario.command.kill"])],
|
|
});
|
|
plugin.roleMeta.set(new messages.RoleMetaRecord(5, 2, 1, "Mod"));
|
|
|
|
const record = plugin.buildRoleRecord(plugin.controller.roles.get(5));
|
|
t2.strictSame(record.name, "Moderator", "the name comes from the role");
|
|
t2.strictSame(record.permissions, ["exp_scenario.command.kill"], "the permissions come from the role");
|
|
t2.strictSame(record.meta.shortHand, "Mod", "the properties come from the datastore");
|
|
t2.notOk(record.isDefault, "not the default role");
|
|
t2.ok(plugin.buildRoleRecord(plugin.controller.roles.get(1)).isDefault, "the default role is marked");
|
|
});
|
|
|
|
t2.test("rolesUpdated and roleMetaUpdated broadcast to subscribers", async t2 => {
|
|
const { plugin, controller, state } = await startPlugin(t2, { roles: [role(1, "Player")] });
|
|
|
|
state.broadcasts.length = 0;
|
|
controller.roles.set(role(5, "Moderator"));
|
|
const updated = state.broadcasts.flatMap(event => event.updates.map(update => update.name));
|
|
t2.ok(updated.includes("Moderator"), "a new role is broadcast");
|
|
|
|
state.broadcasts.length = 0;
|
|
plugin.roleMeta.set(new messages.RoleMetaRecord(5, 2, 0, "Mod"));
|
|
t2.ok(state.broadcasts.some(
|
|
event => event instanceof messages.RoleUpdatedEvent && event.updates[0].meta.shortHand === "Mod"
|
|
), "a property change is broadcast as its role");
|
|
|
|
state.broadcasts.length = 0;
|
|
await plugin.onControllerConfigFieldChanged("controller.default_role_id");
|
|
t2.ok(state.broadcasts.some(event => event instanceof messages.RoleUpdatedEvent), "a default role change rebroadcasts");
|
|
});
|
|
|
|
t2.test("handleRoleSubscription replays only newer records", async t2 => {
|
|
const { plugin, controller } = await startPlugin(t2, { roles: [role(1, "Player")] });
|
|
controller.roles.set(role(5, "Moderator"));
|
|
const updatedAtMs = plugin.buildRoleRecord(controller.roles.get(5)).updatedAtMs;
|
|
|
|
const all = await plugin.handleRoleSubscription({ lastRequestTimeMs: 0 });
|
|
t2.strictSame(all.updates.length, 2, "everything is replayed from the start");
|
|
const none = await plugin.handleRoleSubscription({ lastRequestTimeMs: updatedAtMs });
|
|
t2.strictSame(none, null, "nothing is replayed when up to date");
|
|
});
|
|
|
|
t2.test("listAssignmentRecords mirrors users without the default role", async t2 => {
|
|
const alice = new FakeUser("alice", [lib.Role.DefaultPlayerRoleId, 5]);
|
|
alice.updatedAtMs = 10;
|
|
const bob = new FakeUser("bob", [lib.Role.DefaultPlayerRoleId]);
|
|
const { plugin } = await startPlugin(t2, { roles: [role(1, "Player"), role(5, "Moderator")], users: [alice, bob] });
|
|
|
|
const records = plugin.listAssignmentRecords();
|
|
t2.strictSame(records.length, 1, "users with only the default role are left out");
|
|
t2.strictSame(records[0].name, "alice");
|
|
t2.strictSame([...records[0].roleIds], [5], "the default role is not listed");
|
|
});
|
|
|
|
t2.test("handleAssignmentUpdateRequest validates the user and roles", async t2 => {
|
|
const alice = new FakeUser("alice", [5]);
|
|
const { plugin, state } = await startPlugin(t2, {
|
|
roles: [role(1, "Player"), role(5, "Moderator"), role(6, "Regular")], users: [alice],
|
|
});
|
|
|
|
await t2.rejects(
|
|
plugin.handleAssignmentUpdateRequest(new messages.AssignmentUpdateRequest("nobody", [], [])),
|
|
{ message: /does not exist/ }, "an unknown user is refused",
|
|
);
|
|
await t2.rejects(
|
|
plugin.handleAssignmentUpdateRequest(new messages.AssignmentUpdateRequest("alice", [99], [])),
|
|
{ message: /does not exist/ }, "an unknown role is refused",
|
|
);
|
|
|
|
await plugin.handleAssignmentUpdateRequest(new messages.AssignmentUpdateRequest("alice", [6], [5]));
|
|
t2.strictSame([...alice.roleIds], [6], "roles are added and removed");
|
|
t2.ok(state.permissionUpdates.includes("alice"), "permission changes are pushed to the user");
|
|
});
|
|
|
|
t2.test("applyAutoAssign and onPlayerEvent grant roles from online time", async t2 => {
|
|
const newcomer = new FakeUser("newcomer");
|
|
newcomer.playerStats.onlineTimeMs = 3600000;
|
|
const veteran = new FakeUser("veteran");
|
|
veteran.playerStats.onlineTimeMs = 7200000;
|
|
const jailed = new FakeUser("jailed", [7]);
|
|
jailed.playerStats.onlineTimeMs = 7200000;
|
|
|
|
const { plugin } = await startPlugin(t2, {
|
|
roles: [role(1, "Player"), role(6, "Regular"), role(7, "Jail")],
|
|
users: [newcomer, veteran, jailed],
|
|
});
|
|
// The blocking role is marked first, since setting properties applies auto assignment
|
|
plugin.roleMeta.set(new messages.RoleMetaRecord(7, 3, 1, "", "", null, null, true));
|
|
plugin.roleMeta.set(new messages.RoleMetaRecord(6, 2, 0, "", "", null, 7200000));
|
|
t2.ok(veteran.roleIds.has(6), "enough online time grants the role");
|
|
t2.notOk(newcomer.roleIds.has(6), "not enough online time does not");
|
|
t2.notOk(jailed.roleIds.has(6), "a blocking role prevents the grant");
|
|
|
|
veteran.roleIds.delete(6);
|
|
await plugin.onPlayerEvent(undefined, { type: "leave", name: "veteran" });
|
|
t2.ok(veteran.roleIds.has(6), "granted again when the player leaves");
|
|
});
|
|
|
|
t2.test("handleSeedRolesRequest creates the roles and reuses them by name", async t2 => {
|
|
const { plugin, controller } = await startPlugin(t2, {
|
|
roles: [role(0, "Cluster Admin", ["core.admin"]), role(1, "Player")],
|
|
});
|
|
|
|
await plugin.handleSeedRolesRequest();
|
|
t2.strictSame(controller.roles.size, seedRoles.length, "every seed role exists");
|
|
|
|
const moderator = [...controller.roles.values()].find(other => other.name === "Moderator");
|
|
t2.ok(moderator.permissions.has("exp_scenario.command.jail"), "parent permissions are flattened in");
|
|
t2.ok(plugin.roleMeta.get(moderator.id), "the role properties are created");
|
|
t2.strictSame(plugin.roleMeta.get(moderator.id).shortHand, "Mod", "the properties match the seed");
|
|
|
|
await plugin.handleSeedRolesRequest();
|
|
t2.strictSame(controller.roles.size, seedRoles.length, "seeding again reuses the roles");
|
|
});
|
|
t2.end();
|
|
});
|