// novox/hq issue 1830: nextcloud reaches only-office's document server by its mesh-delivered // config, not by hand-set settings, and the loop that applies it never leaks the shared secret. // Run against the compiled module, the way the runtime loads it. import { test, mock } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { NextcloudClient } from "../dist/client.js"; const SECRET = "the-shared-jwt-secret"; const FULL_CONFIG = { onlyofficeUrl: "https://office.example/", onlyofficeInternalUrl: "http://10.10.0.1:9070/", onlyofficeStorageUrl: "https://drive.example/", onlyofficeSecret: SECRET, }; function configFile(values: Record): string { const dir = mkdtempSync(join(tmpdir(), "nextcloud-1830-")); const path = join(dir, "config.json"); writeFileSync(path, JSON.stringify(values)); return path; } function client(): any { return new (NextcloudClient as any)("nextcloud", "http://127.0.0.1", "admin", "irrelevant"); } /** A stand-in occ: config:app:get answers `current`, or throws like the real occ when unset. */ function standInOcc(current: Record, opts: { checkFails?: boolean } = {}) { const calls: string[][] = []; const impl = (args: string[]): string => { calls.push(args); if (args[0] === "config:app:get") { const key = args[2]; if (!(key in current)) throw new Error("occ produced no output"); return current[key]; } if (args[0] === "config:app:set") { const key = args[2]; const value = args[3].slice("--value=".length); current[key] = value; return "System config value onlyoffice => " + key + " set to string " + value; } if (args[0] === "onlyoffice:documentserver") { if (opts.checkFails) throw new Error("occ produced no output"); return ""; } throw new Error("unexpected occ call: " + args.join(" ")); }; return { calls, impl }; } // 1830/AC3a: when the delivered values differ from Nextcloud's settings, it sets exactly the // differing ones and runs OnlyOffice's connection check. test("sets exactly the keys that differ, and runs the connection check", () => { const c = client(); const current: Record = { DocumentServerUrl: "https://old-office.example/", StorageUrl: FULL_CONFIG.onlyofficeStorageUrl, // already correct jwt_secret: "a-stale-secret", // DocumentServerInternalUrl absent: occ's config:app:get throws, as for a never-set key. }; const stand = standInOcc(current); mock.method(c, "occ", stand.impl); const result = c.applyOnlyOffice(configFile(FULL_CONFIG)); assert.deepEqual(result.changed, ["DocumentServerUrl", "DocumentServerInternalUrl", "jwt_secret"]); assert.equal(result.checkOk, true); assert.ok(stand.calls.some((a) => a[0] === "onlyoffice:documentserver" && a[1] === "--check"), "the connection check ran"); assert.equal(current.StorageUrl, FULL_CONFIG.onlyofficeStorageUrl, "the matching key was left alone"); }); // 1830/AC3b: when they match, it changes nothing and runs no check. test("changes nothing and runs no check when every value already matches", () => { const c = client(); const current: Record = { DocumentServerUrl: FULL_CONFIG.onlyofficeUrl, DocumentServerInternalUrl: FULL_CONFIG.onlyofficeInternalUrl, StorageUrl: FULL_CONFIG.onlyofficeStorageUrl, jwt_secret: FULL_CONFIG.onlyofficeSecret, }; const stand = standInOcc(current); mock.method(c, "occ", stand.impl); const result = c.applyOnlyOffice(configFile(FULL_CONFIG)); assert.deepEqual(result.changed, []); assert.equal(result.checkOk, undefined); assert.ok(!stand.calls.some((a) => a[0] === "config:app:set"), "nothing was set"); assert.ok(!stand.calls.some((a) => a[0] === "onlyoffice:documentserver"), "no check ran"); }); // 1830/AC3c: when a value is empty (not bound or not given yet), it changes nothing, and says so // once rather than on every call. test("changes nothing when a value is not given yet, and says so only once", () => { const c = client(); const stand = standInOcc({}); mock.method(c, "occ", stand.impl); const err = mock.method(console, "error", () => {}); try { const partial = { ...FULL_CONFIG, onlyofficeSecret: "" }; const file = configFile(partial); const first = c.applyOnlyOffice(file); const second = c.applyOnlyOffice(file); assert.deepEqual(first.changed, []); assert.deepEqual(second.changed, []); assert.equal(stand.calls.length, 0, "occ was never called"); assert.equal(err.mock.callCount(), 1, "said so once, not on every call"); } finally { err.mock.restore(); } }); // 1830/AC3d: the secret appears in no log line and no return value. test("the secret's value never appears in what is logged or returned", () => { const c = client(); const current: Record = {}; // every key unset, so jwt_secret is among those changed const stand = standInOcc(current); mock.method(c, "occ", stand.impl); const log = mock.method(console, "log", () => {}); const err = mock.method(console, "error", () => {}); try { const result = c.applyOnlyOffice(configFile(FULL_CONFIG)); assert.ok(result.changed.includes("jwt_secret"), "the key name is returned"); assert.ok(!result.changed.some((k: string) => k.includes(SECRET)), "never the value, in changed"); const said = [...log.mock.calls, ...err.mock.calls].map((c) => c.arguments.join(" ")).join("\n"); assert.ok(!said.includes(SECRET), "never the value, in a log line"); } finally { log.mock.restore(); err.mock.restore(); } });