Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/JAVA/Openclaw/extensions/msteams/src/   (KI Agentensystem Version 22©)  Datei vom 26.3.2026 mit Größe 17 kB image not shown  

Quelle  monitor-handler.sso.test.ts

  Sprache: JAVA
 

import { beforeAll, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../runtime-api.js";
import {
  type MSTeamsActivityHandler,
  type MSTeamsMessageHandlerDeps,
  registerMSTeamsHandlers,
} from "./monitor-handler.js";
import {
  createActivityHandler as baseCreateActivityHandler,
  createMSTeamsMessageHandlerDeps,
  installMSTeamsTestRuntime,
} from "./monitor-handler.test-helpers.js";
import type { MSTeamsTurnContext } from "./sdk-types.js";
import { createMSTeamsSsoTokenStoreMemory } from "./sso-token-store.js";
import {
  type MSTeamsSsoFetch,
  handleSigninTokenExchangeInvoke,
  handleSigninVerifyStateInvoke,
  parseSigninTokenExchangeValue,
  parseSigninVerifyStateValue,
} from "./sso.js";

function createActivityHandler() {
  const run = vi.fn(async () => undefined);
  const handler = baseCreateActivityHandler(run);
  return { handler, run };
}

function createDepsWithoutSso(
  overrides: Partial<MSTeamsMessageHandlerDeps> = {},
): MSTeamsMessageHandlerDeps {
  const base = createMSTeamsMessageHandlerDeps();
  return { ...base, ...overrides };
}

function createSsoDeps(params: { fetchImpl: MSTeamsSsoFetch }) {
  const tokenStore = createMSTeamsSsoTokenStoreMemory();
  const tokenProvider = {
    getAccessToken: vi.fn(async () => "bf-service-token"),
  };
  return {
    sso: {
      tokenProvider,
      tokenStore,
      connectionName: "GraphConnection",
      fetchImpl: params.fetchImpl,
    },
    tokenStore,
    tokenProvider,
  };
}

function createRegisteredSsoHandler(sso: MSTeamsMessageHandlerDeps["sso"]) {
  const deps = createDepsWithoutSso({ sso });
  const { handler } = createActivityHandler();
  const registered = registerMSTeamsHandlers(handler, deps) as MSTeamsActivityHandler &&nbsp;{
    run: NonNullable<MSTeamsActivityHandler["run"]>;
  };
  return { deps, registered };
}

function createSigninInvokeContext(params: {
  name: "signin/tokenExchange" | "signin/verifyState";
  value: unknown;
  userAadId?: string;
  userBfId?: string;
  conversationId?: string;
  conversationType?: "personal" | "groupChat" | "channel";
  teamId?: string;
  channelName?: string;
}): MSTeamsTurnContext & { sendActivity: ReturnType<typeof vi.fn> } {
  const conversationType = params.conversationType ?? "personal";
  const conversationId =
    params.conversationId ??
    (conversationType === "personal"
      ? "19:personal-chat"
      : conversationType === "channel"
        ? "19:channel@thread.tacv2"
        : "19:group@thread.tacv2");

  return {
    activity: {
      id: "invoke-1",
      type: "invoke",
      name: params.name,
      channelId: "msteams",
      serviceUrl: "https://service.example.test",
      from: {
        id: params.userBfId ?? "bf-user",
        aadObjectId: params.userAadId ?? "aad-user-guid",
        name: "Test User",
      },
      recipient: { id: "bot-id", name: "Bot" },
      conversation: {
        id: conversationId,
        conversationType,
        tenantId: params.teamId ? "tenant-1" : undefined,
      },
      channelData: params.teamId
        ? {
            team: { id: params.teamId, name: "Team 1" },
            channel: params.channelName ? { name: params.channelName } : undefined,
          }
        : {},
      attachments: [],
      value: params.value,
    },
    sendActivity: vi.fn(async () => ({ id: "ack-id" })),
    sendActivities: vi.fn(async () => []),
    updateActivity: vi.fn(async () => ({ id: "update" })),
    deleteActivity: vi.fn(async () => {}),
  } as unknown as MSTeamsTurnContext & {
    sendActivity: ReturnType<typeof vi.fn>;
  };
}

function createFakeFetch(handlers: Array<(url: string, init?: unknown) => unknown>) {
  const calls: Array<{ url: string; init?: unknown }> = [];
  const fetchImpl: MSTeamsSsoFetch = async (url, init) => {
    calls.push({ url, init });
    const handler = handlers.shift();
    if (!handler) {
      throw new Error("unexpected fetch call");
    }
    const response = handler(url, init) as {
      ok: boolean;
      status: number;
      body: unknown;
    };
    return {
      ok: response.ok,
      status: response.status,
      json: async () => response.body,
      text: async () =>
        typeof response.body === "string" ? response.body : JSON.stringify(response.body ?? ""),
    };
  };
  return { fetchImpl, calls };
}

function createBlockedSigninScenarios() {
  return [
    {
      name: "DM sender outside allowlist",
      cfg: {
        channels: {
          msteams: {
            dmPolicy: "allowlist",
            allowFrom: ["owner-aad"],
          },
        },
      } as OpenClawConfig,
      context: {
        userAadId: "blocked-dm-aad",
      },
      expectedDropLog: "dropping signin invoke (dm sender not allowlisted)",
    },
    {
      name: "channel outside route allowlist",
      cfg: {
        channels: {
          msteams: {
            groupPolicy: "allowlist",
            groupAllowFrom: ["blocked-channel-aad"],
            teams: {
              "team-allowlisted": {
                channels: {
                  "19:allowlisted@thread.tacv2": { requireMention: false },
                },
              },
            },
          },
        },
      } as OpenClawConfig,
      context: {
        userAadId: "blocked-channel-aad",
        conversationType: "channel" as const,
        conversationId: "19:blocked-channel@thread.tacv2",
        teamId: "team-blocked",
        channelName: "General",
      },
      expectedDropLog: "dropping signin invoke (not in team/channel allowlist)",
    },
    {
      name: "group sender outside group allowlist",
      cfg: {
        channels: {
          msteams: {
            groupPolicy: "allowlist",
            groupAllowFrom: ["owner-aad"],
          },
        },
      } as OpenClawConfig,
      context: {
        userAadId: "blocked-group-aad",
        conversationType: "groupChat" as const,
        conversationId: "19:group-chat@thread.v2",
      },
      expectedDropLog: "dropping signin invoke (group sender not allowlisted)",
    },
  ];
}

describe("msteams signin invoke value parsers", () => {
  it("parses signin/tokenExchange values", () => {
    expect(
      parseSigninTokenExchangeValue({
        id: "flow-1",
        connectionName: "Graph",
        token: "eyJ...",
      }),
    ).toEqual({ id: "flow-1", connectionName: "Graph", token: "eyJ..." });
  });

  it("rejects non-object signin/tokenExchange values", () => {
    expect(parseSigninTokenExchangeValue(null)).toBeNull();
    expect(parseSigninTokenExchangeValue("nope")).toBeNull();
  });

  it("parses signin/verifyState values", () => {
    expect(parseSigninVerifyStateValue({ state: "123456" })).toEqual({ state: "123456" });
    expect(parseSigninVerifyStateValue({})).toEqual({ state: undefined });
    expect(parseSigninVerifyStateValue(null)).toBeNull();
  });
});

describe("handleSigninTokenExchangeInvoke", () => {
  it("exchanges the Teams token and persists the result", async () => {
    const { fetchImpl, calls } = createFakeFetch([
      () => ({
        ok: true,
        status: 200,
        body: {
          channelId: "msteams",
          connectionName: "GraphConnection",
          token: "delegated-graph-token",
          expiration: "2030-01-01T00:00:00Z",
        },
      }),
    ]);
    const { sso, tokenStore } = createSsoDeps({ fetchImpl });

    const result = await handleSigninTokenExchangeInvoke({
      value: { id: "flow-1", connectionName: "GraphConnection", token: "exchangeable-token" },
      user: { userId: "aad-user-guid", channelId: "msteams" },
      deps: sso,
    });

    expect(result).toEqual({
      ok: true,
      token: "delegated-graph-token",
      expiresAt: "2030-01-01T00:00:00Z",
    });
    expect(calls).toHaveLength(1);
    expect(calls[0]?.url).toContain("/api/usertoken/exchange");
    expect(calls[0]?.url).toContain("userId=aad-user-guid");
    expect(calls[0]?.url).toContain("connectionName=GraphConnection");
    expect(calls[0]?.url).toContain("channelId=msteams");

    const init = calls[0]?.init as {
      method?: string;
      headers?: Record<string, string>;
      body?: string;
    };
    expect(init?.method).toBe("POST");
    expect(init?.headers?.Authorization).toBe("Bearer bf-service-token");
    expect(JSON.parse(init?.body ?? "{}")).toEqual({ token: "exchangeable-token" });

    const stored = await tokenStore.get({
      connectionName: "GraphConnection",
      userId: "aad-user-guid",
    });
    expect(stored?.token).toBe("delegated-graph-token");
    expect(stored?.expiresAt).toBe("2030-01-01T00:00:00Z");
  });

  it("returns a service error when the User Token service rejects the exchange", async () => {
    const { fetchImpl } = createFakeFetch([
      () => ({ ok: false, status: 502, body: "bad gateway" }),
    ]);
    const { sso, tokenStore } = createSsoDeps({ fetchImpl });

    const result = await handleSigninTokenExchangeInvoke({
      value: { id: "flow-1", connectionName: "GraphConnection", token: "exchangeable-token" },
      user: { userId: "aad-user-guid", channelId: "msteams" },
      deps: sso,
    });

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.code).toBe("service_error");
      expect(result.status).toBe(502);
      expect(result.message).toContain("bad gateway");
    }
    const stored = await tokenStore.get({
      connectionName: "GraphConnection",
      userId: "aad-user-guid",
    });
    expect(stored).toBeNull();
  });

  it("refuses to exchange without a user id", async () => {
    const { fetchImpl, calls } = createFakeFetch([]);
    const { sso } = createSsoDeps({ fetchImpl });

    const result = await handleSigninTokenExchangeInvoke({
      value: { id: "flow-1", connectionName: "GraphConnection", token: "exchangeable-token" },
      user: { userId: "", channelId: "msteams" },
      deps: sso,
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.code).toBe("missing_user");
    }
    expect(calls).toHaveLength(0);
  });
});

describe("handleSigninVerifyStateInvoke", () => {
  it("fetches the user token for the magic code and persists it", async () => {
    const { fetchImpl, calls } = createFakeFetch([
      () => ({
        ok: true,
        status: 200,
        body: {
          channelId: "msteams",
          connectionName: "GraphConnection",
          token: "delegated-token-2",
          expiration: "2031-02-03T04:05:06Z",
        },
      }),
    ]);
    const { sso, tokenStore } = createSsoDeps({ fetchImpl });

    const result = await handleSigninVerifyStateInvoke({
      value: { state: "654321" },
      user: { userId: "aad-user-guid", channelId: "msteams" },
      deps: sso,
    });

    expect(result.ok).toBe(true);
    expect(calls[0]?.url).toContain("/api/usertoken/GetToken");
    expect(calls[0]?.url).toContain("code=654321");
    const init = calls[0]?.init as { method?: string };
    expect(init?.method).toBe("GET");

    const stored = await tokenStore.get({
      connectionName: "GraphConnection",
      userId: "aad-user-guid",
    });
    expect(stored?.token).toBe("delegated-token-2");
  });

  it("rejects invocations without a state code", async () => {
    const { fetchImpl, calls } = createFakeFetch([]);
    const { sso } = createSsoDeps({ fetchImpl });
    const result = await handleSigninVerifyStateInvoke({
      value: { state: "   " },
      user: { userId: "aad-user-guid", channelId: "msteams" },
      deps: sso,
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.code).toBe("missing_state");
    }
    expect(calls).toHaveLength(0);
  });
});

describe("msteams signin invoke handler registration", () => {
  beforeAll(() => {
    installMSTeamsTestRuntime();
  });

  const blockedSigninScenarios = createBlockedSigninScenarios();
  const invokeVariants = [
    {
      name: "signin/tokenExchange" as const,
      value: { id: "x", connectionName: "GraphConnection", token: "exchangeable" },
    },
    {
      name: "signin/verifyState" as const,
      value: { state: "112233" },
    },
  ];

  it("acks signin invokes even when sso is not configured", async () => {
    const deps = createDepsWithoutSso();
    const { handler, run } = createActivityHandler();
    const registered = registerMSTeamsHandlers(handler, deps) as MSTeamsActivityHandler &&nbsp;{
      run: NonNullable<MSTeamsActivityHandler["run"]>;
    };

    const ctx = createSigninInvokeContext({
      name: "signin/tokenExchange",
      value: { id: "x", connectionName: "Graph", token: "exchangeable" },
    });

    await registered.run(ctx);

    expect(ctx.sendActivity).toHaveBeenCalledWith(
      expect.objectContaining({
        type: "invokeResponse",
        value: expect.objectContaining({ status: 200 }),
      }),
    );
    expect(run).not.toHaveBeenCalled();
    expect(deps.log.debug).toHaveBeenCalledWith(
      "signin invoke received but msteams.sso is not configured",
      expect.objectContaining({ name: "signin/tokenExchange" }),
    );
  });

  for (const invoke of invokeVariants) {
    for (const scenario of blockedSigninScenarios) {
      it(`does not process ${invoke.name} for ${scenario.name}`, async () => {
        const { fetchImpl, calls } = createFakeFetch([
          () => ({
            ok: true,
            status: 200,
            body: {
              channelId: "msteams",
              connectionName: "GraphConnection",
              token: "delegated-graph-token",
              expiration: "2030-01-01T00:00:00Z",
            },
          }),
        ]);
        const { sso, tokenStore } = createSsoDeps({ fetchImpl });
        const deps = createDepsWithoutSso({ cfg: scenario.cfg, sso });
        const { handler } = createActivityHandler();
        const registered = registerMSTeamsHandlers(handler, deps) as MSTeamsActivityHandler &&nbsp;{
          run: NonNullable<MSTeamsActivityHandler["run"]>;
        };

        const ctx = createSigninInvokeContext({
          name: invoke.name,
          value: invoke.value,
          ...scenario.context,
        });

        await registered.run(ctx);

        expect(ctx.sendActivity).toHaveBeenCalledWith(
          expect.objectContaining({
            type: "invokeResponse",
            value: expect.objectContaining({ status: 200 }),
          }),
        );
        expect(calls).toHaveLength(0);
        const stored = await tokenStore.get({
          connectionName: "GraphConnection",
          userId: scenario.context.userAadId ?? "aad-user-guid",
        });
        expect(stored).toBeNull();
        expect(deps.log.debug).toHaveBeenCalledWith(
          scenario.expectedDropLog,
          expect.objectContaining({ name: invoke.name }),
        );
      });
    }
  }

  it("invokes the token exchange handler when sso is configured", async () => {
    const { fetchImpl } = createFakeFetch([
      () => ({
        ok: true,
        status: 200,
        body: {
          channelId: "msteams",
          connectionName: "GraphConnection",
          token: "delegated-graph-token",
          expiration: "2030-01-01T00:00:00Z",
        },
      }),
    ]);
    const { sso, tokenStore } = createSsoDeps({ fetchImpl });
    const { deps, registered } = createRegisteredSsoHandler(sso);

    const ctx = createSigninInvokeContext({
      name: "signin/tokenExchange",
      value: { id: "x", connectionName: "GraphConnection", token: "exchangeable" },
    });

    await registered.run(ctx);

    expect(ctx.sendActivity).toHaveBeenCalledWith(
      expect.objectContaining({
        type: "invokeResponse",
        value: expect.objectContaining({ status: 200 }),
      }),
    );
    expect(deps.log.info).toHaveBeenCalledWith(
      "msteams sso token exchanged",
      expect.objectContaining({ userId: "aad-user-guid", hasExpiry: true }),
    );
    const stored = await tokenStore.get({
      connectionName: "GraphConnection",
      userId: "aad-user-guid",
    });
    expect(stored?.token).toBe("delegated-graph-token");
  });

  it("logs an error when the token exchange fails", async () => {
    const { fetchImpl } = createFakeFetch([
      () => ({ ok: false, status: 400, body: "bad request" }),
    ]);
    const { sso } = createSsoDeps({ fetchImpl });
    const { deps, registered } = createRegisteredSsoHandler(sso);

    const ctx = createSigninInvokeContext({
      name: "signin/tokenExchange",
      value: { id: "x", connectionName: "GraphConnection", token: "exchangeable" },
    });

    await registered.run(ctx);

    expect(ctx.sendActivity).toHaveBeenCalledWith(
      expect.objectContaining({ type: "invokeResponse" }),
    );
    expect(deps.log.error).toHaveBeenCalledWith(
      "msteams sso token exchange failed",
      expect.objectContaining({ code: "unexpected_response", status: 400 }),
    );
  });

  it("handles signin/verifyState via the magic-code flow", async () => {
    const { fetchImpl } = createFakeFetch([
      () => ({
        ok: true,
        status: 200,
        body: {
          channelId: "msteams",
          connectionName: "GraphConnection",
          token: "delegated-token-3",
        },
      }),
    ]);
    const { sso, tokenStore } = createSsoDeps({ fetchImpl });
    const deps = createDepsWithoutSso({ sso });
    const { handler } = createActivityHandler();
    const registered = registerMSTeamsHandlers(handler, deps) as MSTeamsActivityHandler &&nbsp;{
      run: NonNullable<MSTeamsActivityHandler["run"]>;
    };

    const ctx = createSigninInvokeContext({
      name: "signin/verifyState",
      value: { state: "112233" },
    });

    await registered.run(ctx);

    expect(deps.log.info).toHaveBeenCalledWith(
      "msteams sso verifyState succeeded",
      expect.objectContaining({ userId: "aad-user-guid" }),
    );
    const stored = await tokenStore.get({
      connectionName: "GraphConnection",
      userId: "aad-user-guid",
    });
    expect(stored?.token).toBe("delegated-token-3");
  });
});

Messung V0.5 in Prozent
C=100 H=97 G=98

¤ Dauer der Verarbeitung: 0.2 Sekunden  (vorverarbeitet am  2026-05-26) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.