Files
multica/packages/views/autopilots/components/webhook-url-field.test.tsx
Bohan Jiang 50c89c7094 feat(autopilots): hide webhook URL token by default (MUL-5374) (#6015)
* feat(autopilots): hide webhook URL token by default (MUL-5374)

The webhook URL is a bearer credential — anyone who reads it off a screen
share or screenshot can fire the autopilot. GH #6004 reports exactly that
leak during a live demo.

The trigger row and the post-create panel now render the URL through a
shared WebhookUrlField that masks the token segment by default. Clicking
the value (or the eye toggle) reveals it; Copy keeps working while hidden,
so the common case never needs a reveal. The plaintext token is not in the
DOM until the user asks for it — this is a real display boundary, not a
CSS blur.

Co-authored-by: multica-agent <github@multica.ai>

* fix(autopilots): scope webhook URL reveal to the URL it was granted for (MUL-5374)

The reveal was a bare boolean, so a token rotation under a mounted trigger
row swapped in the new URL while `revealed` was still true — exposing the
new credential in the clear at the exact moment the user rotated to contain
a leak.

Track which URL the reveal was granted for and derive `revealed` during
render. Deriving it (rather than resetting in an effect) means the new
token never reaches the DOM, not even for the pre-effect frame.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-28 14:10:42 +08:00

75 lines
3.3 KiB
TypeScript

import { describe, it, expect, beforeAll, vi } from "vitest";
import { screen, fireEvent } from "@testing-library/react";
import { renderWithI18n } from "../../test/i18n";
import { WebhookUrlField } from "./webhook-url-field";
// sonner.toast is a fire-and-forget side-effect; stub it so Copy doesn't blow
// up on toast invocation.
vi.mock("sonner", () => ({
toast: { success: vi.fn(), error: vi.fn() },
}));
const writeText = vi.fn().mockResolvedValue(undefined);
// jsdom doesn't provide navigator.clipboard by default. Stub it once.
beforeAll(() => {
Object.assign(navigator, { clipboard: { writeText } });
});
const URL = "https://api.example.com/api/webhooks/autopilots/awt_supersecret";
describe("WebhookUrlField", () => {
it("hides the token by default — the plaintext is not in the DOM", () => {
const { container } = renderWithI18n(<WebhookUrlField url={URL} />);
expect(container.textContent).not.toContain("awt_supersecret");
expect(screen.getByText(/api\/webhooks\/autopilots\/•+/)).toBeInTheDocument();
});
it("reveals the full URL when the masked value is clicked", () => {
const { container } = renderWithI18n(<WebhookUrlField url={URL} />);
fireEvent.click(screen.getByRole("button", { name: "Webhook URL hidden — click to show" }));
expect(container.textContent).toContain(URL);
});
it("hides again via the toggle", () => {
const { container } = renderWithI18n(<WebhookUrlField url={URL} />);
fireEvent.click(screen.getByRole("button", { name: "Webhook URL hidden — click to show" }));
fireEvent.click(screen.getByRole("button", { name: "Hide URL" }));
expect(container.textContent).not.toContain("awt_supersecret");
});
it("copies the real URL while it is still hidden", async () => {
writeText.mockClear();
const { container } = renderWithI18n(<WebhookUrlField url={URL} />);
fireEvent.click(screen.getByRole("button", { name: "Copy URL" }));
expect(writeText).toHaveBeenCalledWith(URL);
expect(container.textContent).not.toContain("awt_supersecret");
});
it("re-hides when the URL changes — a rotated token is never inherited revealed", () => {
const rotated = "https://api.example.com/api/webhooks/autopilots/awt_rotatedsecret";
const { container, rerender } = renderWithI18n(<WebhookUrlField url={URL} />);
fireEvent.click(screen.getByRole("button", { name: "Webhook URL hidden — click to show" }));
expect(container.textContent).toContain(URL);
// Rotate: the row stays mounted and only the prop changes. The new
// credential must come back masked, with no revealed frame in between.
rerender(<WebhookUrlField url={rotated} />);
expect(container.textContent).not.toContain("awt_rotatedsecret");
expect(
screen.getByRole("button", { name: "Webhook URL hidden — click to show" }),
).toBeInTheDocument();
// The new URL can still be revealed on its own.
fireEvent.click(screen.getByRole("button", { name: "Webhook URL hidden — click to show" }));
expect(container.textContent).toContain(rotated);
});
it("renders trailing actions alongside the field", () => {
renderWithI18n(
<WebhookUrlField url={URL} actions={<button type="button">Rotate</button>} />,
);
expect(screen.getByRole("button", { name: "Rotate" })).toBeInTheDocument();
});
});