mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
Over Tor the redirect chain behind a login/logout/OIDC click routinely exceeds Playwright's fixed 30s action timeout, so the click throws before the flow's scaled waits run. Sweep all role Playwright suites and the shared persona helpers: give every navigation/auth-transition click an onion-scaled timeout (.click({ timeout: resolveTimeout(30_000) })) and route the remaining raw millisecond literals (timeout options, waitForTimeout, sleep-promise setTimeout, positional wait args) through resolveTimeout. Local UI clicks (menus, toggles, modal dismissals, DOM clicks inside evaluate) stay untouched.
Two new lints keep it that way: test_no_raw_timeout_literals forbids raw numeric timeout literals in Playwright specs (opt-out: nocheck: raw-timeout), test_persona_clicks_scaled requires every click in the shared persona auth flows to carry a resolveTimeout timeout (opt-out: nocheck: unscaled-click).
Verified: node --check on all 133 changed JS files, all 13 playwright lints green, make autoformat clean, make test green (2745+196+163+4 tests OK).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
95 lines
4.4 KiB
JavaScript
95 lines
4.4 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");
|
|
|
|
// Full-coupling check for nextcloud/integration_mattermost.
|
|
//
|
|
// The addon hook (tasks/addons/integration_mattermost_provision.yml) registers an
|
|
// OAuth 2.0 application on the partner Mattermost and persists oauth_instance_url +
|
|
// client_id + client_secret into the app. This spec proves the coupling behaviourally:
|
|
//
|
|
// 1) the integration_mattermost app is enabled (admin app-detail "Disable"),
|
|
// 2) the personal "Connected accounts" page exposes the Mattermost connect control,
|
|
// 3) clicking connect performs the OAuth authorize redirect to the PARTNER Mattermost
|
|
// host (not Nextcloud) carrying the provisioned client_id and response_type=code —
|
|
// which can only happen once the hook persisted client_id + oauth_instance_url. A
|
|
// missing coupling would keep the browser on Nextcloud.
|
|
//
|
|
// The partner bounces the unauthenticated request through /login?redirect_to=<authorize>,
|
|
// so the authorize URL can be percent-encoded in the landing URL — decode before matching.
|
|
test("integration integration_mattermost: OAuth client provisioned and connects to Mattermost", async ({ browser }) => {
|
|
skipUnlessAddonEnabled("integration_mattermost");
|
|
test.setTimeout(resolveTimeout(120_000));
|
|
|
|
const context = await browser.newContext({ ignoreHTTPSErrors: true });
|
|
const page = await context.newPage();
|
|
const nextcloudHost = new URL(shared.env.nextcloudBaseUrl).host;
|
|
|
|
try {
|
|
await shared.loginToStandaloneNextcloud(page);
|
|
|
|
// 1) App is installed AND enabled.
|
|
await gotoOnion(page,
|
|
new URL("settings/apps/installed/integration_mattermost", shared.env.nextcloudBaseUrl).toString(),
|
|
{ waitUntil: "domcontentloaded", timeout: resolveTimeout(60_000) }
|
|
);
|
|
await shared.dismissBlockingNextcloudModals(page, page).catch(() => {});
|
|
await page.waitForLoadState("networkidle").catch(() => {});
|
|
|
|
await expect(
|
|
page.getByRole("button", { name: /^disable$/i }).or(page.locator('input[value="Disable"]')).first(),
|
|
"integration_mattermost must be enabled (admin app-detail Disable action)"
|
|
).toBeVisible({ timeout: resolveTimeout(60_000) });
|
|
|
|
// 2) Personal connect surface renders (only when the app is enabled).
|
|
await gotoOnion(page,
|
|
new URL("settings/user/connected-accounts", shared.env.nextcloudBaseUrl).toString(),
|
|
{ waitUntil: "domcontentloaded", timeout: resolveTimeout(60_000) }
|
|
);
|
|
await shared.dismissBlockingNextcloudModals(page, page).catch(() => {});
|
|
await page.waitForLoadState("networkidle").catch(() => {});
|
|
|
|
const connect = page
|
|
.locator("#mattermost-connect")
|
|
.or(page.getByRole("button", { name: /connect to mattermost/i }))
|
|
.or(page.getByRole("link", { name: /connect to mattermost/i }))
|
|
.first();
|
|
await expect(
|
|
connect,
|
|
"the Mattermost connect control must render on Connected accounts when the app is enabled"
|
|
).toBeVisible({ timeout: resolveTimeout(60_000) });
|
|
|
|
// 3) Connect performs the partner OAuth authorize redirect with the provisioned client.
|
|
const popupPromise = page.waitForEvent("popup", { timeout: resolveTimeout(15_000) }).catch(() => null);
|
|
await Promise.all([
|
|
page.waitForEvent("framenavigated", { timeout: resolveTimeout(60_000) }).catch(() => {}),
|
|
connect.click({ timeout: resolveTimeout(30_000) }),
|
|
]);
|
|
const popup = await popupPromise;
|
|
const rawUrl = () => (popup ? popup.url() : page.url());
|
|
const landing = () => decodeURIComponent(rawUrl());
|
|
|
|
await expect
|
|
.poll(landing, {
|
|
timeout: resolveTimeout(60_000),
|
|
message: "connect must redirect to the partner Mattermost OAuth authorize endpoint",
|
|
})
|
|
.toMatch(/\/oauth\/authorize/i);
|
|
|
|
const decoded = landing();
|
|
expect(
|
|
new URL(rawUrl()).host,
|
|
"the OAuth authorize must be served by the partner Mattermost host, not Nextcloud"
|
|
).not.toBe(nextcloudHost);
|
|
expect(decoded, "the OAuth authorize must carry the provisioned Mattermost client_id").toMatch(/client_id=/i);
|
|
expect(decoded, "the OAuth authorize must be an authorization-code-flow request").toMatch(/response_type=code/i);
|
|
|
|
if (popup) await popup.close().catch(() => {});
|
|
} finally {
|
|
await page.close().catch(() => {});
|
|
await context.close().catch(() => {});
|
|
}
|
|
});
|