91 lines
5.6 KiB
TypeScript
91 lines
5.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import type { QqConfig } from "../src/config.js";
|
|
import { QqAdapter } from "../src/platforms/qq/adapter.js";
|
|
|
|
const config: QqConfig = {
|
|
type: "qq", connectionMode: "webhook", appId: "id", clientSecret: "secret", botSecret: "", verifySignature: false,
|
|
botNames: ["Bot"], intents: 33_554_432, shard: [0, 1]
|
|
};
|
|
|
|
test("normalizes GROUP and C2C author openid while webhook ACK remains immediate", async () => {
|
|
const received: any[] = [];
|
|
const gateway = { receive: async (message: unknown) => { received.push(message); throw new Error("ACP failed"); } };
|
|
const adapter = new QqAdapter(config, gateway as never);
|
|
const group = await adapter.handleWebhook({ body: { op: 0, t: "GROUP_AT_MESSAGE_CREATE", d: { id: "m1", group_openid: "g1", author: { user_openid: "u1", member_openid: "mem1" }, content: "@Bot hi" } }, headers: {}, query: {}, req: {} as never });
|
|
const c2c = await adapter.handleWebhook({ body: { op: 0, t: "C2C_MESSAGE_CREATE", d: { id: "m2", author: { user_openid: "u2", member_openid: "mem2" }, content: "hello" } }, headers: {}, query: {}, req: {} as never });
|
|
assert.deepEqual(group.body, { op: 12 }); assert.deepEqual(c2c.body, { op: 12 });
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(received[0].chatId, "group:g1"); assert.equal(received[0].userId, "mem1");
|
|
assert.equal(received[1].chatId, "user:u2"); assert.equal(received[1].userId, "u2");
|
|
});
|
|
|
|
test("group messages without member_openid fall back to user_openid", async () => {
|
|
const received: any[] = [];
|
|
const gateway = { receive: async (message: unknown) => { received.push(message); } };
|
|
const adapter = new QqAdapter(config, gateway as never);
|
|
await adapter.handleWebhook({ body: { op: 0, t: "GROUP_AT_MESSAGE_CREATE", d: { id: "m3", group_openid: "g1", author: { user_openid: "u3" }, content: "@Bot hi" } }, headers: {}, query: {}, req: {} as never });
|
|
await adapter.handleWebhook({ body: { op: 0, t: "GROUP_AT_MESSAGE_CREATE", d: { id: "m4", group_openid: "g1", member_openid: "mem4", author: {}, content: "@Bot hi" } }, headers: {}, query: {}, req: {} as never });
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(received[0].userId, "u3");
|
|
assert.equal(received[1].userId, "mem4");
|
|
});
|
|
|
|
test("sendMessage uses C2C endpoint and forwards reply sequences as msg_seq", async () => {
|
|
const original = globalThis.fetch; const urls: string[] = []; const bodies: Array<Record<string, unknown>> = [];
|
|
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
urls.push(String(input));
|
|
if (String(input).includes("getAppAccessToken")) return new Response(JSON.stringify({ access_token: "token", expires_in: 7200 }), { status: 200 });
|
|
bodies.push(JSON.parse(String(init?.body)) as Record<string, unknown>);
|
|
return new Response(JSON.stringify({ id: "sent" }), { status: 200 });
|
|
}) as typeof fetch;
|
|
try {
|
|
const adapter = new QqAdapter(config, { receive: async () => ({ ok: true }) } as never);
|
|
const target = { platform: "qq", chatId: "user:u2", raw: { author: { user_openid: "u2" } } };
|
|
await adapter.sendMessage({ target, text: "first", replyTo: "m2", replySequence: 1 });
|
|
await adapter.sendMessage({ target, text: "later", replyTo: "m2", replySequence: 7 });
|
|
assert.ok(urls.some((url) => url.endsWith("/v2/users/u2/messages")));
|
|
assert.deepEqual(bodies, [
|
|
{ content: "first", msg_id: "m2", msg_seq: 1 },
|
|
{ content: "later", msg_id: "m2", msg_seq: 7 }
|
|
]);
|
|
} finally { globalThis.fetch = original; }
|
|
});
|
|
|
|
test("sendMessage without replyTo omits msg_id and msg_seq from the body", async () => {
|
|
const original = globalThis.fetch; const bodies: Array<Record<string, unknown>> = [];
|
|
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
if (String(input).includes("getAppAccessToken")) return new Response(JSON.stringify({ access_token: "token", expires_in: 7200 }), { status: 200 });
|
|
bodies.push(JSON.parse(String(init?.body)) as Record<string, unknown>);
|
|
return new Response(JSON.stringify({ id: "sent" }), { status: 200 });
|
|
}) as typeof fetch;
|
|
try {
|
|
const adapter = new QqAdapter(config, { receive: async () => ({ ok: true }) } as never);
|
|
const target = { platform: "qq", chatId: "group:g1", raw: { group_openid: "g1" } };
|
|
await adapter.sendMessage({ target, text: "proactive" });
|
|
assert.deepEqual(bodies, [{ content: "proactive" }]);
|
|
} finally { globalThis.fetch = original; }
|
|
});
|
|
|
|
test("sendMessage surfaces safe QQ error details (HTTP status and code) without the request body", async () => {
|
|
const original = globalThis.fetch;
|
|
globalThis.fetch = (async (input: string | URL | Request) => {
|
|
if (String(input).includes("getAppAccessToken")) return new Response(JSON.stringify({ access_token: "token", expires_in: 7200 }), { status: 200 });
|
|
return new Response(JSON.stringify({ code: 40034105, message: "proactive message not allowed" }), { status: 400 });
|
|
}) as typeof fetch;
|
|
try {
|
|
const adapter = new QqAdapter(config, { receive: async () => ({ ok: true }) } as never);
|
|
const target = { platform: "qq", chatId: "group:g1", raw: { group_openid: "g1" } };
|
|
await assert.rejects(
|
|
adapter.sendMessage({ target, text: "secret message content" }),
|
|
(error: Error) => {
|
|
assert.match(error.message, /HTTP 400/);
|
|
assert.match(error.message, /code=40034105/);
|
|
assert.match(error.message, /proactive message not allowed/);
|
|
assert.doesNotMatch(error.message, /secret message content/);
|
|
return true;
|
|
}
|
|
);
|
|
} finally { globalThis.fetch = original; }
|
|
});
|