Quellcodebibliothek Statistik Leitseite products/Sources   (Android Betriebssystem Version 17©)  Datei vom mit Größe 0 B image not shown  

SSL monitor-webhook.test.ts

  Sprache: JAVA
 

import type { IncomingMessage, ServerResponse } from "node:http";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { WebhookTarget } from "./monitor-types.js";
import type { GoogleChatEvent } from "./types.js";

const readJsonWebhookBodyOrReject = vi.hoisted(() => vi.fn());
const resolveWebhookTargetWithAuthOrReject = vi.hoisted(() => vi.fn());
const withResolvedWebhookRequestPipeline = vi.hoisted(() => vi.fn());
const verifyGoogleChatRequest = vi.hoisted(() => vi.fn());

vi.mock("openclaw/plugin-sdk/webhook-request-guards", () => ({
  readJsonWebhookBodyOrReject,
}));

vi.mock("openclaw/plugin-sdk/webhook-targets", () => ({
  resolveWebhookTargetWithAuthOrReject,
  withResolvedWebhookRequestPipeline,
}));

vi.mock("./auth.js", () => ({
  verifyGoogleChatRequest,
}));

type ProcessEventFn = (event: GoogleChatEvent, target: WebhookTarget) => Promise<void>;
let createGoogleChatWebhookRequestHandler: typeof import("./monitor-webhook.js").createGoogleChatWebhookRequestHandler;
let warnAppPrincipalMisconfiguration: typeof import("./monitor-webhook.js").warnAppPrincipalMisconfiguration;

function createRequest(authorization?: string): IncomingMessage {
  return {
    method: "POST",
    url: "/googlechat",
    headers: {
      authorization: authorization ?? "",
      "content-type""application/json",
    },
  } as IncomingMessage;
}

function createResponse() {
  const res = {
    statusCode: 0,
    headers: {} as Record<string, string>,
    body: "",
    setHeader: (name: string, value: string) => {
      res.headers[name] = value;
    },
    end: (payload?: string) => {
      res.body = payload ?? "";
      return res;
    },
  } as ServerResponse & { headers: Record<string, string>; body: string };
  return res;
}

function installSimplePipeline(targets: unknown[]) {
  withResolvedWebhookRequestPipeline.mockImplementation(
    async ({
      handle,
      req,
      res,
    }: {
      handle: (input: {
        targets: unknown[];
        req: IncomingMessage;
        res: ServerResponse;
      }) => Promise<unknown>;
      req: IncomingMessage;
      res: ServerResponse;
    }) =>
      await handle({
        targets,
        req,
        res,
      }),
  );
}

async function runWebhookHandler(options?: {
  processEvent?: ProcessEventFn;
  authorization?: string;
}) {
  const processEvent: ProcessEventFn =
    options?.processEvent ?? (vi.fn(async () => {}) as ProcessEventFn);
  const handler = createGoogleChatWebhookRequestHandler({
    webhookTargets: new Map(),
    webhookInFlightLimiter: {} as never,
    processEvent,
  });
  const req = createRequest(options?.authorization);
  const res = createResponse();
  await expect(handler(req, res)).resolves.toBe(true);
  return { processEvent, res };
}

describe("googlechat monitor webhook", () => {
  beforeAll(async () => {
    ({ createGoogleChatWebhookRequestHandler, warnAppPrincipalMisconfiguration } =
      await import("./monitor-webhook.js"));
  });

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("accepts add-on payloads that carry systemIdToken in the body", async () => {
    installSimplePipeline([
      {
        account: {
          accountId: "default",
          config: { appPrincipal: "chat-app" },
        },
        runtime: { error: vi.fn() },
        statusSink: vi.fn(),
        audienceType: "app-url",
        audience: "https://example.com/googlechat",
      },
    ]);
    readJsonWebhookBodyOrReject.mockResolvedValue({
      ok: true,
      value: {
        commonEventObject: { hostApp: "CHAT" },
        authorizationEventObject: { systemIdToken: "addon-token" },
        chat: {
          eventTime: "2026-03-22T00:00:00.000Z",
          user: { name: "users/123" },
          messagePayload: {
            space: { name: "spaces/AAA" },
            message: { name: "spaces/AAA/messages/1", text: "hello" },
          },
        },
      },
    });
    resolveWebhookTargetWithAuthOrReject.mockImplementation(async ({ isMatch, targets }) => {
      for (const target of targets) {
        if (await isMatch(target)) {
          return target;
        }
      }
      return null;
    });
    verifyGoogleChatRequest.mockResolvedValue({ ok: true });
    const { processEvent, res } = await runWebhookHandler();

    expect(verifyGoogleChatRequest).toHaveBeenCalledWith(
      expect.objectContaining({
        bearer: "addon-token",
        expectedAddOnPrincipal: "chat-app",
      }),
    );
    expect(processEvent).toHaveBeenCalledWith(
      expect.objectContaining({
        type: "MESSAGE",
        space: { name: "spaces/AAA" },
      }),
      expect.anything(),
    );
    expect(res.statusCode).toBe(200);
    expect(res.headers["Content-Type"]).toBe("application/json");
  });

  it("logs WARN with reason when verification fails (missing token)", async () => {
    const logFn = vi.fn();
    installSimplePipeline([
      {
        account: {
          accountId: "acct-1",
          config: { appPrincipal: "chat-app" },
        },
        runtime: { log: logFn, error: vi.fn() },
        audienceType: "app-url",
        audience: "https://example.com/googlechat",
      },
    ]);
    readJsonWebhookBodyOrReject.mockResolvedValue({
      ok: true,
      value: {
        commonEventObject: { hostApp: "CHAT" },
        authorizationEventObject: { systemIdToken: "bad-token" },
        chat: {
          messagePayload: {
            space: { name: "spaces/AAA" },
            message: { name: "spaces/AAA/messages/1", text: "hi" },
          },
        },
      },
    });
    resolveWebhookTargetWithAuthOrReject.mockImplementation(async ({ isMatch, targets, res }) => {
      for (const target of targets) {
        if (await isMatch(target)) {
          return target;
        }
      }
      res.statusCode = 401;
      res.end("unauthorized");
      return null;
    });
    verifyGoogleChatRequest.mockResolvedValue({ ok: false, reason: "missing token" });
    const { processEvent, res } = await runWebhookHandler();

    expect(logFn).toHaveBeenCalledWith(expect.stringContaining("acct-1"));
    expect(logFn).toHaveBeenCalledWith(expect.stringContaining("missing token"));
    expect(processEvent).not.toHaveBeenCalled();
    expect(res.statusCode).toBe(401);
  });

  it("logs WARN with reason when verification fails (unexpected principal)", async () => {
    const logFn = vi.fn();
    installSimplePipeline([
      {
        account: {
          accountId: "acct-2",
          config: { appPrincipal: "chat-app" },
        },
        runtime: { log: logFn, error: vi.fn() },
        audienceType: "app-url",
        audience: "https://example.com/googlechat",
      },
    ]);
    readJsonWebhookBodyOrReject.mockResolvedValue({
      ok: true,
      value: {
        commonEventObject: { hostApp: "CHAT" },
        authorizationEventObject: { systemIdToken: "bad-token" },
        chat: {
          messagePayload: {
            space: { name: "spaces/AAA" },
            message: { name: "spaces/AAA/messages/1", text: "hi" },
          },
        },
      },
    });
    resolveWebhookTargetWithAuthOrReject.mockImplementation(async ({ isMatch, targets, res }) => {
      for (const target of targets) {
        if (await isMatch(target)) {
          return target;
        }
      }
      res.statusCode = 401;
      res.end("unauthorized");
      return null;
    });
    verifyGoogleChatRequest.mockResolvedValue({
      ok: false,
      reason: "unexpected add-on principal: 999999999999999999999",
    });
    const { processEvent, res } = await runWebhookHandler();

    expect(logFn).toHaveBeenCalledWith(expect.stringContaining("acct-2"));
    expect(logFn).toHaveBeenCalledWith(
      expect.stringContaining("unexpected add-on principal: 999999999999999999999"),
    );
    expect(processEvent).not.toHaveBeenCalled();
    expect(res.statusCode).toBe(401);
  });

  it("does not log WARN when verification succeeds", async () => {
    const logFn = vi.fn();
    installSimplePipeline([
      {
        account: {
          accountId: "acct-ok",
          config: { appPrincipal: "chat-app" },
        },
        runtime: { log: logFn, error: vi.fn() },
        statusSink: vi.fn(),
        audienceType: "app-url",
        audience: "https://example.com/googlechat",
      },
    ]);
    readJsonWebhookBodyOrReject.mockResolvedValue({
      ok: true,
      value: {
        commonEventObject: { hostApp: "CHAT" },
        authorizationEventObject: { systemIdToken: "good-token" },
        chat: {
          eventTime: "2026-03-22T00:00:00.000Z",
          user: { name: "users/123" },
          messagePayload: {
            space: { name: "spaces/AAA" },
            message: { name: "spaces/AAA/messages/1", text: "hi" },
          },
        },
      },
    });
    resolveWebhookTargetWithAuthOrReject.mockImplementation(async ({ isMatch, targets }) => {
      for (const target of targets) {
        if (await isMatch(target)) {
          return target;
        }
      }
      return null;
    });
    verifyGoogleChatRequest.mockResolvedValue({ ok: true });
    const { res } = await runWebhookHandler();

    expect(logFn).not.toHaveBeenCalled();
    expect(res.statusCode).toBe(200);
  });

  it("does not log failed candidate targets when another target verifies", async () => {
    const logA = vi.fn();
    const logB = vi.fn();
    installSimplePipeline([
      {
        account: {
          accountId: "acct-a",
          config: { appPrincipal: "chat-app-a" },
        },
        runtime: { log: logA, error: vi.fn() },
        audienceType: "app-url",
        audience: "https://example.com/googlechat",
      },
      {
        account: {
          accountId: "acct-b",
          config: { appPrincipal: "chat-app-b" },
        },
        runtime: { log: logB, error: vi.fn() },
        statusSink: vi.fn(),
        audienceType: "app-url",
        audience: "https://example.com/googlechat",
      },
    ]);
    readJsonWebhookBodyOrReject.mockResolvedValue({
      ok: true,
      value: {
        commonEventObject: { hostApp: "CHAT" },
        authorizationEventObject: { systemIdToken: "shared-path-token" },
        chat: {
          eventTime: "2026-03-22T00:00:00.000Z",
          user: { name: "users/123" },
          messagePayload: {
            space: { name: "spaces/BBB" },
            message: { name: "spaces/BBB/messages/1", text: "hi" },
          },
        },
      },
    });
    resolveWebhookTargetWithAuthOrReject.mockImplementation(async ({ isMatch, targets }) => {
      for (const target of targets) {
        if (await isMatch(target)) {
          return target;
        }
      }
      return null;
    });
    verifyGoogleChatRequest
      .mockResolvedValueOnce({ ok: false, reason: "unexpected add-on principal: 111" })
      .mockResolvedValueOnce({ ok: true });
    const { processEvent, res } = await runWebhookHandler();

    expect(logA).not.toHaveBeenCalled();
    expect(logB).not.toHaveBeenCalled();
    expect(processEvent).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        account: expect.objectContaining({ accountId: "acct-b" }),
      }),
    );
    expect(res.statusCode).toBe(200);
  });

  it("rejects missing add-on bearer tokens before dispatch", async () => {
    const logFn = vi.fn();
    installSimplePipeline([
      {
        account: {
          accountId: "default",
          config: { appPrincipal: "chat-app" },
        },
        runtime: { log: logFn, error: vi.fn() },
      },
    ]);
    readJsonWebhookBodyOrReject.mockResolvedValue({
      ok: true,
      value: {
        commonEventObject: { hostApp: "CHAT" },
        chat: {
          messagePayload: {
            space: { name: "spaces/AAA" },
            message: { name: "spaces/AAA/messages/1", text: "hello" },
          },
        },
      },
    });
    const { processEvent, res } = await runWebhookHandler();

    expect(processEvent).not.toHaveBeenCalled();
    expect(logFn).toHaveBeenCalledWith(expect.stringContaining("default"));
    expect(logFn).toHaveBeenCalledWith(expect.stringContaining("missing token"));
    expect(res.statusCode).toBe(401);
    expect(res.body).toBe("unauthorized");
  });
});

describe("warnAppPrincipalMisconfiguration", () => {
  it("warns when appPrincipal is missing for app-url audience", () => {
    const log = vi.fn();
    warnAppPrincipalMisconfiguration({
      accountId: "acct-missing",
      audienceType: "app-url",
      appPrincipal: undefined,
      log,
    });
    expect(log).toHaveBeenCalledOnce();
    expect(log).toHaveBeenCalledWith(expect.stringContaining("acct-missing"));
    expect(log).toHaveBeenCalledWith(expect.stringContaining("appPrincipal is missing"));
    expect(log).toHaveBeenCalledWith(expect.stringContaining("numeric OAuth 2.0 client ID"));
  });

  it("warns when appPrincipal contains @ for app-url audience", () => {
    const log = vi.fn();
    warnAppPrincipalMisconfiguration({
      accountId: "acct-email",
      audienceType: "app-url",
      appPrincipal: "bot@example.iam.gserviceaccount.com",
      log,
    });
    expect(log).toHaveBeenCalledOnce();
    expect(log).toHaveBeenCalledWith(expect.stringContaining("acct-email"));
    expect(log).toHaveBeenCalledWith(expect.stringContaining("looks like an email"));
    expect(log).toHaveBeenCalledWith(expect.stringContaining("numeric OAuth 2.0 client ID"));
  });

  it("does not warn for valid numeric appPrincipal with app-url audience", () => {
    const log = vi.fn();
    warnAppPrincipalMisconfiguration({
      accountId: "acct-ok",
      audienceType: "app-url",
      appPrincipal: "123456789012345678901",
      log,
    });
    expect(log).not.toHaveBeenCalled();
  });

  it("does not warn for project-number audience even with missing appPrincipal", () => {
    const log = vi.fn();
    warnAppPrincipalMisconfiguration({
      accountId: "acct-pn",
      audienceType: "project-number",
      appPrincipal: undefined,
      log,
    });
    expect(log).not.toHaveBeenCalled();
  });
});

Messung V0.5 in Prozent
C=99 H=89 G=94

¤ Dauer der Verarbeitung: 0.1 Sekunden  (vorverarbeitet am  2026-06-07) ¤

*© Formatika GbR, Deutschland






Wurzel

Ziele

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Ergonomie der
Schnittstellen

Diese beiden folgenden Angebotsgruppen bietet das Unternehmen

Angebot

Hier finden Sie eine Liste der Produkte des Unternehmens