evOo-local/packages/adapters/src/lark-messaging.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

213 lines
7.1 KiB
TypeScript

import { createCipheriv, createHash } from "node:crypto";
import type { AdapterContext, MessagingInboundEvent } from "@rakazo/adapter-kit";
import type * as LarkAdapterModule from "chat-adapter-lark";
import type { HttpInstance } from "chat-adapter-lark";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ChatSdkMessagingSurface } from "./chat-sdk-surface.js";
import { messagingPlatformsFromEnv } from "./messaging-platforms.js";
const { request } = vi.hoisted(() => ({ request: vi.fn() }));
// Exercise the pinned adapter and SDK; replace only their outbound HTTP boundary.
vi.mock("chat-adapter-lark", async (importOriginal) => {
const actual = await importOriginal<typeof LarkAdapterModule>();
return {
...actual,
createLarkAdapter: (config: Parameters<typeof actual.createLarkAdapter>[0]) =>
actual.createLarkAdapter({
...config,
httpInstance: {
request,
get: request,
delete: request,
head: request,
options: request,
put: request,
patch: request,
post: (url: string, data: unknown) => request({ url, data, method: "POST" }),
} satisfies HttpInstance,
}),
};
});
const context: AdapterContext = {
operationId: "op-test",
traceId: "trace-test",
spaceId: "space-test",
userId: "user-test",
signal: new AbortController().signal,
};
const token = "test-verification-token";
const encryptKey = "test-encryption-key";
function createSurface(encrypted = false) {
const platforms = messagingPlatformsFromEnv({
larkAppId: "cli-test",
larkAppSecret: "test-app-secret",
larkVerificationToken: token,
...(encrypted ? { larkEncryptKey: encryptKey } : {}),
});
const surface = new ChatSdkMessagingSurface(platforms);
const events: MessagingInboundEvent[] = [];
surface.onInbound(async (event) => {
events.push(event);
});
return { surface, events };
}
function message(chatType = "p2p", sender = "ou-test") {
return {
schema: "2.0",
header: { event_type: "im.message.receive_v1", event_id: "event-test", token },
event: {
sender: { sender_type: "user", sender_id: { open_id: sender } },
message: {
message_id: "om-test",
chat_id: "oc-test",
chat_type: chatType,
message_type: "text",
content: JSON.stringify({ text: "hello" }),
create_time: "1700000000000",
},
},
};
}
function webhook(body: unknown, headers: Record<string, string> = {}) {
return new Request("https://rakazo.test/api/v1/messaging/webhook/lark", {
method: "POST",
headers: { "content-type": "application/json", ...headers },
body: JSON.stringify(body),
});
}
function encryptedWebhook(payload: unknown, validSignature = true) {
const iv = Buffer.alloc(16, 1);
const key = createHash("sha256").update(encryptKey).digest();
const cipher = createCipheriv("aes-256-cbc", key, iv);
const encrypt = Buffer.concat([
iv,
cipher.update(JSON.stringify(payload)),
cipher.final(),
]).toString("base64");
const body = { encrypt };
const timestamp = "1700000000";
const nonce = "test-nonce";
const signature = createHash("sha256")
.update(timestamp + nonce + encryptKey + JSON.stringify(body))
.digest("hex");
return webhook(body, {
"x-lark-request-timestamp": timestamp,
"x-lark-request-nonce": nonce,
"x-lark-signature": validSignature ? signature : "invalid",
});
}
beforeEach(() => {
request.mockReset();
request.mockImplementation(async ({ url }: { url: string }) => {
if (url.endsWith("/auth/v3/tenant_access_token/internal")) {
return { code: 0, tenant_access_token: "test-access-token", expire: 7200 };
}
if (url.endsWith("/bot/v3/info")) {
return { code: 0, bot: { open_id: "ou-bot", app_name: "Test bot" } };
}
if (url.endsWith("/contact/v3/users/ou-test")) {
return { code: 0, data: { user: { name: "Test user" } } };
}
if (url.endsWith("/im/v1/messages")) {
return { code: 0, data: { message_id: "om-reply" } };
}
if (url.endsWith("/im/v1/chats")) {
return { code: 0, data: { chat_id: "oc-test" } };
}
throw new Error(`Unexpected HTTP request: ${url}`);
});
});
describe("Lark messaging conformance", () => {
it.each([undefined, "wrong-token"])("rejects a webhook with token %s", async (invalidToken) => {
const { surface, events } = createSurface();
const payload = message();
const response = await surface.handleWebhook(
"lark",
webhook({ ...payload, header: { ...payload.header, token: invalidToken } }),
);
expect(response?.status).toBe(403);
expect(events).toEqual([]);
});
it.each([false, true])(
"answers authenticated URL verification (encrypted: %s)",
async (encrypted) => {
const { surface, events } = createSurface(encrypted);
const payload = { type: "url_verification", token, challenge: "test-challenge" };
const response = await surface.handleWebhook(
"lark",
encrypted ? encryptedWebhook(payload) : webhook(payload),
);
expect(response?.status).toBe(200);
expect(await response?.json()).toEqual({ challenge: "test-challenge" });
expect(events).toEqual([]);
},
);
it.each([false, true])(
"delivers direct messages and sends replies (encrypted: %s)",
async (encrypted) => {
const { surface, events } = createSurface(encrypted);
const response = await surface.handleWebhook(
"lark",
encrypted ? encryptedWebhook(message()) : webhook(message()),
);
expect(response?.status).toBe(200);
expect(events).toEqual([
expect.objectContaining({
type: "message",
provider: "lark",
isDirect: true,
from: "ou-test",
handle: "om-test",
content: "hello",
}),
]);
const threadId = await surface.openDirectThread("lark", "ou-test", context);
expect(await surface.sendToThread({ threadId, body: "reply" }, context)).toEqual({
handle: "om-reply",
});
expect(request).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ receive_id: "oc-test" }),
}),
);
},
);
it("rejects an invalid encrypted signature", async () => {
const { surface, events } = createSurface(true);
expect((await surface.handleWebhook("lark", encryptedWebhook(message(), false)))?.status).toBe(
403,
);
expect(events).toEqual([]);
});
it.each([
["group", "ou-test"],
["p2p", "ou-bot"],
])("ignores %s messages from %s", async (chatType, sender) => {
const { surface, events } = createSurface();
expect((await surface.handleWebhook("lark", webhook(message(chatType, sender))))?.status).toBe(
200,
);
expect(events).toEqual([]);
});
it("returns a retryable failure when inbound persistence fails", async () => {
const { surface } = createSurface();
surface.onInbound(async () => {
throw new Error("Persistence unavailable");
});
expect((await surface.handleWebhook("lark", webhook(message())))?.status).toBe(500);
});
});