openclaw/extensions/nextcloud-talk/src/setup.test.ts

417 lines
12 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
import { describe, expect, it } from "vitest";
import { resolveNextcloudTalkAccount } from "./accounts.js";
import {
clearNextcloudTalkAccountFields,
nextcloudTalkDmPolicy,
nextcloudTalkSetupAdapter,
normalizeNextcloudTalkBaseUrl,
setNextcloudTalkAccountConfig,
validateNextcloudTalkBaseUrl,
} from "./setup-core.js";
import { nextcloudTalkSetupWizard } from "./setup-surface.js";
import type { CoreConfig } from "./types.js";
describe("nextcloud talk setup", () => {
it("normalizes and validates base urls", () => {
expect(normalizeNextcloudTalkBaseUrl(" https://cloud.example.com/// ")).toBe(
"https://cloud.example.com",
);
expect(normalizeNextcloudTalkBaseUrl(undefined)).toBe("");
expect(validateNextcloudTalkBaseUrl("")).toBe("Required");
expect(validateNextcloudTalkBaseUrl("cloud.example.com")).toBe(
"URL must start with http:// or https://",
);
expect(validateNextcloudTalkBaseUrl("https://cloud.example.com")).toBeUndefined();
});
it("patches scoped account config and clears selected fields", () => {
const cfg: CoreConfig = {
channels: {
"nextcloud-talk": {
baseUrl: "https://cloud.example.com",
botSecret: "top-secret",
accounts: {
work: {
botSecret: "work-secret",
botSecretFile: "/tmp/work-secret",
apiPassword: "api-secret",
},
},
},
},
};
expect(
setNextcloudTalkAccountConfig(cfg, DEFAULT_ACCOUNT_ID, {
apiUser: "bot",
}),
).toMatchObject({
channels: {
"nextcloud-talk": {
apiUser: "bot",
},
},
});
expect(clearNextcloudTalkAccountFields(cfg, DEFAULT_ACCOUNT_ID, ["botSecret"])).toMatchObject({
channels: {
"nextcloud-talk": {
baseUrl: "https://cloud.example.com",
},
},
});
expect(
clearNextcloudTalkAccountFields(cfg, DEFAULT_ACCOUNT_ID, ["botSecret"]),
).not.toMatchObject({
channels: {
"nextcloud-talk": {
botSecret: expect.anything(),
},
},
});
expect(
clearNextcloudTalkAccountFields(cfg, "work", ["botSecret", "botSecretFile"]),
).toMatchObject({
channels: {
"nextcloud-talk": {
accounts: {
work: {
apiPassword: "api-secret",
},
},
},
},
});
});
it("sets top-level DM policy state", async () => {
const base: CoreConfig = {
channels: {
"nextcloud-talk": {},
},
};
expect(nextcloudTalkDmPolicy.getCurrent(base)).toBe("pairing");
expect(nextcloudTalkDmPolicy.setPolicy(base, "open")).toMatchObject({
channels: {
"nextcloud-talk": {
dmPolicy: "open",
},
},
});
});
it("honors named-account DM policy state and config keys", () => {
const base: CoreConfig = {
channels: {
"nextcloud-talk": {
dmPolicy: "disabled",
accounts: {
work: {
baseUrl: "https://cloud.example.com",
botSecret: "work-secret",
dmPolicy: "allowlist",
},
},
},
},
};
expect(nextcloudTalkDmPolicy.getCurrent(base, "work")).toBe("allowlist");
expect(nextcloudTalkDmPolicy.resolveConfigKeys?.(base, "work")).toEqual({
policyKey: "channels.nextcloud-talk.accounts.work.dmPolicy",
allowFromKey: "channels.nextcloud-talk.accounts.work.allowFrom",
});
});
it("uses configured defaultAccount for omitted DM policy account context", () => {
const base: CoreConfig = {
channels: {
"nextcloud-talk": {
defaultAccount: "work",
dmPolicy: "disabled",
accounts: {
work: {
baseUrl: "https://cloud.example.com",
botSecret: "work-secret",
dmPolicy: "allowlist",
},
},
},
},
};
expect(nextcloudTalkDmPolicy.getCurrent(base)).toBe("allowlist");
expect(nextcloudTalkDmPolicy.resolveConfigKeys?.(base)).toEqual({
policyKey: "channels.nextcloud-talk.accounts.work.dmPolicy",
allowFromKey: "channels.nextcloud-talk.accounts.work.allowFrom",
});
const next = nextcloudTalkDmPolicy.setPolicy(base, "open");
expect(next.channels?.["nextcloud-talk"]?.dmPolicy).toBe("disabled");
expect(next.channels?.["nextcloud-talk"]?.accounts?.work?.dmPolicy).toBe("open");
});
it('writes open DM policy to the named account and preserves inherited allowFrom with "*"', () => {
const next = nextcloudTalkDmPolicy.setPolicy(
{
channels: {
"nextcloud-talk": {
allowFrom: ["alice"],
accounts: {
work: {
baseUrl: "https://cloud.example.com",
botSecret: "work-secret",
},
},
},
},
},
"open",
"work",
);
expect(next.channels?.["nextcloud-talk"]?.dmPolicy).toBeUndefined();
expect(next.channels?.["nextcloud-talk"]?.accounts?.work?.dmPolicy).toBe("open");
expect(next.channels?.["nextcloud-talk"]?.accounts?.work?.allowFrom).toEqual(["alice", "*"]);
});
it("validates env/default-account constraints and applies config patches", () => {
const validateInput = nextcloudTalkSetupAdapter.validateInput;
const applyAccountConfig = nextcloudTalkSetupAdapter.applyAccountConfig;
expect(validateInput).toBeTypeOf("function");
expect(applyAccountConfig).toBeTypeOf("function");
expect(
validateInput!({
accountId: "work",
input: { useEnv: true },
} as never),
).toBe("NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.");
expect(
validateInput!({
accountId: DEFAULT_ACCOUNT_ID,
input: { useEnv: false, baseUrl: "", secret: "" },
} as never),
).toBe("Nextcloud Talk requires bot secret or --secret-file (or --use-env).");
expect(
validateInput!({
accountId: DEFAULT_ACCOUNT_ID,
input: { useEnv: false, secret: "secret", baseUrl: "" },
} as never),
).toBe("Nextcloud Talk requires --base-url.");
expect(
applyAccountConfig!({
cfg: {
channels: {
"nextcloud-talk": {},
},
},
accountId: DEFAULT_ACCOUNT_ID,
input: {
name: "Default",
baseUrl: "https://cloud.example.com///",
secret: "bot-secret",
},
} as never),
).toEqual({
channels: {
"nextcloud-talk": {
enabled: true,
name: "Default",
baseUrl: "https://cloud.example.com",
botSecret: "bot-secret",
},
},
});
expect(
applyAccountConfig!({
cfg: {
channels: {
"nextcloud-talk": {
accounts: {
work: {
botSecret: "old-secret",
},
},
},
},
},
accountId: "work",
input: {
name: "Work",
useEnv: true,
baseUrl: "https://cloud.example.com",
},
} as never),
).toMatchObject({
channels: {
"nextcloud-talk": {
accounts: {
work: {
enabled: true,
name: "Work",
baseUrl: "https://cloud.example.com",
},
},
},
},
});
});
it("clears stored bot secret fields when switching the default account to env", () => {
type ApplyAccountConfigContext = Parameters<
typeof nextcloudTalkSetupAdapter.applyAccountConfig
>[0];
const next = nextcloudTalkSetupAdapter.applyAccountConfig({
cfg: {
channels: {
"nextcloud-talk": {
enabled: true,
baseUrl: "https://cloud.old.example",
botSecret: "stored-secret",
botSecretFile: "/tmp/secret.txt",
},
},
},
accountId: DEFAULT_ACCOUNT_ID,
input: {
baseUrl: "https://cloud.example.com",
useEnv: true,
},
} as unknown as ApplyAccountConfigContext);
expect(next.channels?.["nextcloud-talk"]?.baseUrl).toBe("https://cloud.example.com");
expect(next.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecret");
expect(next.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecretFile");
});
it("clears stored bot secret fields when the wizard switches to env", async () => {
const credential = nextcloudTalkSetupWizard.credentials[0];
const next = await credential.applyUseEnv?.({
cfg: {
channels: {
"nextcloud-talk": {
enabled: true,
baseUrl: "https://cloud.example.com",
botSecret: "stored-secret",
botSecretFile: "/tmp/secret.txt",
},
},
},
accountId: DEFAULT_ACCOUNT_ID,
});
expect(next?.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecret");
expect(next?.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecretFile");
});
});
describe("resolveNextcloudTalkAccount", () => {
it("matches normalized configured account ids", () => {
const account = resolveNextcloudTalkAccount({
cfg: {
channels: {
"nextcloud-talk": {
accounts: {
"Ops Team": {
baseUrl: "https://cloud.example.com",
botSecret: "bot-secret",
},
},
},
},
} as CoreConfig,
accountId: "ops-team",
});
expect(account.accountId).toBe("ops-team");
expect(account.baseUrl).toBe("https://cloud.example.com");
expect(account.secret).toBe("bot-secret");
expect(account.secretSource).toBe("config");
});
it.runIf(process.platform !== "win32")("rejects symlinked botSecretFile paths", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-nextcloud-talk-"));
const secretFile = path.join(dir, "secret.txt");
const secretLink = path.join(dir, "secret-link.txt");
fs.writeFileSync(secretFile, "bot-secret\n", "utf8");
fs.symlinkSync(secretFile, secretLink);
const cfg = {
channels: {
"nextcloud-talk": {
baseUrl: "https://cloud.example.com",
botSecretFile: secretLink,
},
},
} as CoreConfig;
const account = resolveNextcloudTalkAccount({ cfg });
expect(account.secret).toBe("");
expect(account.secretSource).toBe("none");
fs.rmSync(dir, { recursive: true, force: true });
});
it("uses configured defaultAccount when accountId is omitted", () => {
const account = resolveNextcloudTalkAccount({
cfg: {
channels: {
"nextcloud-talk": {
defaultAccount: "work",
botSecret: "top-secret",
accounts: {
work: {
baseUrl: "https://cloud.example.com",
botSecret: "work-secret",
},
},
},
},
} as CoreConfig,
});
expect(account.accountId).toBe("work");
expect(account.baseUrl).toBe("https://cloud.example.com");
expect(account.secret).toBe("work-secret");
expect(account.secretSource).toBe("config");
});
it("uses configured defaultAccount for omitted setup configured state", () => {
const configured = nextcloudTalkSetupWizard.status.resolveConfigured({
cfg: {
channels: {
"nextcloud-talk": {
defaultAccount: "work",
baseUrl: "https://root.example.com",
botSecret: "root-secret",
accounts: {
alerts: {
baseUrl: "https://alerts.example.com",
botSecret: "alerts-secret",
},
work: {
baseUrl: "",
botSecret: "",
},
},
},
},
} as CoreConfig,
});
expect(configured).toBe(false);
});
});