Files
clawdbot/extensions/twitch/src/access-control.test.ts
jaydenfyi f5c90f0e5c feat: Twitch Plugin (#1612)
* 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>
2026-01-26 13:48:10 -06:00

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([]);
});
});