134 lines
6.8 KiB
JavaScript
134 lines
6.8 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const root = path.join(__dirname, "..");
|
|
const sandbox = fs.mkdtempSync(path.join(root, ".tmp-lumi-command-policies-"));
|
|
const serviceDir = path.join(sandbox, "src", "services");
|
|
let database = null;
|
|
|
|
(async () => {
|
|
try {
|
|
fs.cpSync(path.join(root, "src", "services"), serviceDir, { recursive: true });
|
|
database = require(path.join(serviceDir, "db.js"));
|
|
database.migrate();
|
|
const conditional = require(path.join(serviceDir, "command-conditional.js"));
|
|
const policies = require(path.join(serviceDir, "command-policies.js"));
|
|
const defaultGroup = policies.listGroups().find((group) => group.is_default);
|
|
assert(defaultGroup, "a protected default command group must be created automatically");
|
|
assert.throws(() => policies.deleteGroup(defaultGroup.id), /cannot be deleted/);
|
|
|
|
const replies = conditional.normalizeConditionalReplies([
|
|
{ keyword: "backseating", response: "Please avoid backseating." },
|
|
{ keyword: "spoilers", response: "No spoilers." }
|
|
]);
|
|
assert.strictEqual(conditional.findConditionalReply(replies, "backseating").match.response, "Please avoid backseating.");
|
|
const fuzzy = conditional.findConditionalReply(replies, "backseatng");
|
|
assert.strictEqual(fuzzy.match.keyword, "backseating");
|
|
assert.strictEqual(fuzzy.fuzzy, true);
|
|
assert.strictEqual(conditional.findConditionalReply(replies, "something unrelated").match, null);
|
|
const aliases = conditional.conditionalRepliesFromBody({
|
|
conditional_keyword: "backseat, backseating, hints",
|
|
conditional_response: "Please ask before helping."
|
|
});
|
|
assert.strictEqual(aliases.ok, true);
|
|
assert.strictEqual(aliases.replies[0].keyword, "backseat, backseating, hints");
|
|
assert.strictEqual(conditional.normalizeConditionalReplyRows(JSON.stringify(aliases.replies))[0].keyword, "backseat, backseating, hints");
|
|
for (const alias of ["backseat", "backseating", "hints"]) {
|
|
assert.strictEqual(conditional.findConditionalReply(aliases.replies, alias).match.response, "Please ask before helping.");
|
|
}
|
|
const duplicateAlias = conditional.conditionalRepliesFromBody({
|
|
conditional_keyword: ["spoilers, leaks", "leaks"],
|
|
conditional_response: ["First", "Second"]
|
|
});
|
|
assert.strictEqual(duplicateAlias.ok, false);
|
|
assert.match(duplicateAlias.errors[0], /leaks.*more than once/);
|
|
|
|
const groupId = policies.createGroup({
|
|
name: "Verification group",
|
|
policy: {
|
|
window: { uses: 3, seconds: 30, scope: "global" },
|
|
requirement: { role: "subscriber", subscriberTier: 2 },
|
|
cost: { amount: 5 }
|
|
}
|
|
});
|
|
const key = policies.commandKey("verification", "hello");
|
|
policies.saveCommandPolicy(key, {
|
|
window: { uses: 1, seconds: 60, scope: "user" },
|
|
requirement: false,
|
|
cost: { amount: 2 }
|
|
}, [groupId]);
|
|
const resolved = policies.resolvePolicy(key);
|
|
assert.strictEqual(resolved.windows.length, 1, "a direct rate limit must replace group limits");
|
|
assert.strictEqual(resolved.windows[0].value.uses, 1);
|
|
assert.strictEqual(resolved.requirements.length, 0, "a direct public setting must replace group roles");
|
|
assert.strictEqual(resolved.cost, 2, "a direct cost must replace the group cost");
|
|
policies.updateGroup(defaultGroup.id, {
|
|
name: defaultGroup.name,
|
|
description: defaultGroup.description,
|
|
policy: { window: { uses: 4, seconds: 90, scope: "global" } }
|
|
});
|
|
const inheritedKey = policies.commandKey("verification", "inherited");
|
|
assert.strictEqual(policies.resolvePolicy(inheritedKey).windows[0].value.uses, 4, "the default group must apply without explicit membership");
|
|
policies.saveCommandPolicies([
|
|
{ commandKey: inheritedKey, policy: { cost: { amount: 3 } }, groupIds: [] },
|
|
{ commandKey: policies.commandKey("verification", "second"), policy: { requirement: false }, groupIds: [groupId] }
|
|
]);
|
|
assert.strictEqual(policies.resolvePolicy(inheritedKey).cost, 3, "batch saves must persist every submitted command");
|
|
|
|
const charges = [];
|
|
global.lumiFrameworks = {
|
|
economy: {
|
|
removeBalance(payload) { charges.push(["charge", payload]); return { ok: true }; },
|
|
addBalance(payload) { charges.push(["refund", payload]); return "refund-id"; }
|
|
}
|
|
};
|
|
const ctx = {
|
|
platform: "discord",
|
|
trigger: "hello",
|
|
raw: "!hello",
|
|
user: { id: "verification-user", platformId: "platform-user" },
|
|
platformUser: { id: "platform-user" },
|
|
meta: {}
|
|
};
|
|
const first = await policies.beginCommandUse({ commandKey: key, ctx });
|
|
assert.strictEqual(first.allowed, true);
|
|
assert.strictEqual(charges[0][0], "charge");
|
|
const limited = await policies.beginCommandUse({ commandKey: key, ctx });
|
|
assert.strictEqual(limited.allowed, false, "the second use inside the window must be denied");
|
|
await first.rollback("verification");
|
|
assert.strictEqual(charges[1][0], "refund", "failed commands must refund their logged currency cost");
|
|
const afterRollback = await policies.beginCommandUse({ commandKey: key, ctx });
|
|
assert.strictEqual(afterRollback.allowed, true, "rollback must release the rate-limit reservation");
|
|
await afterRollback.commit();
|
|
|
|
const dbColumns = database.db.prepare("PRAGMA table_info(custom_commands)").all().map((column) => column.name);
|
|
assert(dbColumns.includes("conditional_replies_json"));
|
|
assert(dbColumns.includes("conditional_fuzzy"));
|
|
assert(database.db.prepare("PRAGMA table_info(command_groups)").all().some((column) => column.name === "is_default"));
|
|
for (const table of ["command_groups", "command_group_members", "command_policies", "command_policy_uses", "command_stream_sessions"]) {
|
|
assert(database.db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table), `missing ${table}`);
|
|
}
|
|
|
|
const server = fs.readFileSync(path.join(root, "src", "web", "server.js"), "utf8");
|
|
const view = fs.readFileSync(path.join(root, "src", "web", "views", "admin-command-policies.ejs"), "utf8");
|
|
const commandView = fs.readFileSync(path.join(root, "src", "web", "views", "admin-commands.ejs"), "utf8");
|
|
assert(server.includes('/admin/command-policies') && server.includes('commandPolicyFromBody'));
|
|
assert(view.includes("Command groups") && view.includes("Save all command settings"));
|
|
assert(view.includes("data-policy-search") && view.includes("data-policy-setting-filter"));
|
|
assert(view.includes("Protected default") && view.includes("data-command-policy-batch"));
|
|
assert(commandView.includes('value="conditional"') && commandView.includes("data-conditional-replies"));
|
|
assert(commandView.includes("Separate aliases with commas"));
|
|
|
|
database.db.close();
|
|
database = null;
|
|
console.log("Command policies and conditional commands verified.");
|
|
} finally {
|
|
try { database?.db?.close?.(); } catch {}
|
|
fs.rmSync(sandbox, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|