Files
openclaw/extensions/irc/src/accounts.test.ts
Dallin Romney 4d47f9a4c0 test(secret-file): cover NickServ + account-level symlinks, narrow inspect catch (#84713)
Followup nits from the #84711 review:

- Narrow the inspectTokenFile catch in
  extensions/telegram/src/account-inspect.ts to FsSafeError so only
  fs-safe validation throws map to configured_unavailable; any other
  throw (programmer error, unexpected I/O) is rethrown.
- Add a regression test for the IRC NickServ password file symlink
  rejection path (extensions/irc/src/accounts.ts:118), paralleling the
  existing top-level passwordFile test.
- Add a regression test for the Telegram account-level tokenFile
  symlink rejection path (extensions/telegram/src/token.ts:149),
  paralleling the existing channel-level tokenFile test.

Behavior was already correct after #84711; this just locks coverage and
tightens the catch.
2026-05-20 15:35:52 -07:00

224 lines
5.8 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount } from "./accounts.js";
import type { CoreConfig } from "./types.js";
function asConfig(value: unknown): CoreConfig {
return value as CoreConfig;
}
describe("listIrcAccountIds", () => {
it("returns default when no accounts are configured", () => {
expect(listIrcAccountIds(asConfig({}))).toEqual(["default"]);
});
it("normalizes, deduplicates, and sorts configured account ids", () => {
const cfg = asConfig({
channels: {
irc: {
accounts: {
"Ops Team": {},
"ops-team": {},
Work: {},
},
},
},
});
expect(listIrcAccountIds(cfg)).toEqual(["ops-team", "work"]);
});
it("keeps the implicit default account when named accounts are added to top-level connection config", () => {
const cfg = asConfig({
channels: {
irc: {
host: "irc.example.com",
nick: "claw",
accounts: {
work: {
enabled: false,
host: "irc-work.example.com",
nick: "claw-work",
},
},
},
},
});
expect(listIrcAccountIds(cfg)).toEqual(["default", "work"]);
expect(resolveDefaultIrcAccountId(cfg)).toBe("default");
});
});
describe("resolveDefaultIrcAccountId", () => {
it("prefers configured defaultAccount when it matches", () => {
const cfg = asConfig({
channels: {
irc: {
defaultAccount: "Ops Team",
accounts: {
default: {},
"ops-team": {},
},
},
},
});
expect(resolveDefaultIrcAccountId(cfg)).toBe("ops-team");
});
it("falls back to default when configured defaultAccount is missing", () => {
const cfg = asConfig({
channels: {
irc: {
defaultAccount: "missing",
accounts: {
default: {},
work: {},
},
},
},
});
expect(resolveDefaultIrcAccountId(cfg)).toBe("default");
});
it("falls back to first sorted account when default is absent", () => {
const cfg = asConfig({
channels: {
irc: {
accounts: {
zzz: {},
aaa: {},
},
},
},
});
expect(resolveDefaultIrcAccountId(cfg)).toBe("aaa");
});
});
describe("resolveIrcAccount", () => {
it("matches normalized configured account ids", () => {
const account = resolveIrcAccount({
cfg: asConfig({
channels: {
irc: {
accounts: {
"Ops Team": {
host: "irc.example.com",
nick: "claw",
},
},
},
},
}),
accountId: "ops-team",
});
expect(account.accountId).toBe("ops-team");
expect(account.host).toBe("irc.example.com");
expect(account.nick).toBe("claw");
expect(account.configured).toBe(true);
});
it("parses delimited IRC_CHANNELS env values for the default account", () => {
vi.stubEnv("IRC_CHANNELS", "alpha, beta\ngamma; delta");
try {
const account = resolveIrcAccount({
cfg: asConfig({
channels: {
irc: {
host: "irc.example.com",
nick: "claw",
},
},
}),
});
expect(account.config.channels).toEqual(["alpha", "beta", "gamma", "delta"]);
} finally {
vi.unstubAllEnvs();
}
});
it.runIf(process.platform !== "win32")("rejects symlinked password files", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-irc-account-"));
const passwordFile = path.join(dir, "password.txt");
const passwordLink = path.join(dir, "password-link.txt");
fs.writeFileSync(passwordFile, "secret-pass\n", "utf8");
fs.symlinkSync(passwordFile, passwordLink);
const cfg = asConfig({
channels: {
irc: {
host: "irc.example.com",
nick: "claw",
passwordFile: passwordLink,
},
},
});
expect(() => resolveIrcAccount({ cfg })).toThrow(/IRC password file.*must not be a symlink/);
fs.rmSync(dir, { recursive: true, force: true });
});
it.runIf(process.platform !== "win32")("rejects symlinked NickServ password files", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-irc-nickserv-"));
const passwordFile = path.join(dir, "nickserv-password.txt");
const passwordLink = path.join(dir, "nickserv-password-link.txt");
fs.writeFileSync(passwordFile, "nickserv-pass\n", "utf8");
fs.symlinkSync(passwordFile, passwordLink);
const cfg = asConfig({
channels: {
irc: {
host: "irc.example.com",
nick: "claw",
nickserv: {
passwordFile: passwordLink,
},
},
},
});
expect(() => resolveIrcAccount({ cfg })).toThrow(
/IRC NickServ password file.*must not be a symlink/,
);
fs.rmSync(dir, { recursive: true, force: true });
});
it("preserves shared NickServ config when an account overrides one NickServ field", () => {
const account = resolveIrcAccount({
cfg: asConfig({
channels: {
irc: {
host: "irc.example.com",
nick: "claw",
nickserv: {
service: "NickServ",
},
accounts: {
work: {
nickserv: {
registerEmail: "work@example.com",
},
},
},
},
},
}),
accountId: "work",
});
expect(account.config.nickserv).toEqual({
service: "NickServ",
registerEmail: "work@example.com",
});
});
});