Files
openclaw/extensions/msteams/src/setup-surface.test.ts
Peter Steinberger 96f0983a85 fix(onboarding): skip setup for configured gateways and require inference first (#102883)
* fix(crestodian): keep onboarding RPCs restart-safe

* fix(profiles): isolate approval state migrations

* fix(crestodian): bypass configured gateway setup

* test(crestodian): type onboarding mocks

* fix(onboarding): require inference before Crestodian

* fix(onboarding): enforce verified inference handoff

* fix(macos): reset setup on gateway endpoint edits

* chore(i18n): refresh native source inventory

* fix(gateway): keep socket on request cancellation

* test(packaging): require workspace templates

* fix(onboarding): bind setup to verified inference

* fix(onboarding): align inference gate contracts

* fix(crestodian): classify concurrent policy rejection

* test(crestodian): expect registry restoration

* fix(onboarding): bind setup to configured gateways

* fix(codex): preserve startup phase deadlines

* test(crestodian): match fail-closed policy ordering

* test(onboarding): assert bound gateway handoff

* fix(codex): bind runtime resolution to spawn cwd

* test(crestodian): assert policy rejection order

* fix(cli): preserve gateway routing across restarts

* fix(macos): fail closed during gateway edits

* test(macos): cover gateway route generation races

* chore: keep release notes out of onboarding PR

* fix(ci): refresh onboarding generated checks

* style(swift): align gateway channel formatting

* fix(ci): refresh plugin SDK surface budgets

* fix(ci): resync native string inventory

* refactor(swift): split gateway channel support

* test(doctor): isolate plugin compatibility registry

* test(macos): isolate gateway onboarding fixtures

* test(macos): assert gateway lease health ordering

* fix(codex): reconcile computer-use startup changes
2026-07-11 10:25:14 -07:00

280 lines
8.7 KiB
TypeScript

// Msteams tests cover setup surface plugin behavior.
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createMSTeamsSetupWizardBase, msteamsSetupAdapter } from "./setup-core.js";
const resolveMSTeamsUserAllowlist = vi.hoisted(() => vi.fn());
const resolveMSTeamsChannelAllowlist = vi.hoisted(() => vi.fn());
const normalizeSecretInputString = vi.hoisted(() =>
vi.fn((value: unknown) => (typeof value === "string" ? value.trim() || undefined : undefined)),
);
const hasConfiguredMSTeamsCredentials = vi.hoisted(() => vi.fn());
const resolveMSTeamsCredentials = vi.hoisted(() => vi.fn());
const saveDelegatedTokens = vi.hoisted(() => vi.fn());
const loginMSTeamsDelegated = vi.hoisted(() => vi.fn());
const oauthModuleState = vi.hoisted(() => ({ loaded: false }));
vi.mock("./resolve-allowlist.js", () => ({
parseMSTeamsTeamEntry: vi.fn(),
resolveMSTeamsChannelAllowlist,
resolveMSTeamsUserAllowlist,
}));
vi.mock("./secret-input.js", () => ({
normalizeSecretInputString,
}));
vi.mock("./token.js", () => ({
hasConfiguredMSTeamsCredentials,
resolveMSTeamsCredentials,
saveDelegatedTokens,
}));
vi.mock("./oauth.js", () => {
oauthModuleState.loaded = true;
return { loginMSTeamsDelegated };
});
import { msteamsSetupWizard as delegatedMsteamsSetupWizard } from "./setup-surface.js";
describe("msteams setup surface", () => {
const msteamsSetupWizard = createMSTeamsSetupWizardBase();
beforeEach(() => {
resolveMSTeamsUserAllowlist.mockReset();
resolveMSTeamsChannelAllowlist.mockReset();
normalizeSecretInputString.mockClear();
hasConfiguredMSTeamsCredentials.mockReset();
resolveMSTeamsCredentials.mockReset();
saveDelegatedTokens.mockReset();
loginMSTeamsDelegated.mockReset();
});
afterEach(() => {
vi.unstubAllEnvs();
});
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", () => {
resolveMSTeamsCredentials.mockReturnValue({
appId: "app",
});
hasConfiguredMSTeamsCredentials.mockReturnValue(false);
expect(
msteamsSetupWizard.status.resolveConfigured({
cfg: { channels: { msteams: {} } },
} as never),
).toBe(true);
});
it("reports configured status from configured credentials and renders status lines", async () => {
resolveMSTeamsCredentials.mockReturnValue(null);
hasConfiguredMSTeamsCredentials.mockReturnValue(true);
expect(
msteamsSetupWizard.status.resolveConfigured({
cfg: { channels: { msteams: {} } },
} as never),
).toBe(true);
hasConfiguredMSTeamsCredentials.mockReturnValue(false);
expect(msteamsSetupWizard.status.resolveStatusLines).toBeTypeOf("function");
await expect(
msteamsSetupWizard.status.resolveStatusLines?.({
cfg: { channels: { msteams: {} } },
} as never),
).resolves.toEqual(["MS Teams: needs app credentials"]);
});
it("finalize keeps env credentials when available and accepted", async () => {
vi.stubEnv("MSTEAMS_APP_ID", "env-app");
vi.stubEnv("MSTEAMS_APP_PASSWORD", "env-secret");
vi.stubEnv("MSTEAMS_TENANT_ID", "env-tenant");
resolveMSTeamsCredentials.mockReturnValue(null);
hasConfiguredMSTeamsCredentials.mockReturnValue(false);
const result = await msteamsSetupWizard.finalize?.({
cfg: { channels: { msteams: { existing: true } } },
prompter: {
confirm: vi.fn(async () => true),
note: vi.fn(async () => {}),
text: vi.fn(),
},
} as never);
expect(result).toEqual({
accountId: "default",
cfg: {
channels: {
msteams: {
existing: true,
enabled: true,
},
},
},
});
});
it("finalize prompts for manual credentials when env/config creds are unavailable", async () => {
resolveMSTeamsCredentials.mockReturnValue(null);
hasConfiguredMSTeamsCredentials.mockReturnValue(false);
const note = vi.fn(async () => {});
const confirm = vi.fn(async () => false);
const text = vi.fn(async ({ message }: { message: string }) => {
if (message === "Enter MS Teams App ID") {
return "app-id";
}
if (message === "Enter MS Teams App Password") {
return "app-password";
}
if (message === "Enter MS Teams Tenant ID") {
return "tenant-id";
}
throw new Error(`Unexpected prompt: ${message}`);
});
const result = await msteamsSetupWizard.finalize?.({
cfg: { channels: { msteams: {} } },
prompter: {
confirm,
note,
text,
},
} as never);
expect(note).toHaveBeenCalled();
expect(result).toEqual({
accountId: "default",
cfg: {
channels: {
msteams: {
enabled: true,
appId: "app-id",
appPassword: "app-password",
tenantId: "tenant-id",
},
},
},
});
});
it("revalidates before delegated OAuth and immediately before saving tokens", async () => {
const tokens = {
accessToken: "access-token",
refreshToken: "refresh-token",
expiresAt: Date.now() + 60_000,
scopes: ["User.Read"],
};
resolveMSTeamsCredentials.mockReturnValue({
type: "secret",
appId: "app-id",
appPassword: "app-password",
tenantId: "tenant-id",
});
hasConfiguredMSTeamsCredentials.mockReturnValue(true);
loginMSTeamsDelegated.mockResolvedValue(tokens);
expect(oauthModuleState.loaded).toBe(false);
const beforePersistentEffect = vi.fn(async () => {
expect(oauthModuleState.loaded).toBe(true);
});
const progress = { update: vi.fn(), stop: vi.fn() };
await delegatedMsteamsSetupWizard.finalize?.({
cfg: { channels: { msteams: {} } },
prompter: {
confirm: vi.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(true),
note: vi.fn(async () => {}),
progress: vi.fn(() => progress),
text: vi.fn(),
},
options: { beforePersistentEffect },
} as never);
expect(beforePersistentEffect).toHaveBeenCalledTimes(2);
expect(loginMSTeamsDelegated).toHaveBeenCalledTimes(1);
expect(saveDelegatedTokens).toHaveBeenCalledWith(tokens);
expect(beforePersistentEffect.mock.invocationCallOrder[0]).toBeLessThan(
loginMSTeamsDelegated.mock.invocationCallOrder[0]!,
);
expect(loginMSTeamsDelegated.mock.invocationCallOrder[0]).toBeLessThan(
beforePersistentEffect.mock.invocationCallOrder[1]!,
);
expect(beforePersistentEffect.mock.invocationCallOrder[1]).toBeLessThan(
saveDelegatedTokens.mock.invocationCallOrder[0]!,
);
});
it("propagates a stale inference guard instead of treating it as an OAuth failure", async () => {
const guardError = new Error("verified inference changed");
resolveMSTeamsCredentials.mockReturnValue({
type: "secret",
appId: "app-id",
appPassword: "app-password",
tenantId: "tenant-id",
});
hasConfiguredMSTeamsCredentials.mockReturnValue(true);
loginMSTeamsDelegated.mockResolvedValue({
accessToken: "access-token",
refreshToken: "refresh-token",
expiresAt: Date.now() + 60_000,
scopes: ["User.Read"],
});
const beforePersistentEffect = vi
.fn()
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(guardError);
const note = vi.fn(async () => {});
const progress = { update: vi.fn(), stop: vi.fn() };
await expect(
delegatedMsteamsSetupWizard.finalize?.({
cfg: { channels: { msteams: {} } },
prompter: {
confirm: vi.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(true),
note,
progress: vi.fn(() => progress),
text: vi.fn(),
},
options: { beforePersistentEffect },
} as never),
).rejects.toBe(guardError);
expect(loginMSTeamsDelegated).toHaveBeenCalledTimes(1);
expect(saveDelegatedTokens).not.toHaveBeenCalled();
expect(progress.stop).toHaveBeenCalledWith();
expect(note).not.toHaveBeenCalledWith(
expect.stringContaining("Delegated auth setup failed"),
expect.anything(),
);
});
});