mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-09-21 17:04:00 +00:00
Address review on the suite and instance tests
- The instance tests run against a real Instance with a real InstanceConfig, matching the controller tests: the spies are sendTo and the server, which record what leaves the instance, so sendRcon passes through the real script command gate. The plugin's config fields are registered so the sync mode is a real field. - Shallow eq is gone and eq compares recursively; the suite is created with Framework.suite(extend_env), which hands the extension a fresh set of stubs per test, so the plugin's env.lua is only the extension. - add_player assigns continuous indexes itself. - The lua fixtures are referenced by role name: assignments take names, and ids in expectations come from the role object. The name lookup in env.R uses a fixture index rather than searching the roles. - Test names format functions as name() and methods as .name(). - Branch coverage filled in: sort tie breaking, the highest role assertion when a player has no roles, has all for the server, the by player default from game.player, deleted assignment records on initialise, set_emit_events with no argument, printing to a role with no holders, role property update validation, the assignment subscription replay, unassignment over the ipc, and a refused removal having nothing to roll back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e6552a39be
commit
3218351db6
@@ -52,7 +52,7 @@ async function startPlugin(t2, { roles = [] } = {}) {
|
||||
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 t3 => {
|
||||
t2.test(".init() and .sweepRoleMeta() manage the role properties", async t3 => {
|
||||
const { plugin } = await startPlugin(t3, { roles: [role(0, "Cluster Admin"), role(5, "Moderator")] });
|
||||
|
||||
t3.strictSame(plugin.roleMeta.get(0).order, 1, "the first role is ordered first");
|
||||
@@ -64,7 +64,7 @@ t.test("class ControllerPlugin", t2 => {
|
||||
t3.ok(plugin.roleMeta.get(5), "properties with a role are kept");
|
||||
});
|
||||
|
||||
t2.test("buildRoleRecord combines the role with its properties", async t3 => {
|
||||
t2.test(".buildRoleRecord() combines the role with its properties", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3, {
|
||||
roles: [role(1, "Player"), role(5, "Moderator", ["exp_scenario.command.kill"])],
|
||||
});
|
||||
@@ -78,7 +78,7 @@ t.test("class ControllerPlugin", t2 => {
|
||||
t3.ok(plugin.buildRoleRecord(controller.roles.get(1)).isDefault, "the default role is marked");
|
||||
});
|
||||
|
||||
t2.test("rolesUpdated and roleMetaUpdated broadcast to subscribers", async t3 => {
|
||||
t2.test(".rolesUpdated() and .roleMetaUpdated() broadcast to subscribers", async t3 => {
|
||||
const { plugin, controller, state } = await startPlugin(t3, { roles: [role(1, "Player")] });
|
||||
|
||||
state.broadcasts.length = 0;
|
||||
@@ -102,7 +102,7 @@ t.test("class ControllerPlugin", t2 => {
|
||||
);
|
||||
});
|
||||
|
||||
t2.test("handleRoleSubscription replays only newer records", async t3 => {
|
||||
t2.test(".handleRoleSubscription() replays only newer records", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3, { roles: [role(1, "Player")] });
|
||||
controller.roles.set(role(5, "Moderator"));
|
||||
const updatedAtMs = plugin.buildRoleRecord(controller.roles.get(5)).updatedAtMs;
|
||||
@@ -113,7 +113,7 @@ t.test("class ControllerPlugin", t2 => {
|
||||
t3.strictSame(none, null, "nothing is replayed when up to date");
|
||||
});
|
||||
|
||||
t2.test("listAssignmentRecords mirrors users without the default role", async t3 => {
|
||||
t2.test(".listAssignmentRecords() mirrors users without the default role", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3, { roles: [role(1, "Player"), role(5, "Moderator")] });
|
||||
controller.users.createUser("alice").set("roleIds", new Set([lib.Role.DefaultPlayerRoleId, 5]));
|
||||
controller.users.createUser("bob");
|
||||
@@ -124,7 +124,7 @@ t.test("class ControllerPlugin", t2 => {
|
||||
t3.strictSame([...records[0].roleIds], [5], "the default role is not listed");
|
||||
});
|
||||
|
||||
t2.test("handleAssignmentUpdateRequest validates the user and roles", async t3 => {
|
||||
t2.test(".handleAssignmentUpdateRequest() validates the user and roles", async t3 => {
|
||||
const { plugin, controller, state } = await startPlugin(t3, {
|
||||
roles: [role(1, "Player"), role(5, "Moderator"), role(6, "Regular")],
|
||||
});
|
||||
@@ -144,7 +144,7 @@ t.test("class ControllerPlugin", t2 => {
|
||||
t3.ok(state.permissionUpdates.includes("alice"), "permission changes are pushed to the user");
|
||||
});
|
||||
|
||||
t2.test("applyAutoAssign and onPlayerEvent grant roles from online time", async t3 => {
|
||||
t2.test(".applyAutoAssign() and .onPlayerEvent() grant roles from online time", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3, {
|
||||
roles: [role(1, "Player"), role(6, "Regular"), role(7, "Jail")],
|
||||
});
|
||||
@@ -173,7 +173,32 @@ t.test("class ControllerPlugin", t2 => {
|
||||
t3.strictSame(applied.length, 1, "other player events do not");
|
||||
});
|
||||
|
||||
t2.test("handleSeedRolesRequest creates the roles and reuses them by name", async t3 => {
|
||||
t2.test(".handleRoleMetaUpdateRequest() validates the role", async t3 => {
|
||||
const { plugin } = await startPlugin(t3, { roles: [role(1, "Player"), role(5, "Moderator")] });
|
||||
|
||||
await t3.rejects(
|
||||
plugin.handleRoleMetaUpdateRequest(new messages.RoleMetaUpdateRequest(new messages.RoleMetaRecord(99, 1))),
|
||||
{ message: /does not exist/ }, "properties for an unknown role are refused",
|
||||
);
|
||||
|
||||
await plugin.handleRoleMetaUpdateRequest(new messages.RoleMetaUpdateRequest(
|
||||
new messages.RoleMetaRecord(5, 2, 0, "Mod"),
|
||||
));
|
||||
t3.strictSame(plugin.roleMeta.get(5).shortHand, "Mod", "the properties are stored");
|
||||
});
|
||||
|
||||
t2.test(".handleAssignmentSubscription() replays only newer records", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3, { roles: [role(1, "Player"), role(5, "Moderator")] });
|
||||
controller.users.createUser("alice").set("roleIds", new Set([5]));
|
||||
const updatedAtMs = plugin.listAssignmentRecords()[0].updatedAtMs;
|
||||
|
||||
const all = await plugin.handleAssignmentSubscription({ lastRequestTimeMs: 0 });
|
||||
t3.strictSame(all.updates.length, 1, "everything is replayed from the start");
|
||||
const none = await plugin.handleAssignmentSubscription({ lastRequestTimeMs: updatedAtMs });
|
||||
t3.strictSame(none, null, "nothing is replayed when up to date");
|
||||
});
|
||||
|
||||
t2.test(".handleSeedRolesRequest() creates the roles and reuses them by name", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3, {
|
||||
roles: [role(0, "Cluster Admin", ["core.admin"]), role(1, "Player")],
|
||||
});
|
||||
|
||||
+103
-64
@@ -1,14 +1,32 @@
|
||||
"use strict";
|
||||
const t = require("tap");
|
||||
const lib = require("@clusterio/lib");
|
||||
const { Instance } = require("@clusterio/host");
|
||||
const { InstancePlugin } = require("../dist/node/instance");
|
||||
const { plugin: pluginDeclaration } = require("../dist/node/index");
|
||||
const messages = require("../dist/node/messages");
|
||||
|
||||
// The instance validates message classes against the link registry, and the
|
||||
// plugin's config fields must be defined before an InstanceConfig can set them
|
||||
lib.Link.register(messages.RoleUpdatedEvent);
|
||||
lib.Link.register(messages.AssignmentUpdatedEvent);
|
||||
lib.Link.register(messages.RoleListRequest);
|
||||
lib.Link.register(messages.AssignmentListRequest);
|
||||
lib.Link.register(messages.AssignmentUpdateRequest);
|
||||
lib.addPluginConfigFields([pluginDeclaration]);
|
||||
|
||||
const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} };
|
||||
|
||||
// Subscribing validates event names against the link registry
|
||||
lib.Link.register(messages.RoleUpdatedEvent);
|
||||
lib.Link.register(messages.AssignmentUpdatedEvent);
|
||||
class TestConnector extends lib.BaseConnector {
|
||||
constructor() {
|
||||
super(lib.Address.fromShorthand({ instanceId: 1 }), lib.Address.fromShorthand({ hostId: 1 }));
|
||||
this.valid = true;
|
||||
this.connected = true;
|
||||
this.hasSession = true;
|
||||
}
|
||||
|
||||
send() {}
|
||||
}
|
||||
|
||||
const sampleRoles = () => [
|
||||
new messages.RoleRecord(1, "Player", ["exp_scenario.gui.readme"], new messages.RoleMetaRecord(1, 2), true),
|
||||
@@ -16,30 +34,40 @@ const sampleRoles = () => [
|
||||
];
|
||||
const sampleAssignments = () => [new messages.AssignmentRecord("alice", new Set([5]))];
|
||||
|
||||
/** Build a plugin around a fake instance, started on first use by each test. */
|
||||
/** Build a plugin around a real instance with spies on what leaves it, started on first use by each test. */
|
||||
async function startPlugin(t2, { syncMode = "bidirectional", roles = sampleRoles() } = {}) {
|
||||
const instanceConfig = new lib.InstanceConfig("host");
|
||||
instanceConfig.set("instance.id", 1);
|
||||
instanceConfig.set("instance.name", "test");
|
||||
instanceConfig.set("exp_roles.sync_mode", syncMode);
|
||||
|
||||
const instance = new Instance(
|
||||
{ assignGamePort: () => 1 }, new TestConnector(), t2.testdir(), "factorioDir", instanceConfig
|
||||
);
|
||||
|
||||
// Spies which record the messages and commands leaving the instance
|
||||
const state = { sent: [], rcons: [], warnings: [], failNext: null };
|
||||
const instance = {
|
||||
logger,
|
||||
config: { get: field => (field === "exp_roles.sync_mode" ? syncMode : undefined) },
|
||||
instance.server = {
|
||||
handle: () => {},
|
||||
server: { handle: () => {} },
|
||||
sendTo: async (dst, request) => {
|
||||
state.sent.push(request);
|
||||
if (state.failNext) {
|
||||
const error = state.failNext;
|
||||
state.failNext = null;
|
||||
throw error;
|
||||
}
|
||||
if (request instanceof messages.RoleListRequest) {
|
||||
return roles;
|
||||
}
|
||||
if (request instanceof messages.AssignmentListRequest) {
|
||||
return sampleAssignments();
|
||||
}
|
||||
return undefined;
|
||||
sendRcon: async command => {
|
||||
state.rcons.push(command);
|
||||
return "";
|
||||
},
|
||||
sendRcon: async command => state.rcons.push(command),
|
||||
};
|
||||
instance.sendTo = async (dst, request) => {
|
||||
state.sent.push(request);
|
||||
if (state.failNext) {
|
||||
const error = state.failNext;
|
||||
state.failNext = null;
|
||||
throw error;
|
||||
}
|
||||
if (request instanceof messages.RoleListRequest) {
|
||||
return roles;
|
||||
}
|
||||
if (request instanceof messages.AssignmentListRequest) {
|
||||
return sampleAssignments();
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const plugin = new InstancePlugin({ name: "exp_roles" }, instance, {});
|
||||
@@ -55,92 +83,103 @@ function decodeRcon(command) {
|
||||
}
|
||||
|
||||
t.test("class InstancePlugin", t2 => {
|
||||
t2.test("onStart subscribes and initialises the lua module", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2, { syncMode: "enabled" });
|
||||
t2.test(".onStart() subscribes and initialises the lua module", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3, { syncMode: "enabled" });
|
||||
await plugin.onStart();
|
||||
|
||||
const subscribed = state.sent.filter(request => request instanceof lib.SubscriptionRequest);
|
||||
t2.strictSame(subscribed.length, 2, "roles and assignments are subscribed to");
|
||||
t2.ok(state.sent.some(request => request instanceof messages.RoleListRequest), "the roles are requested");
|
||||
t2.ok(state.sent.some(request => request instanceof messages.AssignmentListRequest), "the assignments are requested");
|
||||
t3.strictSame(subscribed.length, 2, "roles and assignments are subscribed to");
|
||||
t3.ok(state.sent.some(request => request instanceof messages.RoleListRequest), "the roles are requested");
|
||||
t3.ok(
|
||||
state.sent.some(request => request instanceof messages.AssignmentListRequest),
|
||||
"the assignments are requested",
|
||||
);
|
||||
|
||||
const initialise = decodeRcon(state.rcons[0]);
|
||||
t2.strictSame(initialise.receiver, "initialise", "the lua module is initialised");
|
||||
t2.strictSame(initialise.payload.permission_names, ["exp_scenario.gui.readme", "core.admin"],
|
||||
t3.strictSame(initialise.receiver, "initialise", "the lua module is initialised");
|
||||
t3.strictSame(initialise.payload.permission_names, ["exp_scenario.gui.readme", "core.admin"],
|
||||
"each permission name is sent once");
|
||||
t2.strictSame(initialise.payload.roles[1].permissions, [0, 1], "roles reference permissions by index");
|
||||
t2.strictSame(initialise.payload.assignments, [{ name: "alice", role_ids: [5] }], "the assignments are sent");
|
||||
t3.strictSame(initialise.payload.roles[1].permissions, [0, 1], "roles reference permissions by index");
|
||||
t3.strictSame(initialise.payload.assignments, [{ name: "alice", role_ids: [5] }], "the assignments are sent");
|
||||
|
||||
const emit = decodeRcon(state.rcons[1]);
|
||||
t2.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "enabled does not emit changes back");
|
||||
t2.strictSame(state.warnings.length, 0, "no warnings with a default role");
|
||||
t3.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "enabled does not emit changes back");
|
||||
t3.strictSame(state.warnings.length, 0, "no warnings with a default role");
|
||||
});
|
||||
|
||||
t2.test("onStart with bidirectional emits changes back", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2);
|
||||
t2.test(".onStart() with bidirectional emits changes back", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3);
|
||||
await plugin.onStart();
|
||||
const emit = decodeRcon(state.rcons[state.rcons.length - 1]);
|
||||
t2.strictSame([emit.receiver, emit.payload], ["set_emit_events", true]);
|
||||
t3.strictSame([emit.receiver, emit.payload], ["set_emit_events", true]);
|
||||
});
|
||||
|
||||
t2.test("onStart with disabled does nothing", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2, { syncMode: "disabled" });
|
||||
t2.test(".onStart() with disabled does nothing", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3, { syncMode: "disabled" });
|
||||
await plugin.onStart();
|
||||
t2.strictSame(state.sent.length, 0, "nothing is sent");
|
||||
t2.strictSame(state.rcons.length, 0, "no commands are run");
|
||||
t3.strictSame(state.sent.length, 0, "nothing is sent");
|
||||
t3.strictSame(state.rcons.length, 0, "no commands are run");
|
||||
});
|
||||
|
||||
t2.test("onStart warns when no default role is set", async t2 => {
|
||||
t2.test(".onStart() warns when no default role is set", async t3 => {
|
||||
const roles = sampleRoles().map(record => { record.isDefault = false; return record; });
|
||||
const { plugin, state } = await startPlugin(t2, { roles });
|
||||
const { plugin, state } = await startPlugin(t3, { roles });
|
||||
await plugin.onStart();
|
||||
t2.ok(state.warnings.some(message => /default role/.test(message)), "the warning names the default role");
|
||||
t3.ok(state.warnings.some(message => /default role/.test(message)), "the warning names the default role");
|
||||
});
|
||||
|
||||
t2.test("handleRoleUpdatedEvent and handleAssignmentUpdatedEvent forward to lua", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2);
|
||||
t2.test(".handleRoleUpdatedEvent() and .handleAssignmentUpdatedEvent() forward to lua", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3);
|
||||
await plugin.handleRoleUpdatedEvent(new messages.RoleUpdatedEvent(sampleRoles()));
|
||||
const roles = decodeRcon(state.rcons[0]);
|
||||
t2.strictSame(roles.receiver, "receive_role_updates", "role updates are forwarded");
|
||||
t2.strictSame(roles.payload[0].name, "Player", "the records are sent in plain form");
|
||||
t3.strictSame(roles.receiver, "receive_role_updates", "role updates are forwarded");
|
||||
t3.strictSame(roles.payload[0].name, "Player", "the records are sent in plain form");
|
||||
|
||||
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
|
||||
const assignments = decodeRcon(state.rcons[1]);
|
||||
t2.strictSame(assignments.receiver, "receive_assignment_updates", "assignment updates are forwarded");
|
||||
t3.strictSame(assignments.receiver, "receive_assignment_updates", "assignment updates are forwarded");
|
||||
});
|
||||
|
||||
t2.test("handleRoleUpdatedEvent and handleAssignmentUpdatedEvent are gated while disabled", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2, { syncMode: "disabled" });
|
||||
t2.test(".handleRoleUpdatedEvent() and .handleAssignmentUpdatedEvent() are gated while disabled", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3, { syncMode: "disabled" });
|
||||
await plugin.handleRoleUpdatedEvent(new messages.RoleUpdatedEvent(sampleRoles()));
|
||||
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
|
||||
t2.strictSame(state.rcons.length, 0, "no commands are run");
|
||||
t3.strictSame(state.rcons.length, 0, "no commands are run");
|
||||
});
|
||||
|
||||
t2.test("handleAssignmentUpdateIPC sends the change and rolls back a refusal", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2);
|
||||
t2.test(".handleAssignmentUpdateIPC() sends the change and rolls back a refusal", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3);
|
||||
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
|
||||
const request = state.sent[0];
|
||||
t2.ok(request instanceof messages.AssignmentUpdateRequest, "the change is sent as a request");
|
||||
t2.strictSame([request.name, request.assign, request.unassign], ["alice", [5], []], "the request names the change");
|
||||
t3.ok(request instanceof messages.AssignmentUpdateRequest, "the change is sent as a request");
|
||||
t3.strictSame([request.name, request.assign, request.unassign], ["alice", [5], []], "the request names the change");
|
||||
|
||||
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: undefined, unassign: [5] });
|
||||
t3.strictSame(state.sent[1].unassign, [5], "removals are sent the same way");
|
||||
|
||||
state.failNext = new Error("User 'alice' does not exist");
|
||||
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
|
||||
const rollback = decodeRcon(state.rcons[0]);
|
||||
t2.strictSame(rollback.receiver, "reject_assignment", "the refusal is rolled back in lua");
|
||||
t2.strictSame(rollback.payload, { name: "alice", role_ids: [5] }, "the rollback names the refused roles");
|
||||
t3.strictSame(rollback.receiver, "reject_assignment", "the refusal is rolled back in lua");
|
||||
t3.strictSame(rollback.payload, { name: "alice", role_ids: [5] }, "the rollback names the refused roles");
|
||||
|
||||
state.failNext = new Error("User 'alice' does not exist");
|
||||
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: undefined, unassign: [5] });
|
||||
t3.strictSame(state.rcons.length, 1, "a refused removal has nothing to roll back");
|
||||
});
|
||||
|
||||
t2.test("handleAssignmentUpdateIPC is gated unless bidirectional", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2, { syncMode: "enabled" });
|
||||
t2.test(".handleAssignmentUpdateIPC() is gated unless bidirectional", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3, { syncMode: "enabled" });
|
||||
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
|
||||
t2.strictSame(state.sent.length, 0, "nothing is sent");
|
||||
t3.strictSame(state.sent.length, 0, "nothing is sent");
|
||||
});
|
||||
|
||||
t2.test("onInstanceConfigFieldChanged toggles emit with the sync mode", async t2 => {
|
||||
const { plugin, state } = await startPlugin(t2);
|
||||
t2.test(".onInstanceConfigFieldChanged() toggles emit with the sync mode", async t3 => {
|
||||
const { plugin, state } = await startPlugin(t3);
|
||||
await plugin.onInstanceConfigFieldChanged("exp_roles.sync_mode", "enabled", "bidirectional");
|
||||
const emit = decodeRcon(state.rcons[0]);
|
||||
t2.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "leaving bidirectional stops emitting");
|
||||
t3.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "leaving bidirectional stops emitting");
|
||||
});
|
||||
|
||||
t2.end();
|
||||
});
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
--- 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, deep_eq = Suite.test, Suite.check, Suite.eq, Suite.deep_eq
|
||||
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)
|
||||
local players = {
|
||||
alice = env.add_player("alice", 1),
|
||||
bob = env.add_player("bob", 2),
|
||||
alice = env.add_player("alice"),
|
||||
bob = env.add_player("bob"),
|
||||
}
|
||||
env.initialise{
|
||||
env.assignment("alice", { 5 }),
|
||||
env.assignment("bob", { 6 }),
|
||||
env.assignment("alice", { "Moderator" }),
|
||||
env.assignment("bob", { "Regular" }),
|
||||
}
|
||||
env.Roles.set_emit_events(true)
|
||||
env.reset_log()
|
||||
return players
|
||||
end
|
||||
|
||||
test("role:assign applies locally and is sent to the controller", function(env)
|
||||
test(".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" })
|
||||
deep_eq(env.sent, {
|
||||
{ channel = "exp_roles:assignment_update", data = { name = "bob", assign = { 5 } } },
|
||||
local moderator = env.R("Moderator")
|
||||
moderator:assign(players.bob, { by_player_name = "alice" })
|
||||
eq(env.sent, {
|
||||
{ channel = "exp_roles:assignment_update", data = { name = "bob", assign = { moderator.id } } },
|
||||
}, "the assignment is sent to the controller")
|
||||
check(env.R("Moderator"):has_player(players.bob), "the role applies before confirmation")
|
||||
deep_eq(env.events, {
|
||||
check(moderator:has_player(players.bob), "the role applies before confirmation")
|
||||
eq(env.events, {
|
||||
{
|
||||
name = env.Roles.events.on_player_roles_changed,
|
||||
tick = 1,
|
||||
player_index = 2,
|
||||
by_player_index = 1,
|
||||
assigned = { 5 },
|
||||
player_index = players.bob.index,
|
||||
by_player_index = players.alice.index,
|
||||
assigned = { moderator.id },
|
||||
unassigned = {},
|
||||
},
|
||||
}, "the event carries the change")
|
||||
@@ -38,19 +39,26 @@ test("role:assign applies locally and is sent to the controller", function(env)
|
||||
eq(env.sounds, { "bob:utility/achievement_unlocked" }, "the assign sound plays")
|
||||
|
||||
local sd = env.Roles._script_data()
|
||||
eq(sd.local_players.bob, { 5 }, "the role is held locally")
|
||||
eq(sd.pending.bob, { 5 }, "the role is pending")
|
||||
eq(sd.local_players.bob, { moderator.id }, "the role is held locally")
|
||||
eq(sd.pending.bob, { moderator.id }, "the role is pending")
|
||||
|
||||
env.reset_log()
|
||||
env.R("Moderator"):assign(players.bob)
|
||||
moderator:assign(players.bob)
|
||||
check(#env.sent == 0 and #env.events == 0, "assigning a held role is a no-op")
|
||||
end)
|
||||
|
||||
test("receive_assignment_updates releases the local hold once confirmed", function(env)
|
||||
test(".assign() defaults the by player to game.player", function(env)
|
||||
local players = setup(env)
|
||||
game.player = players.alice
|
||||
env.R("Jail"):assign(players.bob, { silent = true })
|
||||
check(env.events[1].by_player_index == players.alice.index, "the acting player is game.player")
|
||||
end)
|
||||
|
||||
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", { "Regular", "Moderator" }) }
|
||||
check(#env.events == 0 and #env.printed == 0, "a confirmation raises nothing")
|
||||
|
||||
local sd = env.Roles._script_data()
|
||||
@@ -58,44 +66,46 @@ test("receive_assignment_updates releases the local hold once confirmed", functi
|
||||
check(env.R("Moderator"):has_player(players.bob), "the role is still held after confirmation")
|
||||
end)
|
||||
|
||||
test("role:unassign removes a synced role", function(env)
|
||||
test(".unassign() removes a synced role", function(env)
|
||||
local players = setup(env)
|
||||
env.R("Regular"):unassign(players.bob, { by_player_name = "alice", silent = true })
|
||||
deep_eq(env.sent, {
|
||||
{ channel = "exp_roles:assignment_update", data = { name = "bob", unassign = { 6 } } },
|
||||
local regular = env.R("Regular")
|
||||
regular:unassign(players.bob, { by_player_name = "alice", silent = true })
|
||||
eq(env.sent, {
|
||||
{ channel = "exp_roles:assignment_update", data = { name = "bob", unassign = { regular.id } } },
|
||||
}, "the unassignment is sent to the controller")
|
||||
check(not env.R("Regular"):has_player(players.bob), "the role is removed locally")
|
||||
deep_eq(env.events, {
|
||||
check(not regular:has_player(players.bob), "the role is removed locally")
|
||||
eq(env.events, {
|
||||
{
|
||||
name = env.Roles.events.on_player_roles_changed,
|
||||
tick = 1,
|
||||
player_index = 2,
|
||||
by_player_index = 1,
|
||||
player_index = players.bob.index,
|
||||
by_player_index = players.alice.index,
|
||||
assigned = {},
|
||||
unassigned = { 6 },
|
||||
unassigned = { regular.id },
|
||||
},
|
||||
}, "the event carries the change")
|
||||
check(#env.printed == 0, "silent suppresses the announcement")
|
||||
eq(env.sounds, { "bob:utility/game_lost" }, "the unassign sound plays")
|
||||
end)
|
||||
|
||||
test("reject_assignment 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")
|
||||
local jail = env.R("Jail")
|
||||
jail:assign(players.alice)
|
||||
check(jail:has_player(players.alice), "the role applies before rejection")
|
||||
|
||||
env.reset_log()
|
||||
env.Roles.reject_assignment{ name = "alice", role_ids = { 7 } }
|
||||
check(not env.R("Jail"):has_player(players.alice), "the rejected role is rolled back")
|
||||
env.Roles.reject_assignment{ name = "alice", role_ids = { jail.id } }
|
||||
check(not jail:has_player(players.alice), "the rejected role is rolled back")
|
||||
check(#env.sent == 0, "the rollback is not sent to the controller")
|
||||
deep_eq(env.events, {
|
||||
eq(env.events, {
|
||||
{
|
||||
name = env.Roles.events.on_player_roles_changed,
|
||||
tick = 1,
|
||||
player_index = 1,
|
||||
player_index = players.alice.index,
|
||||
by_player_index = 0,
|
||||
assigned = {},
|
||||
unassigned = { 7 },
|
||||
unassigned = { jail.id },
|
||||
},
|
||||
}, "the rollback raises the event")
|
||||
|
||||
@@ -103,44 +113,46 @@ test("reject_assignment rolls the assignment back", function(env)
|
||||
check(sd.local_players.alice == nil and sd.pending.alice == nil, "the rollback clears the local state")
|
||||
end)
|
||||
|
||||
test("role:assign with local_only never reaches the controller", function(env)
|
||||
test(".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 })
|
||||
local regular = env.R("Regular")
|
||||
regular:assign(players.alice, { silent = true, local_only = true })
|
||||
check(#env.sent == 0, "the assignment is not sent")
|
||||
check(env.R("Regular"):has_player(players.alice), "the role applies")
|
||||
check(regular:has_player(players.alice), "the role applies")
|
||||
|
||||
local sd = env.Roles._script_data()
|
||||
check(sd.pending.alice == nil, "the role is not pending")
|
||||
eq(sd.local_players.alice, { 6 }, "the role is held locally")
|
||||
eq(sd.local_players.alice, { regular.id }, "the role is held locally")
|
||||
|
||||
env.Roles.receive_assignment_updates{ env.assignment("alice", { 5 }) }
|
||||
check(env.R("Regular"):has_player(players.alice), "the role survives controller updates")
|
||||
env.Roles.receive_assignment_updates{ env.assignment("alice", { "Moderator" }) }
|
||||
check(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")
|
||||
regular:unassign(players.alice, { local_only = true })
|
||||
check(not regular:has_player(players.alice) and #env.sent == 0, "removed without sync")
|
||||
end)
|
||||
|
||||
test("role:assign of a higher priority role suppresses the rest", function(env)
|
||||
test(".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 })
|
||||
local jail = env.R("Jail")
|
||||
jail:assign(players.alice, { by_player_name = "<server>", silent = true })
|
||||
eq(Suite.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")
|
||||
eq(env.events[1].assigned, { 7 }, "the event lists the jail role")
|
||||
eq(env.events[1].assigned, { jail.id }, "the event lists the jail role")
|
||||
check(#env.events[1].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 })
|
||||
jail:unassign(players.alice, { by_player_name = "<server>", silent = true })
|
||||
eq(Suite.sorted(Suite.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" },
|
||||
"unjail restores the roles")
|
||||
check(#env.events[1].assigned == 0, "the restored roles are not listed as assigned")
|
||||
eq(env.events[1].unassigned, { 7 }, "the unjail event lists the jail role")
|
||||
eq(env.events[1].unassigned, { jail.id }, "the unjail event lists the jail role")
|
||||
end)
|
||||
|
||||
test("role:assign ignores the server", function(env)
|
||||
test(".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")
|
||||
|
||||
+22
-14
@@ -1,8 +1,7 @@
|
||||
--[[-- Test environment for module/control.lua
|
||||
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.
|
||||
extends each environment with a fresh copy of the roles module and the
|
||||
fixtures below, and hands each test file a suite built around that.
|
||||
|
||||
Run as a chunk by test/lua/runner.js, receiving the shared folder. The suite
|
||||
returned here becomes `...` in each test file.
|
||||
@@ -12,7 +11,6 @@ local shared_root = ... --- @type string
|
||||
local source = assert(debug.getinfo(1, "S")).source
|
||||
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"))()
|
||||
|
||||
--- Standard fixture: permission names sent once and referenced by zero based index
|
||||
@@ -43,21 +41,33 @@ local function role_records()
|
||||
}
|
||||
end
|
||||
|
||||
--- Create an independent environment with a fresh copy of the roles module
|
||||
local function make_env()
|
||||
local env = Stubs.new()
|
||||
--- Fixture role ids by name, avoiding a search of the roles on every lookup
|
||||
local role_ids = {}
|
||||
for _, record in ipairs(role_records()) do
|
||||
role_ids[record.name] = record.id
|
||||
end
|
||||
|
||||
return Framework.suite(function(env)
|
||||
env.Roles = assert(loadfile(plugin_root .. "/module/control.lua"))()
|
||||
env.Roles.on_server_startup()
|
||||
|
||||
--- Get a role by name, failing the test when it does not exist
|
||||
--- Get a fixture role by name, failing the test when it does not exist
|
||||
--- Roles added by a test are found by a search instead
|
||||
function env.R(name)
|
||||
local role_id = role_ids[name]
|
||||
if role_id then
|
||||
return (assert(env.Roles.get_role(role_id), "No role with id " .. role_id))
|
||||
end
|
||||
return (assert(env.Roles.get_role_by_name(name), "No role named " .. name))
|
||||
end
|
||||
|
||||
--- An assignment record as the controller would send it
|
||||
function env.assignment(name, role_ids)
|
||||
return { name = name, role_ids = role_ids }
|
||||
--- An assignment record as the controller would send it, roles by name
|
||||
function env.assignment(player_name, role_names)
|
||||
local ids = {}
|
||||
for index, role_name in ipairs(role_names) do
|
||||
ids[index] = assert(role_ids[role_name], "No fixture role named " .. role_name)
|
||||
end
|
||||
return { name = player_name, role_ids = ids }
|
||||
end
|
||||
|
||||
--- Load the standard fixture and the given assignments
|
||||
@@ -70,6 +80,4 @@ local function make_env()
|
||||
end
|
||||
|
||||
return env
|
||||
end
|
||||
|
||||
return Framework.new_suite(make_env)
|
||||
end)
|
||||
|
||||
@@ -5,52 +5,53 @@ 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)
|
||||
local players = {
|
||||
alice = env.add_player("alice", 1),
|
||||
bob = env.add_player("bob", 2),
|
||||
dave = env.add_player("dave", 4, false),
|
||||
alice = env.add_player("alice"),
|
||||
bob = env.add_player("bob"),
|
||||
dave = env.add_player("dave", false),
|
||||
}
|
||||
env.initialise{
|
||||
env.assignment("alice", { 5 }),
|
||||
env.assignment("bob", { 6 }),
|
||||
env.assignment("dave", { 5 }),
|
||||
env.assignment("zed", { 5 }),
|
||||
env.assignment("alice", { "Moderator" }),
|
||||
env.assignment("bob", { "Regular" }),
|
||||
env.assignment("dave", { "Moderator" }),
|
||||
env.assignment("zed", { "Moderator" }),
|
||||
}
|
||||
env.Roles.set_emit_events(true)
|
||||
return players
|
||||
end
|
||||
|
||||
test("role:get_player_names includes players not on this map", function(env)
|
||||
test(".get_player_names() includes players not on this map", function(env)
|
||||
setup(env)
|
||||
eq(Suite.sorted(env.R("Moderator"):get_player_names()), { "alice", "dave", "zed" },
|
||||
"every player given the role is listed")
|
||||
end)
|
||||
|
||||
test("role:get_player_names counts a player in both lists once", function(env)
|
||||
test(".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", { "Moderator", "Regular" }) }
|
||||
|
||||
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")
|
||||
eq(Suite.sorted(env.R("Regular"):get_player_names()), { "alice", "bob" }, "the player is counted once")
|
||||
end)
|
||||
|
||||
test("role:get_players is limited to this map and can be filtered", function(env)
|
||||
test(".get_players() is limited to this map and can be filtered", function(env)
|
||||
setup(env)
|
||||
local mod = env.R("Moderator")
|
||||
eq(Suite.sorted(Suite.names(mod:get_players())), { "alice", "dave" }, "players are limited to this map")
|
||||
eq(Suite.names(mod:get_players(true)), { "alice" }, "filtered to connected players")
|
||||
eq(Suite.names(mod:get_players(false)), { "dave" }, "filtered to offline players")
|
||||
local moderator = env.R("Moderator")
|
||||
eq(Suite.sorted(Suite.names(moderator:get_players())), { "alice", "dave" }, "players are limited to this map")
|
||||
eq(Suite.names(moderator:get_players(true)), { "alice" }, "filtered to connected players")
|
||||
eq(Suite.names(moderator:get_players(false)), { "dave" }, "filtered to offline players")
|
||||
end)
|
||||
|
||||
test("role:print reaches online holders", function(env)
|
||||
test(".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")
|
||||
check(env.R("Jail"):print("hello") == 0, "a role with no online holders reaches nobody")
|
||||
end)
|
||||
|
||||
test("role:print over get_higher_roles can repeat", function(env)
|
||||
test(".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()
|
||||
|
||||
@@ -2,42 +2,48 @@
|
||||
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("get_role returns the role with the given clusterio id", function(env)
|
||||
test("get_role() returns the role with the given clusterio id", function(env)
|
||||
env.initialise{}
|
||||
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(env.R("Regular").id) == env.R("Regular"), "the role is found by id")
|
||||
check(env.Roles.get_role(9) == nil, "deleted roles are not known")
|
||||
end)
|
||||
|
||||
test("get_role_by_name searches the roles", 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("Moderator") == env.R("Moderator"), "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)
|
||||
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)
|
||||
test("get_ordered_roles() returns the most privileged first", function(env)
|
||||
env.initialise{}
|
||||
eq(Suite.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)
|
||||
test("sort_roles() orders most privileged first and breaks ties on the id", function(env)
|
||||
env.initialise{}
|
||||
local sorted = env.Roles.sort_roles{ env.R("Player"), env.R("Cluster Admin"), env.R("Regular") }
|
||||
eq(Suite.names(sorted), { "Cluster Admin", "Regular", "Player" }, "the list is sorted in place")
|
||||
|
||||
-- A role with the same order as Regular but a higher id sorts after it
|
||||
env.Roles.receive_role_updates{
|
||||
{ id = 10, name = "Twin", permissions = {}, meta = { id = 0, order = env.R("Regular").order } },
|
||||
}
|
||||
local tied = env.Roles.sort_roles{ env.R("Twin"), env.R("Regular") }
|
||||
eq(Suite.names(tied), { "Regular", "Twin" }, "order ties break on the id")
|
||||
end)
|
||||
|
||||
test("get_default_role follows is_default", function(env)
|
||||
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)
|
||||
test("get_higher_roles() and get_lower_roles() include the role and exclude the default", function(env)
|
||||
env.initialise{}
|
||||
eq(Suite.sorted(Suite.names(env.Roles.get_higher_roles(env.R("Regular")))),
|
||||
{ "Cluster Admin", "Moderator", "Regular" }, "higher roles")
|
||||
@@ -45,14 +51,14 @@ test("get_higher_roles and get_lower_roles include the role and exclude the defa
|
||||
{ "Jail", "Regular" }, "lower roles")
|
||||
end)
|
||||
|
||||
test("role:is_higher_than and role:is_lower_than compare on order", function(env)
|
||||
test(".is_higher_than() and .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)
|
||||
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")
|
||||
|
||||
@@ -5,18 +5,18 @@ 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)
|
||||
local players = {
|
||||
alice = env.add_player("alice", 1),
|
||||
bob = env.add_player("bob", 2),
|
||||
carol = env.add_player("carol", 3),
|
||||
alice = env.add_player("alice"),
|
||||
bob = env.add_player("bob"),
|
||||
carol = env.add_player("carol"),
|
||||
}
|
||||
env.initialise{
|
||||
env.assignment("alice", { 5 }),
|
||||
env.assignment("bob", { 6 }),
|
||||
env.assignment("alice", { "Moderator" }),
|
||||
env.assignment("bob", { "Regular" }),
|
||||
}
|
||||
return players
|
||||
end
|
||||
|
||||
test("get_player_roles includes the default role", function(env)
|
||||
test("get_player_roles() includes the default role", function(env)
|
||||
local players = setup(env)
|
||||
eq(Suite.sorted(Suite.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" },
|
||||
"held roles and the default role")
|
||||
@@ -24,7 +24,7 @@ test("get_player_roles includes the default role", function(env)
|
||||
"a player with no roles has the default role")
|
||||
end)
|
||||
|
||||
test("get_player_roles treats nil and index 0 as the server", function(env)
|
||||
test("get_player_roles() treats nil and index 0 as the server", function(env)
|
||||
setup(env)
|
||||
eq(Suite.names(env.Roles.get_player_roles(nil)), { "<server>" }, "nil is the server")
|
||||
eq(Suite.names(env.Roles.get_player_roles(env.server)), { "<server>" }, "index 0 is the server")
|
||||
@@ -32,20 +32,28 @@ test("get_player_roles treats nil and index 0 as the server", function(env)
|
||||
check(not env.R("Player"):has_player(nil), "the server does not have the default role")
|
||||
end)
|
||||
|
||||
test("get_player_highest_role", function(env)
|
||||
test("get_player_highest_role() returns the most privileged role", function(env)
|
||||
local players = setup(env)
|
||||
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("player_has_permission", function(env)
|
||||
test("get_player_highest_role() errors when a player has no roles", function(env)
|
||||
local players = setup(env)
|
||||
env.Roles.receive_role_updates{
|
||||
{ id = env.R("Player").id, name = "Player", permissions = {}, meta = { id = 0, order = 5 }, is_deleted = true },
|
||||
}
|
||||
check(not pcall(env.Roles.get_player_highest_role, players.carol), "no default role and no held roles errors")
|
||||
end)
|
||||
|
||||
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("player_has_any_permission and player_has_all_permission", 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")
|
||||
@@ -56,23 +64,24 @@ test("player_has_any_permission and player_has_all_permission", function(env)
|
||||
check(not env.Roles.player_has_all_permission(players.bob, jail, kill), "all fails when one is missing")
|
||||
check(env.Roles.player_has_all_permission(players.bob), "all of none is true")
|
||||
check(env.Roles.player_has_any_permission(nil, "anything.at.all"), "the server passes any")
|
||||
check(env.Roles.player_has_all_permission(nil, "anything.at.all", "and.this.too"), "the server passes all")
|
||||
end)
|
||||
|
||||
test("role:has_permission", function(env)
|
||||
test(".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")
|
||||
end)
|
||||
|
||||
test("role:has_player", function(env)
|
||||
test(".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("player_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")
|
||||
|
||||
+44
-31
@@ -1,61 +1,69 @@
|
||||
--- 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
|
||||
local test, check, eq = Suite.test, Suite.check, Suite.eq
|
||||
|
||||
--- alice is a moderator and carol has only the default role, both connected
|
||||
local function setup(env)
|
||||
local players = {
|
||||
alice = env.add_player("alice", 1),
|
||||
carol = env.add_player("carol", 3),
|
||||
alice = env.add_player("alice"),
|
||||
carol = env.add_player("carol"),
|
||||
}
|
||||
env.admin_state = {}
|
||||
env.Roles.define_permission_trigger("exp_scenario.player.admin", function(player, state)
|
||||
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", { "Moderator" }),
|
||||
env.assignment("zed", { "Moderator", "Regular" }), -- has never joined this map
|
||||
}
|
||||
env.Roles.set_emit_events(true)
|
||||
return players
|
||||
end
|
||||
|
||||
test("initialise applies triggers and raises empty events", function(env)
|
||||
test("initialise() applies triggers and raises empty events", function(env)
|
||||
setup(env)
|
||||
deep_eq(env.admin_state, { alice = true, carol = false }, "triggers run for connected players")
|
||||
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].assigned == 0 and #env.events[1].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("initialise is authoritative for pending roles", function(env)
|
||||
test("initialise() skips deleted assignments", function(env)
|
||||
local players = setup(env)
|
||||
local record = env.assignment("carol", { "Moderator" })
|
||||
record.is_deleted = true
|
||||
env.initialise{ record }
|
||||
check(not env.R("Moderator"):has_player(players.carol), "a deleted assignment is ignored")
|
||||
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()
|
||||
eq(sd.pending.carol, { 5 }, "the role is pending before initialise")
|
||||
eq(sd.pending.carol, { env.R("Moderator").id }, "the role is pending before initialise")
|
||||
|
||||
env.reset_log()
|
||||
env.initialise{ env.assignment("alice", { 5 }) }
|
||||
env.initialise{ env.assignment("alice", { "Moderator" }) }
|
||||
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")
|
||||
eq(sd.local_players.carol, { env.R("Jail").id }, "local only roles are kept")
|
||||
check(#env.sent == 0, "initialise sends nothing")
|
||||
end)
|
||||
|
||||
test("receive_assignment_updates applies controller changes", function(env)
|
||||
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", { "Moderator" }) }
|
||||
check(env.R("Moderator"):has_player(players.carol), "the assignment applies")
|
||||
deep_eq(env.events, {
|
||||
eq(env.events, {
|
||||
{
|
||||
name = env.Roles.events.on_player_roles_changed,
|
||||
tick = 1,
|
||||
player_index = 3,
|
||||
player_index = players.carol.index,
|
||||
by_player_index = 0,
|
||||
assigned = { 5 },
|
||||
assigned = { env.R("Moderator").id },
|
||||
unassigned = {},
|
||||
},
|
||||
}, "the event carries the change with no by player")
|
||||
@@ -65,53 +73,58 @@ test("receive_assignment_updates applies controller changes", 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")
|
||||
eq(env.events[1].unassigned, { 5 }, "the removal raises the event")
|
||||
eq(env.events[1].unassigned, { env.R("Moderator").id }, "the removal raises the event")
|
||||
check(env.admin_state.carol == false, "triggers follow the removal")
|
||||
end)
|
||||
|
||||
test("receive_assignment_updates for players not on this map raises 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", { "Moderator" }) }
|
||||
check(#env.events == 0 and #env.printed == 0, "no event and no announcement")
|
||||
end)
|
||||
|
||||
test("receive_role_updates applies to the holders", function(env)
|
||||
test("receive_role_updates() applies to the holders", function(env)
|
||||
setup(env)
|
||||
env.reset_log()
|
||||
local regular_id = env.R("Regular").id
|
||||
env.Roles.receive_role_updates{
|
||||
{ id = 6, name = "Regular", permissions = { "exp_scenario.command.jail" }, meta = { id = 0, order = 3 } },
|
||||
{ id = regular_id, name = "Regular", permissions = { "exp_scenario.command.jail" }, meta = { id = 0, order = 3 } },
|
||||
}
|
||||
check(env.R("Regular"):has_permission("exp_scenario.command.jail"), "the permission change applies")
|
||||
check(#env.events == 2, "a role change raises for every connected player")
|
||||
check(#env.events[1].assigned == 0, "role change events are empty")
|
||||
|
||||
env.Roles.receive_role_updates{
|
||||
{ id = 6, name = "Regular", permissions = {}, meta = { id = 0, order = 3 }, is_deleted = true },
|
||||
{ id = regular_id, name = "Regular", permissions = {}, meta = { id = 0, order = 3 }, is_deleted = true },
|
||||
}
|
||||
check(env.Roles.get_role(6) == nil, "a deleted role is removed")
|
||||
check(env.Roles.get_role(regular_id) == nil, "a deleted role is removed")
|
||||
end)
|
||||
|
||||
test("receive_role_updates moves the default role", 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 = env.R("Player").id, 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")
|
||||
eq(Suite.names(env.Roles.get_player_roles(players.carol)), { "Guest" }, "players pick up the new default")
|
||||
end)
|
||||
|
||||
test("set_emit_events gates what is sent", function(env)
|
||||
test("set_emit_events() gates what is sent and defaults to enabled", function(env)
|
||||
local players = setup(env)
|
||||
env.Roles.set_emit_events(false)
|
||||
env.reset_log()
|
||||
env.R("Regular"):assign(players.alice)
|
||||
check(#env.sent == 0, "the assignment is not sent")
|
||||
check(#env.sent == 0, "nothing is sent while disabled")
|
||||
check(env.R("Regular"):has_player(players.alice), "the assignment still applies locally")
|
||||
|
||||
env.Roles.set_emit_events()
|
||||
env.R("Jail"):assign(players.alice, { silent = true })
|
||||
check(#env.sent == 1, "no argument enables sending")
|
||||
end)
|
||||
|
||||
test("on_load restores the state after a save and load", function(env)
|
||||
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()
|
||||
@@ -123,10 +136,10 @@ test("on_load restores the state after a save and load", function(env)
|
||||
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)
|
||||
test("on_player_joined_game() runs the triggers", function(env)
|
||||
local players = setup(env)
|
||||
env.admin_state.alice = nil
|
||||
env.Roles.on_player_joined_game{ player_index = 1 }
|
||||
env.Roles.on_player_joined_game{ player_index = players.alice.index }
|
||||
check(env.admin_state.alice ~= nil, "triggers run when a player joins")
|
||||
end)
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ t.test("class AssignmentUpdateRequest", subtest => {
|
||||
subtest.end();
|
||||
});
|
||||
|
||||
t.test("encodeRolesForLua sends each permission name once", subtest => {
|
||||
t.test("encodeRolesForLua() sends each permission name once", subtest => {
|
||||
const meta = id => new messages.RoleMetaRecord(id, id);
|
||||
const encoded = messages.encodeRolesForLua([
|
||||
new messages.RoleRecord(1, "A", ["p.one", "p.two"], meta(1)),
|
||||
|
||||
@@ -15,7 +15,7 @@ t.test("seedRoles grant only defined permissions", subtest => {
|
||||
subtest.end();
|
||||
});
|
||||
|
||||
t.test("flattenSeedPermissions carries parents into their children", 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) {
|
||||
|
||||
+13
-21
@@ -1,11 +1,15 @@
|
||||
--[[-- Test framework for plugin module tests
|
||||
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.
|
||||
or method it covers. Every test function receives a fresh environment: the
|
||||
stubs extended by the function the suite was created with, so tests are
|
||||
independent and can not leak state into each other.
|
||||
]]
|
||||
|
||||
local source = assert(debug.getinfo(1, "S")).source
|
||||
local shared_root = assert(source:match("^@(.*)/framework%.lua$"))
|
||||
local Stubs = assert(loadfile(shared_root .. "/stubs.lua"))()
|
||||
|
||||
local Framework = {}
|
||||
|
||||
--- Turn a value into a string for failure messages
|
||||
@@ -21,8 +25,8 @@ local function repr(value, depth)
|
||||
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)
|
||||
--- @param extend_env fun(env: table): table Extends the stubs given to each test
|
||||
function Framework.suite(extend_env)
|
||||
--- @class Suite
|
||||
local Suite = {
|
||||
tests = {}, -- { name, fn } in declaration order
|
||||
@@ -70,20 +74,6 @@ function Framework.new_suite(make_env)
|
||||
end
|
||||
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
|
||||
local function deep_equal(a, b)
|
||||
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
|
||||
@@ -97,7 +87,7 @@ function Framework.new_suite(make_env)
|
||||
end
|
||||
|
||||
--- Check two values are equal, comparing tables recursively
|
||||
function Suite.deep_eq(a, b, name)
|
||||
function Suite.eq(a, b, name)
|
||||
Suite.check(deep_equal(a, b), name, ("%s ~= %s"):format(repr(a, 1), repr(b, 1)))
|
||||
end
|
||||
|
||||
@@ -126,7 +116,9 @@ function Framework.new_suite(make_env)
|
||||
function Suite.run()
|
||||
for _, test in ipairs(Suite.tests) do
|
||||
current_test = test.name
|
||||
local ok, err = pcall(test.fn, make_env())
|
||||
local ok, err = pcall(function()
|
||||
test.fn(extend_env(Stubs.new()))
|
||||
end)
|
||||
if not ok then
|
||||
Suite.fail("did not error", tostring(err))
|
||||
end
|
||||
|
||||
+3
-2
@@ -95,8 +95,9 @@ function Stubs.new()
|
||||
get_player = function(key) return players[key] end,
|
||||
}, { "player" })
|
||||
|
||||
--- Add a player to the stubbed game
|
||||
function stubs.add_player(name, index, is_connected)
|
||||
--- Add a player to the stubbed game, indexes are assigned in join order
|
||||
function stubs.add_player(name, is_connected)
|
||||
local index = #players_by_index + 1
|
||||
local player = Stubs.strict("LuaPlayer " .. name, {
|
||||
name = name,
|
||||
index = index,
|
||||
|
||||
Reference in New Issue
Block a user