mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-29 10:02:04 +00:00
234 lines
6.9 KiB
TypeScript
234 lines
6.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { OpenClawConfig } from "../runtime-api.js";
|
|
import { searchMessagesMSTeams } from "./graph-messages.js";
|
|
|
|
const mockState = vi.hoisted(() => ({
|
|
resolveGraphToken: vi.fn(),
|
|
fetchGraphJson: vi.fn(),
|
|
postGraphJson: vi.fn(),
|
|
postGraphBetaJson: vi.fn(),
|
|
deleteGraphRequest: vi.fn(),
|
|
findPreferredDmByUserId: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./graph.js", () => {
|
|
return {
|
|
resolveGraphToken: mockState.resolveGraphToken,
|
|
fetchGraphJson: mockState.fetchGraphJson,
|
|
postGraphJson: mockState.postGraphJson,
|
|
postGraphBetaJson: mockState.postGraphBetaJson,
|
|
deleteGraphRequest: mockState.deleteGraphRequest,
|
|
escapeOData: vi.fn((value: string) => value.replaceAll("'", "''")),
|
|
};
|
|
});
|
|
|
|
vi.mock("./conversation-store-fs.js", () => ({
|
|
createMSTeamsConversationStoreFs: () => ({
|
|
findPreferredDmByUserId: mockState.findPreferredDmByUserId,
|
|
}),
|
|
}));
|
|
|
|
const TOKEN = "test-graph-token";
|
|
const CHAT_ID = "19:abc@thread.tacv2";
|
|
const CHANNEL_TO = "team-id-1/channel-id-1";
|
|
|
|
describe("searchMessagesMSTeams", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockState.resolveGraphToken.mockResolvedValue(TOKEN);
|
|
});
|
|
|
|
it("searches chat messages with query string", async () => {
|
|
mockState.fetchGraphJson.mockResolvedValue({
|
|
value: [
|
|
{
|
|
id: "msg-1",
|
|
body: { content: "Meeting notes from Monday" },
|
|
from: { user: { id: "u1", displayName: "Alice" } },
|
|
createdDateTime: "2026-03-25T10:00:00Z",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchMessagesMSTeams({
|
|
cfg: {} as OpenClawConfig,
|
|
to: CHAT_ID,
|
|
query: "meeting notes",
|
|
});
|
|
|
|
expect(result.messages).toEqual([
|
|
{
|
|
id: "msg-1",
|
|
text: "Meeting notes from Monday",
|
|
from: { user: { id: "u1", displayName: "Alice" } },
|
|
createdAt: "2026-03-25T10:00:00Z",
|
|
},
|
|
]);
|
|
const calledPath = mockState.fetchGraphJson.mock.calls[0][0].path as string;
|
|
expect(calledPath).toContain(`/chats/${encodeURIComponent(CHAT_ID)}/messages?`);
|
|
expect(calledPath).toContain("$search=");
|
|
expect(calledPath).toContain("$top=25");
|
|
const decoded = decodeURIComponent(calledPath);
|
|
expect(decoded).toContain('$search="meeting notes"');
|
|
});
|
|
|
|
it("searches channel messages", async () => {
|
|
mockState.fetchGraphJson.mockResolvedValue({
|
|
value: [
|
|
{
|
|
id: "msg-2",
|
|
body: { content: "Sprint review" },
|
|
from: { user: { id: "u2", displayName: "Bob" } },
|
|
createdDateTime: "2026-03-25T11:00:00Z",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchMessagesMSTeams({
|
|
cfg: {} as OpenClawConfig,
|
|
to: CHANNEL_TO,
|
|
query: "sprint",
|
|
});
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
const calledPath = mockState.fetchGraphJson.mock.calls[0][0].path as string;
|
|
expect(calledPath).toContain("/teams/team-id-1/channels/channel-id-1/messages?");
|
|
});
|
|
|
|
it("applies limit parameter", async () => {
|
|
mockState.fetchGraphJson.mockResolvedValue({ value: [] });
|
|
|
|
await searchMessagesMSTeams({
|
|
cfg: {} as OpenClawConfig,
|
|
to: CHAT_ID,
|
|
query: "test",
|
|
limit: 10,
|
|
});
|
|
|
|
const calledPath = mockState.fetchGraphJson.mock.calls[0][0].path as string;
|
|
expect(calledPath).toContain("$top=10");
|
|
});
|
|
|
|
it("clamps limit to max 50", async () => {
|
|
mockState.fetchGraphJson.mockResolvedValue({ value: [] });
|
|
|
|
await searchMessagesMSTeams({
|
|
cfg: {} as OpenClawConfig,
|
|
to: CHAT_ID,
|
|
query: "test",
|
|
limit: 100,
|
|
});
|
|
|
|
const calledPath = mockState.fetchGraphJson.mock.calls[0][0].path as string;
|
|
expect(calledPath).toContain("$top=50");
|
|
});
|
|
|
|
it("clamps limit to min 1", async () => {
|
|
mockState.fetchGraphJson.mockResolvedValue({ value: [] });
|
|
|
|
await searchMessagesMSTeams({
|
|
cfg: {} as OpenClawConfig,
|
|
to: CHAT_ID,
|
|
query: "test",
|
|
limit: 0,
|
|
});
|
|
|
|
const calledPath = mockState.fetchGraphJson.mock.calls[0][0].path as string;
|
|
expect(calledPath).toContain("$top=1");
|
|
});
|
|
|
|
it("applies from filter", async () => {
|
|
mockState.fetchGraphJson.mockResolvedValue({ value: [] });
|
|
|
|
await searchMessagesMSTeams({
|
|
cfg: {} as OpenClawConfig,
|
|
to: CHAT_ID,
|
|
query: "budget",
|
|
from: "Alice",
|
|
});
|
|
|
|
const calledPath = mockState.fetchGraphJson.mock.calls[0][0].path as string;
|
|
expect(calledPath).toContain("$filter=");
|
|
const decoded = decodeURIComponent(calledPath);
|
|
expect(decoded).toContain("from/user/displayName eq 'Alice'");
|
|
});
|
|
|
|
it("escapes single quotes in from filter", async () => {
|
|
mockState.fetchGraphJson.mockResolvedValue({ value: [] });
|
|
|
|
await searchMessagesMSTeams({
|
|
cfg: {} as OpenClawConfig,
|
|
to: CHAT_ID,
|
|
query: "test",
|
|
from: "O'Brien",
|
|
});
|
|
|
|
const calledPath = mockState.fetchGraphJson.mock.calls[0][0].path as string;
|
|
const decoded = decodeURIComponent(calledPath);
|
|
expect(decoded).toContain("O''Brien");
|
|
});
|
|
|
|
it("strips double quotes from query to prevent injection", async () => {
|
|
mockState.fetchGraphJson.mockResolvedValue({ value: [] });
|
|
|
|
await searchMessagesMSTeams({
|
|
cfg: {} as OpenClawConfig,
|
|
to: CHAT_ID,
|
|
query: 'say "hello" world',
|
|
});
|
|
|
|
const calledPath = mockState.fetchGraphJson.mock.calls[0][0].path as string;
|
|
const decoded = decodeURIComponent(calledPath);
|
|
expect(decoded).toContain('$search="say hello world"');
|
|
expect(decoded).not.toContain('""');
|
|
});
|
|
|
|
it("passes ConsistencyLevel: eventual header", async () => {
|
|
mockState.fetchGraphJson.mockResolvedValue({ value: [] });
|
|
|
|
await searchMessagesMSTeams({
|
|
cfg: {} as OpenClawConfig,
|
|
to: CHAT_ID,
|
|
query: "test",
|
|
});
|
|
|
|
expect(mockState.fetchGraphJson).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
headers: { ConsistencyLevel: "eventual" },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("returns empty array when no messages match", async () => {
|
|
mockState.fetchGraphJson.mockResolvedValue({ value: [] });
|
|
|
|
const result = await searchMessagesMSTeams({
|
|
cfg: {} as OpenClawConfig,
|
|
to: CHAT_ID,
|
|
query: "nonexistent",
|
|
});
|
|
|
|
expect(result.messages).toEqual([]);
|
|
});
|
|
|
|
it("resolves user: target through conversation store", async () => {
|
|
mockState.findPreferredDmByUserId.mockResolvedValue({
|
|
conversationId: "a:bot-id",
|
|
reference: { graphChatId: "19:dm-chat@thread.tacv2" },
|
|
});
|
|
mockState.fetchGraphJson.mockResolvedValue({ value: [] });
|
|
|
|
await searchMessagesMSTeams({
|
|
cfg: {} as OpenClawConfig,
|
|
to: "user:aad-user-1",
|
|
query: "hello",
|
|
});
|
|
|
|
expect(mockState.findPreferredDmByUserId).toHaveBeenCalledWith("aad-user-1");
|
|
const calledPath = mockState.fetchGraphJson.mock.calls[0][0].path as string;
|
|
expect(calledPath).toContain(
|
|
`/chats/${encodeURIComponent("19:dm-chat@thread.tacv2")}/messages?`,
|
|
);
|
|
});
|
|
});
|