Files
openclaw/extensions/msteams/src/token.test.ts
T
Peter Steinberger b080dd1e76 refactor: consolidate coercion contracts (#122458)
* refactor: consolidate coercion contracts

Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics.

Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit.

* fix: preserve standalone script coercions

Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
2026-08-11 23:26:37 -07:00

497 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Msteams tests cover token plugin behavior.
import { generateKeyPairSync } from "node:crypto";
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { resetPluginStateStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { MSTeamsConfig } from "../runtime-api.js";
import { setMSTeamsRuntime } from "./runtime.js";
import { loadMSTeamsSdkWithAuth } from "./sdk.js";
import { msteamsRuntimeStub } from "./test-support/runtime.js";
import { readAccessToken } from "./token-response.js";
import {
hasConfiguredMSTeamsCredentials,
loadDelegatedTokens,
resolveDelegatedAccessToken,
resolveMSTeamsCredentials,
saveDelegatedTokens,
} from "./token.js";
const oauthTokenMocks = vi.hoisted(() => ({
refreshMSTeamsDelegatedTokens: vi.fn(),
}));
vi.mock("./oauth.token.js", () => ({
refreshMSTeamsDelegatedTokens: oauthTokenMocks.refreshMSTeamsDelegatedTokens,
}));
vi.mock("./secret-input.js", async () => {
const { normalizeOptionalString } = await import("openclaw/plugin-sdk/string-coerce-runtime");
return {
normalizeSecretInputString: normalizeOptionalString,
normalizeResolvedSecretInputString: (opts: { value: unknown; path: string }) =>
typeof opts.value === "string" && opts.value.trim() ? opts.value.trim() : undefined,
hasConfiguredSecretInput: (v: unknown) => typeof v === "string" && v.trim().length > 0,
};
});
const ENV_KEYS = [
"MSTEAMS_APP_ID",
"MSTEAMS_APP_PASSWORD",
"MSTEAMS_TENANT_ID",
"MSTEAMS_AUTH_TYPE",
"MSTEAMS_CERTIFICATE_PATH",
"MSTEAMS_CERTIFICATE_THUMBPRINT",
"MSTEAMS_USE_MANAGED_IDENTITY",
"MSTEAMS_MANAGED_IDENTITY_CLIENT_ID",
"OPENCLAW_STATE_DIR",
] as const;
let savedEnv: Record<string, string | undefined> = {};
function saveAndClearEnv() {
savedEnv = {};
for (const key of ENV_KEYS) {
savedEnv[key] = process.env[key];
delete process.env[key];
}
}
function restoreEnv() {
for (const key of ENV_KEYS) {
if (savedEnv[key] !== undefined) {
process.env[key] = savedEnv[key];
} else {
delete process.env[key];
}
}
}
describe("token secret credentials", () => {
beforeEach(saveAndClearEnv);
afterEach(restoreEnv);
it("returns true when appId + appPassword + tenantId are provided in config", () => {
const cfg = {
appId: "app-id",
appPassword: "app-pw",
tenantId: "tenant-id",
} satisfies MSTeamsConfig;
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(true);
});
it("returns false when appPassword is missing", () => {
const cfg = { appId: "app-id", tenantId: "tenant-id" } satisfies MSTeamsConfig;
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(false);
});
it("returns false when no config is given and no env vars set", () => {
expect(hasConfiguredMSTeamsCredentials(undefined)).toBe(false);
});
it("resolves secret credentials from config", () => {
const cfg = {
appId: "app-id",
appPassword: "app-pw",
tenantId: "tenant-id",
} satisfies MSTeamsConfig;
const result = resolveMSTeamsCredentials(cfg);
expect(result).toEqual({
type: "secret",
appId: "app-id",
appPassword: "app-pw",
tenantId: "tenant-id",
});
});
it("resolves secret credentials from env vars", () => {
process.env.MSTEAMS_APP_ID = "env-app-id";
process.env.MSTEAMS_APP_PASSWORD = "env-app-pw";
process.env.MSTEAMS_TENANT_ID = "env-tenant-id";
const result = resolveMSTeamsCredentials(undefined);
expect(result).toEqual({
type: "secret",
appId: "env-app-id",
appPassword: "env-app-pw",
tenantId: "env-tenant-id",
});
});
it("returns undefined when appPassword is missing", () => {
const cfg = { appId: "app-id", tenantId: "tenant-id" } satisfies MSTeamsConfig;
expect(resolveMSTeamsCredentials(cfg)).toBeUndefined();
});
});
describe("token federated credentials (certificate)", () => {
beforeEach(saveAndClearEnv);
afterEach(restoreEnv);
it("hasConfigured returns true when certificate path is provided", () => {
const cfg = {
appId: "app-id",
tenantId: "tenant-id",
authType: "federated",
certificatePath: "/cert.pem",
} satisfies MSTeamsConfig;
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(true);
});
it("hasConfigured returns false when neither cert nor MI is provided", () => {
const cfg = {
appId: "app-id",
tenantId: "tenant-id",
authType: "federated",
} satisfies MSTeamsConfig;
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(false);
});
it("ignores blank certificate settings", () => {
process.env.MSTEAMS_CERTIFICATE_PATH = " ";
const cfg = {
appId: "app-id",
tenantId: "tenant-id",
authType: "federated",
certificatePath: " ",
} satisfies MSTeamsConfig;
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(false);
expect(resolveMSTeamsCredentials(cfg)).toBeUndefined();
});
it("falls back to env certificate settings without changing path bytes", () => {
process.env.MSTEAMS_CERTIFICATE_PATH = " /env/cert.pem ";
process.env.MSTEAMS_CERTIFICATE_THUMBPRINT = "environment-thumbprint";
process.env.MSTEAMS_MANAGED_IDENTITY_CLIENT_ID = "environment-managed-identity";
const cfg = {
appId: "app-id",
tenantId: "tenant-id",
authType: "federated",
certificatePath: " ",
certificateThumbprint: " configured-thumbprint ",
managedIdentityClientId: " configured-managed-identity ",
} satisfies MSTeamsConfig;
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(true);
expect(resolveMSTeamsCredentials(cfg)).toEqual({
type: "federated",
appId: "app-id",
tenantId: "tenant-id",
certificatePath: " /env/cert.pem ",
certificateThumbprint: " configured-thumbprint ",
useManagedIdentity: undefined,
managedIdentityClientId: " configured-managed-identity ",
});
});
it("opens the exact environment certificate path when the configured path is blank", async () => {
const certificateDirectory = mkdtempSync(
path.join(os.tmpdir(), "openclaw-msteams-real-certificate-"),
);
const certificatePath = path.join(certificateDirectory, " certificate.pem");
try {
const { privateKey } = generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs8", format: "pem" },
});
writeFileSync(certificatePath, privateKey, { mode: 0o600 });
process.env.MSTEAMS_CERTIFICATE_PATH = certificatePath;
const cfg = {
appId: "app-id",
tenantId: "tenant-id",
authType: "federated",
certificatePath: " ",
} satisfies MSTeamsConfig;
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(true);
const credentials = resolveMSTeamsCredentials(cfg);
expect(credentials).toMatchObject({
type: "federated",
certificatePath,
});
if (!credentials) {
throw new Error("expected configured Microsoft Teams certificate credentials");
}
const { app } = await loadMSTeamsSdkWithAuth(credentials);
expect(app.tokenManager).toBeDefined();
} finally {
rmSync(certificateDirectory, { recursive: true, force: true });
}
});
it("preserves configured nonblank certificate path bytes and precedence", () => {
process.env.MSTEAMS_CERTIFICATE_PATH = "/environment/certificate.pem";
const certificatePath = " /configured/certificate.pem ";
const cfg = {
appId: "app-id",
tenantId: "tenant-id",
authType: "federated",
certificatePath,
} satisfies MSTeamsConfig;
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(true);
expect(resolveMSTeamsCredentials(cfg)).toMatchObject({ certificatePath });
});
it("keeps managed identity configured when the optional certificate path is blank", () => {
const cfg = {
appId: "app-id",
tenantId: "tenant-id",
authType: "federated",
certificatePath: " ",
useManagedIdentity: true,
managedIdentityClientId: " configured-managed-identity ",
} satisfies MSTeamsConfig;
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(true);
expect(resolveMSTeamsCredentials(cfg)).toEqual({
type: "federated",
appId: "app-id",
tenantId: "tenant-id",
certificatePath: undefined,
certificateThumbprint: undefined,
useManagedIdentity: true,
managedIdentityClientId: " configured-managed-identity ",
});
});
it("resolves federated credentials with certificate from config", () => {
const cfg = {
appId: "app-id",
tenantId: "tenant-id",
authType: "federated",
certificatePath: "/cert.pem",
certificateThumbprint: "AABBCCDD",
} satisfies MSTeamsConfig;
const result = resolveMSTeamsCredentials(cfg);
expect(result).toEqual({
type: "federated",
appId: "app-id",
tenantId: "tenant-id",
certificatePath: "/cert.pem",
certificateThumbprint: "AABBCCDD",
useManagedIdentity: undefined,
managedIdentityClientId: undefined,
});
});
it("resolves federated credentials from env vars", () => {
process.env.MSTEAMS_AUTH_TYPE = "federated";
process.env.MSTEAMS_APP_ID = "env-app-id";
process.env.MSTEAMS_TENANT_ID = "env-tenant-id";
process.env.MSTEAMS_CERTIFICATE_PATH = "/env/cert.pem";
process.env.MSTEAMS_CERTIFICATE_THUMBPRINT = "EEFF0011";
const result = resolveMSTeamsCredentials(undefined);
expect(result).toEqual({
type: "federated",
appId: "env-app-id",
tenantId: "env-tenant-id",
certificatePath: "/env/cert.pem",
certificateThumbprint: "EEFF0011",
useManagedIdentity: undefined,
managedIdentityClientId: undefined,
});
});
});
describe("token federated credentials (managed identity)", () => {
beforeEach(saveAndClearEnv);
afterEach(restoreEnv);
it("resolves managed identity from config", () => {
const cfg = {
appId: "app-id",
tenantId: "tenant-id",
authType: "federated",
useManagedIdentity: true,
managedIdentityClientId: "mi-client-id",
} satisfies MSTeamsConfig;
const result = resolveMSTeamsCredentials(cfg);
expect(result).toEqual({
type: "federated",
appId: "app-id",
tenantId: "tenant-id",
certificatePath: undefined,
certificateThumbprint: undefined,
useManagedIdentity: true,
managedIdentityClientId: "mi-client-id",
});
});
it("resolves system-assigned managed identity (no clientId)", () => {
const cfg = {
appId: "app-id",
tenantId: "tenant-id",
authType: "federated",
useManagedIdentity: true,
} satisfies MSTeamsConfig;
const result = resolveMSTeamsCredentials(cfg);
expect(result).toEqual({
type: "federated",
appId: "app-id",
tenantId: "tenant-id",
certificatePath: undefined,
certificateThumbprint: undefined,
useManagedIdentity: true,
managedIdentityClientId: undefined,
});
});
it("hasConfigured returns true for managed identity via env", () => {
process.env.MSTEAMS_AUTH_TYPE = "federated";
process.env.MSTEAMS_APP_ID = "env-app-id";
process.env.MSTEAMS_TENANT_ID = "env-tenant-id";
process.env.MSTEAMS_USE_MANAGED_IDENTITY = "true";
expect(hasConfiguredMSTeamsCredentials(undefined)).toBe(true);
});
it("config useManagedIdentity=false overrides env MSTEAMS_USE_MANAGED_IDENTITY=true", () => {
process.env.MSTEAMS_USE_MANAGED_IDENTITY = "true";
const cfg = {
appId: "app-id",
tenantId: "tenant-id",
authType: "federated",
certificatePath: "/cert.pem",
useManagedIdentity: false,
} satisfies MSTeamsConfig;
const result = resolveMSTeamsCredentials(cfg);
expect(result).toEqual({
type: "federated",
appId: "app-id",
tenantId: "tenant-id",
certificatePath: "/cert.pem",
certificateThumbprint: undefined,
useManagedIdentity: undefined,
managedIdentityClientId: undefined,
});
});
});
describe("token backward compatibility", () => {
beforeEach(saveAndClearEnv);
afterEach(restoreEnv);
it("defaults to secret when authType is absent", () => {
const cfg = {
appId: "app-id",
appPassword: "pw",
tenantId: "tenant-id",
} satisfies MSTeamsConfig;
const result = resolveMSTeamsCredentials(cfg);
expect(result).toEqual({
type: "secret",
appId: "app-id",
appPassword: "pw",
tenantId: "tenant-id",
});
});
it("explicit authType=secret behaves same as absent", () => {
const cfg = {
appId: "app-id",
appPassword: "pw",
tenantId: "tenant-id",
authType: "secret",
} satisfies MSTeamsConfig;
const result = resolveMSTeamsCredentials(cfg);
expect(result).toEqual({
type: "secret",
appId: "app-id",
appPassword: "pw",
tenantId: "tenant-id",
});
});
});
describe("resolveDelegatedAccessToken", () => {
let stateDir: string | undefined;
beforeEach(() => {
resetPluginStateStoreForTests();
setMSTeamsRuntime(msteamsRuntimeStub);
saveAndClearEnv();
stateDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-msteams-token-"));
process.env.OPENCLAW_STATE_DIR = stateDir;
oauthTokenMocks.refreshMSTeamsDelegatedTokens.mockReset();
});
afterEach(() => {
restoreEnv();
resetPluginStateStoreForTests();
if (stateDir) {
rmSync(stateDir, { recursive: true, force: true });
stateDir = undefined;
}
});
function writeDelegatedTokens(expiresAt: number) {
if (!stateDir) {
throw new Error("missing stateDir");
}
saveDelegatedTokens({
accessToken: "stale-access",
refreshToken: "refresh-token",
expiresAt,
scopes: ["User.Read"],
});
}
it("roundtrips delegated tokens through plugin-state SQLite without a sidecar", () => {
writeDelegatedTokens(Date.now() + 60_000);
expect(loadDelegatedTokens()).toMatchObject({
accessToken: "stale-access",
refreshToken: "refresh-token",
});
expect(existsSync(path.join(stateDir!, "state", "openclaw.sqlite"))).toBe(true);
expect(existsSync(path.join(stateDir!, "msteams-delegated.json"))).toBe(false);
});
it("reuses a valid delegated access token before expiry", async () => {
writeDelegatedTokens(Date.now() + 60_000);
await expect(
resolveDelegatedAccessToken({
tenantId: "tenant",
clientId: "client",
clientSecret: "secret",
}),
).resolves.toBe("stale-access");
expect(oauthTokenMocks.refreshMSTeamsDelegatedTokens).not.toHaveBeenCalled();
});
it("does not reuse delegated tokens with invalid Date-range expiry", async () => {
writeDelegatedTokens(Number.MAX_VALUE);
oauthTokenMocks.refreshMSTeamsDelegatedTokens.mockRejectedValueOnce(new Error("expired"));
await expect(
resolveDelegatedAccessToken({
tenantId: "tenant",
clientId: "client",
clientSecret: "secret",
}),
).resolves.toBeUndefined();
expect(oauthTokenMocks.refreshMSTeamsDelegatedTokens).toHaveBeenCalledOnce();
});
});
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();
});
});