Files
computer-playbook/roles/web-app-nextcloud/files/playwright/addons/onlyoffice.spec.js
kevinveenbirkenbachandClaude Opus 5.5 8dce5b3caa fix(web-svc-collabora): name the WOPI host and follow the vhost's TLS state
The role restricted the WOPI hosts with `domain` and with
`--o:net.wopi_allowed_hosts`. The distroless image that #775 brought in
applies neither. `domain` was translated by the start script of the older
image, which this one does not have, and coolwsd knows no setting called
`net.wopi_allowed_hosts`. Its startup log listed that key among the
overrides and no trusted WOPI host, so the server ran with the policy the
image ships instead of the one the role declares.

The allowlist now goes through `aliasgroup1`, the variable coolwsd reads
itself. It carries the origin of the Nextcloud domain as the deployment
resolves it, with scheme and port, so it also holds in a Tor variant. The
old expression was built from DOMAIN_PRIMARY and would not have matched an
onion address.

No spec opened a document, which is why this went unnoticed. The
richdocuments spec now uploads a document, opens it and waits for the
`status:` frame on the editor websocket.

Its first run on the Tor variant found a second defect. `ssl.termination`
was fixed to true, so coolwsd advertised https URLs in its discovery while
an onion vhost only serves http, and the browser never reached the editor.
The setting now follows the TLS state of the Collabora vhost.

Closes #802

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 09:18:04 +02:00

122 lines
4.9 KiB
JavaScript

const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const shared = require("../_shared");
const { gotoOnion } = require("../personas");
test.use({ ignoreHTTPSErrors: true });
test("onlyoffice addon: opening a document loads the partner document-server editor", async ({ browser }) => {
skipUnlessAddonEnabled("onlyoffice");
test.setTimeout(resolveTimeout(180_000));
const unquote = (v) => ((v || "").trim().replace(/^"(.*)"$/, "$1"));
const expectedDsUrl = unquote(process.env.NEXTCLOUD_ONLYOFFICE_EXPECTED_DOCUMENT_SERVER_URL);
expect(
expectedDsUrl,
"NEXTCLOUD_ONLYOFFICE_EXPECTED_DOCUMENT_SERVER_URL must be rendered into the Playwright env so the spec can prove the editor iframe is served by the real web-svc-onlyoffice partner host",
).toBeTruthy();
const documentServerHost = new URL(expectedDsUrl).host;
const nextcloudHost = new URL(shared.env.nextcloudBaseUrl).host;
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
try {
await shared.loginToStandaloneNextcloud(page);
const filesUrl = new URL("apps/files/", shared.env.nextcloudBaseUrl).toString();
await gotoOnion(page, filesUrl, { waitUntil: "domcontentloaded", timeout: resolveTimeout(60_000) });
await shared.dismissBlockingNextcloudModals(page, page);
const docName = `infinito-onlyoffice-${Date.now()}.docx`;
await page
.locator('input[type="file"]')
.first()
.setInputFiles({
name: docName,
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
buffer: Buffer.from(shared.MINIMAL_DOCX_BASE64, "base64"),
});
const docBasename = docName.replace(/\.docx$/, "");
const uploadedRow = page.getByText(docBasename, { exact: false }).first();
await expect(
uploadedRow,
`the uploaded document '${docName}' must appear in the Files listing before it can be opened in ONLYOFFICE`,
).toBeVisible({ timeout: resolveTimeout(60_000) });
await shared.dismissBlockingNextcloudModals(page, page);
await uploadedRow.click();
const connectorIframe = page.locator("iframe#onlyofficeFrame").first();
await expect(
connectorIframe,
"opening the .docx must mount the onlyoffice connector iframe (#onlyofficeFrame) into the Files view",
).toBeVisible({ timeout: resolveTimeout(90_000) });
const frameUrlMatching = (predicate) => {
const frame = page.frames().find((candidate) => {
try {
return predicate(new URL(candidate.url()));
} catch {
return false;
}
});
return frame ? frame.url() : "";
};
await expect
.poll(
() =>
frameUrlMatching(
(url) =>
url.host === nextcloudHost &&
/^\/apps\/onlyoffice\/\d+$/.test(url.pathname) &&
url.searchParams.get("inframe") === "true",
),
{
message: `the connector iframe must load the onlyoffice editor view '/apps/onlyoffice/<fileid>?inframe=true' from Nextcloud ('${nextcloudHost}'), which is what hands the document over to the document server`,
timeout: resolveTimeout(90_000),
},
)
.toBeTruthy();
await expect
.poll(
() =>
frameUrlMatching(
(url) => url.host === documentServerHost && url.pathname.includes("/web-apps/apps/documenteditor/"),
),
{
message: `the connector view must embed the document editor served by the web-svc-onlyoffice partner host '${documentServerHost}' (the DocumentServerUrl coupling), not by Nextcloud ('${nextcloudHost}')`,
timeout: resolveTimeout(120_000),
},
)
.toBeTruthy();
const editorFrame = page
.frameLocator("iframe#onlyofficeFrame")
.frameLocator("iframe[name='frameEditor']");
const jwtError = editorFrame.getByText(
/security token is not correctly formed|token is not valid|invalid token|error while downloading|download failed/i,
);
await expect(
jwtError,
"the ONLYOFFICE editor must not show a JWT/security-token or download error: that means the shared jwt_secret coupling or the document-server <-> Nextcloud round trip is broken",
).toHaveCount(0, { timeout: resolveTimeout(90_000) });
const editorSurface = editorFrame.locator(
"#editor_sdk, #id_main_view, .asc-window, canvas, #toolbar, .toolbar",
).first();
await expect(
editorSurface,
"the ONLYOFFICE editor surface (toolbar/canvas) must render inside the partner iframe, proving the full JWT-authenticated document-server round trip works end to end",
).toBeVisible({ timeout: resolveTimeout(120_000) });
} finally {
await page.close().catch(() => {});
await context.close().catch(() => {});
}
});