mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-09-21 17:04:00 +00:00
Address review on the test framework
- 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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
24d36db601
commit
7adcdde9dc
@@ -6,6 +6,9 @@ const { reportLuaTests } = require("../../test/lua/runner");
|
||||
const envFile = path.join(__dirname, "lua", "env.lua");
|
||||
|
||||
// Each file runs in its own lua state, and each test in a fresh environment
|
||||
for (const file of ["lookup.lua", "players.lua", "assignment.lua", "sync.lua", "holders.lua"]) {
|
||||
t.test(file, subtest => reportLuaTests(subtest, envFile, path.join(__dirname, "lua", file)));
|
||||
}
|
||||
t.test("module/control.lua", t2 => {
|
||||
for (const file of ["lookup.lua", "players.lua", "assignment.lua", "sync.lua", "holders.lua"]) {
|
||||
t2.test(`lua/${file}`, subtest => reportLuaTests(subtest, envFile, path.join(__dirname, "lua", file)));
|
||||
}
|
||||
t2.end();
|
||||
});
|
||||
+121
-118
@@ -65,131 +65,134 @@ async function startPlugin(t2, { roles = [], users = [], config = {} } = {}) {
|
||||
|
||||
const role = (id, name, permissions = []) => new lib.Role(id, name, "", new Set(permissions));
|
||||
|
||||
t.test("init creates properties for existing roles and sweeps orphans", async t2 => {
|
||||
const { plugin } = await startPlugin(t2, { roles: [role(0, "Cluster Admin"), role(5, "Moderator")] });
|
||||
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");
|
||||
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");
|
||||
});
|
||||
|
||||
t.test("role records combine 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");
|
||||
});
|
||||
|
||||
t.test("role changes 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");
|
||||
});
|
||||
|
||||
t.test("subscriptions replay 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");
|
||||
});
|
||||
|
||||
t.test("assignments mirror 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");
|
||||
});
|
||||
|
||||
t.test("assignment updates validate 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],
|
||||
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");
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
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"));
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
t.test("roles are granted 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");
|
||||
});
|
||||
|
||||
t.test("seeding 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")],
|
||||
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");
|
||||
});
|
||||
|
||||
await plugin.handleSeedRolesRequest();
|
||||
t2.strictSame(controller.roles.size, seedRoles.length, "every seed role exists");
|
||||
t2.test("rolesUpdated and roleMetaUpdated broadcast to subscribers", async t2 => {
|
||||
const { plugin, controller, state } = await startPlugin(t2, { roles: [role(1, "Player")] });
|
||||
|
||||
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");
|
||||
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");
|
||||
|
||||
await plugin.handleSeedRolesRequest();
|
||||
t2.strictSame(controller.roles.size, seedRoles.length, "seeding again reuses the roles");
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -54,90 +54,93 @@ function decodeRcon(command) {
|
||||
return { receiver: match[1], payload: JSON.parse(match[2]) };
|
||||
}
|
||||
|
||||
t.test("start subscribes and initialises the lua module", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2, { syncMode: "enabled" });
|
||||
await plugin.onStart();
|
||||
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 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 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");
|
||||
});
|
||||
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");
|
||||
});
|
||||
|
||||
t.test("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 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]);
|
||||
});
|
||||
|
||||
t.test("disabled does nothing on start", 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 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");
|
||||
});
|
||||
|
||||
t.test("a missing default role is warned about", 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("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");
|
||||
});
|
||||
|
||||
t.test("updates from the controller are forwarded 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");
|
||||
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");
|
||||
});
|
||||
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
|
||||
const assignments = decodeRcon(state.rcons[1]);
|
||||
t2.strictSame(assignments.receiver, "receive_assignment_updates", "assignment updates are forwarded");
|
||||
});
|
||||
|
||||
t.test("updates are not forwarded 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("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");
|
||||
});
|
||||
|
||||
t.test("in game changes are sent up and rolled back when refused", 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");
|
||||
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");
|
||||
});
|
||||
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");
|
||||
});
|
||||
|
||||
t.test("in game changes are dropped 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("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");
|
||||
});
|
||||
|
||||
t.test("changing the sync mode toggles emit", 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.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();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
--- Assignment through role methods, confirmation, and rejection
|
||||
local Env = ...
|
||||
local Test = Env.Test
|
||||
local test, check, eq = Test.test, Test.check, Test.eq
|
||||
--- Tests for the assignment methods of module/control.lua
|
||||
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
|
||||
local test, check, eq = Suite.test, Suite.check, Suite.eq
|
||||
|
||||
--- alice is a moderator and bob a regular, with changes sent to the controller
|
||||
local function setup(env)
|
||||
@@ -10,38 +9,41 @@ local function setup(env)
|
||||
bob = env.add_player("bob", 2),
|
||||
}
|
||||
env.initialise{
|
||||
Env.assignment("alice", { 5 }),
|
||||
Env.assignment("bob", { 6 }),
|
||||
env.assignment("alice", { 5 }),
|
||||
env.assignment("bob", { 6 }),
|
||||
}
|
||||
env.Roles.set_emit_events(true)
|
||||
env.reset_log()
|
||||
return players
|
||||
end
|
||||
|
||||
test("assign applies locally and is sent to the controller", function(env)
|
||||
test("role:assign applies locally and is sent to the controller", function(env)
|
||||
local players = setup(env)
|
||||
env.R("Moderator"):assign(players.bob, { by_player_name = "alice" })
|
||||
check(#env.sent == 1 and env.sent[1].channel == "exp_roles:assignment_update", "the assignment is sent")
|
||||
check(eq(env.sent[1].data.assign, { 5 }) and env.sent[1].data.name == "bob", "the payload names the role and player")
|
||||
check(env.sent[1].data.name == "bob", "the payload names the player")
|
||||
eq(env.sent[1].data.assign, { 5 }, "the payload names the role")
|
||||
check(env.R("Moderator"):has_player(players.bob), "the role applies before confirmation")
|
||||
check(#env.events == 1 and eq(env.events[1].data.assigned, { 5 }), "the event carries the assigned role id")
|
||||
check(#env.events == 1, "one event is raised")
|
||||
eq(env.events[1].data.assigned, { 5 }, "the event carries the assigned role id")
|
||||
check(env.events[1].data.by_player_index == 1, "the event carries who made the change")
|
||||
check(#env.printed == 1 and env.printed[1][1] == "exp-roles.game-message-assign", "the change is announced")
|
||||
check(#env.sounds == 1 and env.sounds[1] == "bob:utility/achievement_unlocked", "the assign sound plays")
|
||||
eq(env.sounds, { "bob:utility/achievement_unlocked" }, "the assign sound plays")
|
||||
|
||||
local sd = env.Roles._script_data()
|
||||
check(eq(sd.local_players.bob, { 5 }) and eq(sd.pending.bob, { 5 }), "the role is held locally and pending")
|
||||
eq(sd.local_players.bob, { 5 }, "the role is held locally")
|
||||
eq(sd.pending.bob, { 5 }, "the role is pending")
|
||||
|
||||
env.reset_log()
|
||||
env.R("Moderator"):assign(players.bob)
|
||||
check(#env.sent == 0 and #env.events == 0, "assigning a held role is a no-op")
|
||||
end)
|
||||
|
||||
test("a confirmation releases the local hold", function(env)
|
||||
test("receive_assignment_updates releases the local hold once confirmed", function(env)
|
||||
local players = setup(env)
|
||||
env.R("Moderator"):assign(players.bob)
|
||||
env.reset_log()
|
||||
env.Roles.receive_assignment_updates{ Env.assignment("bob", { 6, 5 }) }
|
||||
env.Roles.receive_assignment_updates{ env.assignment("bob", { 6, 5 }) }
|
||||
check(#env.events == 0 and #env.printed == 0, "a confirmation raises nothing")
|
||||
|
||||
local sd = env.Roles._script_data()
|
||||
@@ -49,17 +51,19 @@ test("a confirmation releases the local hold", function(env)
|
||||
check(env.R("Moderator"):has_player(players.bob), "the role is still held after confirmation")
|
||||
end)
|
||||
|
||||
test("unassign a synced role", function(env)
|
||||
test("role:unassign removes a synced role", function(env)
|
||||
local players = setup(env)
|
||||
env.R("Regular"):unassign(players.bob, { by_player_name = "alice", silent = true })
|
||||
check(#env.sent == 1 and eq(env.sent[1].data.unassign, { 6 }), "the unassignment is sent")
|
||||
check(#env.sent == 1, "the unassignment is sent")
|
||||
eq(env.sent[1].data.unassign, { 6 }, "the payload names the role")
|
||||
check(not env.R("Regular"):has_player(players.bob), "the role is removed locally")
|
||||
check(#env.events == 1 and eq(env.events[1].data.unassigned, { 6 }), "the event carries the unassigned role id")
|
||||
check(#env.events == 1, "one event is raised")
|
||||
eq(env.events[1].data.unassigned, { 6 }, "the event carries the unassigned role id")
|
||||
check(#env.printed == 0, "silent suppresses the announcement")
|
||||
check(#env.sounds == 1 and env.sounds[1] == "bob:utility/game_lost", "the unassign sound plays")
|
||||
eq(env.sounds, { "bob:utility/game_lost" }, "the unassign sound plays")
|
||||
end)
|
||||
|
||||
test("a rejection rolls the assignment back", function(env)
|
||||
test("reject_assignment rolls the assignment back", function(env)
|
||||
local players = setup(env)
|
||||
env.R("Jail"):assign(players.alice)
|
||||
check(env.R("Jail"):has_player(players.alice), "the role applies before rejection")
|
||||
@@ -68,52 +72,54 @@ test("a rejection rolls the assignment back", function(env)
|
||||
env.Roles.reject_assignment{ name = "alice", role_ids = { 7 } }
|
||||
check(not env.R("Jail"):has_player(players.alice), "the rejected role is rolled back")
|
||||
check(#env.sent == 0, "the rollback is not sent to the controller")
|
||||
check(#env.events == 1 and eq(env.events[1].data.unassigned, { 7 }), "the rollback raises the event")
|
||||
check(#env.events == 1, "the rollback raises the event")
|
||||
eq(env.events[1].data.unassigned, { 7 }, "the event carries the refused role id")
|
||||
|
||||
local sd = env.Roles._script_data()
|
||||
check(sd.local_players.alice == nil and sd.pending.alice == nil, "the rollback clears the local state")
|
||||
end)
|
||||
|
||||
test("local only roles never reach the controller", function(env)
|
||||
test("role:assign with local_only never reaches the controller", function(env)
|
||||
local players = setup(env)
|
||||
env.R("Regular"):assign(players.alice, { silent = true, local_only = true })
|
||||
check(#env.sent == 0, "a local only role is not sent")
|
||||
check(env.R("Regular"):has_player(players.alice), "a local only role applies")
|
||||
check(#env.sent == 0, "the assignment is not sent")
|
||||
check(env.R("Regular"):has_player(players.alice), "the role applies")
|
||||
|
||||
local sd = env.Roles._script_data()
|
||||
check(sd.pending.alice == nil and eq(sd.local_players.alice, { 6 }), "a local only role is not pending")
|
||||
check(sd.pending.alice == nil, "the role is not pending")
|
||||
eq(sd.local_players.alice, { 6 }, "the role is held locally")
|
||||
|
||||
env.Roles.receive_assignment_updates{ Env.assignment("alice", { 5 }) }
|
||||
check(env.R("Regular"):has_player(players.alice), "a local only role survives controller updates")
|
||||
env.Roles.receive_assignment_updates{ env.assignment("alice", { 5 }) }
|
||||
check(env.R("Regular"):has_player(players.alice), "the role survives controller updates")
|
||||
|
||||
env.reset_log()
|
||||
env.R("Regular"):unassign(players.alice, { local_only = true })
|
||||
check(not env.R("Regular"):has_player(players.alice) and #env.sent == 0, "removed without sync")
|
||||
end)
|
||||
|
||||
test("jail suppresses every other role", function(env)
|
||||
test("role:assign of a higher priority role suppresses the rest", function(env)
|
||||
local players = setup(env)
|
||||
env.R("Jail"):assign(players.alice, { by_player_name = "<server>", silent = true })
|
||||
check(eq(Test.names(env.Roles.get_player_roles(players.alice)), { "Jail" }), "only the jail role applies")
|
||||
eq(env.names(env.Roles.get_player_roles(players.alice)), { "Jail" }, "only the jail role applies")
|
||||
check(env.R("Moderator"):has_player(players.alice), "a suppressed role is still held")
|
||||
check(not env.Roles.player_has_permission(players.alice, "exp_scenario.command.kill"), "permissions are lost")
|
||||
check(not env.Roles.player_has_permission(players.alice, "exp_scenario.gui.readme"), "the default role is lost")
|
||||
check(not env.Roles.player_outranks(players.alice, players.bob), "a jailed player no longer outranks")
|
||||
check(eq(env.events[1].data.assigned, { 7 }) and #env.events[1].data.unassigned == 0,
|
||||
"the event lists only the jail role")
|
||||
eq(env.events[1].data.assigned, { 7 }, "the event lists the jail role")
|
||||
check(#env.events[1].data.unassigned == 0, "the suppressed roles are not listed as unassigned")
|
||||
|
||||
env.reset_log()
|
||||
env.R("Jail"):unassign(players.alice, { by_player_name = "<server>", silent = true })
|
||||
check(eq(Test.sorted(Test.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" }),
|
||||
eq(Suite.sorted(env.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" },
|
||||
"unjail restores the roles")
|
||||
check(#env.events[1].data.assigned == 0 and eq(env.events[1].data.unassigned, { 7 }),
|
||||
"the unjail event lists only the jail role")
|
||||
check(#env.events[1].data.assigned == 0, "the restored roles are not listed as assigned")
|
||||
eq(env.events[1].data.unassigned, { 7 }, "the unjail event lists the jail role")
|
||||
end)
|
||||
|
||||
test("the server can not be assigned roles", function(env)
|
||||
test("role:assign ignores the server", function(env)
|
||||
setup(env)
|
||||
env.R("Moderator"):assign(env.server)
|
||||
check(#env.sent == 0 and #env.events == 0, "assigning to the server is a no-op")
|
||||
end)
|
||||
|
||||
return Env.finish()
|
||||
return Suite.run()
|
||||
|
||||
+33
-26
@@ -1,10 +1,11 @@
|
||||
--[[-- Test environment for module/control.lua
|
||||
Composes the shared stubs and framework from the repository test folder with
|
||||
the fixtures for this plugin. Each call to Env.new() is an independent
|
||||
environment with a fresh copy of the roles module.
|
||||
The module specific extension between the shared stubs and the test files: it
|
||||
creates environments which combine the stubs with a fresh copy of the roles
|
||||
module and the fixtures below, and hands each test file a suite built around
|
||||
that factory.
|
||||
|
||||
Run as a chunk by test/lua/runner.js, receiving the shared folder and
|
||||
returning this table, which is in turn passed to each test chunk.
|
||||
Run as a chunk by test/lua/runner.js, receiving the shared folder. The suite
|
||||
returned here becomes `...` in each test file.
|
||||
]]
|
||||
|
||||
local shared_root = ... --- @type string
|
||||
@@ -14,13 +15,8 @@ local plugin_root = assert(source:match("^@(.*)/test/lua/env%.lua$"))
|
||||
local Stubs = assert(loadfile(shared_root .. "/stubs.lua"))()
|
||||
local Framework = assert(loadfile(shared_root .. "/framework.lua"))()
|
||||
|
||||
local Env = {
|
||||
--- Test registry for the file being run, see framework.lua
|
||||
Test = Framework.new(),
|
||||
}
|
||||
|
||||
--- Standard fixture: permission names sent once and referenced by zero based index
|
||||
Env.permission_names = {
|
||||
local permission_names = {
|
||||
"core.admin", -- 0
|
||||
"exp_scenario.command.kill", -- 1
|
||||
"exp_scenario.command.jail", -- 2
|
||||
@@ -36,7 +32,7 @@ local function meta(order, extra)
|
||||
end
|
||||
|
||||
--- Standard fixture: the roles as the controller would send them on initialise
|
||||
function Env.role_records()
|
||||
local function role_records()
|
||||
return {
|
||||
{ id = 0, name = "Cluster Admin", permissions = { 0 }, meta = meta(1, { short_hand = "SYS" }) },
|
||||
{ id = 5, name = "Moderator", permissions = { 1, 2, 3, 5 }, meta = meta(2, { color = { r = 0, g = 170, b = 0 } }) },
|
||||
@@ -47,12 +43,8 @@ function Env.role_records()
|
||||
}
|
||||
end
|
||||
|
||||
function Env.assignment(name, role_ids)
|
||||
return { name = name, role_ids = role_ids }
|
||||
end
|
||||
|
||||
--- Create an independent environment with a fresh copy of the roles module
|
||||
function Env.new()
|
||||
local function make_env()
|
||||
local env = Stubs.new()
|
||||
|
||||
env.Roles = assert(loadfile(plugin_root .. "/module/control.lua"))()
|
||||
@@ -63,22 +55,37 @@ function Env.new()
|
||||
return (assert(env.Roles.get_role_by_name(name), "No role named " .. name))
|
||||
end
|
||||
|
||||
--- Names of an array of roles
|
||||
function env.names(roles)
|
||||
local rtn = {}
|
||||
for index, role in ipairs(roles) do
|
||||
rtn[index] = role.name
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
--- An assignment record as the controller would send it
|
||||
function env.assignment(name, role_ids)
|
||||
return { name = name, role_ids = role_ids }
|
||||
end
|
||||
|
||||
--- Load the standard fixture and the given assignments
|
||||
function env.initialise(assignments)
|
||||
env.Roles.initialise{
|
||||
permission_names = Env.permission_names,
|
||||
roles = Env.role_records(),
|
||||
permission_names = permission_names,
|
||||
roles = role_records(),
|
||||
assignments = assignments or {},
|
||||
}
|
||||
end
|
||||
|
||||
--- Save and load the map, as far as the roles module can tell
|
||||
local stub_save_load = env.save_load
|
||||
function env.save_load()
|
||||
stub_save_load()
|
||||
env.Roles.on_load()
|
||||
end
|
||||
|
||||
return env
|
||||
end
|
||||
|
||||
--- Run the declared tests, each against a fresh environment
|
||||
function Env.finish()
|
||||
Env.Test.run(Env.new)
|
||||
return Env.Test.results_json()
|
||||
end
|
||||
|
||||
return Env
|
||||
return Framework.new_suite(make_env)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
--- Listing and printing to the players who hold a role
|
||||
local Env = ...
|
||||
local Test = Env.Test
|
||||
local test, check, eq = Test.test, Test.check, Test.eq
|
||||
--- Tests for listing and printing to the players who hold a role
|
||||
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
|
||||
local test, check, eq = Suite.test, Suite.check, Suite.eq
|
||||
|
||||
--- alice and dave are moderators with dave offline, zed has never joined
|
||||
local function setup(env)
|
||||
@@ -11,47 +10,47 @@ local function setup(env)
|
||||
dave = env.add_player("dave", 4, false),
|
||||
}
|
||||
env.initialise{
|
||||
Env.assignment("alice", { 5 }),
|
||||
Env.assignment("bob", { 6 }),
|
||||
Env.assignment("dave", { 5 }),
|
||||
Env.assignment("zed", { 5 }),
|
||||
env.assignment("alice", { 5 }),
|
||||
env.assignment("bob", { 6 }),
|
||||
env.assignment("dave", { 5 }),
|
||||
env.assignment("zed", { 5 }),
|
||||
}
|
||||
env.Roles.set_emit_events(true)
|
||||
return players
|
||||
end
|
||||
|
||||
test("player names include players not on this map", function(env)
|
||||
test("role:get_player_names includes players not on this map", function(env)
|
||||
setup(env)
|
||||
check(eq(Test.sorted(env.R("Moderator"):get_player_names()), { "alice", "dave", "zed" }),
|
||||
eq(Suite.sorted(env.R("Moderator"):get_player_names()), { "alice", "dave", "zed" },
|
||||
"every player given the role is listed")
|
||||
end)
|
||||
|
||||
test("players are limited to this map and can be filtered", function(env)
|
||||
setup(env)
|
||||
local mod = env.R("Moderator")
|
||||
check(eq(Test.sorted(Test.names(mod:get_players())), { "alice", "dave" }), "players are limited to this map")
|
||||
check(eq(Test.names(mod:get_players(true)), { "alice" }), "filtered to connected players")
|
||||
check(eq(Test.names(mod:get_players(false)), { "dave" }), "filtered to offline players")
|
||||
end)
|
||||
|
||||
test("a player holding the role in both lists is counted once", function(env)
|
||||
test("role:get_player_names counts a player in both lists once", function(env)
|
||||
local players = setup(env)
|
||||
env.R("Regular"):assign(players.alice, { silent = true, local_only = true })
|
||||
env.Roles.receive_assignment_updates{ Env.assignment("alice", { 5, 6 }) }
|
||||
env.Roles.receive_assignment_updates{ env.assignment("alice", { 5, 6 }) }
|
||||
|
||||
local sd = env.Roles._script_data()
|
||||
check(sd.local_players.alice ~= nil and sd.synced_players.alice ~= nil, "the role is held in both lists")
|
||||
check(eq(Test.sorted(env.R("Regular"):get_player_names()), { "alice", "bob" }), "the player is counted once")
|
||||
eq(Suite.sorted(env.R("Regular"):get_player_names()), { "alice", "bob" }, "the player is counted once")
|
||||
end)
|
||||
|
||||
test("print reaches online holders", function(env)
|
||||
test("role:get_players is limited to this map and can be filtered", function(env)
|
||||
setup(env)
|
||||
local mod = env.R("Moderator")
|
||||
eq(Suite.sorted(env.names(mod:get_players())), { "alice", "dave" }, "players are limited to this map")
|
||||
eq(env.names(mod:get_players(true)), { "alice" }, "filtered to connected players")
|
||||
eq(env.names(mod:get_players(false)), { "dave" }, "filtered to offline players")
|
||||
end)
|
||||
|
||||
test("role:print reaches online holders", function(env)
|
||||
setup(env)
|
||||
env.reset_log()
|
||||
check(env.R("Moderator"):print("hello") == 1, "print returns the number of players reached")
|
||||
check(#env.printed == 1 and env.printed[1].to == "alice", "only online holders are reached")
|
||||
end)
|
||||
|
||||
test("printing to the higher roles", function(env)
|
||||
test("role:print over get_higher_roles can repeat", function(env)
|
||||
local players = setup(env)
|
||||
env.R("Regular"):assign(players.alice, { silent = true, local_only = true })
|
||||
env.reset_log()
|
||||
@@ -62,15 +61,7 @@ test("printing to the higher roles", function(env)
|
||||
local got = {}
|
||||
for _, entry in ipairs(env.printed) do got[#got + 1] = entry.to end
|
||||
-- alice holds two of the roles, so like the legacy system the message can repeat
|
||||
check(eq(Test.sorted(got), { "alice", "alice", "bob" }), "the online holders of each role are reached")
|
||||
eq(Suite.sorted(got), { "alice", "alice", "bob" }, "the online holders of each role are reached")
|
||||
end)
|
||||
|
||||
test("deep equality helper", function(env)
|
||||
check(Test.deep_eq({ a = { 1, 2 }, b = "x" }, { a = { 1, 2 }, b = "x" }), "equal nested tables")
|
||||
check(not Test.deep_eq({ a = { 1, 2 } }, { a = { 1, 3 } }), "differing nested values")
|
||||
check(not Test.deep_eq({ a = 1 }, { a = 1, b = 2 }), "extra keys on the right")
|
||||
check(not Test.deep_eq({ a = 1, b = 2 }, { a = 1 }), "extra keys on the left")
|
||||
check(Test.deep_eq(env.Roles._script_data().pending, {}), "works against module state")
|
||||
end)
|
||||
|
||||
return Env.finish()
|
||||
return Suite.run()
|
||||
|
||||
@@ -1,28 +1,58 @@
|
||||
--- Role lookup, decoding, and comparisons
|
||||
local Env = ...
|
||||
local Test = Env.Test
|
||||
local test, check, eq = Test.test, Test.check, Test.eq
|
||||
--- Tests for the role lookup functions of module/control.lua
|
||||
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
|
||||
local test, check, eq = Suite.test, Suite.check, Suite.eq
|
||||
|
||||
test("roles are ordered most privileged first", function(env)
|
||||
test("get_role returns the role with the given clusterio id", function(env)
|
||||
env.initialise{}
|
||||
check(eq(Test.names(env.Roles.get_ordered_roles()), { "Cluster Admin", "Moderator", "Regular", "Jail", "Player" }),
|
||||
"ordered roles follow their order with deleted roles skipped")
|
||||
check(#env.Roles.get_roles() == 5, "get_roles returns every role")
|
||||
|
||||
local sorted = env.Roles.sort_roles{ env.R("Player"), env.R("Cluster Admin"), env.R("Regular") }
|
||||
check(eq(Test.names(sorted), { "Cluster Admin", "Regular", "Player" }), "sort_roles orders most privileged first")
|
||||
end)
|
||||
|
||||
test("roles are looked up by id", function(env)
|
||||
env.initialise{}
|
||||
check(env.Roles.get_role(6).name == "Regular", "get_role looks up by clusterio id")
|
||||
check(env.Roles.get_role_by_name("Moderator").id == 5, "get_role_by_name searches the roles")
|
||||
check(env.Roles.get_role(6).name == "Regular", "the role is found by id")
|
||||
check(env.Roles.get_role(env.R("Jail").id) == env.R("Jail"), "lookups return the same object")
|
||||
check(env.Roles.get_role(9) == nil and env.Roles.get_role_by_name("Gone") == nil, "deleted roles are not known")
|
||||
check(env.Roles.get_default_role() == env.R("Player"), "the default role comes from is_default")
|
||||
check(env.Roles.get_role(9) == nil, "deleted roles are not known")
|
||||
end)
|
||||
|
||||
test("role records are decoded", function(env)
|
||||
test("get_role_by_name searches the roles", function(env)
|
||||
env.initialise{}
|
||||
check(env.Roles.get_role_by_name("Moderator").id == 5, "the role is found by name")
|
||||
check(env.Roles.get_role_by_name("Gone") == nil, "deleted roles are not known")
|
||||
end)
|
||||
|
||||
test("get_roles returns every role", function(env)
|
||||
env.initialise{}
|
||||
check(#env.Roles.get_roles() == 5, "every role is returned with deleted roles skipped")
|
||||
end)
|
||||
|
||||
test("get_ordered_roles returns the most privileged first", function(env)
|
||||
env.initialise{}
|
||||
eq(env.names(env.Roles.get_ordered_roles()), { "Cluster Admin", "Moderator", "Regular", "Jail", "Player" },
|
||||
"roles follow their order")
|
||||
end)
|
||||
|
||||
test("sort_roles orders most privileged first", function(env)
|
||||
env.initialise{}
|
||||
local sorted = env.Roles.sort_roles{ env.R("Player"), env.R("Cluster Admin"), env.R("Regular") }
|
||||
eq(env.names(sorted), { "Cluster Admin", "Regular", "Player" }, "the list is sorted in place")
|
||||
end)
|
||||
|
||||
test("get_default_role follows is_default", function(env)
|
||||
env.initialise{}
|
||||
check(env.Roles.get_default_role() == env.R("Player"), "the default role is known")
|
||||
end)
|
||||
|
||||
test("get_higher_roles and get_lower_roles include the role and exclude the default", function(env)
|
||||
env.initialise{}
|
||||
eq(Suite.sorted(env.names(env.Roles.get_higher_roles(env.R("Regular")))),
|
||||
{ "Cluster Admin", "Moderator", "Regular" }, "higher roles")
|
||||
eq(Suite.sorted(env.names(env.Roles.get_lower_roles(env.R("Regular")))),
|
||||
{ "Jail", "Regular" }, "lower roles")
|
||||
end)
|
||||
|
||||
test("role:is_higher_than and role:is_lower_than compare on order", function(env)
|
||||
env.initialise{}
|
||||
check(env.R("Moderator"):is_higher_than(env.R("Player")), "a lower order is more privileged")
|
||||
check(env.R("Player"):is_lower_than(env.R("Moderator")), "is_lower_than is the reverse")
|
||||
check(not env.R("Moderator"):is_higher_than(env.R("Moderator")), "a role is not higher than itself")
|
||||
end)
|
||||
|
||||
test("initialise decodes the role records", function(env)
|
||||
env.initialise{}
|
||||
check(env.R("Cluster Admin").short_hand == "SYS", "short hand decoded")
|
||||
check(env.R("Moderator").short_hand == "Moderator", "short hand falls back to the name")
|
||||
@@ -30,19 +60,4 @@ test("role records are decoded", function(env)
|
||||
check(env.R("Jail").priority == 1 and env.R("Jail").block_auto_assign, "priority and block auto assign decoded")
|
||||
end)
|
||||
|
||||
test("roles compare on their order", function(env)
|
||||
env.initialise{}
|
||||
check(env.R("Moderator"):is_higher_than(env.R("Player")), "a lower order is more privileged")
|
||||
check(env.R("Player"):is_lower_than(env.R("Moderator")), "is_lower_than is the reverse")
|
||||
check(not env.R("Moderator"):is_higher_than(env.R("Moderator")), "a role is not higher than itself")
|
||||
end)
|
||||
|
||||
test("higher and lower roles", function(env)
|
||||
env.initialise{}
|
||||
check(eq(Test.sorted(Test.names(env.Roles.get_higher_roles(env.R("Regular")))), { "Cluster Admin", "Moderator", "Regular" }),
|
||||
"higher roles include the role itself and exclude the default role")
|
||||
check(eq(Test.sorted(Test.names(env.Roles.get_lower_roles(env.R("Regular")))), { "Jail", "Regular" }),
|
||||
"lower roles include the role itself and exclude the default role")
|
||||
end)
|
||||
|
||||
return Env.finish()
|
||||
return Suite.run()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
--- Player role and permission checks
|
||||
local Env = ...
|
||||
local Test = Env.Test
|
||||
local test, check, eq = Test.test, Test.check, Test.eq
|
||||
--- Tests for the player checks of module/control.lua
|
||||
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
|
||||
local test, check, eq = Suite.test, Suite.check, Suite.eq
|
||||
|
||||
--- alice is a moderator, bob is a regular, and carol has only the default role
|
||||
local function setup(env)
|
||||
@@ -11,42 +10,42 @@ local function setup(env)
|
||||
carol = env.add_player("carol", 3),
|
||||
}
|
||||
env.initialise{
|
||||
Env.assignment("alice", { 5 }),
|
||||
Env.assignment("bob", { 6 }),
|
||||
env.assignment("alice", { 5 }),
|
||||
env.assignment("bob", { 6 }),
|
||||
}
|
||||
return players
|
||||
end
|
||||
|
||||
test("player roles include the default role", function(env)
|
||||
test("get_player_roles includes the default role", function(env)
|
||||
local players = setup(env)
|
||||
check(eq(Test.sorted(Test.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" }),
|
||||
eq(Suite.sorted(env.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" },
|
||||
"held roles and the default role")
|
||||
check(eq(Test.names(env.Roles.get_player_roles(players.carol)), { "Player" }),
|
||||
eq(env.names(env.Roles.get_player_roles(players.carol)), { "Player" },
|
||||
"a player with no roles has the default role")
|
||||
end)
|
||||
|
||||
test("the server is nil or a player with index 0", function(env)
|
||||
test("get_player_roles treats nil and index 0 as the server", function(env)
|
||||
setup(env)
|
||||
check(eq(Test.names(env.Roles.get_player_roles(nil)), { "<server>" }), "nil is the server")
|
||||
check(eq(Test.names(env.Roles.get_player_roles(env.server)), { "<server>" }), "index 0 is the server")
|
||||
eq(env.names(env.Roles.get_player_roles(nil)), { "<server>" }, "nil is the server")
|
||||
eq(env.names(env.Roles.get_player_roles(env.server)), { "<server>" }, "index 0 is the server")
|
||||
check(env.Roles.player_has_permission(nil, "anything.at.all"), "the server has every permission")
|
||||
check(not env.R("Player"):has_player(nil), "the server does not have the default role")
|
||||
end)
|
||||
|
||||
test("the highest role", function(env)
|
||||
test("get_player_highest_role", function(env)
|
||||
local players = setup(env)
|
||||
check(env.Roles.get_player_highest_role(players.alice) == env.R("Moderator"), "highest held role")
|
||||
check(env.Roles.get_player_highest_role(players.alice) == env.R("Moderator"), "the highest held role")
|
||||
check(env.Roles.get_player_highest_role(players.carol) == env.R("Player"), "the default role when none are held")
|
||||
end)
|
||||
|
||||
test("permission checks", function(env)
|
||||
test("player_has_permission", function(env)
|
||||
local players = setup(env)
|
||||
check(env.Roles.player_has_permission(players.alice, "exp_scenario.command.kill"), "granted by a held role")
|
||||
check(env.Roles.player_has_permission(players.alice, "exp_scenario.gui.readme"), "granted by the default role")
|
||||
check(not env.Roles.player_has_permission(players.bob, "exp_scenario.command.jail"), "not granted")
|
||||
end)
|
||||
|
||||
test("has any and has all", function(env)
|
||||
test("player_has_any_permission and player_has_all_permission", function(env)
|
||||
local players = setup(env)
|
||||
local jail, kill = "exp_scenario.command.jail", "exp_scenario.command.kill"
|
||||
check(env.Roles.player_has_any_permission(players.bob, jail, kill), "any passes when one is granted")
|
||||
@@ -59,17 +58,21 @@ test("has any and has all", function(env)
|
||||
check(env.Roles.player_has_any_permission(nil, "anything.at.all"), "the server passes any")
|
||||
end)
|
||||
|
||||
test("role permission and player methods", function(env)
|
||||
local players = setup(env)
|
||||
check(env.R("Moderator"):has_permission("exp_scenario.command.kill"), "has_permission granted")
|
||||
check(not env.R("Regular"):has_permission("exp_scenario.command.jail"), "has_permission not granted")
|
||||
test("role:has_permission", function(env)
|
||||
setup(env)
|
||||
check(env.R("Moderator"):has_permission("exp_scenario.command.kill"), "granted")
|
||||
check(not env.R("Regular"):has_permission("exp_scenario.command.jail"), "not granted")
|
||||
check(env.R("Cluster Admin"):has_permission("anything.at.all"), "core.admin grants everything")
|
||||
check(env.R("Moderator"):has_player(players.alice), "has_player for a held role")
|
||||
check(not env.R("Moderator"):has_player(players.bob), "has_player for a role not held")
|
||||
end)
|
||||
|
||||
test("role:has_player", function(env)
|
||||
local players = setup(env)
|
||||
check(env.R("Moderator"):has_player(players.alice), "a held role")
|
||||
check(not env.R("Moderator"):has_player(players.bob), "a role not held")
|
||||
check(env.R("Player"):has_player(players.carol), "everyone has the default role")
|
||||
end)
|
||||
|
||||
test("outranks", function(env)
|
||||
test("player_outranks", function(env)
|
||||
local players = setup(env)
|
||||
check(env.Roles.player_outranks(players.alice, players.bob), "a higher role outranks a lower one")
|
||||
check(not env.Roles.player_outranks(players.bob, players.alice), "a lower role does not outrank a higher one")
|
||||
@@ -78,4 +81,4 @@ test("outranks", function(env)
|
||||
check(not env.Roles.player_outranks(players.alice, nil), "nobody outranks the server")
|
||||
end)
|
||||
|
||||
return Env.finish()
|
||||
return Suite.run()
|
||||
|
||||
+46
-39
@@ -1,7 +1,6 @@
|
||||
--- Initialise semantics and updates from the controller
|
||||
local Env = ...
|
||||
local Test = Env.Test
|
||||
local test, check, eq = Test.test, Test.check, Test.eq
|
||||
--- Tests for the sync entry points of module/control.lua
|
||||
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
|
||||
local test, check, eq, deep_eq = Suite.test, Suite.check, Suite.eq, Suite.deep_eq
|
||||
|
||||
--- alice is a moderator and carol has only the default role, both connected
|
||||
local function setup(env)
|
||||
@@ -14,8 +13,8 @@ local function setup(env)
|
||||
env.admin_state[player.name] = state
|
||||
end)
|
||||
env.initialise{
|
||||
Env.assignment("alice", { 5 }),
|
||||
Env.assignment("zed", { 5, 6 }), -- has never joined this map
|
||||
env.assignment("alice", { 5 }),
|
||||
env.assignment("zed", { 5, 6 }), -- has never joined this map
|
||||
}
|
||||
env.Roles.set_emit_events(true)
|
||||
return players
|
||||
@@ -23,19 +22,35 @@ end
|
||||
|
||||
test("initialise applies triggers and raises empty events", function(env)
|
||||
setup(env)
|
||||
check(env.admin_state.alice == true and env.admin_state.carol == false,
|
||||
"triggers run for connected players")
|
||||
deep_eq(env.admin_state, { alice = true, carol = false }, "triggers run for connected players")
|
||||
check(#env.events == 2, "the event is raised once per connected player")
|
||||
check(#env.events[1].data.assigned == 0 and #env.events[1].data.unassigned == 0, "the events are empty")
|
||||
check(#env.printed == 0 and #env.sent == 0 and #env.sounds == 0, "initialise is silent and sends nothing")
|
||||
end)
|
||||
|
||||
test("controller assignments apply and are announced", function(env)
|
||||
test("initialise is authoritative for pending roles", function(env)
|
||||
local players = setup(env)
|
||||
env.R("Moderator"):assign(players.carol)
|
||||
env.R("Jail"):assign(players.carol, { silent = true, local_only = true })
|
||||
|
||||
local sd = env.Roles._script_data()
|
||||
eq(sd.pending.carol, { 5 }, "the role is pending before initialise")
|
||||
|
||||
env.reset_log()
|
||||
env.initialise{ env.assignment("alice", { 5 }) }
|
||||
check(sd.pending.carol == nil, "pending roles are cleared")
|
||||
check(not env.R("Moderator"):has_player(players.carol), "an unconfirmed role is given up")
|
||||
eq(sd.local_players.carol, { 7 }, "local only roles are kept")
|
||||
check(#env.sent == 0, "initialise sends nothing")
|
||||
end)
|
||||
|
||||
test("receive_assignment_updates applies controller changes", function(env)
|
||||
local players = setup(env)
|
||||
env.reset_log()
|
||||
env.Roles.receive_assignment_updates{ Env.assignment("carol", { 5 }) }
|
||||
env.Roles.receive_assignment_updates{ env.assignment("carol", { 5 }) }
|
||||
check(env.R("Moderator"):has_player(players.carol), "the assignment applies")
|
||||
check(#env.events == 1 and eq(env.events[1].data.assigned, { 5 }), "the event is raised")
|
||||
check(#env.events == 1, "the event is raised")
|
||||
eq(env.events[1].data.assigned, { 5 }, "the event carries the role id")
|
||||
check(env.events[1].data.by_player_index == 0, "controller changes have no by player")
|
||||
check(#env.printed == 1 and env.printed[1][4] == "<server>", "the change is announced by the server")
|
||||
check(env.admin_state.carol == true, "triggers follow the assignment")
|
||||
@@ -43,18 +58,18 @@ test("controller assignments apply and are announced", function(env)
|
||||
env.reset_log()
|
||||
env.Roles.receive_assignment_updates{ { name = "carol", role_ids = {}, is_deleted = true } }
|
||||
check(not env.R("Moderator"):has_player(players.carol), "a removal applies")
|
||||
check(#env.events == 1 and eq(env.events[1].data.unassigned, { 5 }), "the removal raises the event")
|
||||
eq(env.events[1].data.unassigned, { 5 }, "the removal raises the event")
|
||||
check(env.admin_state.carol == false, "triggers follow the removal")
|
||||
end)
|
||||
|
||||
test("changes for players not on this map raise nothing", function(env)
|
||||
test("receive_assignment_updates for players not on this map raises nothing", function(env)
|
||||
setup(env)
|
||||
env.reset_log()
|
||||
env.Roles.receive_assignment_updates{ Env.assignment("zed", { 5 }) }
|
||||
env.Roles.receive_assignment_updates{ env.assignment("zed", { 5 }) }
|
||||
check(#env.events == 0 and #env.printed == 0, "no event and no announcement")
|
||||
end)
|
||||
|
||||
test("role updates apply to their holders", function(env)
|
||||
test("receive_role_updates applies to the holders", function(env)
|
||||
setup(env)
|
||||
env.reset_log()
|
||||
env.Roles.receive_role_updates{
|
||||
@@ -70,33 +85,17 @@ test("role updates apply to their holders", function(env)
|
||||
check(env.Roles.get_role(6) == nil, "a deleted role is removed")
|
||||
end)
|
||||
|
||||
test("the default role follows is_default", function(env)
|
||||
test("receive_role_updates moves the default role", function(env)
|
||||
local players = setup(env)
|
||||
env.Roles.receive_role_updates{
|
||||
{ id = 1, name = "Player", permissions = {}, meta = { id = 0, order = 5 } },
|
||||
{ id = 8, name = "Guest", permissions = {}, meta = { id = 0, order = 7 }, is_default = true },
|
||||
}
|
||||
check(env.Roles.get_default_role() == env.R("Guest"), "the new default role is known")
|
||||
check(eq(Test.names(env.Roles.get_player_roles(players.carol)), { "Guest" }), "players pick up the new default")
|
||||
eq(env.names(env.Roles.get_player_roles(players.carol)), { "Guest" }, "players pick up the new default")
|
||||
end)
|
||||
|
||||
test("initialise is authoritative for pending roles", function(env)
|
||||
local players = setup(env)
|
||||
env.R("Moderator"):assign(players.carol)
|
||||
env.R("Jail"):assign(players.carol, { silent = true, local_only = true })
|
||||
|
||||
local sd = env.Roles._script_data()
|
||||
check(eq(sd.pending.carol, { 5 }), "the role is pending before initialise")
|
||||
|
||||
env.reset_log()
|
||||
env.initialise{ Env.assignment("alice", { 5 }) }
|
||||
check(sd.pending.carol == nil, "pending roles are cleared")
|
||||
check(not env.R("Moderator"):has_player(players.carol), "an unconfirmed role is given up")
|
||||
check(eq(sd.local_players.carol, { 7 }), "local only roles are kept")
|
||||
check(#env.sent == 0, "initialise sends nothing")
|
||||
end)
|
||||
|
||||
test("nothing is sent while emit is disabled", function(env)
|
||||
test("set_emit_events gates what is sent", function(env)
|
||||
local players = setup(env)
|
||||
env.Roles.set_emit_events(false)
|
||||
env.reset_log()
|
||||
@@ -105,14 +104,22 @@ test("nothing is sent while emit is disabled", function(env)
|
||||
check(env.R("Regular"):has_player(players.alice), "the assignment still applies locally")
|
||||
end)
|
||||
|
||||
test("roles are usable after load and triggers run on join", function(env)
|
||||
setup(env)
|
||||
env.Roles.on_load()
|
||||
check(env.R("Moderator"):is_higher_than(env.R("Regular")), "roles are usable after load")
|
||||
test("on_load restores the state after a save and load", function(env)
|
||||
local players = setup(env)
|
||||
env.R("Jail"):assign(players.carol, { silent = true, local_only = true })
|
||||
env.save_load()
|
||||
|
||||
check(env.R("Moderator"):is_higher_than(env.R("Regular")), "role methods survive through the registered metatable")
|
||||
check(env.R("Moderator"):has_player(players.alice), "synced roles survive")
|
||||
check(env.R("Jail"):has_player(players.carol), "local roles survive")
|
||||
check(env.Roles.player_has_permission(players.alice, "exp_scenario.command.kill"), "permission checks still answer")
|
||||
end)
|
||||
|
||||
test("on_player_joined_game runs the triggers", function(env)
|
||||
setup(env)
|
||||
env.admin_state.alice = nil
|
||||
env.Roles.on_player_joined_game{ player_index = 1 }
|
||||
check(env.admin_state.alice ~= nil, "triggers run when a player joins")
|
||||
end)
|
||||
|
||||
return Env.finish()
|
||||
return Suite.run()
|
||||
|
||||
@@ -7,7 +7,7 @@ const fullMeta = new messages.RoleMetaRecord(
|
||||
7, 3, 1, "Mod", "[Mod]", new messages.RoleColor(1, 2, 3), 3600000, true, 12345, false,
|
||||
);
|
||||
|
||||
t.test("RoleColor", subtest => {
|
||||
t.test("class RoleColor", subtest => {
|
||||
testRoundTripJsonSerialisable(messages.RoleColor, testMatrix(
|
||||
[0, 255], // r
|
||||
[0, 128], // g
|
||||
@@ -17,7 +17,7 @@ t.test("RoleColor", subtest => {
|
||||
subtest.end();
|
||||
});
|
||||
|
||||
t.test("RoleMetaRecord", subtest => {
|
||||
t.test("class RoleMetaRecord", subtest => {
|
||||
testRoundTripJsonSerialisable(messages.RoleMetaRecord, testMatrix(
|
||||
[7], // id
|
||||
[3], // order
|
||||
@@ -34,7 +34,7 @@ t.test("RoleMetaRecord", subtest => {
|
||||
subtest.end();
|
||||
});
|
||||
|
||||
t.test("RoleRecord", subtest => {
|
||||
t.test("class RoleRecord", subtest => {
|
||||
testRoundTripJsonSerialisable(messages.RoleRecord, testMatrix(
|
||||
[7], // id
|
||||
["Moderator"], // name
|
||||
@@ -48,7 +48,7 @@ t.test("RoleRecord", subtest => {
|
||||
subtest.end();
|
||||
});
|
||||
|
||||
t.test("AssignmentRecord", subtest => {
|
||||
t.test("class AssignmentRecord", subtest => {
|
||||
testRoundTripJsonSerialisable(messages.AssignmentRecord, testMatrix(
|
||||
["alice"], // name
|
||||
[new Set(), new Set([5, 6])], // roleIds
|
||||
@@ -59,19 +59,34 @@ t.test("AssignmentRecord", subtest => {
|
||||
subtest.end();
|
||||
});
|
||||
|
||||
t.test("update events and requests", subtest => {
|
||||
const record = new messages.RoleRecord(7, "Moderator", ["a.b"], fullMeta, true, 12345);
|
||||
const assignment = new messages.AssignmentRecord("alice", new Set([5]), 12345);
|
||||
const sampleRecord = new messages.RoleRecord(7, "Moderator", ["a.b"], fullMeta, true, 12345);
|
||||
const sampleAssignment = new messages.AssignmentRecord("alice", new Set([5]), 12345);
|
||||
|
||||
t.test("class RoleUpdatedEvent", subtest => {
|
||||
testRoundTripJsonSerialisable(messages.RoleUpdatedEvent, testMatrix(
|
||||
[[], [record]], // updates
|
||||
[[], [sampleRecord]], // updates
|
||||
));
|
||||
subtest.pass("round trips");
|
||||
subtest.end();
|
||||
});
|
||||
|
||||
t.test("class AssignmentUpdatedEvent", subtest => {
|
||||
testRoundTripJsonSerialisable(messages.AssignmentUpdatedEvent, testMatrix(
|
||||
[[], [assignment]], // updates
|
||||
[[], [sampleAssignment]], // updates
|
||||
));
|
||||
subtest.pass("round trips");
|
||||
subtest.end();
|
||||
});
|
||||
|
||||
t.test("class RoleMetaUpdateRequest", subtest => {
|
||||
testRoundTripJsonSerialisable(messages.RoleMetaUpdateRequest, testMatrix(
|
||||
[new messages.RoleMetaRecord(7, 3), fullMeta], // meta
|
||||
));
|
||||
subtest.pass("round trips");
|
||||
subtest.end();
|
||||
});
|
||||
|
||||
t.test("class AssignmentUpdateRequest", subtest => {
|
||||
testRoundTripJsonSerialisable(messages.AssignmentUpdateRequest, testMatrix(
|
||||
["alice"], // name
|
||||
[[], [5]], // assign
|
||||
|
||||
@@ -6,7 +6,7 @@ const { seedRoles, flattenSeedPermissions } = require("../dist/node/seed");
|
||||
// Importing this defines the exp_scenario permissions the seed grants
|
||||
require("@expcluster/scenario/dist/node/permissions");
|
||||
|
||||
t.test("every seed permission is defined", subtest => {
|
||||
t.test("seedRoles grant only defined permissions", subtest => {
|
||||
for (const role of seedRoles) {
|
||||
for (const permission of flattenSeedPermissions(role)) {
|
||||
subtest.ok(lib.permissions.has(permission), `${role.name} grants defined permission ${permission}`);
|
||||
@@ -15,7 +15,7 @@ t.test("every seed permission is defined", subtest => {
|
||||
subtest.end();
|
||||
});
|
||||
|
||||
t.test("parents exist and their permissions are inherited", subtest => {
|
||||
t.test("flattenSeedPermissions carries parents into their children", subtest => {
|
||||
const byName = new Map(seedRoles.map(role => [role.name, role]));
|
||||
for (const role of seedRoles) {
|
||||
if (role.parent === undefined) {
|
||||
@@ -31,7 +31,7 @@ t.test("parents exist and their permissions are inherited", subtest => {
|
||||
subtest.end();
|
||||
});
|
||||
|
||||
t.test("seed role names are unique with one default and one admin", subtest => {
|
||||
t.test("seedRoles are unique with one default and one admin", subtest => {
|
||||
const names = seedRoles.map(role => role.name);
|
||||
subtest.strictSame(names.length, new Set(names).size, "names are unique");
|
||||
subtest.strictSame(seedRoles.filter(role => role.isDefault).length, 1, "one default role");
|
||||
|
||||
+73
-41
@@ -1,15 +1,30 @@
|
||||
--[[-- Test framework for plugin module tests
|
||||
Test files declare named tests with `Test.test(name, function(env) ... end)`.
|
||||
The runner creates a fresh environment for every test, so tests are
|
||||
independent and can not leak state into each other.
|
||||
A suite is a collection of named tests. Test files declare them with
|
||||
`Suite.test(name, function(env) ... end)`, naming each test after the function
|
||||
or method it covers. Every test function receives a fresh environment from the
|
||||
factory the suite was created with, so tests are independent and can not leak
|
||||
state into each other.
|
||||
]]
|
||||
|
||||
local Framework = {}
|
||||
|
||||
--- Create a test registry, one is made per test file
|
||||
function Framework.new()
|
||||
--- @class Test
|
||||
local Test = {
|
||||
--- Turn a value into a string for failure messages
|
||||
local function repr(value, depth)
|
||||
if type(value) ~= "table" then return tostring(value) end
|
||||
if depth > 3 then return "{...}" end
|
||||
|
||||
local parts = {}
|
||||
for key, entry in pairs(value) do
|
||||
parts[#parts + 1] = tostring(key) .. " = " .. repr(entry, depth + 1)
|
||||
end
|
||||
return "{ " .. table.concat(parts, ", ") .. " }"
|
||||
end
|
||||
|
||||
--- Create a suite of tests, one is made per test file
|
||||
--- @param make_env fun(): table Creates the environment given to each test
|
||||
function Framework.new_suite(make_env)
|
||||
--- @class Suite
|
||||
local Suite = {
|
||||
tests = {}, -- { name, fn } in declaration order
|
||||
results = {}, -- { name, ok, detail? } accumulated across tests
|
||||
}
|
||||
@@ -19,37 +34,61 @@ function Framework.new()
|
||||
--- Declare a named test, its function receives a fresh environment
|
||||
--- @param name string
|
||||
--- @param fn fun(env: table)
|
||||
function Test.test(name, fn)
|
||||
Test.tests[#Test.tests + 1] = { name = name, fn = fn }
|
||||
function Suite.test(name, fn)
|
||||
Suite.tests[#Suite.tests + 1] = { name = name, fn = fn }
|
||||
end
|
||||
|
||||
--- Record one check within the current test
|
||||
--- @param ok any Truthy when the check passed
|
||||
--- Record a passing check within the current test
|
||||
--- @param name string
|
||||
function Suite.pass(name)
|
||||
Suite.results[#Suite.results + 1] = {
|
||||
name = current_test and (current_test .. ": " .. name) or name,
|
||||
ok = true,
|
||||
}
|
||||
end
|
||||
|
||||
--- Record a failing check within the current test
|
||||
--- @param name string
|
||||
--- @param detail string?
|
||||
function Test.check(ok, name, detail)
|
||||
Test.results[#Test.results + 1] = {
|
||||
function Suite.fail(name, detail)
|
||||
Suite.results[#Suite.results + 1] = {
|
||||
name = current_test and (current_test .. ": " .. name) or name,
|
||||
ok = not not ok,
|
||||
ok = false,
|
||||
detail = detail,
|
||||
}
|
||||
end
|
||||
|
||||
--- Shallow array equality
|
||||
function Test.eq(a, b)
|
||||
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
|
||||
if #a ~= #b then return false end
|
||||
for i = 1, #a do
|
||||
if a[i] ~= b[i] then return false end
|
||||
--- Check a condition
|
||||
--- @param ok any Truthy when the check passed
|
||||
--- @param name string
|
||||
--- @param detail string?
|
||||
function Suite.check(ok, name, detail)
|
||||
if ok then
|
||||
Suite.pass(name)
|
||||
else
|
||||
Suite.fail(name, detail)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Check two values or arrays are shallowly equal
|
||||
function Suite.eq(a, b, name)
|
||||
local equal
|
||||
if type(a) ~= "table" or type(b) ~= "table" then
|
||||
equal = a == b
|
||||
else
|
||||
equal = #a == #b
|
||||
for i = 1, #a do
|
||||
if a[i] ~= b[i] then equal = false break end
|
||||
end
|
||||
end
|
||||
Suite.check(equal, name, ("%s ~= %s"):format(repr(a, 1), repr(b, 1)))
|
||||
end
|
||||
|
||||
--- Recursive table equality, keys checked from both sides
|
||||
function Test.deep_eq(a, b)
|
||||
local function deep_equal(a, b)
|
||||
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
|
||||
for key, value in pairs(a) do
|
||||
if not Test.deep_eq(value, b[key]) then return false end
|
||||
if not deep_equal(value, b[key]) then return false end
|
||||
end
|
||||
for key in pairs(b) do
|
||||
if a[key] == nil then return false end
|
||||
@@ -57,38 +96,31 @@ function Framework.new()
|
||||
return true
|
||||
end
|
||||
|
||||
--- Names of an array of roles
|
||||
function Test.names(roles)
|
||||
local rtn = {}
|
||||
for index, role in ipairs(roles) do
|
||||
rtn[index] = role.name
|
||||
end
|
||||
return rtn
|
||||
--- Check two values are equal, comparing tables recursively
|
||||
function Suite.deep_eq(a, b, name)
|
||||
Suite.check(deep_equal(a, b), name, ("%s ~= %s"):format(repr(a, 1), repr(b, 1)))
|
||||
end
|
||||
|
||||
--- Sorted copy of an array of strings
|
||||
function Test.sorted(list)
|
||||
function Suite.sorted(list)
|
||||
local rtn = {}
|
||||
for index, value in ipairs(list) do rtn[index] = value end
|
||||
table.sort(rtn)
|
||||
return rtn
|
||||
end
|
||||
|
||||
--- Run every declared test against a fresh environment
|
||||
--- @param make_env fun(): table
|
||||
function Test.run(make_env)
|
||||
for _, test in ipairs(Test.tests) do
|
||||
--- Run every declared test, each against a fresh environment, and return
|
||||
--- the results as JSON for the javascript side
|
||||
function Suite.run()
|
||||
for _, test in ipairs(Suite.tests) do
|
||||
current_test = test.name
|
||||
local ok, err = pcall(test.fn, make_env())
|
||||
if not ok then
|
||||
Test.check(false, "did not error", tostring(err))
|
||||
Suite.fail("did not error", tostring(err))
|
||||
end
|
||||
end
|
||||
current_test = nil
|
||||
end
|
||||
|
||||
--- Encode the results as JSON for the javascript side
|
||||
function Test.results_json()
|
||||
local function escape(value)
|
||||
return (value:gsub('[%c"\\]', function(c)
|
||||
return string.format("\\u%04x", c:byte())
|
||||
@@ -96,14 +128,14 @@ function Framework.new()
|
||||
end
|
||||
|
||||
local parts = {}
|
||||
for index, result in ipairs(Test.results) do
|
||||
for index, result in ipairs(Suite.results) do
|
||||
local detail = result.detail and string.format(',"detail":"%s"', escape(result.detail)) or ""
|
||||
parts[index] = string.format('{"name":"%s","ok":%s%s}', escape(result.name), tostring(result.ok), detail)
|
||||
end
|
||||
return "[" .. table.concat(parts, ",") .. "]"
|
||||
end
|
||||
|
||||
return Test
|
||||
return Suite
|
||||
end
|
||||
|
||||
return Framework
|
||||
|
||||
+68
-9
@@ -3,8 +3,8 @@ Provides the surface a clusterio module touches, recording what the module does
|
||||
to it. Every stub raises an error when an unimplemented property is read, which
|
||||
mirrors the game api and catches mistakes in tests early.
|
||||
|
||||
Plugins compose these from their own test/lua/env.lua, adding stubs of their
|
||||
own through `stubs.requires` and `Stubs.strict`.
|
||||
Plugins compose these from their own test/lua/env.lua, adding their own stubs
|
||||
with the extend methods rather than by mutating the tables directly.
|
||||
]]
|
||||
|
||||
local Stubs = {}
|
||||
@@ -29,6 +29,25 @@ function Stubs.strict(name, tbl, allowed_nil)
|
||||
})
|
||||
end
|
||||
|
||||
--- Merge extra into target recursively, raising when a value already exists
|
||||
--- @generic T : table
|
||||
--- @param target T
|
||||
--- @param extra table
|
||||
--- @return T
|
||||
function Stubs.extend(target, extra)
|
||||
for key, value in pairs(extra) do
|
||||
local existing = rawget(target, key)
|
||||
if existing == nil then
|
||||
rawset(target, key, value)
|
||||
elseif type(existing) == "table" and type(value) == "table" then
|
||||
Stubs.extend(existing, value)
|
||||
else
|
||||
error("Stub already implements: " .. tostring(key), 2)
|
||||
end
|
||||
end
|
||||
return target
|
||||
end
|
||||
|
||||
--- Create an independent set of stubs, installed as the lua globals
|
||||
function Stubs.new()
|
||||
local stubs = {
|
||||
@@ -39,7 +58,8 @@ function Stubs.new()
|
||||
}
|
||||
|
||||
local next_event_id = 100
|
||||
script = Stubs.strict("script", {
|
||||
local registered_metatables = {} --- @type table<table, true>
|
||||
script = Stubs.strict("LuaBootstrap", {
|
||||
generate_event_name = function()
|
||||
next_event_id = next_event_id + 1
|
||||
return next_event_id
|
||||
@@ -47,7 +67,9 @@ function Stubs.new()
|
||||
raise_event = function(id, data)
|
||||
stubs.events[#stubs.events + 1] = { id = id, data = data }
|
||||
end,
|
||||
register_metatable = function() end,
|
||||
register_metatable = function(_, metatable)
|
||||
registered_metatables[metatable] = true
|
||||
end,
|
||||
})
|
||||
|
||||
defines = Stubs.strict("defines", {
|
||||
@@ -58,7 +80,7 @@ function Stubs.new()
|
||||
})
|
||||
|
||||
local players_by_name, players_by_index, connected = {}, {}, {}
|
||||
game = Stubs.strict("game", {
|
||||
game = Stubs.strict("LuaGameScript", {
|
||||
tick = 1,
|
||||
players = players_by_name,
|
||||
connected_players = connected,
|
||||
@@ -87,10 +109,10 @@ function Stubs.new()
|
||||
end
|
||||
|
||||
--- A player object which represents the server
|
||||
stubs.server = Stubs.strict("server player", { index = 0, name = "<server>" })
|
||||
stubs.server = Stubs.strict("LuaPlayer <server>", { index = 0, name = "<server>" })
|
||||
|
||||
--- Modules resolved by the stubbed require, extend before loading the module
|
||||
stubs.requires = {
|
||||
--- Modules resolved by the stubbed require, add to them with extend_requires
|
||||
local requires = {
|
||||
["modules/clusterio/api"] = Stubs.strict("clusterio api", {
|
||||
send_json = function(channel, data)
|
||||
stubs.sent[#stubs.sent + 1] = { channel = channel, data = data }
|
||||
@@ -104,7 +126,44 @@ function Stubs.new()
|
||||
}),
|
||||
}
|
||||
require = function(name)
|
||||
return assert(stubs.requires[name], "Unexpected require: " .. name)
|
||||
return assert(rawget(requires, name), "Unexpected require: " .. name)
|
||||
end
|
||||
|
||||
--- Add modules or properties to the stubbed require
|
||||
function stubs.extend_requires(extra) return Stubs.extend(requires, extra) end
|
||||
|
||||
--- Add properties to the script, game, and defines globals
|
||||
function stubs.extend_script(extra) return Stubs.extend(script, extra) end
|
||||
function stubs.extend_game(extra) return Stubs.extend(game, extra) end
|
||||
function stubs.extend_defines(extra) return Stubs.extend(defines, extra) end
|
||||
|
||||
--- Copy a value the way factorio saves script data: functions are refused
|
||||
--- and only metatables registered with script.register_metatable survive
|
||||
local function save_load_copy(value, copies)
|
||||
if type(value) == "function" then
|
||||
error("Functions can not be stored in script data", 0)
|
||||
end
|
||||
if type(value) ~= "table" then return value end
|
||||
if copies[value] then return copies[value] end
|
||||
|
||||
local copy = {}
|
||||
copies[value] = copy
|
||||
for key, entry in pairs(value) do
|
||||
copy[save_load_copy(key, copies)] = save_load_copy(entry, copies)
|
||||
end
|
||||
|
||||
local metatable = getmetatable(value)
|
||||
if metatable ~= nil and registered_metatables[metatable] then
|
||||
setmetatable(copy, metatable)
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
--- Replace the script data with a copy of itself as if the map was saved
|
||||
--- and loaded, the module's on_load handler should be called afterwards
|
||||
function stubs.save_load()
|
||||
local compat = requires["modules/clusterio/compat"]
|
||||
compat.script_data = save_load_copy(compat.script_data, {})
|
||||
end
|
||||
|
||||
--- Forget everything recorded so far
|
||||
|
||||
Reference in New Issue
Block a user