test: collapse helper extension test suites

This commit is contained in:
Peter Steinberger
2026-03-25 05:19:46 +00:00
parent 6f137fff76
commit bc8622c659
11 changed files with 566 additions and 579 deletions

View File

@@ -1,25 +0,0 @@
import { describe, expect, it } from "vitest";
import { resolveBlueBubblesAccount } from "./accounts.js";
describe("resolveBlueBubblesAccount", () => {
it("treats SecretRef passwords as configured when serverUrl exists", () => {
const resolved = resolveBlueBubblesAccount({
cfg: {
channels: {
bluebubbles: {
enabled: true,
serverUrl: "http://localhost:1234",
password: {
source: "env",
provider: "default",
id: "BLUEBUBBLES_PASSWORD",
},
},
},
},
});
expect(resolved.configured).toBe(true);
expect(resolved.baseUrl).toBe("http://localhost:1234");
});
});

View File

@@ -13,6 +13,15 @@ import {
resolveBlueBubblesGroupRequireMention,
resolveBlueBubblesGroupToolPolicy,
} from "./group-policy.js";
import {
inferBlueBubblesTargetChatType,
isAllowedBlueBubblesSender,
looksLikeBlueBubblesExplicitTargetId,
looksLikeBlueBubblesTargetId,
normalizeBlueBubblesMessagingTarget,
parseBlueBubblesAllowTarget,
parseBlueBubblesTarget,
} from "./targets.js";
import { DEFAULT_WEBHOOK_PATH } from "./webhook-shared.js";
async function createBlueBubblesConfigureAdapter() {
@@ -144,6 +153,29 @@ describe("bluebubbles setup surface", () => {
});
});
describe("resolveBlueBubblesAccount", () => {
it("treats SecretRef passwords as configured when serverUrl exists", () => {
const resolved = resolveBlueBubblesAccount({
cfg: {
channels: {
bluebubbles: {
enabled: true,
serverUrl: "http://localhost:1234",
password: {
source: "env",
provider: "default",
id: "BLUEBUBBLES_PASSWORD",
},
},
},
},
});
expect(resolved.configured).toBe(true);
expect(resolved.baseUrl).toBe("http://localhost:1234");
});
});
describe("BlueBubblesConfigSchema", () => {
it("accepts account config when serverUrl and password are both set", () => {
const parsed = BlueBubblesConfigSchema.safeParse({
@@ -239,3 +271,218 @@ describe("bluebubbles group policy", () => {
});
});
});
describe("normalizeBlueBubblesMessagingTarget", () => {
it("normalizes chat_guid targets", () => {
expect(normalizeBlueBubblesMessagingTarget("chat_guid:ABC-123")).toBe("chat_guid:ABC-123");
});
it("normalizes group numeric targets to chat_id", () => {
expect(normalizeBlueBubblesMessagingTarget("group:123")).toBe("chat_id:123");
});
it("strips provider prefix and normalizes handles", () => {
expect(normalizeBlueBubblesMessagingTarget("bluebubbles:imessage:User@Example.com")).toBe(
"imessage:user@example.com",
);
});
it("extracts handle from DM chat_guid for cross-context matching", () => {
expect(normalizeBlueBubblesMessagingTarget("chat_guid:iMessage;-;+19257864429")).toBe(
"+19257864429",
);
expect(normalizeBlueBubblesMessagingTarget("chat_guid:SMS;-;+15551234567")).toBe(
"+15551234567",
);
expect(normalizeBlueBubblesMessagingTarget("chat_guid:iMessage;-;user@example.com")).toBe(
"user@example.com",
);
});
it("preserves group chat_guid format", () => {
expect(normalizeBlueBubblesMessagingTarget("chat_guid:iMessage;+;chat123456789")).toBe(
"chat_guid:iMessage;+;chat123456789",
);
});
it("normalizes raw chat_guid values", () => {
expect(normalizeBlueBubblesMessagingTarget("iMessage;+;chat660250192681427962")).toBe(
"chat_guid:iMessage;+;chat660250192681427962",
);
expect(normalizeBlueBubblesMessagingTarget("iMessage;-;+19257864429")).toBe("+19257864429");
});
it("normalizes chat<digits> pattern to chat_identifier format", () => {
expect(normalizeBlueBubblesMessagingTarget("chat660250192681427962")).toBe(
"chat_identifier:chat660250192681427962",
);
expect(normalizeBlueBubblesMessagingTarget("chat123")).toBe("chat_identifier:chat123");
expect(normalizeBlueBubblesMessagingTarget("Chat456789")).toBe("chat_identifier:Chat456789");
});
it("normalizes UUID/hex chat identifiers", () => {
expect(normalizeBlueBubblesMessagingTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toBe(
"chat_identifier:8b9c1a10536d4d86a336ea03ab7151cc",
);
expect(normalizeBlueBubblesMessagingTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toBe(
"chat_identifier:1C2D3E4F-1234-5678-9ABC-DEF012345678",
);
});
});
describe("looksLikeBlueBubblesTargetId", () => {
it("accepts chat targets", () => {
expect(looksLikeBlueBubblesTargetId("chat_guid:ABC-123")).toBe(true);
});
it("accepts email handles", () => {
expect(looksLikeBlueBubblesTargetId("user@example.com")).toBe(true);
});
it("accepts phone numbers with punctuation", () => {
expect(looksLikeBlueBubblesTargetId("+1 (555) 123-4567")).toBe(true);
});
it("accepts raw chat_guid values", () => {
expect(looksLikeBlueBubblesTargetId("iMessage;+;chat660250192681427962")).toBe(true);
});
it("accepts chat<digits> pattern as chat_id", () => {
expect(looksLikeBlueBubblesTargetId("chat660250192681427962")).toBe(true);
expect(looksLikeBlueBubblesTargetId("chat123")).toBe(true);
expect(looksLikeBlueBubblesTargetId("Chat456789")).toBe(true);
});
it("accepts UUID/hex chat identifiers", () => {
expect(looksLikeBlueBubblesTargetId("8b9c1a10536d4d86a336ea03ab7151cc")).toBe(true);
expect(looksLikeBlueBubblesTargetId("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toBe(true);
});
it("rejects display names", () => {
expect(looksLikeBlueBubblesTargetId("Jane Doe")).toBe(false);
});
});
describe("looksLikeBlueBubblesExplicitTargetId", () => {
it("treats explicit chat targets as immediate ids", () => {
expect(looksLikeBlueBubblesExplicitTargetId("chat_guid:ABC-123")).toBe(true);
expect(looksLikeBlueBubblesExplicitTargetId("imessage:+15551234567")).toBe(true);
});
it("prefers directory fallback for bare handles and phone numbers", () => {
expect(looksLikeBlueBubblesExplicitTargetId("+1 (555) 123-4567")).toBe(false);
expect(looksLikeBlueBubblesExplicitTargetId("user@example.com")).toBe(false);
});
});
describe("inferBlueBubblesTargetChatType", () => {
it("infers direct chat for handles and dm chat_guids", () => {
expect(inferBlueBubblesTargetChatType("+15551234567")).toBe("direct");
expect(inferBlueBubblesTargetChatType("chat_guid:iMessage;-;+15551234567")).toBe("direct");
});
it("infers group chat for explicit group targets", () => {
expect(inferBlueBubblesTargetChatType("chat_id:123")).toBe("group");
expect(inferBlueBubblesTargetChatType("chat_guid:iMessage;+;chat123")).toBe("group");
});
});
describe("parseBlueBubblesTarget", () => {
it("parses chat<digits> pattern as chat_identifier", () => {
expect(parseBlueBubblesTarget("chat660250192681427962")).toEqual({
kind: "chat_identifier",
chatIdentifier: "chat660250192681427962",
});
expect(parseBlueBubblesTarget("chat123")).toEqual({
kind: "chat_identifier",
chatIdentifier: "chat123",
});
expect(parseBlueBubblesTarget("Chat456789")).toEqual({
kind: "chat_identifier",
chatIdentifier: "Chat456789",
});
});
it("parses UUID/hex chat identifiers as chat_identifier", () => {
expect(parseBlueBubblesTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toEqual({
kind: "chat_identifier",
chatIdentifier: "8b9c1a10536d4d86a336ea03ab7151cc",
});
expect(parseBlueBubblesTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toEqual({
kind: "chat_identifier",
chatIdentifier: "1C2D3E4F-1234-5678-9ABC-DEF012345678",
});
});
it("parses explicit chat_id: prefix", () => {
expect(parseBlueBubblesTarget("chat_id:123")).toEqual({ kind: "chat_id", chatId: 123 });
});
it("parses phone numbers as handles", () => {
expect(parseBlueBubblesTarget("+19257864429")).toEqual({
kind: "handle",
to: "+19257864429",
service: "auto",
});
});
it("parses raw chat_guid format", () => {
expect(parseBlueBubblesTarget("iMessage;+;chat660250192681427962")).toEqual({
kind: "chat_guid",
chatGuid: "iMessage;+;chat660250192681427962",
});
});
});
describe("parseBlueBubblesAllowTarget", () => {
it("parses chat<digits> pattern as chat_identifier", () => {
expect(parseBlueBubblesAllowTarget("chat660250192681427962")).toEqual({
kind: "chat_identifier",
chatIdentifier: "chat660250192681427962",
});
expect(parseBlueBubblesAllowTarget("chat123")).toEqual({
kind: "chat_identifier",
chatIdentifier: "chat123",
});
});
it("parses UUID/hex chat identifiers as chat_identifier", () => {
expect(parseBlueBubblesAllowTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toEqual({
kind: "chat_identifier",
chatIdentifier: "8b9c1a10536d4d86a336ea03ab7151cc",
});
expect(parseBlueBubblesAllowTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toEqual({
kind: "chat_identifier",
chatIdentifier: "1C2D3E4F-1234-5678-9ABC-DEF012345678",
});
});
it("parses explicit chat_id: prefix", () => {
expect(parseBlueBubblesAllowTarget("chat_id:456")).toEqual({ kind: "chat_id", chatId: 456 });
});
it("parses phone numbers as handles", () => {
expect(parseBlueBubblesAllowTarget("+19257864429")).toEqual({
kind: "handle",
handle: "+19257864429",
});
});
});
describe("isAllowedBlueBubblesSender", () => {
it("denies when allowFrom is empty", () => {
const allowed = isAllowedBlueBubblesSender({
allowFrom: [],
sender: "+15551234567",
});
expect(allowed).toBe(false);
});
it("allows wildcard entries", () => {
const allowed = isAllowedBlueBubblesSender({
allowFrom: ["*"],
sender: "+15551234567",
});
expect(allowed).toBe(true);
});
});

View File

@@ -1,228 +0,0 @@
import { describe, expect, it } from "vitest";
import {
inferBlueBubblesTargetChatType,
looksLikeBlueBubblesExplicitTargetId,
isAllowedBlueBubblesSender,
looksLikeBlueBubblesTargetId,
normalizeBlueBubblesMessagingTarget,
parseBlueBubblesTarget,
parseBlueBubblesAllowTarget,
} from "./targets.js";
describe("normalizeBlueBubblesMessagingTarget", () => {
it("normalizes chat_guid targets", () => {
expect(normalizeBlueBubblesMessagingTarget("chat_guid:ABC-123")).toBe("chat_guid:ABC-123");
});
it("normalizes group numeric targets to chat_id", () => {
expect(normalizeBlueBubblesMessagingTarget("group:123")).toBe("chat_id:123");
});
it("strips provider prefix and normalizes handles", () => {
expect(normalizeBlueBubblesMessagingTarget("bluebubbles:imessage:User@Example.com")).toBe(
"imessage:user@example.com",
);
});
it("extracts handle from DM chat_guid for cross-context matching", () => {
// DM format: service;-;handle
expect(normalizeBlueBubblesMessagingTarget("chat_guid:iMessage;-;+19257864429")).toBe(
"+19257864429",
);
expect(normalizeBlueBubblesMessagingTarget("chat_guid:SMS;-;+15551234567")).toBe(
"+15551234567",
);
// Email handles
expect(normalizeBlueBubblesMessagingTarget("chat_guid:iMessage;-;user@example.com")).toBe(
"user@example.com",
);
});
it("preserves group chat_guid format", () => {
// Group format: service;+;groupId
expect(normalizeBlueBubblesMessagingTarget("chat_guid:iMessage;+;chat123456789")).toBe(
"chat_guid:iMessage;+;chat123456789",
);
});
it("normalizes raw chat_guid values", () => {
expect(normalizeBlueBubblesMessagingTarget("iMessage;+;chat660250192681427962")).toBe(
"chat_guid:iMessage;+;chat660250192681427962",
);
expect(normalizeBlueBubblesMessagingTarget("iMessage;-;+19257864429")).toBe("+19257864429");
});
it("normalizes chat<digits> pattern to chat_identifier format", () => {
expect(normalizeBlueBubblesMessagingTarget("chat660250192681427962")).toBe(
"chat_identifier:chat660250192681427962",
);
expect(normalizeBlueBubblesMessagingTarget("chat123")).toBe("chat_identifier:chat123");
expect(normalizeBlueBubblesMessagingTarget("Chat456789")).toBe("chat_identifier:Chat456789");
});
it("normalizes UUID/hex chat identifiers", () => {
expect(normalizeBlueBubblesMessagingTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toBe(
"chat_identifier:8b9c1a10536d4d86a336ea03ab7151cc",
);
expect(normalizeBlueBubblesMessagingTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toBe(
"chat_identifier:1C2D3E4F-1234-5678-9ABC-DEF012345678",
);
});
});
describe("looksLikeBlueBubblesTargetId", () => {
it("accepts chat targets", () => {
expect(looksLikeBlueBubblesTargetId("chat_guid:ABC-123")).toBe(true);
});
it("accepts email handles", () => {
expect(looksLikeBlueBubblesTargetId("user@example.com")).toBe(true);
});
it("accepts phone numbers with punctuation", () => {
expect(looksLikeBlueBubblesTargetId("+1 (555) 123-4567")).toBe(true);
});
it("accepts raw chat_guid values", () => {
expect(looksLikeBlueBubblesTargetId("iMessage;+;chat660250192681427962")).toBe(true);
});
it("accepts chat<digits> pattern as chat_id", () => {
expect(looksLikeBlueBubblesTargetId("chat660250192681427962")).toBe(true);
expect(looksLikeBlueBubblesTargetId("chat123")).toBe(true);
expect(looksLikeBlueBubblesTargetId("Chat456789")).toBe(true);
});
it("accepts UUID/hex chat identifiers", () => {
expect(looksLikeBlueBubblesTargetId("8b9c1a10536d4d86a336ea03ab7151cc")).toBe(true);
expect(looksLikeBlueBubblesTargetId("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toBe(true);
});
it("rejects display names", () => {
expect(looksLikeBlueBubblesTargetId("Jane Doe")).toBe(false);
});
});
describe("looksLikeBlueBubblesExplicitTargetId", () => {
it("treats explicit chat targets as immediate ids", () => {
expect(looksLikeBlueBubblesExplicitTargetId("chat_guid:ABC-123")).toBe(true);
expect(looksLikeBlueBubblesExplicitTargetId("imessage:+15551234567")).toBe(true);
});
it("prefers directory fallback for bare handles and phone numbers", () => {
expect(looksLikeBlueBubblesExplicitTargetId("+1 (555) 123-4567")).toBe(false);
expect(looksLikeBlueBubblesExplicitTargetId("user@example.com")).toBe(false);
});
});
describe("inferBlueBubblesTargetChatType", () => {
it("infers direct chat for handles and dm chat_guids", () => {
expect(inferBlueBubblesTargetChatType("+15551234567")).toBe("direct");
expect(inferBlueBubblesTargetChatType("chat_guid:iMessage;-;+15551234567")).toBe("direct");
});
it("infers group chat for explicit group targets", () => {
expect(inferBlueBubblesTargetChatType("chat_id:123")).toBe("group");
expect(inferBlueBubblesTargetChatType("chat_guid:iMessage;+;chat123")).toBe("group");
});
});
describe("parseBlueBubblesTarget", () => {
it("parses chat<digits> pattern as chat_identifier", () => {
expect(parseBlueBubblesTarget("chat660250192681427962")).toEqual({
kind: "chat_identifier",
chatIdentifier: "chat660250192681427962",
});
expect(parseBlueBubblesTarget("chat123")).toEqual({
kind: "chat_identifier",
chatIdentifier: "chat123",
});
expect(parseBlueBubblesTarget("Chat456789")).toEqual({
kind: "chat_identifier",
chatIdentifier: "Chat456789",
});
});
it("parses UUID/hex chat identifiers as chat_identifier", () => {
expect(parseBlueBubblesTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toEqual({
kind: "chat_identifier",
chatIdentifier: "8b9c1a10536d4d86a336ea03ab7151cc",
});
expect(parseBlueBubblesTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toEqual({
kind: "chat_identifier",
chatIdentifier: "1C2D3E4F-1234-5678-9ABC-DEF012345678",
});
});
it("parses explicit chat_id: prefix", () => {
expect(parseBlueBubblesTarget("chat_id:123")).toEqual({ kind: "chat_id", chatId: 123 });
});
it("parses phone numbers as handles", () => {
expect(parseBlueBubblesTarget("+19257864429")).toEqual({
kind: "handle",
to: "+19257864429",
service: "auto",
});
});
it("parses raw chat_guid format", () => {
expect(parseBlueBubblesTarget("iMessage;+;chat660250192681427962")).toEqual({
kind: "chat_guid",
chatGuid: "iMessage;+;chat660250192681427962",
});
});
});
describe("parseBlueBubblesAllowTarget", () => {
it("parses chat<digits> pattern as chat_identifier", () => {
expect(parseBlueBubblesAllowTarget("chat660250192681427962")).toEqual({
kind: "chat_identifier",
chatIdentifier: "chat660250192681427962",
});
expect(parseBlueBubblesAllowTarget("chat123")).toEqual({
kind: "chat_identifier",
chatIdentifier: "chat123",
});
});
it("parses UUID/hex chat identifiers as chat_identifier", () => {
expect(parseBlueBubblesAllowTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toEqual({
kind: "chat_identifier",
chatIdentifier: "8b9c1a10536d4d86a336ea03ab7151cc",
});
expect(parseBlueBubblesAllowTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toEqual({
kind: "chat_identifier",
chatIdentifier: "1C2D3E4F-1234-5678-9ABC-DEF012345678",
});
});
it("parses explicit chat_id: prefix", () => {
expect(parseBlueBubblesAllowTarget("chat_id:456")).toEqual({ kind: "chat_id", chatId: 456 });
});
it("parses phone numbers as handles", () => {
expect(parseBlueBubblesAllowTarget("+19257864429")).toEqual({
kind: "handle",
handle: "+19257864429",
});
});
});
describe("isAllowedBlueBubblesSender", () => {
it("denies when allowFrom is empty", () => {
const allowed = isAllowedBlueBubblesSender({
allowFrom: [],
sender: "+15551234567",
});
expect(allowed).toBe(false);
});
it("allows wildcard entries", () => {
const allowed = isAllowedBlueBubblesSender({
allowFrom: ["*"],
sender: "+15551234567",
});
expect(allowed).toBe(true);
});
});

View File

@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../runtime-api.js";
import { looksLikeFeishuId, normalizeFeishuTarget, resolveReceiveIdType } from "./targets.js";
const probeFeishuMock = vi.hoisted(() => vi.fn());
const createFeishuClientMock = vi.hoisted(() => vi.fn());
@@ -615,3 +616,71 @@ describe("feishuPlugin actions", () => {
).rejects.toThrow('Unsupported Feishu action: "search"');
});
});
describe("resolveReceiveIdType", () => {
it("resolves chat IDs by oc_ prefix", () => {
expect(resolveReceiveIdType("oc_123")).toBe("chat_id");
});
it("resolves open IDs by ou_ prefix", () => {
expect(resolveReceiveIdType("ou_123")).toBe("open_id");
});
it("defaults unprefixed IDs to user_id", () => {
expect(resolveReceiveIdType("u_123")).toBe("user_id");
});
it("treats explicit group targets as chat_id", () => {
expect(resolveReceiveIdType("group:oc_123")).toBe("chat_id");
});
it("treats explicit channel targets as chat_id", () => {
expect(resolveReceiveIdType("channel:oc_123")).toBe("chat_id");
});
it("treats dm-prefixed open IDs as open_id", () => {
expect(resolveReceiveIdType("dm:ou_123")).toBe("open_id");
});
});
describe("normalizeFeishuTarget", () => {
it("strips provider and user prefixes", () => {
expect(normalizeFeishuTarget("feishu:user:ou_123")).toBe("ou_123");
expect(normalizeFeishuTarget("lark:user:ou_123")).toBe("ou_123");
});
it("strips provider and chat prefixes", () => {
expect(normalizeFeishuTarget("feishu:chat:oc_123")).toBe("oc_123");
});
it("normalizes group/channel prefixes to chat ids", () => {
expect(normalizeFeishuTarget("group:oc_123")).toBe("oc_123");
expect(normalizeFeishuTarget("feishu:group:oc_123")).toBe("oc_123");
expect(normalizeFeishuTarget("channel:oc_456")).toBe("oc_456");
expect(normalizeFeishuTarget("lark:channel:oc_456")).toBe("oc_456");
});
it("accepts provider-prefixed raw ids", () => {
expect(normalizeFeishuTarget("feishu:ou_123")).toBe("ou_123");
});
it("strips provider and dm prefixes", () => {
expect(normalizeFeishuTarget("lark:dm:ou_123")).toBe("ou_123");
});
});
describe("looksLikeFeishuId", () => {
it("accepts provider-prefixed user targets", () => {
expect(looksLikeFeishuId("feishu:user:ou_123")).toBe(true);
});
it("accepts provider-prefixed chat targets", () => {
expect(looksLikeFeishuId("lark:chat:oc_123")).toBe(true);
});
it("accepts group/channel targets", () => {
expect(looksLikeFeishuId("feishu:group:oc_123")).toBe(true);
expect(looksLikeFeishuId("group:oc_123")).toBe(true);
expect(looksLikeFeishuId("channel:oc_456")).toBe(true);
});
});

View File

@@ -1,28 +0,0 @@
import { describe, expect, it } from "vitest";
import { createPluginSetupWizardStatus } from "../../../test/helpers/extensions/setup-wizard.js";
import type { OpenClawConfig } from "../runtime-api.js";
import { feishuPlugin } from "./channel.js";
const feishuGetStatus = createPluginSetupWizardStatus(feishuPlugin);
describe("feishu setup wizard status", () => {
it("treats SecretRef appSecret as configured when appId is present", async () => {
const status = await feishuGetStatus({
cfg: {
channels: {
feishu: {
appId: "cli_a123456",
appSecret: {
source: "env",
provider: "default",
id: "FEISHU_APP_SECRET",
},
},
},
} as OpenClawConfig,
accountOverrides: {},
});
expect(status.configured).toBe(true);
});
});

View File

@@ -93,6 +93,26 @@ describe("feishu setup wizard", () => {
});
describe("feishu setup wizard status", () => {
it("treats SecretRef appSecret as configured when appId is present", async () => {
const status = await feishuGetStatus({
cfg: {
channels: {
feishu: {
appId: "cli_a123456",
appSecret: {
source: "env",
provider: "default",
id: "FEISHU_APP_SECRET",
},
},
},
} as never,
accountOverrides: {},
});
expect(status.configured).toBe(true);
});
it("does not fallback to top-level appId when account explicitly sets empty appId", async () => {
const status = await feishuGetStatus({
cfg: {

View File

@@ -1,70 +0,0 @@
import { describe, expect, it } from "vitest";
import { looksLikeFeishuId, normalizeFeishuTarget, resolveReceiveIdType } from "./targets.js";
describe("resolveReceiveIdType", () => {
it("resolves chat IDs by oc_ prefix", () => {
expect(resolveReceiveIdType("oc_123")).toBe("chat_id");
});
it("resolves open IDs by ou_ prefix", () => {
expect(resolveReceiveIdType("ou_123")).toBe("open_id");
});
it("defaults unprefixed IDs to user_id", () => {
expect(resolveReceiveIdType("u_123")).toBe("user_id");
});
it("treats explicit group targets as chat_id", () => {
expect(resolveReceiveIdType("group:oc_123")).toBe("chat_id");
});
it("treats explicit channel targets as chat_id", () => {
expect(resolveReceiveIdType("channel:oc_123")).toBe("chat_id");
});
it("treats dm-prefixed open IDs as open_id", () => {
expect(resolveReceiveIdType("dm:ou_123")).toBe("open_id");
});
});
describe("normalizeFeishuTarget", () => {
it("strips provider and user prefixes", () => {
expect(normalizeFeishuTarget("feishu:user:ou_123")).toBe("ou_123");
expect(normalizeFeishuTarget("lark:user:ou_123")).toBe("ou_123");
});
it("strips provider and chat prefixes", () => {
expect(normalizeFeishuTarget("feishu:chat:oc_123")).toBe("oc_123");
});
it("normalizes group/channel prefixes to chat ids", () => {
expect(normalizeFeishuTarget("group:oc_123")).toBe("oc_123");
expect(normalizeFeishuTarget("feishu:group:oc_123")).toBe("oc_123");
expect(normalizeFeishuTarget("channel:oc_456")).toBe("oc_456");
expect(normalizeFeishuTarget("lark:channel:oc_456")).toBe("oc_456");
});
it("accepts provider-prefixed raw ids", () => {
expect(normalizeFeishuTarget("feishu:ou_123")).toBe("ou_123");
});
it("strips provider and dm prefixes", () => {
expect(normalizeFeishuTarget("lark:dm:ou_123")).toBe("ou_123");
});
});
describe("looksLikeFeishuId", () => {
it("accepts provider-prefixed user targets", () => {
expect(looksLikeFeishuId("feishu:user:ou_123")).toBe(true);
});
it("accepts provider-prefixed chat targets", () => {
expect(looksLikeFeishuId("lark:chat:oc_123")).toBe(true);
});
it("accepts group/channel targets", () => {
expect(looksLikeFeishuId("feishu:group:oc_123")).toBe(true);
expect(looksLikeFeishuId("group:oc_123")).toBe(true);
expect(looksLikeFeishuId("channel:oc_456")).toBe(true);
});
});

View File

@@ -1,105 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ResolvedGoogleChatAccount } from "./accounts.js";
import { downloadGoogleChatMedia, sendGoogleChatMessage } from "./api.js";
vi.mock("./auth.js", () => ({
getGoogleChatAccessToken: vi.fn().mockResolvedValue("token"),
}));
const account = {
accountId: "default",
enabled: true,
credentialSource: "inline",
config: {},
} as ResolvedGoogleChatAccount;
function stubSuccessfulSend(name: string) {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response(JSON.stringify({ name }), { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
async function expectDownloadToRejectForResponse(response: Response) {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(response));
await expect(
downloadGoogleChatMedia({ account, resourceName: "media/123", maxBytes: 10 }),
).rejects.toThrow(/max bytes/i);
}
describe("downloadGoogleChatMedia", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("rejects when content-length exceeds max bytes", async () => {
const body = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([1, 2, 3]));
controller.close();
},
});
const response = new Response(body, {
status: 200,
headers: { "content-length": "50", "content-type": "application/octet-stream" },
});
await expectDownloadToRejectForResponse(response);
});
it("rejects when streamed payload exceeds max bytes", async () => {
const chunks = [new Uint8Array(6), new Uint8Array(6)];
let index = 0;
const body = new ReadableStream({
pull(controller) {
if (index < chunks.length) {
controller.enqueue(chunks[index++]);
} else {
controller.close();
}
},
});
const response = new Response(body, {
status: 200,
headers: { "content-type": "application/octet-stream" },
});
await expectDownloadToRejectForResponse(response);
});
});
describe("sendGoogleChatMessage", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("adds messageReplyOption when sending to an existing thread", async () => {
const fetchMock = stubSuccessfulSend("spaces/AAA/messages/123");
await sendGoogleChatMessage({
account,
space: "spaces/AAA",
text: "hello",
thread: "spaces/AAA/threads/xyz",
});
const [url, init] = fetchMock.mock.calls[0] ?? [];
expect(String(url)).toContain("messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD"); // pragma: allowlist secret
expect(JSON.parse(String(init?.body))).toMatchObject({
text: "hello",
thread: { name: "spaces/AAA/threads/xyz" },
});
});
it("does not set messageReplyOption for non-thread sends", async () => {
const fetchMock = stubSuccessfulSend("spaces/AAA/messages/124");
await sendGoogleChatMessage({
account,
space: "spaces/AAA",
text: "hello",
});
const [url] = fetchMock.mock.calls[0] ?? [];
expect(String(url)).not.toContain("messageReplyOption=");
});
});

View File

@@ -1,97 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
verifyIdToken: vi.fn(),
}));
vi.mock("google-auth-library", () => ({
GoogleAuth: class {},
OAuth2Client: class {
verifyIdToken = mocks.verifyIdToken;
},
}));
const { verifyGoogleChatRequest } = await import("./auth.js");
function mockTicket(payload: Record<string, unknown>) {
mocks.verifyIdToken.mockResolvedValue({
getPayload: () => payload,
});
}
describe("verifyGoogleChatRequest", () => {
beforeEach(() => {
mocks.verifyIdToken.mockReset();
});
it("accepts Google Chat app-url tokens from the Chat issuer", async () => {
mockTicket({
email: "chat@system.gserviceaccount.com",
email_verified: true,
});
await expect(
verifyGoogleChatRequest({
bearer: "token",
audienceType: "app-url",
audience: "https://example.com/googlechat",
}),
).resolves.toEqual({ ok: true });
});
it("rejects add-on tokens when no principal binding is configured", async () => {
mockTicket({
email: "service-123@gcp-sa-gsuiteaddons.iam.gserviceaccount.com",
email_verified: true,
sub: "principal-1",
});
await expect(
verifyGoogleChatRequest({
bearer: "token",
audienceType: "app-url",
audience: "https://example.com/googlechat",
}),
).resolves.toEqual({
ok: false,
reason: "missing add-on principal binding",
});
});
it("accepts add-on tokens only when the bound principal matches", async () => {
mockTicket({
email: "service-123@gcp-sa-gsuiteaddons.iam.gserviceaccount.com",
email_verified: true,
sub: "principal-1",
});
await expect(
verifyGoogleChatRequest({
bearer: "token",
audienceType: "app-url",
audience: "https://example.com/googlechat",
expectedAddOnPrincipal: "principal-1",
}),
).resolves.toEqual({ ok: true });
});
it("rejects add-on tokens when the bound principal does not match", async () => {
mockTicket({
email: "service-123@gcp-sa-gsuiteaddons.iam.gserviceaccount.com",
email_verified: true,
sub: "principal-2",
});
await expect(
verifyGoogleChatRequest({
bearer: "token",
audienceType: "app-url",
audience: "https://example.com/googlechat",
expectedAddOnPrincipal: "principal-1",
}),
).resolves.toEqual({
ok: false,
reason: "unexpected add-on principal: principal-2",
});
});
});

View File

@@ -1,25 +0,0 @@
import { describe, expect, it } from "vitest";
import { isSenderAllowed } from "./monitor.js";
describe("isSenderAllowed", () => {
it("matches raw email entries only when dangerous name matching is enabled", () => {
expect(isSenderAllowed("users/123", "Jane@Example.com", ["jane@example.com"])).toBe(false);
expect(isSenderAllowed("users/123", "Jane@Example.com", ["jane@example.com"], true)).toBe(true);
});
it("does not treat users/<email> entries as email allowlist (deprecated form)", () => {
expect(isSenderAllowed("users/123", "Jane@Example.com", ["users/jane@example.com"])).toBe(
false,
);
});
it("still matches user id entries", () => {
expect(isSenderAllowed("users/abc", "jane@example.com", ["users/abc"])).toBe(true);
});
it("rejects non-matching raw email entries", () => {
expect(isSenderAllowed("users/123", "jane@example.com", ["other@example.com"], true)).toBe(
false,
);
});
});

View File

@@ -1,11 +1,58 @@
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ResolvedGoogleChatAccount } from "./accounts.js";
import { downloadGoogleChatMedia, sendGoogleChatMessage } from "./api.js";
import { resolveGoogleChatGroupRequireMention } from "./group-policy.js";
import { isSenderAllowed } from "./monitor.js";
import {
isGoogleChatSpaceTarget,
isGoogleChatUserTarget,
normalizeGoogleChatTarget,
} from "./targets.js";
const mocks = vi.hoisted(() => ({
verifyIdToken: vi.fn(),
getGoogleChatAccessToken: vi.fn().mockResolvedValue("token"),
}));
vi.mock("google-auth-library", () => ({
GoogleAuth: class {},
OAuth2Client: class {
verifyIdToken = mocks.verifyIdToken;
},
}));
vi.mock("./auth.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./auth.js")>();
return {
...actual,
getGoogleChatAccessToken: mocks.getGoogleChatAccessToken,
};
});
const { verifyGoogleChatRequest } = await import("./auth.js");
const account = {
accountId: "default",
enabled: true,
credentialSource: "inline",
config: {},
} as ResolvedGoogleChatAccount;
function stubSuccessfulSend(name: string) {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response(JSON.stringify({ name }), { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
async function expectDownloadToRejectForResponse(response: Response) {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(response));
await expect(
downloadGoogleChatMedia({ account, resourceName: "media/123", maxBytes: 10 }),
).rejects.toThrow(/max bytes/i);
}
describe("normalizeGoogleChatTarget", () => {
it("normalizes provider prefixes", () => {
expect(normalizeGoogleChatTarget("googlechat:users/123")).toBe("users/123");
@@ -54,3 +101,185 @@ describe("googlechat group policy", () => {
expect(resolveGoogleChatGroupRequireMention({ cfg, groupId: "spaces/BBB" })).toBe(true);
});
});
describe("isSenderAllowed", () => {
it("matches raw email entries only when dangerous name matching is enabled", () => {
expect(isSenderAllowed("users/123", "Jane@Example.com", ["jane@example.com"])).toBe(false);
expect(isSenderAllowed("users/123", "Jane@Example.com", ["jane@example.com"], true)).toBe(true);
});
it("does not treat users/<email> entries as email allowlist (deprecated form)", () => {
expect(isSenderAllowed("users/123", "Jane@Example.com", ["users/jane@example.com"])).toBe(
false,
);
});
it("still matches user id entries", () => {
expect(isSenderAllowed("users/abc", "jane@example.com", ["users/abc"])).toBe(true);
});
it("rejects non-matching raw email entries", () => {
expect(isSenderAllowed("users/123", "jane@example.com", ["other@example.com"], true)).toBe(
false,
);
});
});
describe("downloadGoogleChatMedia", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("rejects when content-length exceeds max bytes", async () => {
const body = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([1, 2, 3]));
controller.close();
},
});
const response = new Response(body, {
status: 200,
headers: { "content-length": "50", "content-type": "application/octet-stream" },
});
await expectDownloadToRejectForResponse(response);
});
it("rejects when streamed payload exceeds max bytes", async () => {
const chunks = [new Uint8Array(6), new Uint8Array(6)];
let index = 0;
const body = new ReadableStream({
pull(controller) {
if (index < chunks.length) {
controller.enqueue(chunks[index++]);
} else {
controller.close();
}
},
});
const response = new Response(body, {
status: 200,
headers: { "content-type": "application/octet-stream" },
});
await expectDownloadToRejectForResponse(response);
});
});
describe("sendGoogleChatMessage", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("adds messageReplyOption when sending to an existing thread", async () => {
const fetchMock = stubSuccessfulSend("spaces/AAA/messages/123");
await sendGoogleChatMessage({
account,
space: "spaces/AAA",
text: "hello",
thread: "spaces/AAA/threads/xyz",
});
const [url, init] = fetchMock.mock.calls[0] ?? [];
expect(String(url)).toContain("messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD");
expect(JSON.parse(String(init?.body))).toMatchObject({
text: "hello",
thread: { name: "spaces/AAA/threads/xyz" },
});
});
it("does not set messageReplyOption for non-thread sends", async () => {
const fetchMock = stubSuccessfulSend("spaces/AAA/messages/124");
await sendGoogleChatMessage({
account,
space: "spaces/AAA",
text: "hello",
});
const [url] = fetchMock.mock.calls[0] ?? [];
expect(String(url)).not.toContain("messageReplyOption=");
});
});
function mockTicket(payload: Record<string, unknown>) {
mocks.verifyIdToken.mockResolvedValue({
getPayload: () => payload,
});
}
describe("verifyGoogleChatRequest", () => {
it("accepts Google Chat app-url tokens from the Chat issuer", async () => {
mocks.verifyIdToken.mockReset();
mockTicket({
email: "chat@system.gserviceaccount.com",
email_verified: true,
});
await expect(
verifyGoogleChatRequest({
bearer: "token",
audienceType: "app-url",
audience: "https://example.com/googlechat",
}),
).resolves.toEqual({ ok: true });
});
it("rejects add-on tokens when no principal binding is configured", async () => {
mocks.verifyIdToken.mockReset();
mockTicket({
email: "service-123@gcp-sa-gsuiteaddons.iam.gserviceaccount.com",
email_verified: true,
sub: "principal-1",
});
await expect(
verifyGoogleChatRequest({
bearer: "token",
audienceType: "app-url",
audience: "https://example.com/googlechat",
}),
).resolves.toEqual({
ok: false,
reason: "missing add-on principal binding",
});
});
it("accepts add-on tokens only when the bound principal matches", async () => {
mocks.verifyIdToken.mockReset();
mockTicket({
email: "service-123@gcp-sa-gsuiteaddons.iam.gserviceaccount.com",
email_verified: true,
sub: "principal-1",
});
await expect(
verifyGoogleChatRequest({
bearer: "token",
audienceType: "app-url",
audience: "https://example.com/googlechat",
expectedAddOnPrincipal: "principal-1",
}),
).resolves.toEqual({ ok: true });
});
it("rejects add-on tokens when the bound principal does not match", async () => {
mocks.verifyIdToken.mockReset();
mockTicket({
email: "service-123@gcp-sa-gsuiteaddons.iam.gserviceaccount.com",
email_verified: true,
sub: "principal-2",
});
await expect(
verifyGoogleChatRequest({
bearer: "token",
audienceType: "app-url",
audience: "https://example.com/googlechat",
expectedAddOnPrincipal: "principal-1",
}),
).resolves.toEqual({
ok: false,
reason: "unexpected add-on principal: principal-2",
});
});
});