diff --git a/extensions/ringcentral/src/accounts.test.ts b/extensions/ringcentral/src/accounts.test.ts new file mode 100644 index 000000000..07df5110f --- /dev/null +++ b/extensions/ringcentral/src/accounts.test.ts @@ -0,0 +1,291 @@ +import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"; +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; +import { + listRingCentralAccountIds, + resolveDefaultRingCentralAccountId, + resolveRingCentralAccount, + listEnabledRingCentralAccounts, +} from "./accounts.js"; + +describe("listRingCentralAccountIds", () => { + it("returns default account when no accounts configured", () => { + const cfg = { channels: {} } as ClawdbotConfig; + expect(listRingCentralAccountIds(cfg)).toEqual(["default"]); + }); + + it("returns default account when ringcentral channel not configured", () => { + const cfg = { channels: { telegram: { enabled: true } } } as ClawdbotConfig; + expect(listRingCentralAccountIds(cfg)).toEqual(["default"]); + }); + + it("returns configured account IDs sorted alphabetically", () => { + const cfg = { + channels: { + ringcentral: { + accounts: { + work: { enabled: true }, + personal: { enabled: true }, + }, + }, + }, + } as ClawdbotConfig; + expect(listRingCentralAccountIds(cfg)).toEqual(["personal", "work"]); + }); +}); + +describe("resolveDefaultRingCentralAccountId", () => { + it("returns explicitly set default account", () => { + const cfg = { + channels: { + ringcentral: { + defaultAccount: "work", + accounts: { + work: {}, + personal: {}, + }, + }, + }, + } as ClawdbotConfig; + expect(resolveDefaultRingCentralAccountId(cfg)).toBe("work"); + }); + + it("returns default if included in accounts", () => { + const cfg = { + channels: { + ringcentral: { + accounts: { + default: {}, + other: {}, + }, + }, + }, + } as ClawdbotConfig; + expect(resolveDefaultRingCentralAccountId(cfg)).toBe("default"); + }); + + it("returns first account ID when no default specified", () => { + const cfg = { + channels: { + ringcentral: { + accounts: { + zebra: {}, + alpha: {}, + }, + }, + }, + } as ClawdbotConfig; + expect(resolveDefaultRingCentralAccountId(cfg)).toBe("alpha"); + }); +}); + +describe("resolveRingCentralAccount", () => { + const originalEnv = process.env; + + beforeEach(() => { + vi.resetModules(); + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it("resolves account from config credentials", () => { + const cfg = { + channels: { + ringcentral: { + enabled: true, + clientId: "test-client-id", + clientSecret: "test-client-secret", + jwt: "test-jwt", + server: "https://platform.devtest.ringcentral.com", + }, + }, + } as ClawdbotConfig; + + const account = resolveRingCentralAccount({ cfg }); + + expect(account.accountId).toBe("default"); + expect(account.enabled).toBe(true); + expect(account.credentialSource).toBe("config"); + expect(account.clientId).toBe("test-client-id"); + expect(account.clientSecret).toBe("test-client-secret"); + expect(account.jwt).toBe("test-jwt"); + expect(account.server).toBe("https://platform.devtest.ringcentral.com"); + }); + + it("resolves account from environment variables", () => { + process.env.RINGCENTRAL_CLIENT_ID = "env-client-id"; + process.env.RINGCENTRAL_CLIENT_SECRET = "env-client-secret"; + process.env.RINGCENTRAL_JWT = "env-jwt"; + process.env.RINGCENTRAL_SERVER = "https://platform.devtest.ringcentral.com"; + + const cfg = { + channels: { + ringcentral: { + enabled: true, + }, + }, + } as ClawdbotConfig; + + const account = resolveRingCentralAccount({ cfg }); + + expect(account.credentialSource).toBe("env"); + expect(account.clientId).toBe("env-client-id"); + expect(account.clientSecret).toBe("env-client-secret"); + expect(account.jwt).toBe("env-jwt"); + expect(account.server).toBe("https://platform.devtest.ringcentral.com"); + }); + + it("returns none source when no credentials configured", () => { + const cfg = { + channels: { + ringcentral: { + enabled: true, + }, + }, + } as ClawdbotConfig; + + const account = resolveRingCentralAccount({ cfg }); + + expect(account.credentialSource).toBe("none"); + expect(account.clientId).toBeUndefined(); + }); + + it("uses default server when not specified", () => { + const cfg = { + channels: { + ringcentral: { + enabled: true, + clientId: "test-client-id", + clientSecret: "test-client-secret", + jwt: "test-jwt", + }, + }, + } as ClawdbotConfig; + + const account = resolveRingCentralAccount({ cfg }); + + expect(account.server).toBe("https://platform.ringcentral.com"); + }); + + it("resolves specific account from accounts map", () => { + const cfg = { + channels: { + ringcentral: { + enabled: true, + accounts: { + work: { + clientId: "work-client-id", + clientSecret: "work-client-secret", + jwt: "work-jwt", + name: "Work Account", + }, + }, + }, + }, + } as ClawdbotConfig; + + const account = resolveRingCentralAccount({ cfg, accountId: "work" }); + + expect(account.accountId).toBe("work"); + expect(account.name).toBe("Work Account"); + expect(account.clientId).toBe("work-client-id"); + }); + + it("merges base config with account config", () => { + const cfg = { + channels: { + ringcentral: { + enabled: true, + server: "https://base.server.com", + accounts: { + work: { + clientId: "work-client-id", + clientSecret: "work-client-secret", + jwt: "work-jwt", + }, + }, + }, + }, + } as ClawdbotConfig; + + const account = resolveRingCentralAccount({ cfg, accountId: "work" }); + + expect(account.server).toBe("https://base.server.com"); + }); + + it("account config overrides base config", () => { + const cfg = { + channels: { + ringcentral: { + enabled: true, + server: "https://base.server.com", + accounts: { + work: { + clientId: "work-client-id", + clientSecret: "work-client-secret", + jwt: "work-jwt", + server: "https://work.server.com", + }, + }, + }, + }, + } as ClawdbotConfig; + + const account = resolveRingCentralAccount({ cfg, accountId: "work" }); + + expect(account.server).toBe("https://work.server.com"); + }); +}); + +describe("listEnabledRingCentralAccounts", () => { + it("returns only enabled accounts", () => { + const cfg = { + channels: { + ringcentral: { + enabled: true, + clientId: "base-id", + clientSecret: "base-secret", + jwt: "base-jwt", + accounts: { + enabled1: { + enabled: true, + clientId: "e1-id", + clientSecret: "e1-secret", + jwt: "e1-jwt", + }, + disabled1: { + enabled: false, + clientId: "d1-id", + clientSecret: "d1-secret", + jwt: "d1-jwt", + }, + }, + }, + }, + } as ClawdbotConfig; + + const accounts = listEnabledRingCentralAccounts(cfg); + + expect(accounts.map((a) => a.accountId)).toContain("enabled1"); + expect(accounts.map((a) => a.accountId)).not.toContain("disabled1"); + }); + + it("returns empty array when channel is disabled", () => { + const cfg = { + channels: { + ringcentral: { + enabled: false, + accounts: { + work: { enabled: true }, + }, + }, + }, + } as ClawdbotConfig; + + const accounts = listEnabledRingCentralAccounts(cfg); + + expect(accounts).toEqual([]); + }); +}); diff --git a/extensions/ringcentral/src/monitor.test.ts b/extensions/ringcentral/src/monitor.test.ts new file mode 100644 index 000000000..3242cb3d4 --- /dev/null +++ b/extensions/ringcentral/src/monitor.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { isSenderAllowed } from "./monitor.js"; + +describe("isSenderAllowed", () => { + it("returns true when allowFrom contains wildcard", () => { + expect(isSenderAllowed("12345", ["*"])).toBe(true); + expect(isSenderAllowed("any-user", ["*"])).toBe(true); + }); + + it("returns true when sender ID matches exactly", () => { + expect(isSenderAllowed("12345", ["12345"])).toBe(true); + expect(isSenderAllowed("12345", ["other", "12345"])).toBe(true); + }); + + it("returns true when sender ID matches with ringcentral: prefix", () => { + expect(isSenderAllowed("12345", ["ringcentral:12345"])).toBe(true); + expect(isSenderAllowed("12345", ["RINGCENTRAL:12345"])).toBe(true); + }); + + it("returns true when sender ID matches with rc: prefix", () => { + expect(isSenderAllowed("12345", ["rc:12345"])).toBe(true); + expect(isSenderAllowed("12345", ["RC:12345"])).toBe(true); + }); + + it("returns true when sender ID matches with user: prefix", () => { + expect(isSenderAllowed("12345", ["user:12345"])).toBe(true); + expect(isSenderAllowed("12345", ["USER:12345"])).toBe(true); + }); + + it("returns false when sender ID not in allowFrom", () => { + expect(isSenderAllowed("12345", ["67890"])).toBe(false); + expect(isSenderAllowed("12345", [])).toBe(false); + }); + + it("handles case-insensitive matching", () => { + expect(isSenderAllowed("ABC123", ["abc123"])).toBe(true); + expect(isSenderAllowed("abc123", ["ABC123"])).toBe(true); + }); + + it("handles whitespace in allowFrom entries", () => { + expect(isSenderAllowed("12345", [" 12345 "])).toBe(true); + }); + + it("ignores empty entries in allowFrom", () => { + expect(isSenderAllowed("12345", ["", "12345", " "])).toBe(true); + }); +}); diff --git a/extensions/ringcentral/src/targets.test.ts b/extensions/ringcentral/src/targets.test.ts new file mode 100644 index 000000000..4d5a5db4b --- /dev/null +++ b/extensions/ringcentral/src/targets.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it } from "vitest"; +import { + normalizeRingCentralTarget, + isRingCentralChatTarget, + isRingCentralUserTarget, + formatRingCentralChatTarget, + formatRingCentralUserTarget, + parseRingCentralTarget, +} from "./targets.js"; + +describe("normalizeRingCentralTarget", () => { + it("returns null for empty string", () => { + expect(normalizeRingCentralTarget("")).toBeNull(); + expect(normalizeRingCentralTarget(" ")).toBeNull(); + }); + + it("removes ringcentral: prefix", () => { + expect(normalizeRingCentralTarget("ringcentral:12345")).toBe("12345"); + expect(normalizeRingCentralTarget("RINGCENTRAL:12345")).toBe("12345"); + }); + + it("removes rc: prefix", () => { + expect(normalizeRingCentralTarget("rc:12345")).toBe("12345"); + expect(normalizeRingCentralTarget("RC:12345")).toBe("12345"); + }); + + it("removes chat: prefix", () => { + expect(normalizeRingCentralTarget("chat:12345")).toBe("12345"); + }); + + it("removes user: prefix", () => { + expect(normalizeRingCentralTarget("user:12345")).toBe("12345"); + }); + + it("removes combined prefixes", () => { + expect(normalizeRingCentralTarget("rc:chat:12345")).toBe("12345"); + expect(normalizeRingCentralTarget("ringcentral:user:12345")).toBe("12345"); + }); + + it("trims whitespace", () => { + expect(normalizeRingCentralTarget(" 12345 ")).toBe("12345"); + }); + + it("returns plain ID as-is", () => { + expect(normalizeRingCentralTarget("12345")).toBe("12345"); + }); +}); + +describe("isRingCentralChatTarget", () => { + it("returns true for numeric IDs", () => { + expect(isRingCentralChatTarget("12345")).toBe(true); + expect(isRingCentralChatTarget("rc:12345")).toBe(true); + expect(isRingCentralChatTarget("chat:12345")).toBe(true); + }); + + it("returns false for non-numeric IDs", () => { + expect(isRingCentralChatTarget("abc")).toBe(false); + expect(isRingCentralChatTarget("12345abc")).toBe(false); + }); + + it("returns false for empty string", () => { + expect(isRingCentralChatTarget("")).toBe(false); + }); +}); + +describe("isRingCentralUserTarget", () => { + it("returns true for numeric IDs", () => { + expect(isRingCentralUserTarget("12345")).toBe(true); + expect(isRingCentralUserTarget("rc:12345")).toBe(true); + }); + + it("returns false for empty string", () => { + expect(isRingCentralUserTarget("")).toBe(false); + }); +}); + +describe("formatRingCentralChatTarget", () => { + it("formats chat ID with rc:chat: prefix", () => { + expect(formatRingCentralChatTarget("12345")).toBe("rc:chat:12345"); + }); +}); + +describe("formatRingCentralUserTarget", () => { + it("formats user ID with rc:user: prefix", () => { + expect(formatRingCentralUserTarget("12345")).toBe("rc:user:12345"); + }); +}); + +describe("parseRingCentralTarget", () => { + it("parses chat: prefix", () => { + expect(parseRingCentralTarget("chat:12345")).toEqual({ type: "chat", id: "12345" }); + expect(parseRingCentralTarget("rc:chat:12345")).toEqual({ type: "chat", id: "12345" }); + expect(parseRingCentralTarget("ringcentral:chat:12345")).toEqual({ type: "chat", id: "12345" }); + }); + + it("parses user: prefix", () => { + expect(parseRingCentralTarget("user:12345")).toEqual({ type: "user", id: "12345" }); + expect(parseRingCentralTarget("rc:user:12345")).toEqual({ type: "user", id: "12345" }); + }); + + it("parses group: prefix as chat", () => { + expect(parseRingCentralTarget("group:12345")).toEqual({ type: "chat", id: "12345" }); + expect(parseRingCentralTarget("team:12345")).toEqual({ type: "chat", id: "12345" }); + }); + + it("defaults numeric IDs to chat type", () => { + expect(parseRingCentralTarget("12345")).toEqual({ type: "chat", id: "12345" }); + expect(parseRingCentralTarget("rc:12345")).toEqual({ type: "chat", id: "12345" }); + }); + + it("returns unknown for non-numeric IDs without type prefix", () => { + expect(parseRingCentralTarget("abc")).toEqual({ type: "unknown", id: "abc" }); + }); + + it("trims whitespace", () => { + expect(parseRingCentralTarget(" chat:12345 ")).toEqual({ type: "chat", id: "12345" }); + }); +});