test: trim extension setup startup

This commit is contained in:
Peter Steinberger
2026-04-03 17:33:37 +01:00
parent 2e041c8b66
commit d74d47443e
5 changed files with 134 additions and 121 deletions

View File

@@ -0,0 +1,87 @@
import { beforeAll, describe, expect, it, vi } from "vitest";
import type { CoreConfig } from "./types.js";
vi.mock("../../../src/config/bundled-channel-config-runtime.js", () => ({
getBundledChannelRuntimeMap: () => new Map(),
getBundledChannelConfigSchemaMap: () => new Map(),
}));
vi.mock("../../../src/channels/plugins/bundled.js", () => ({
bundledChannelPlugins: [],
bundledChannelSetupPlugins: [],
}));
let nextcloudTalkPlugin: typeof import("./channel.js").nextcloudTalkPlugin;
let NextcloudTalkConfigSchema: typeof import("./config-schema.js").NextcloudTalkConfigSchema;
beforeAll(async () => {
({ nextcloudTalkPlugin } = await import("./channel.js"));
({ NextcloudTalkConfigSchema } = await import("./config-schema.js"));
});
describe("nextcloud talk channel core", () => {
it("accepts SecretRef botSecret and apiPassword at top-level", () => {
const result = NextcloudTalkConfigSchema.safeParse({
baseUrl: "https://cloud.example.com",
botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_BOT_SECRET" },
apiUser: "bot",
apiPassword: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_API_PASSWORD" },
});
expect(result.success).toBe(true);
});
it("accepts SecretRef botSecret and apiPassword on account", () => {
const result = NextcloudTalkConfigSchema.safeParse({
accounts: {
main: {
baseUrl: "https://cloud.example.com",
botSecret: {
source: "env",
provider: "default",
id: "NEXTCLOUD_TALK_MAIN_BOT_SECRET",
},
apiUser: "bot",
apiPassword: {
source: "env",
provider: "default",
id: "NEXTCLOUD_TALK_MAIN_API_PASSWORD",
},
},
},
});
expect(result.success).toBe(true);
});
it("normalizes trimmed DM allowlist prefixes to lowercase ids", () => {
const resolveDmPolicy = nextcloudTalkPlugin.security?.resolveDmPolicy;
if (!resolveDmPolicy) {
throw new Error("resolveDmPolicy unavailable");
}
const cfg = {
channels: {
"nextcloud-talk": {
baseUrl: "https://cloud.example.com",
botSecret: "secret",
dmPolicy: "allowlist",
allowFrom: [" nc:User-Id "],
},
},
} as CoreConfig;
const result = resolveDmPolicy({
cfg,
account: nextcloudTalkPlugin.config.resolveAccount(cfg, "default"),
});
if (!result) {
throw new Error("nextcloud-talk resolveDmPolicy returned null");
}
expect(result.policy).toBe("allowlist");
expect(result.allowFrom).toEqual([" nc:User-Id "]);
expect(result.normalizeEntry?.(" nc:User-Id ")).toBe("user-id");
expect(nextcloudTalkPlugin.pairing?.normalizeAllowEntry?.(" nextcloud-talk:User-Id ")).toBe(
"user-id",
);
});
});

View File

@@ -24,57 +24,40 @@ import {
generateNextcloudTalkSignature,
verifyNextcloudTalkSignature,
} from "./signature.js";
import type { CoreConfig } from "./types.js";
vi.mock("../../../src/config/bundled-channel-config-runtime.js", () => ({
getBundledChannelRuntimeMap: () => new Map(),
getBundledChannelConfigSchemaMap: () => new Map(),
}));
vi.mock("../../../src/channels/plugins/bundled.js", () => ({
bundledChannelPlugins: [],
bundledChannelSetupPlugins: [],
}));
const fetchWithSsrFGuard = vi.hoisted(() => vi.fn());
const readFileSync = vi.hoisted(() => vi.fn());
vi.mock("../runtime-api.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../runtime-api.js")>();
return {
...actual,
fetchWithSsrFGuard,
};
vi.mock("../runtime-api.js", () => {
return vi
.importActual<typeof import("../runtime-api.js")>("../runtime-api.js")
.then((actual) => ({
...actual,
fetchWithSsrFGuard,
}));
});
vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/ssrf-runtime")>();
return {
...actual,
fetchWithSsrFGuard,
};
});
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return {
vi.mock("node:fs", () => {
return vi.importActual<typeof import("node:fs")>("node:fs").then((actual) => ({
...actual,
readFileSync,
};
}));
});
const tempDirs: string[] = [];
let nextcloudTalkPlugin: typeof import("./channel.js").nextcloudTalkPlugin;
let NextcloudTalkConfigSchema: typeof import("./config-schema.js").NextcloudTalkConfigSchema;
let resolveNextcloudTalkRoomKind: typeof import("./room-info.js").resolveNextcloudTalkRoomKind;
let resetNextcloudTalkRoomCache: () => void;
beforeAll(async () => {
({ nextcloudTalkPlugin } = await import("./channel.js"));
({ NextcloudTalkConfigSchema } = await import("./config-schema.js"));
const roomInfo = await import("./room-info.js");
resolveNextcloudTalkRoomKind = roomInfo.resolveNextcloudTalkRoomKind;
resetNextcloudTalkRoomCache = roomInfo.__testing.resetRoomCache;
});
afterEach(async () => {
fetchWithSsrFGuard.mockReset();
readFileSync.mockReset();
resetNextcloudTalkRoomCache();
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
@@ -91,38 +74,6 @@ async function makeTempDir(): Promise<string> {
describe("nextcloud talk core", () => {
it("accepts SecretRef botSecret and apiPassword at top-level", () => {
const result = NextcloudTalkConfigSchema.safeParse({
baseUrl: "https://cloud.example.com",
botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_BOT_SECRET" },
apiUser: "bot",
apiPassword: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_API_PASSWORD" },
});
expect(result.success).toBe(true);
});
it("accepts SecretRef botSecret and apiPassword on account", () => {
const result = NextcloudTalkConfigSchema.safeParse({
accounts: {
main: {
baseUrl: "https://cloud.example.com",
botSecret: {
source: "env",
provider: "default",
id: "NEXTCLOUD_TALK_MAIN_BOT_SECRET",
},
apiUser: "bot",
apiPassword: {
source: "env",
provider: "default",
id: "NEXTCLOUD_TALK_MAIN_API_PASSWORD",
},
},
},
});
expect(result.success).toBe(true);
});
it("keeps markdown mostly intact while trimming outer whitespace", () => {
expect(markdownToNextcloudTalk(" **hello** ")).toBe("**hello**");
});
@@ -289,39 +240,6 @@ describe("nextcloud talk core", () => {
expect(accountBFirst).toBe(true);
});
it("normalizes trimmed DM allowlist prefixes to lowercase ids", () => {
const resolveDmPolicy = nextcloudTalkPlugin.security?.resolveDmPolicy;
if (!resolveDmPolicy) {
throw new Error("resolveDmPolicy unavailable");
}
const cfg = {
channels: {
"nextcloud-talk": {
baseUrl: "https://cloud.example.com",
botSecret: "secret",
dmPolicy: "allowlist",
allowFrom: [" nc:User-Id "],
},
},
} as CoreConfig;
const result = resolveDmPolicy({
cfg,
account: nextcloudTalkPlugin.config.resolveAccount(cfg, "default"),
});
if (!result) {
throw new Error("nextcloud-talk resolveDmPolicy returned null");
}
expect(result.policy).toBe("allowlist");
expect(result.allowFrom).toEqual([" nc:User-Id "]);
expect(result.normalizeEntry?.(" nc:User-Id ")).toBe("user-id");
expect(nextcloudTalkPlugin.pairing?.normalizeAllowEntry?.(" nextcloud-talk:User-Id ")).toBe(
"user-id",
);
});
it("resolves allowlist matches and group policy decisions", () => {
expect(
resolveNextcloudTalkAllowlistMatch({
@@ -429,7 +347,6 @@ describe("nextcloud talk core", () => {
});
it("resolves direct rooms from the room info endpoint", async () => {
vi.resetModules();
const release = vi.fn(async () => {});
fetchWithSsrFGuard.mockResolvedValue({
response: {
@@ -445,7 +362,6 @@ describe("nextcloud talk core", () => {
release,
});
const { resolveNextcloudTalkRoomKind } = await import("./room-info.js");
const kind = await resolveNextcloudTalkRoomKind({
account: {
accountId: "acct-direct",
@@ -469,7 +385,6 @@ describe("nextcloud talk core", () => {
});
it("reads the api password from a file and logs non-ok room info responses", async () => {
vi.resetModules();
const release = vi.fn(async () => {});
const log = vi.fn();
const error = vi.fn();
@@ -484,7 +399,6 @@ describe("nextcloud talk core", () => {
release,
});
const { resolveNextcloudTalkRoomKind } = await import("./room-info.js");
const kind = await resolveNextcloudTalkRoomKind({
account: {
accountId: "acct-group",
@@ -505,9 +419,6 @@ describe("nextcloud talk core", () => {
});
it("returns undefined from room info without credentials or base url", async () => {
vi.resetModules();
const { resolveNextcloudTalkRoomKind } = await import("./room-info.js");
await expect(
resolveNextcloudTalkRoomKind({
account: {

View File

@@ -11,6 +11,12 @@ const roomCache = new Map<
{ kind?: "direct" | "group"; fetchedAt: number; error?: string }
>();
export const __testing = {
resetRoomCache() {
roomCache.clear();
},
};
function resolveRoomCacheKey(params: { accountId: string; roomToken: string }) {
return `${params.accountId}:${params.roomToken}`;
}