Revert the document-server link of nextcloud and only-office (mesh-catalog #438): since its merge at 11:49 novox cannot be resolved, so no machine can get new instructions (issue 1830)
mesh/delivery stopped: the pull request closed unmerged
mesh/merge-gate error: builds nextcloud, only-office → novox; no bus step; the check could not run: build-1792666287056161072-store never became ready
mesh/repo-check error: the check could not run: build-1792666287056161072-store never became ready
mesh/check failed: unit: modules/gitea.test: [gitea] where the poll of merges last looked from could not be read, so none is kept — no runtime | 2e414…
mesh/delivery stopped: the pull request closed unmerged
mesh/merge-gate error: builds nextcloud, only-office → novox; no bus step; the check could not run: build-1792666287056161072-store never became ready
mesh/repo-check error: the check could not run: build-1792666287056161072-store never became ready
mesh/check failed: unit: modules/gitea.test: [gitea] where the poll of merges last looked from could not be read, so none is kept — no runtime | 2e414…
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
// 1830/AC2 (unit: manifest checks)
|
||||
// Given the catalogue, when nextcloud's manifest is parsed, then it requires document-server,
|
||||
// binds it and receives its secret, and its config.json template carries the document server's
|
||||
// public address, bound address and port, its own route name and the shared secret.
|
||||
// Run against the manifest and its templates directly — no build needed, they are read as data.
|
||||
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const moduleDir = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
function readJson(path: string): any {
|
||||
return JSON.parse(readFileSync(join(moduleDir, path), "utf8"));
|
||||
}
|
||||
|
||||
test("the manifest requires document-server, and binds and receives its secret", () => {
|
||||
const manifest = readJson("module.json");
|
||||
assert.ok(
|
||||
Array.isArray(manifest.requires) && manifest.requires.includes("document-server"),
|
||||
"requires names document-server",
|
||||
);
|
||||
assert.equal(manifest.binds?.["document-server"], "${dir:state}/document-server.json");
|
||||
assert.equal(manifest.secrets?.["document-server"], "${dir:state}/document-server.secret");
|
||||
});
|
||||
|
||||
test("config.json carries the document server's public address, bound address, port, route name and secret", () => {
|
||||
const config = readJson("files/config.json");
|
||||
assert.equal(config.onlyofficeUrl, "${bound:document-server:public-root}");
|
||||
assert.equal(config.onlyofficeInternalUrl, "http://${bound:document-server:at}:${bound:document-server:port}/");
|
||||
assert.equal(config.onlyofficeStorageUrl, "https://${bound:route:name}/");
|
||||
assert.equal(config.onlyofficeSecret, "${secret:document-server}");
|
||||
});
|
||||
@@ -1,142 +0,0 @@
|
||||
// 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, string>): 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<string, string>, 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<string, string> = {
|
||||
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<string, string> = {
|
||||
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<string, string> = {}; // 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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user