mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
test: collapse msteams helper suites
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
} from "../../../test/helpers/extensions/directory.js";
|
||||
import type { OpenClawConfig, RuntimeEnv } from "../runtime-api.js";
|
||||
import { msteamsPlugin } from "./channel.js";
|
||||
import { resolveMSTeamsOutboundSessionRoute } from "./session-route.js";
|
||||
|
||||
function requireDirectorySelf(
|
||||
directory: typeof msteamsPlugin.directory | null | undefined,
|
||||
@@ -129,3 +130,70 @@ describe("msteams directory", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("msteams session route", () => {
|
||||
it("builds direct routes for explicit user targets", () => {
|
||||
const route = resolveMSTeamsOutboundSessionRoute({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
accountId: "default",
|
||||
target: "msteams:user:alice-id",
|
||||
});
|
||||
|
||||
expect(route).toMatchObject({
|
||||
peer: {
|
||||
kind: "direct",
|
||||
id: "alice-id",
|
||||
},
|
||||
from: "msteams:alice-id",
|
||||
to: "user:alice-id",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds channel routes for thread conversations and strips suffix metadata", () => {
|
||||
const route = resolveMSTeamsOutboundSessionRoute({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
accountId: "default",
|
||||
target: "teams:19:abc123@thread.tacv2;messageid=42",
|
||||
});
|
||||
|
||||
expect(route).toMatchObject({
|
||||
peer: {
|
||||
kind: "channel",
|
||||
id: "19:abc123@thread.tacv2",
|
||||
},
|
||||
from: "msteams:channel:19:abc123@thread.tacv2",
|
||||
to: "conversation:19:abc123@thread.tacv2",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns group routes for non-user, non-channel conversations", () => {
|
||||
const route = resolveMSTeamsOutboundSessionRoute({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
accountId: "default",
|
||||
target: "msteams:conversation:19:groupchat",
|
||||
});
|
||||
|
||||
expect(route).toMatchObject({
|
||||
peer: {
|
||||
kind: "group",
|
||||
id: "19:groupchat",
|
||||
},
|
||||
from: "msteams:group:19:groupchat",
|
||||
to: "conversation:19:groupchat",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null when the target cannot be normalized", () => {
|
||||
expect(
|
||||
resolveMSTeamsOutboundSessionRoute({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
accountId: "default",
|
||||
target: "msteams:",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
classifyMSTeamsSendError,
|
||||
formatMSTeamsSendErrorHint,
|
||||
formatUnknownError,
|
||||
isRevokedProxyError,
|
||||
} from "./errors.js";
|
||||
import { withRevokedProxyFallback } from "./revoked-context.js";
|
||||
|
||||
describe("msteams errors", () => {
|
||||
it("formats unknown errors", () => {
|
||||
@@ -67,4 +68,41 @@ describe("msteams errors", () => {
|
||||
expect(isRevokedProxyError("proxy that has been revoked")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withRevokedProxyFallback", () => {
|
||||
it("returns primary result when no error occurs", async () => {
|
||||
await expect(
|
||||
withRevokedProxyFallback({
|
||||
run: async () => "ok",
|
||||
onRevoked: async () => "fallback",
|
||||
}),
|
||||
).resolves.toBe("ok");
|
||||
});
|
||||
|
||||
it("uses fallback when proxy-revoked TypeError is thrown", async () => {
|
||||
const onRevokedLog = vi.fn();
|
||||
await expect(
|
||||
withRevokedProxyFallback({
|
||||
run: async () => {
|
||||
throw new TypeError("Cannot perform 'get' on a proxy that has been revoked");
|
||||
},
|
||||
onRevoked: async () => "fallback",
|
||||
onRevokedLog,
|
||||
}),
|
||||
).resolves.toBe("fallback");
|
||||
expect(onRevokedLog).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rethrows non-revoked errors", async () => {
|
||||
const err = Object.assign(new Error("boom"), { statusCode: 500 });
|
||||
await expect(
|
||||
withRevokedProxyFallback({
|
||||
run: async () => {
|
||||
throw err;
|
||||
},
|
||||
onRevoked: async () => "fallback",
|
||||
}),
|
||||
).rejects.toBe(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./runtime.js", () => ({
|
||||
getMSTeamsRuntime: vi.fn(() => ({ version: "2026.3.19" })),
|
||||
}));
|
||||
|
||||
import { fetchGraphJson } from "./graph.js";
|
||||
import { resetUserAgentCache } from "./user-agent.js";
|
||||
|
||||
describe("fetchGraphJson User-Agent", () => {
|
||||
afterEach(() => {
|
||||
resetUserAgentCache();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("sends User-Agent header with OpenClaw version", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ value: [] }),
|
||||
});
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
await fetchGraphJson({ token: "test-token", path: "/groups" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledOnce();
|
||||
const [, init] = mockFetch.mock.calls[0];
|
||||
expect(init.headers["User-Agent"]).toMatch(/^teams\.ts\[apps\]\/.+ OpenClaw\/2026\.3\.19$/);
|
||||
expect(init.headers).toHaveProperty("Authorization", "Bearer test-token");
|
||||
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("allows caller headers to override User-Agent", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ value: [] }),
|
||||
});
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
await fetchGraphJson({
|
||||
token: "test-token",
|
||||
path: "/groups",
|
||||
headers: { "User-Agent": "custom-agent/1.0" },
|
||||
});
|
||||
|
||||
const [, init] = mockFetch.mock.calls[0];
|
||||
// Caller headers spread after, so they override
|
||||
expect(init.headers["User-Agent"]).toBe("custom-agent/1.0");
|
||||
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { withRevokedProxyFallback } from "./revoked-context.js";
|
||||
|
||||
describe("msteams revoked context helper", () => {
|
||||
it("returns primary result when no error occurs", async () => {
|
||||
await expect(
|
||||
withRevokedProxyFallback({
|
||||
run: async () => "ok",
|
||||
onRevoked: async () => "fallback",
|
||||
}),
|
||||
).resolves.toBe("ok");
|
||||
});
|
||||
|
||||
it("uses fallback when proxy-revoked TypeError is thrown", async () => {
|
||||
const onRevokedLog = vi.fn();
|
||||
await expect(
|
||||
withRevokedProxyFallback({
|
||||
run: async () => {
|
||||
throw new TypeError("Cannot perform 'get' on a proxy that has been revoked");
|
||||
},
|
||||
onRevoked: async () => "fallback",
|
||||
onRevokedLog,
|
||||
}),
|
||||
).resolves.toBe("fallback");
|
||||
expect(onRevokedLog).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rethrows non-revoked errors", async () => {
|
||||
const err = Object.assign(new Error("boom"), { statusCode: 500 });
|
||||
await expect(
|
||||
withRevokedProxyFallback({
|
||||
run: async () => {
|
||||
throw err;
|
||||
},
|
||||
onRevoked: async () => "fallback",
|
||||
}),
|
||||
).rejects.toBe(err);
|
||||
});
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveMSTeamsOutboundSessionRoute } from "./session-route.js";
|
||||
|
||||
describe("msteams session route", () => {
|
||||
it("builds direct routes for explicit user targets", () => {
|
||||
const route = resolveMSTeamsOutboundSessionRoute({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
accountId: "default",
|
||||
target: "msteams:user:alice-id",
|
||||
});
|
||||
|
||||
expect(route).toMatchObject({
|
||||
peer: {
|
||||
kind: "direct",
|
||||
id: "alice-id",
|
||||
},
|
||||
from: "msteams:alice-id",
|
||||
to: "user:alice-id",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds channel routes for thread conversations and strips suffix metadata", () => {
|
||||
const route = resolveMSTeamsOutboundSessionRoute({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
accountId: "default",
|
||||
target: "teams:19:abc123@thread.tacv2;messageid=42",
|
||||
});
|
||||
|
||||
expect(route).toMatchObject({
|
||||
peer: {
|
||||
kind: "channel",
|
||||
id: "19:abc123@thread.tacv2",
|
||||
},
|
||||
from: "msteams:channel:19:abc123@thread.tacv2",
|
||||
to: "conversation:19:abc123@thread.tacv2",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns group routes for non-user, non-channel conversations", () => {
|
||||
const route = resolveMSTeamsOutboundSessionRoute({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
accountId: "default",
|
||||
target: "msteams:conversation:19:groupchat",
|
||||
});
|
||||
|
||||
expect(route).toMatchObject({
|
||||
peer: {
|
||||
kind: "group",
|
||||
id: "19:groupchat",
|
||||
},
|
||||
from: "msteams:group:19:groupchat",
|
||||
to: "conversation:19:groupchat",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null when the target cannot be normalized", () => {
|
||||
expect(
|
||||
resolveMSTeamsOutboundSessionRoute({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
accountId: "default",
|
||||
target: "msteams:",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { msteamsSetupAdapter } from "./setup-core.js";
|
||||
|
||||
describe("msteams setup core", () => {
|
||||
it("always resolves to the default account", () => {
|
||||
expect(msteamsSetupAdapter.resolveAccountId?.({ accountId: "work" } as never)).toBe(
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
);
|
||||
});
|
||||
|
||||
it("enables the msteams channel without dropping existing config", () => {
|
||||
expect(
|
||||
msteamsSetupAdapter.applyAccountConfig?.({
|
||||
cfg: {
|
||||
channels: {
|
||||
msteams: {
|
||||
appId: "existing-app",
|
||||
},
|
||||
},
|
||||
},
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
input: {},
|
||||
} as never),
|
||||
).toEqual({
|
||||
channels: {
|
||||
msteams: {
|
||||
appId: "existing-app",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { msteamsSetupAdapter } from "./setup-core.js";
|
||||
|
||||
const resolveMSTeamsUserAllowlist = vi.hoisted(() => vi.fn());
|
||||
const resolveMSTeamsChannelAllowlist = vi.hoisted(() => vi.fn());
|
||||
@@ -37,6 +39,35 @@ describe("msteams setup surface", () => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("always resolves to the default account", () => {
|
||||
expect(msteamsSetupAdapter.resolveAccountId?.({ accountId: "work" } as never)).toBe(
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
);
|
||||
});
|
||||
|
||||
it("enables the msteams channel without dropping existing config", () => {
|
||||
expect(
|
||||
msteamsSetupAdapter.applyAccountConfig?.({
|
||||
cfg: {
|
||||
channels: {
|
||||
msteams: {
|
||||
appId: "existing-app",
|
||||
},
|
||||
},
|
||||
},
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
input: {},
|
||||
} as never),
|
||||
).toEqual({
|
||||
channels: {
|
||||
msteams: {
|
||||
appId: "existing-app",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reports configured status from resolved credentials", async () => {
|
||||
resolveMSTeamsCredentials.mockReturnValue({
|
||||
appId: "app",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readAccessToken } from "./token-response.js";
|
||||
|
||||
describe("readAccessToken", () => {
|
||||
it("returns raw string token values", () => {
|
||||
expect(readAccessToken("abc")).toBe("abc");
|
||||
});
|
||||
|
||||
it("returns accessToken from object value", () => {
|
||||
expect(readAccessToken({ accessToken: "access-token" })).toBe("access-token");
|
||||
});
|
||||
|
||||
it("returns token fallback from object value", () => {
|
||||
expect(readAccessToken({ token: "fallback-token" })).toBe("fallback-token");
|
||||
});
|
||||
|
||||
it("returns null for unsupported values", () => {
|
||||
expect(readAccessToken({ accessToken: 123 })).toBeNull();
|
||||
expect(readAccessToken({ token: false })).toBeNull();
|
||||
expect(readAccessToken(null)).toBeNull();
|
||||
expect(readAccessToken(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { readAccessToken } from "./token-response.js";
|
||||
import { hasConfiguredMSTeamsCredentials, resolveMSTeamsCredentials } from "./token.js";
|
||||
|
||||
const ORIGINAL_ENV = {
|
||||
@@ -70,3 +71,18 @@ describe("hasConfiguredMSTeamsCredentials", () => {
|
||||
expect(configured).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readAccessToken", () => {
|
||||
it("reads string and object token forms", () => {
|
||||
expect(readAccessToken("abc")).toBe("abc");
|
||||
expect(readAccessToken({ accessToken: "access-token" })).toBe("access-token");
|
||||
expect(readAccessToken({ token: "fallback-token" })).toBe("fallback-token");
|
||||
});
|
||||
|
||||
it("returns null for unsupported token payloads", () => {
|
||||
expect(readAccessToken({ accessToken: 123 })).toBeNull();
|
||||
expect(readAccessToken({ token: false })).toBeNull();
|
||||
expect(readAccessToken(null)).toBeNull();
|
||||
expect(readAccessToken(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ vi.mock("./runtime.js", () => ({
|
||||
getMSTeamsRuntime: vi.fn(() => mockRuntime),
|
||||
}));
|
||||
|
||||
import { fetchGraphJson } from "./graph.js";
|
||||
import { getMSTeamsRuntime } from "./runtime.js";
|
||||
import { buildUserAgent, resetUserAgentCache } from "./user-agent.js";
|
||||
|
||||
@@ -42,4 +43,36 @@ describe("buildUserAgent", () => {
|
||||
// SDK version should still be present
|
||||
expect(ua).toMatch(/^teams\.ts\[apps\]\//);
|
||||
});
|
||||
|
||||
it("sends the generated User-Agent in Graph requests by default", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ value: [] }),
|
||||
});
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
await fetchGraphJson({ token: "test-token", path: "/groups" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledOnce();
|
||||
const [, init] = mockFetch.mock.calls[0];
|
||||
expect(init.headers["User-Agent"]).toMatch(/^teams\.ts\[apps\]\/.+ OpenClaw\/2026\.3\.19$/);
|
||||
expect(init.headers).toHaveProperty("Authorization", "Bearer test-token");
|
||||
});
|
||||
|
||||
it("lets caller headers override the default Graph User-Agent", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ value: [] }),
|
||||
});
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
await fetchGraphJson({
|
||||
token: "test-token",
|
||||
path: "/groups",
|
||||
headers: { "User-Agent": "custom-agent/1.0" },
|
||||
});
|
||||
|
||||
const [, init] = mockFetch.mock.calls[0];
|
||||
expect(init.headers["User-Agent"]).toBe("custom-agent/1.0");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user