* wip * copy polugin files * wip type changes * refactor: improve Twitch plugin code quality and fix all tests - Extract client manager registry for centralized lifecycle management - Refactor to use early returns and reduce mutations - Fix status check logic for clientId detection - Add comprehensive test coverage for new modules - Remove tests for unimplemented features (index.test.ts, resolver.test.ts) - Fix mock setup issues in test suite (149 tests now passing) - Improve error handling with errorResponse helper in actions.ts - Normalize token handling to eliminate duplication Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * use accountId * delete md file * delte tsconfig * adjust log level * fix probe logic * format * fix monitor * code review fixes * format * no mutation * less mutation * chain debug log * await authProvider setup * use uuid * use spread * fix tests * update docs and remove bot channel fallback * more readme fixes * remove comments + fromat * fix tests * adjust access control logic * format * install * simplify config object * remove duplicate log tags + log received messages * update docs * update tests * format * strip markdown in monitor * remove strip markdown config, enabled by default * default requireMention to true * fix store path arg * fix multi account id + add unit test * fix multi account id + add unit test * make channel required and update docs * remove whisper functionality * remove duplicate connect log * update docs with convert twitch link * make twitch message processing non blocking * schema consistent casing * remove noisy ignore log * use coreLogger --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
490 lines
13 KiB
TypeScript
490 lines
13 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { checkTwitchAccessControl, extractMentions } from "./access-control.js";
|
|
import type { TwitchAccountConfig, TwitchChatMessage } from "./types.js";
|
|
|
|
describe("checkTwitchAccessControl", () => {
|
|
const mockAccount: TwitchAccountConfig = {
|
|
username: "testbot",
|
|
token: "oauth:test",
|
|
};
|
|
|
|
const mockMessage: TwitchChatMessage = {
|
|
username: "testuser",
|
|
userId: "123456",
|
|
message: "hello bot",
|
|
channel: "testchannel",
|
|
};
|
|
|
|
describe("when no restrictions are configured", () => {
|
|
it("allows messages that mention the bot (default requireMention)", () => {
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
};
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account: mockAccount,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("requireMention default", () => {
|
|
it("defaults to true when undefined", () => {
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "hello bot",
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account: mockAccount,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(false);
|
|
expect(result.reason).toContain("does not mention the bot");
|
|
});
|
|
|
|
it("allows mention when requireMention is undefined", () => {
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account: mockAccount,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("requireMention", () => {
|
|
it("allows messages that mention the bot", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
requireMention: true,
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
|
|
it("blocks messages that don't mention the bot", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
requireMention: true,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message: mockMessage,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(false);
|
|
expect(result.reason).toContain("does not mention the bot");
|
|
});
|
|
|
|
it("is case-insensitive for bot username", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
requireMention: true,
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@TestBot hello",
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("allowFrom allowlist", () => {
|
|
it("allows users in the allowlist", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowFrom: ["123456", "789012"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
expect(result.matchKey).toBe("123456");
|
|
expect(result.matchSource).toBe("allowlist");
|
|
});
|
|
|
|
it("allows users not in allowlist via fallback (open access)", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowFrom: ["789012"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
// Falls through to final fallback since allowedRoles is not set
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
|
|
it("blocks messages without userId", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowFrom: ["123456"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
userId: undefined,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(false);
|
|
expect(result.reason).toContain("user ID not available");
|
|
});
|
|
|
|
it("bypasses role checks when user is in allowlist", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowFrom: ["123456"],
|
|
allowedRoles: ["owner"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
isOwner: false,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
|
|
it("allows user with role even if not in allowlist", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowFrom: ["789012"],
|
|
allowedRoles: ["moderator"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
userId: "123456",
|
|
isMod: true,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
expect(result.matchSource).toBe("role");
|
|
});
|
|
|
|
it("blocks user with neither allowlist nor role", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowFrom: ["789012"],
|
|
allowedRoles: ["moderator"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
userId: "123456",
|
|
isMod: false,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(false);
|
|
expect(result.reason).toContain("does not have any of the required roles");
|
|
});
|
|
});
|
|
|
|
describe("allowedRoles", () => {
|
|
it("allows users with matching role", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowedRoles: ["moderator"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
isMod: true,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
expect(result.matchSource).toBe("role");
|
|
});
|
|
|
|
it("allows users with any of multiple roles", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowedRoles: ["moderator", "vip", "subscriber"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
isVip: true,
|
|
isMod: false,
|
|
isSub: false,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
|
|
it("blocks users without matching role", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowedRoles: ["moderator"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
isMod: false,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(false);
|
|
expect(result.reason).toContain("does not have any of the required roles");
|
|
});
|
|
|
|
it("allows all users when role is 'all'", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowedRoles: ["all"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
expect(result.matchKey).toBe("all");
|
|
});
|
|
|
|
it("handles moderator role", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowedRoles: ["moderator"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
isMod: true,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
|
|
it("handles subscriber role", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowedRoles: ["subscriber"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
isSub: true,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
|
|
it("handles owner role", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowedRoles: ["owner"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
isOwner: true,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
|
|
it("handles vip role", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowedRoles: ["vip"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
isVip: true,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("combined restrictions", () => {
|
|
it("checks requireMention before allowlist", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
requireMention: true,
|
|
allowFrom: ["123456"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "hello", // No mention
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(false);
|
|
expect(result.reason).toContain("does not mention the bot");
|
|
});
|
|
|
|
it("checks allowlist before allowedRoles", () => {
|
|
const account: TwitchAccountConfig = {
|
|
...mockAccount,
|
|
allowFrom: ["123456"],
|
|
allowedRoles: ["owner"],
|
|
};
|
|
const message: TwitchChatMessage = {
|
|
...mockMessage,
|
|
message: "@testbot hello",
|
|
isOwner: false,
|
|
};
|
|
|
|
const result = checkTwitchAccessControl({
|
|
message,
|
|
account,
|
|
botUsername: "testbot",
|
|
});
|
|
expect(result.allowed).toBe(true);
|
|
expect(result.matchSource).toBe("allowlist");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("extractMentions", () => {
|
|
it("extracts single mention", () => {
|
|
const mentions = extractMentions("hello @testbot");
|
|
expect(mentions).toEqual(["testbot"]);
|
|
});
|
|
|
|
it("extracts multiple mentions", () => {
|
|
const mentions = extractMentions("hello @testbot and @otheruser");
|
|
expect(mentions).toEqual(["testbot", "otheruser"]);
|
|
});
|
|
|
|
it("returns empty array when no mentions", () => {
|
|
const mentions = extractMentions("hello everyone");
|
|
expect(mentions).toEqual([]);
|
|
});
|
|
|
|
it("handles mentions at start of message", () => {
|
|
const mentions = extractMentions("@testbot hello");
|
|
expect(mentions).toEqual(["testbot"]);
|
|
});
|
|
|
|
it("handles mentions at end of message", () => {
|
|
const mentions = extractMentions("hello @testbot");
|
|
expect(mentions).toEqual(["testbot"]);
|
|
});
|
|
|
|
it("converts mentions to lowercase", () => {
|
|
const mentions = extractMentions("hello @TestBot");
|
|
expect(mentions).toEqual(["testbot"]);
|
|
});
|
|
|
|
it("extracts alphanumeric usernames", () => {
|
|
const mentions = extractMentions("hello @user123");
|
|
expect(mentions).toEqual(["user123"]);
|
|
});
|
|
|
|
it("handles underscores in usernames", () => {
|
|
const mentions = extractMentions("hello @test_user");
|
|
expect(mentions).toEqual(["test_user"]);
|
|
});
|
|
|
|
it("handles empty string", () => {
|
|
const mentions = extractMentions("");
|
|
expect(mentions).toEqual([]);
|
|
});
|
|
});
|