* test(e2e): cover PROXY_LOGOUT_URL redirect on Logout Env-gated spec mirroring the existing serverRootPathRedirect pattern: when the proxy is booted with PROXY_LOGOUT_URL set, clicking Logout in the navbar must navigate to that external URL. The standard run_e2e.sh exports an empty value so the rest of the suite is unaffected; this spec self-skips unless the env var is populated. * test(e2e): run PROXY_LOGOUT_URL spec in the suite + harden logout assertions Boot the e2e proxy with PROXY_LOGOUT_URL set (job-level env in CircleCI and run_e2e.sh) so proxyLogoutUrl.spec.ts actually runs instead of self-skipping. Nothing else in the suite performs a logout, so this only affects the behavior under test. Harden the spec to verify the logout flow rather than a URL substring: - wait for /sso/get/ui_settings before clicking so logoutUrl is populated (otherwise window.location.href = "" silently reloads same-origin) - assert a token cookie exists first, and is cleared after logout - locate the dropdown via getByRole instead of internal antd CSS classes - stub the external destination and assert on URL origin + path prefix * test(e2e): assert exact PROXY_LOGOUT_URL on logout redirect Replace the origin + startsWith(pathname) checks with a single normalized href comparison. With PROXY_LOGOUT_URL=https://www.example.com the path was "/", so startsWith("/") matched any path and left path/query/hash unchecked. Comparing normalized hrefs pins scheme, host, port, path, query and hash while still tolerating the browser's trailing-slash/default-port normalization.
83 lines
3.8 KiB
TypeScript
83 lines
3.8 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import { ADMIN_STORAGE_PATH } from "../../constants";
|
|
|
|
/**
|
|
* Runs as part of the standard e2e suite: both `run_e2e.sh` and the CircleCI
|
|
* `e2e_ui_testing` job boot the proxy with PROXY_LOGOUT_URL=https://www.example.com
|
|
* and export the same value to this Playwright process. The spec reads it to
|
|
* know where the browser is expected to land.
|
|
*
|
|
* The skip guard below is a safety net for environments that launch the proxy
|
|
* without the env var (e.g. an ad-hoc `npx playwright test` against a default
|
|
* proxy) — there the logout target is empty and this contract can't be checked.
|
|
*/
|
|
const LOGOUT_URL = process.env.PROXY_LOGOUT_URL ?? "";
|
|
|
|
test.skip(!LOGOUT_URL, "Requires PROXY_LOGOUT_URL env var");
|
|
|
|
test.describe("PROXY_LOGOUT_URL redirect", () => {
|
|
test.use({ storageState: ADMIN_STORAGE_PATH });
|
|
|
|
test("Logout clears the session and redirects to PROXY_LOGOUT_URL", async ({ page }) => {
|
|
const target = new URL(LOGOUT_URL);
|
|
|
|
// Stub the external logout destination so the assertion doesn't depend on
|
|
// that host being reachable from CI — we only care that the browser is sent
|
|
// there, not what it serves back.
|
|
await page.route(
|
|
(url) => url.origin === target.origin,
|
|
(route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "text/html",
|
|
body: "<html><body>logged out</body></html>",
|
|
}),
|
|
);
|
|
|
|
// navbar.tsx populates the logout target only after the proxy UI settings
|
|
// fetch (/sso/get/ui_settings) resolves. Clicking Logout before that lands
|
|
// runs `window.location.href = ""` — a same-origin reload, not a redirect —
|
|
// so gate the click on the settings response, not just on first paint.
|
|
const settingsLoaded = page.waitForResponse(
|
|
(r) => r.url().includes("/sso/get/ui_settings") && r.ok(),
|
|
{ timeout: 30_000 },
|
|
);
|
|
await page.goto("/ui");
|
|
await expect(page.getByText("Virtual Keys")).toBeVisible({ timeout: 15_000 });
|
|
await settingsLoaded;
|
|
|
|
// Pre-condition: we start authenticated. The admin storage state carries a
|
|
// `token` cookie, so a real logout has something to tear down.
|
|
const tokensBefore = (await page.context().cookies()).filter((c) => c.name === "token");
|
|
expect(tokensBefore.length, "should start logged in with a token cookie").toBeGreaterThan(0);
|
|
|
|
// Open the navbar account dropdown (trigger=click) and click Logout by role
|
|
// rather than internal Ant Design CSS classes, which are not a stable API.
|
|
await page.getByRole("button", { name: /^Account menu/ }).click();
|
|
const logout = page.getByRole("menuitem", { name: "Logout" });
|
|
await expect(logout).toBeVisible({ timeout: 5_000 });
|
|
|
|
// handleLogout clears cookies/local storage, then assigns window.location.href.
|
|
// Arm the navigation wait before the click so we never miss the redirect.
|
|
await Promise.all([
|
|
page.waitForURL((url) => url.origin === target.origin, { timeout: 15_000 }),
|
|
logout.click(),
|
|
]);
|
|
|
|
// The browser landed on exactly the configured logout URL. Compare normalized
|
|
// hrefs (both sides through URL()) so trailing-slash / default-port rewrites the
|
|
// browser applies are matched on the expected side too — this pins scheme, host,
|
|
// port, path, query and hash, not just the origin.
|
|
const landed = new URL(page.url());
|
|
expect(landed.href).toBe(target.href);
|
|
|
|
// ...and the client-side session cookie is gone (clearTokenCookies ran before
|
|
// the redirect). HttpOnly cookies set server-side can't be cleared from JS,
|
|
// so scope the check to the JS-managed token the UI is responsible for.
|
|
const clientTokensAfter = (await page.context().cookies()).filter(
|
|
(c) => c.name === "token" && !c.httpOnly,
|
|
);
|
|
expect(clientTokensAfter, "client token cookie should be cleared on logout").toHaveLength(0);
|
|
});
|
|
});
|