Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/test/web-credentials-ipc.test.ts

typescript231 行查看原文
import type { EventEmitter } from "node:events";
import { afterAll, beforeAll, beforeEach, expect, test, vi } from "vitest";
import type { DesktopBridge, Result } from "../src/shared/bridge.ts";
import { methods } from "../src/shared/bridge.ts";

type CallHandler = (event: unknown, method: unknown, args: unknown) => Promise<Result<unknown>>;
const fake = vi.hoisted(() => {
  const webContents = {
    mainFrame: {},
    on: vi.fn(),
    once: vi.fn(),
    send: vi.fn(),
    setWindowOpenHandler: vi.fn(),
    session: { setPermissionRequestHandler: vi.fn(), setPermissionCheckHandler: vi.fn() },
  };
  return {
    window: { webContents, on: vi.fn() },
    handle: vi.fn(),
    expose: vi.fn(),
    invoke: vi.fn(),
    fetch: vi.fn(() => {
      throw new Error("Network is forbidden in credential reveal tests");
    }),
    writeError: vi.fn(),
    loadRenderer: vi.fn(),
    settings: {
      get: vi.fn(() => ({
        interface: { theme: "light" },
        shortcuts: { enabled: false },
        providers: [],
      })),
      getWebSearchCredentials: vi.fn(),
      getProviderCredentials: vi.fn(),
    },
  };
});
vi.mock("electron", () => ({
  app: {
    isPackaged: false,
    commandLine: { appendSwitch: vi.fn() },
    setName: vi.fn(),
    setPath: vi.fn(),
    getPath: () => "/unit-test-only",
    on: vi.fn(),
    once: vi.fn(),
    requestSingleInstanceLock: () => true,
    whenReady: async () => {},
    quit: vi.fn(),
  },
  clipboard: {},
  dialog: {},
  globalShortcut: {},
  ipcMain: { handle: fake.handle, on: vi.fn() },
  ipcRenderer: { invoke: fake.invoke, on: vi.fn() },
  contextBridge: { exposeInMainWorld: fake.expose },
  webUtils: {},
  Menu: { setApplicationMenu: vi.fn() },
  nativeTheme: { themeSource: "light", on: vi.fn() },
  net: { fetch: fake.fetch },
  safeStorage: {},
  systemPreferences: { subscribeWorkspaceNotification: vi.fn(), unsubscribeWorkspaceNotification: vi.fn() },
  screen: { getPrimaryDisplay: () => ({ workAreaSize: { width: 1200, height: 800 } }) },
  shell: {},
}));
vi.mock("../src/main/automation.ts", () => ({
  createDesktopWindow: () => fake.window,
  testMode: true,
  hiddenTestMode: true,
}));
vi.mock("../src/main/storage.ts", () => ({
  SettingsStore: class {
    get = fake.settings.get;
    getWebSearchCredentials = fake.settings.getWebSearchCredentials;
    getProviderCredentials = fake.settings.getProviderCredentials;
  },
}));
vi.mock("../src/main/state-store.ts", () => ({
  StateStore: class {
    close() {}
  },
}));
vi.mock("../src/main/storage-generation.ts", () => ({
  openStorageGeneration: () => ({ directory: "/unit-test-only", close: vi.fn() }),
}));
vi.mock("../src/main/error-log.ts", () => ({
  ErrorLog: class {
    write = fake.writeError;
  },
}));
vi.mock("../src/main/session-host.ts", () => ({
  SessionHost: class {
    async init() {}
    subscribe() {}
    setNotificationSink() {}
    async refreshPluginSettings() {}
  },
}));
vi.mock("../src/main/chatgpt-auth.ts", () => ({ ChatGPTAuth: class {} }));
vi.mock("../src/main/pane-services.ts", () => ({ PaneServices: class {} }));
vi.mock("../src/main/preview-pdf-export.ts", () => ({ PreviewPdfExporter: class {}, previewPdfOptions: {} }));
vi.mock("../src/main/task-notifications.ts", () => ({ TaskNotifications: class {} }));
vi.mock("../src/main/web-source-icons.ts", () => ({ WebSourceIcons: class {} }));
vi.mock("../src/main/shortcut-modifier.ts", () => ({
  ShortcutModifier: class {
    observe() {}
    reset() {}
  },
}));
vi.mock("../src/main/shortcut-controller.ts", () => ({
  ShortcutController: class {
    initialize() {}
  },
  forwardBrowserShortcut: vi.fn(),
  setWindowMenuShortcutsIgnored: vi.fn(),
  toggleShortcutWindow: vi.fn(),
}));
vi.mock("../src/main/load-renderer.ts", () => ({ loadRenderer: fake.loadRenderer }));

const processEvents = ["uncaughtExceptionMonitor", "unhandledRejection", "SIGTERM", "SIGINT"] as const;
const processEmitter: EventEmitter = process;
const originalListeners = new Map(
  processEvents.map((event) => [event, new Set(processEmitter.listeners(event))]),
);
let call: CallHandler;
let bridge: DesktopBridge;
const authorized = { sender: fake.window.webContents, senderFrame: fake.window.webContents.mainFrame };
beforeAll(async () => {
  await import("../src/main/index.ts");
  await vi.waitFor(() => expect(fake.loadRenderer).toHaveBeenCalledOnce());
  expect(fake.writeError).not.toHaveBeenCalled();
  expect(fake.handle.mock.calls.map(([channel]) => channel)).toEqual(["zpi:call"]);
  call = fake.handle.mock.calls.find(([channel]) => channel === "zpi:call")?.[1] as CallHandler;
  await import("../src/preload/index.ts");
  bridge = fake.expose.mock.calls[0][1] as DesktopBridge;
  expect(fake.invoke).not.toHaveBeenCalled();
  expect(fake.fetch).not.toHaveBeenCalled();
});
afterAll(() => {
  for (const event of processEvents) {
    for (const listener of processEmitter.listeners(event)) {
      if (!originalListeners.get(event)?.has(listener))
        processEmitter.removeListener(event, listener as (...args: unknown[]) => void);
    }
  }
});
beforeEach(() => {
  vi.clearAllMocks();
  fake.settings.getWebSearchCredentials.mockReset();
});

test("preload reveals credentials only when explicitly called, through the zero-argument allowlisted method", async () => {
  expect(methods).toContain("getWebSearchCredentials");
  expect(fake.invoke).not.toHaveBeenCalled();
  const response = { ok: true, value: { apiKey: "web-ipc-fake-key" } };
  fake.invoke.mockResolvedValueOnce(response);
  await expect(bridge.getWebSearchCredentials()).resolves.toEqual(response);
  expect(fake.invoke).toHaveBeenCalledExactlyOnceWith("zpi:call", "getWebSearchCredentials", []);
});

test.each(["web-ipc-fake-key", ""])(
  "returns only explicit saved credentials without side effects: %s",
  async (apiKey) => {
    fake.settings.getWebSearchCredentials.mockReturnValueOnce({ apiKey });
    await expect(call(authorized, "getWebSearchCredentials", [])).resolves.toEqual({
      ok: true,
      value: { apiKey },
    });
    expect(fake.settings.getWebSearchCredentials).toHaveBeenCalledExactlyOnceWith();
    expect(fake.settings.getProviderCredentials).not.toHaveBeenCalled();
    expect(fake.settings.get).not.toHaveBeenCalled();
    expect(fake.window.webContents.send).not.toHaveBeenCalled();
    expect(fake.writeError).not.toHaveBeenCalled();
    expect(fake.fetch).not.toHaveBeenCalled();
  },
);

test.each([
  { sender: {}, senderFrame: fake.window.webContents.mainFrame },
  { sender: fake.window.webContents, senderFrame: {} },
  { sender: fake.window.webContents, senderFrame: null },
])("rejects non-owner windows and child frames before accessing credentials", async (event) => {
  await expect(call(event, "getWebSearchCredentials", [])).resolves.toEqual({
    ok: false,
    error: { code: "invalid_input", message: "invalid_input: 未授权的 IPC sender" },
  });
  expect(fake.settings.getWebSearchCredentials).not.toHaveBeenCalled();
  expect(fake.fetch).not.toHaveBeenCalled();
});

test.each([
  { args: ["unexpected-fake-key"], message: "invalid_input: 参数数量无效" },
  { args: [{ apiKey: "unexpected-fake-key" }], message: "invalid_input: 参数数量无效" },
  { args: null, message: "invalid_input: 未知 IPC 方法" },
])("rejects arguments without logging their contents: %j", async ({ args, message }) => {
  await expect(call(authorized, "getWebSearchCredentials", args)).resolves.toEqual({
    ok: false,
    error: { code: "invalid_input", message },
  });
  expect(fake.settings.getWebSearchCredentials).not.toHaveBeenCalled();
  expect(JSON.stringify(fake.writeError.mock.calls)).not.toContain("unexpected-fake-key");
  expect(fake.writeError.mock.calls.map((args) => args.map(String).join(" ")).join(" ")).not.toContain(
    "unexpected-fake-key",
  );
  expect(fake.fetch).not.toHaveBeenCalled();
});

test("keeps the execution credential getter outside the public IPC allowlist", async () => {
  expect(methods).not.toContain("getWebSearchApiKey");
  await expect(call(authorized, "getWebSearchApiKey", [])).resolves.toEqual({
    ok: false,
    error: { code: "invalid_input", message: "invalid_input: 未知 IPC 方法" },
  });
  expect(fake.settings.getWebSearchCredentials).not.toHaveBeenCalled();
});

test("returns a controlled locked error without credentials, broadcasts or network access", async () => {
  const message = "configuration: 保存的联网搜索凭据尚未解锁";
  fake.settings.getWebSearchCredentials.mockImplementationOnce(() => {
    throw new Error(message);
  });
  await expect(call(authorized, "getWebSearchCredentials", [])).resolves.toEqual({
    ok: false,
    error: { code: "configuration", message },
  });
  expect(fake.writeError).toHaveBeenCalledExactlyOnceWith("ipc", new Error(message), {
    method: "getWebSearchCredentials",
  });
  expect(fake.window.webContents.send).not.toHaveBeenCalled();
  expect(fake.fetch).not.toHaveBeenCalled();
});