Files
stack/packages/discord/tests/authorize.test.mjs
T
jason.woltjeandClaude Fable 5.1 caaef941e6 feat(discord): binding reload without a restart, and a per-user channel allowlist (#1509)
`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]>
2026-09-13 18:59:31 -05:00

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);
});