`reload` validates the binding file and sends SIGHUP to the live owner; the running connector re-reads it and swaps guildName, channels, users and limits in place. name, seat, guildId, botUserId, tokenFile, engine and context are fixed for the life of the process; a change there, an invalid file or a channel outside the guild refuses the reload and keeps the old binding. Every attempt is one line in reloads.jsonl. The service unit maps `systemctl --user reload` to the same signal. A user entry may carry `channels`, an allowlist of listed channel ids; absent means every listed channel. Outside the list the message is dropped as channel-not-for-user; threads count as their parent. Suite 41/41, 101 node tests. QUEUE rows 19 and 20 opened. Co-Authored-By: Claude Fable 5.1 <[email protected]>
80 lines
5.7 KiB
JavaScript
80 lines
5.7 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { authorize, DROP } from "../src/authorize.mjs";
|
|
import { binding, message, IDS } from "./helpers.mjs";
|
|
|
|
const b = binding();
|
|
const threads = new Map([
|
|
[IDS.threadOfAdmin, { type: 11, parentId: IDS.admin, name: "admin-thread", guildId: IDS.guild }],
|
|
[IDS.threadOfOther, { type: 11, parentId: IDS.other, name: "other-thread", guildId: IDS.guild }],
|
|
["100000000000000030", { type: 12, parentId: IDS.general, name: "private-general-thread", guildId: IDS.guild }],
|
|
["100000000000000031", { type: 0, parentId: null, name: "text-not-thread", guildId: IDS.guild }],
|
|
]);
|
|
const info = (id) => threads.get(id);
|
|
const botMention = [{ id: IDS.bot, username: "bot" }];
|
|
|
|
const table = [
|
|
["open channel, listed user", message(), { ok: true, channel: IDS.admin, thread: null }],
|
|
["wrong guild", message({ guild_id: "100000000000000099" }), { ok: false, reason: DROP.GUILD }],
|
|
["no guild (DM)", message({ guild_id: undefined }), { ok: false, reason: DROP.GUILD }],
|
|
["unlisted channel", message({ channel_id: IDS.other }), { ok: false, reason: DROP.CHANNEL }],
|
|
["unknown channel, no info", message({ channel_id: "100000000000000098" }), { ok: false, reason: DROP.CHANNEL }],
|
|
["thread of listed parent", message({ channel_id: IDS.threadOfAdmin }), { ok: true, channel: IDS.admin, thread: IDS.threadOfAdmin }],
|
|
["thread of unlisted parent", message({ channel_id: IDS.threadOfOther }), { ok: false, reason: DROP.THREAD_PARENT }],
|
|
["text channel that is not a thread and not listed", message({ channel_id: "100000000000000031" }), { ok: false, reason: DROP.CHANNEL }],
|
|
["unlisted user", message({ author: { id: IDS.stranger } }), { ok: false, reason: DROP.USER }],
|
|
["no author", message({ author: undefined }), { ok: false, reason: DROP.USER }],
|
|
["bot author (listed id, bot flag)", message({ author: { id: IDS.owner, bot: true } }), { ok: false, reason: DROP.BOT }],
|
|
["system author", message({ author: { id: IDS.owner, system: true } }), { ok: false, reason: DROP.BOT }],
|
|
["the bot itself", message({ author: { id: IDS.bot } }), { ok: false, reason: DROP.SELF }],
|
|
["webhook", message({ webhook_id: "100000000000000050" }), { ok: false, reason: DROP.WEBHOOK }],
|
|
["mention channel without mention", message({ channel_id: IDS.general }), { ok: false, reason: DROP.MENTION }],
|
|
["mention channel with bot mention", message({ channel_id: IDS.general, mentions: botMention }), { ok: true, channel: IDS.general, thread: null }],
|
|
["mention channel with @everyone only", message({ channel_id: IDS.general, mention_everyone: true, content: "@everyone hi" }), { ok: false, reason: DROP.MENTION }],
|
|
["mention channel mentioning someone else", message({ channel_id: IDS.general, mentions: [{ id: IDS.stranger }] }), { ok: false, reason: DROP.MENTION }],
|
|
["mention channel, content says @bot but mentions empty", message({ channel_id: IDS.general, content: `<@${IDS.bot}> hi` }), { ok: false, reason: DROP.MENTION }],
|
|
["private thread under mention channel, mentioned", message({ channel_id: "100000000000000030", mentions: botMention }), { ok: true, channel: IDS.general, thread: "100000000000000030" }],
|
|
["private thread under mention channel, not mentioned", message({ channel_id: "100000000000000030" }), { ok: false, reason: DROP.MENTION }],
|
|
["thread in another guild per channel info", message({ channel_id: IDS.threadOfAdmin, guild_id: IDS.guild }), { ok: true, channel: IDS.admin, thread: IDS.threadOfAdmin }],
|
|
["not an object", null, { ok: false, reason: DROP.NOT_OBJECT }],
|
|
["no id", message({ id: "" }), { ok: false, reason: DROP.NO_ID }],
|
|
["oversize content is accepted and flagged", message({ content: "x".repeat(4001) }), { ok: true, channel: IDS.admin, thread: null, oversize: true }],
|
|
["exactly the limit is not oversize", message({ content: "x".repeat(4000) }), { ok: true, channel: IDS.admin, thread: null, oversize: false }],
|
|
];
|
|
|
|
for (const [name, msg, expected] of table) {
|
|
test(`authorize: ${name}`, () => {
|
|
const r = authorize(b, msg, info);
|
|
assert.equal(r.ok, expected.ok, JSON.stringify(r));
|
|
if (!expected.ok) assert.equal(r.reason, expected.reason);
|
|
else {
|
|
assert.equal(r.channel.id, expected.channel);
|
|
assert.equal(r.thread ? r.thread.id : null, expected.thread);
|
|
if (expected.oversize !== undefined) assert.equal(r.oversize, expected.oversize);
|
|
}
|
|
});
|
|
}
|
|
|
|
test("authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected", () => {
|
|
const guest = { id: IDS.stranger, name: "guest", channels: [IDS.general] };
|
|
const scoped = binding({ users: [{ id: IDS.owner, name: "owner" }, guest] });
|
|
const asGuest = (o) => authorize(scoped, message({ author: { id: IDS.stranger }, ...o }), info);
|
|
assert.equal(asGuest({ channel_id: IDS.admin }).reason, DROP.USER_CHANNEL);
|
|
assert.equal(asGuest({ channel_id: IDS.threadOfAdmin }).reason, DROP.USER_CHANNEL);
|
|
assert.equal(asGuest({ channel_id: IDS.general, mentions: botMention }).ok, true);
|
|
assert.equal(asGuest({ channel_id: IDS.general }).reason, DROP.MENTION);
|
|
assert.equal(asGuest({ channel_id: "100000000000000030", mentions: botMention }).channel.id, IDS.general);
|
|
assert.equal(authorize(scoped, message({ channel_id: IDS.admin }), info).ok, true);
|
|
});
|
|
|
|
test("authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers)", () => {
|
|
let looked = 0;
|
|
const spy = (id) => {
|
|
looked += 1;
|
|
return info(id);
|
|
};
|
|
assert.equal(authorize(b, message({ guild_id: "100000000000000099", author: { id: IDS.stranger } }), spy).reason, DROP.GUILD);
|
|
assert.equal(authorize(b, message({ channel_id: IDS.threadOfAdmin, author: { id: IDS.stranger } }), spy).reason, DROP.USER);
|
|
assert.equal(looked, 0);
|
|
});
|