Files
computer-playbook/roles/web-app-nextcloud/files/playwright/addons/richdocuments.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
5.0 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("richdocuments addon: Collabora connector wired to the partner WOPI server", async ({ browser }) => {
skipUnlessAddonEnabled("richdocuments");
test.setTimeout(resolveTimeout(120_000));
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
try {
await shared.loginToStandaloneNextcloud(page);
await gotoOnion(page,
new URL("settings/admin/richdocuments", shared.env.nextcloudBaseUrl).toString(),
{ waitUntil: "domcontentloaded", timeout: resolveTimeout(60_000) }
);
await shared.dismissBlockingNextcloudModals(page, page);
const adminSection = page
.locator("#richdocuments, [data-cy='collabora-server-settings']")
.or(page.getByText(/collabora online/i).first());
await expect(
adminSection.first(),
"the Collabora Online (richdocuments) admin settings section must render, proving the app is enabled"
).toBeVisible({ timeout: resolveTimeout(60_000) });
const wopiField = page.locator("input#wopi_url, input[name='wopi_url']").first();
await expect(
wopiField,
"the Collabora server URL (WOPI) field must render in the admin panel"
).toBeVisible({ timeout: resolveTimeout(60_000) });
const wopiValue = ((await wopiField.inputValue().catch(() => "")) || "").trim();
expect(
/^https?:\/\/.+/.test(wopiValue),
"the WOPI server URL must be a real partner URL (config:app:set wopi_url); empty means the partner endpoint was never wired"
).toBeTruthy();
expect(
new URL(wopiValue).host,
"the WOPI server URL must point at the partner Collabora host, not Nextcloud itself"
).not.toBe(new URL(shared.env.nextcloudBaseUrl).host);
const connectionError = page.getByText(
/could not establish connection to the collabora online server|failed to connect|not a valid (collabora|wopi)/i
);
await expect(
connectionError,
"the Collabora connection-failure banner must be absent: richdocuments must reach the partner WOPI server (config:app:set wopi_url + occ richdocuments:activate-config discovery)"
).toHaveCount(0, { timeout: resolveTimeout(30_000) });
} finally {
await page.close().catch(() => {});
await context.close().catch(() => {});
}
});
test("richdocuments addon: opening a document loads it through the partner WOPI server", async ({ browser }) => {
skipUnlessAddonEnabled("richdocuments");
test.setTimeout(resolveTimeout(240_000));
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
const editorReplies = [];
page.on("websocket", (socket) => {
if (!/\/cool\/(?:.+\/)?ws(?:\?|$)/.test(socket.url())) {
return;
}
socket.on("framereceived", ({ payload }) => {
const reply = payload.toString().slice(0, 120);
if (/^(status|error):/.test(reply)) {
editorReplies.push(reply);
}
});
});
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 docBasename = `infinito-collabora-${Date.now()}`;
await page
.locator('input[type="file"]')
.first()
.setInputFiles({
name: `${docBasename}.docx`,
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
buffer: Buffer.from(shared.MINIMAL_DOCX_BASE64, "base64"),
});
const uploadedRow = page.getByText(docBasename, { exact: false }).first();
await expect(
uploadedRow,
`the uploaded document '${docBasename}.docx' must appear in the Files listing before it can be opened in Collabora`
).toBeVisible({ timeout: resolveTimeout(60_000) });
await shared.dismissBlockingNextcloudModals(page, page);
await uploadedRow.click();
await expect
.poll(() => editorReplies.length, {
message:
"opening the document must make Collabora answer on its editor websocket (/cool/ws) with a 'status:' or an 'error:' frame; silence means the browser never reached the editor that the discovery of web-svc-collabora advertises",
timeout: resolveTimeout(180_000),
})
.toBeGreaterThan(0);
expect(
editorReplies[0],
"Collabora must answer the document load with a 'status:' frame; 'error: cmd=internal kind=unauthorized' means it refused the WOPI host (aliasgroup1 of web-svc-collabora) or its CheckFileInfo call to Nextcloud failed, and the Collabora log names which"
).toMatch(/^status:/);
} finally {
await page.close().catch(() => {});
await context.close().catch(() => {});
}
});