Files
openclaw/extensions/line/src/channel.logout.test.ts
2026-02-23 05:45:54 +00:00

143 lines
4.3 KiB
TypeScript

import type { OpenClawConfig, PluginRuntime, ResolvedLineAccount } from "openclaw/plugin-sdk";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createRuntimeEnv } from "../../test-utils/runtime-env.js";
import { linePlugin } from "./channel.js";
import { setLineRuntime } from "./runtime.js";
const DEFAULT_ACCOUNT_ID = "default";
type LineRuntimeMocks = {
writeConfigFile: ReturnType<typeof vi.fn>;
resolveLineAccount: ReturnType<typeof vi.fn>;
};
function createRuntime(): { runtime: PluginRuntime; mocks: LineRuntimeMocks } {
const writeConfigFile = vi.fn(async () => {});
const resolveLineAccount = vi.fn(
({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string }) => {
const lineConfig = (cfg.channels?.line ?? {}) as {
tokenFile?: string;
secretFile?: string;
channelAccessToken?: string;
channelSecret?: string;
accounts?: Record<string, Record<string, unknown>>;
};
const entry =
accountId && accountId !== DEFAULT_ACCOUNT_ID
? (lineConfig.accounts?.[accountId] ?? {})
: lineConfig;
const hasToken =
// oxlint-disable-next-line typescript/no-explicit-any
Boolean((entry as any).channelAccessToken) || Boolean((entry as any).tokenFile);
// oxlint-disable-next-line typescript/no-explicit-any
const hasSecret = Boolean((entry as any).channelSecret) || Boolean((entry as any).secretFile);
return { tokenSource: hasToken && hasSecret ? "config" : "none" };
},
);
const runtime = {
config: { writeConfigFile },
channel: { line: { resolveLineAccount } },
} as unknown as PluginRuntime;
return { runtime, mocks: { writeConfigFile, resolveLineAccount } };
}
function resolveAccount(
resolveLineAccount: LineRuntimeMocks["resolveLineAccount"],
cfg: OpenClawConfig,
accountId: string,
): ResolvedLineAccount {
const resolver = resolveLineAccount as unknown as (params: {
cfg: OpenClawConfig;
accountId?: string;
}) => ResolvedLineAccount;
return resolver({ cfg, accountId });
}
async function runLogoutScenario(params: { cfg: OpenClawConfig; accountId: string }): Promise<{
result: Awaited<ReturnType<NonNullable<NonNullable<typeof linePlugin.gateway>["logoutAccount"]>>>;
mocks: LineRuntimeMocks;
}> {
const { runtime, mocks } = createRuntime();
setLineRuntime(runtime);
const account = resolveAccount(mocks.resolveLineAccount, params.cfg, params.accountId);
const result = await linePlugin.gateway!.logoutAccount!({
accountId: params.accountId,
cfg: params.cfg,
account,
runtime: createRuntimeEnv(),
});
return { result, mocks };
}
describe("linePlugin gateway.logoutAccount", () => {
beforeEach(() => {
setLineRuntime(createRuntime().runtime);
});
it("clears tokenFile/secretFile on default account logout", async () => {
const cfg: OpenClawConfig = {
channels: {
line: {
tokenFile: "/tmp/token",
secretFile: "/tmp/secret",
},
},
};
const { result, mocks } = await runLogoutScenario({
cfg,
accountId: DEFAULT_ACCOUNT_ID,
});
expect(result.cleared).toBe(true);
expect(result.loggedOut).toBe(true);
expect(mocks.writeConfigFile).toHaveBeenCalledWith({});
});
it("clears tokenFile/secretFile on account logout", async () => {
const cfg: OpenClawConfig = {
channels: {
line: {
accounts: {
primary: {
tokenFile: "/tmp/token",
secretFile: "/tmp/secret",
},
},
},
},
};
const { result, mocks } = await runLogoutScenario({
cfg,
accountId: "primary",
});
expect(result.cleared).toBe(true);
expect(result.loggedOut).toBe(true);
expect(mocks.writeConfigFile).toHaveBeenCalledWith({});
});
it("does not write config when account has no token/secret fields", async () => {
const cfg: OpenClawConfig = {
channels: {
line: {
accounts: {
primary: {
name: "Primary",
},
},
},
},
};
const { result, mocks } = await runLogoutScenario({
cfg,
accountId: "primary",
});
expect(result.cleared).toBe(false);
expect(result.loggedOut).toBe(true);
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
});
});