Files
MultiRoombaRover/server/src/services/operatorCommandService/index.test.js
T

168 lines
7.0 KiB
JavaScript

// Operator Command Dispatcher Tests
// Purpose: Pins shared command permissions, access modes, and prefix parsing.
// Scope: Exercises the router with doubles; individual command behavior is covered by each command's own tests.
const test = require('node:test');
const assert = require('node:assert/strict');
const { createCommandHandlers } = require('./index');
const MODES = { OPEN: 'open', TURNS: 'turns', ADMIN: 'admin', LOCKDOWN: 'lockdown' };
const ADMIN_DENIAL = /Only admins can run that command/;
const LOCKDOWN_DENIAL = /Lockdown mode: only lockdown admins/;
const FEATURE_DENIAL = /Admin mode: only admins can run feature commands/;
function createRouter({ mode = MODES.OPEN, featureEnabled = true } = {}) {
const rovers = new Map([['rover-1', {
id: 'rover-1',
ws: {},
meta: { name: 'Roomba One', horn: { enabled: true } },
batteryState: { percentDisplay: 50 },
}]]);
const { handleCommand } = createCommandHandlers({
logger: { warn: () => {}, info: () => {} },
io: { sockets: { sockets: new Map() } },
rovers,
roverManager: {},
getMode: () => mode,
MODES,
getNickname: (entry) => entry?.data?.nickname || '',
getActiveDrivers: () => ({}),
isFeatureEnabled: () => featureEnabled,
sanitizeMentions: (text) => String(text || ''),
homeAssistantService: { getLightPolicyState: () => ({}), setAllControllableEntitiesState: () => Promise.resolve() },
// Green mode is admin-only at the dispatcher, so this double only needs to
// prove that an authorized command reaches the small standalone service.
greenModeService: { setEnabled: (enabled) => Promise.resolve(Boolean(enabled)) },
liftService: null,
neatoService: null,
listVerifiedUsers: () => [],
listDeterredUsers: () => [],
listMutedUsers: () => [],
listUsersForAdmin: () => [],
listUsersWithPermission: () => [],
listRegisteredPermissions: () => [{
key: 'audio.personalAdjustment',
commandName: 'audio-adjustment',
label: 'Personal audio adjustment',
description: 'Allows personal volume adjustments.',
}],
setUserPermission: () => null,
getGlobalObjective: () => null,
getAdminReason: () => null,
config: { commands: { prefix: 'rs' } },
transportHandlers: {
status: async (request) => request.reply({ content: '[status handler]' }),
bridge: async (request) => request.reply({ content: '[bridge handler]' }),
},
});
return async function run(text, actor) {
const replies = [];
await handleCommand({
content: text,
transport: 'web-chat',
actor,
reply: async (payload) => {
replies.push(typeof payload === 'string' ? payload : payload?.content);
},
});
return replies.join('\n');
};
}
const nonAdmin = { id: 's1', userId: 'u-alice', label: 'alice', isAdmin: false, isLockdownAdmin: false };
const admin = { id: 's1', userId: 'u-alice', label: 'alice', isAdmin: true, isLockdownAdmin: false };
test('admin-only commands stay admin-only for a non-admin', async () => {
const run = createRouter();
for (const command of ['rs lock rover-1', 'rs unlock rover-1', 'rs mode open', 'rs green on', 'rs kick alice', 'rs permissions list', 'rs ha lock Fan speed']) {
assert.match(await run(command, nonAdmin), ADMIN_DENIAL, `${command} must stay admin-only`);
}
});
// Activity locks obey the same stronger restriction as other moderation actions.
test('activity lock commands require lockdown admin during lockdown', async () => {
const run = createRouter({ mode: MODES.LOCKDOWN });
assert.match(await run('rs ha unlock Fan speed', admin), LOCKDOWN_DENIAL);
});
test('commands that police themselves still reach their handler as a non-admin', async () => {
const run = createRouter();
// These reply with their own role-specific message, so the dispatcher must not
// short-circuit them with the generic admin denial.
for (const command of ['rs goal', 'rs reason', 'rs verify list', 'rs deter list']) {
assert.doesNotMatch(await run(command, nonAdmin), ADMIN_DENIAL, `${command} enforces its own permission`);
}
});
test('system commands remain reachable by anyone', async () => {
const run = createRouter();
assert.match(await run('rs status', nonAdmin), /\[status handler\]/);
assert.match(await run('rs', nonAdmin), /\[status handler\]/);
assert.match(await run('rs help', nonAdmin), /Rover Bot Commands/);
});
test('admin mode restricts access-mode feature commands', async () => {
const run = createRouter({ mode: MODES.ADMIN });
assert.match(await run('rs lights on', nonAdmin), FEATURE_DENIAL);
});
test('lockdown still suspends the pre-existing moderation-sensitive commands', async () => {
const run = createRouter({ mode: MODES.LOCKDOWN });
for (const command of ['rs lock rover-1', 'rs mode open', 'rs lights on', 'rs green on', 'rs goal', 'rs kick alice']) {
assert.match(await run(command, admin), LOCKDOWN_DENIAL, `${command} should stay lockdown-gated`);
}
});
test('admins can toggle green mode through the shared command path', async () => {
const run = createRouter();
assert.match(await run('rs green on', admin), /Green mode enabled/);
assert.match(await run('rs green off', admin), /Green mode disabled/);
});
test('green mode rejects unknown actions with focused usage guidance', async () => {
const run = createRouter();
assert.match(await run('rs green maybe', admin), /rs green on.*rs green off/);
});
test('status and help survive lockdown', async () => {
const run = createRouter({ mode: MODES.LOCKDOWN });
assert.match(await run('rs status', nonAdmin), /\[status handler\]/);
assert.match(await run('rs help', nonAdmin), /Rover Bot Commands/);
});
test('a disabled required feature is reported before any permission check', async () => {
const run = createRouter({ featureEnabled: false });
assert.match(await run('rs lights on', nonAdmin), /Home Assistant feature is disabled/);
});
test('green mode remains available without optional Home Assistant features', async () => {
const run = createRouter({ featureEnabled: false });
assert.match(await run('rs green on', admin), /Green mode enabled/);
});
test('ordinary words that merely start with the prefix are not commands', async () => {
const run = createRouter();
assert.equal(await run('rsvp', nonAdmin), '');
assert.equal(await run('rspecial delivery', nonAdmin), '');
assert.equal(await run('hello there', nonAdmin), '');
});
test('an unknown command is not treated as public', async () => {
const run = createRouter();
// Unknown actions carry no registry entry, so they must fall through to the
// same admin denial they did before the permission refactor.
assert.match(await run('rs notacommand', nonAdmin), ADMIN_DENIAL);
assert.match(await run('rs notacommand', admin), /Rover Bot Commands/);
});
test('bot actors are ignored entirely', async () => {
const run = createRouter();
assert.equal(await run('rs status', { ...nonAdmin, bot: true }), '');
});
test('command matching is case insensitive', async () => {
const run = createRouter();
assert.match(await run('Rs Status', nonAdmin), /\[status handler\]/);
});