mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
b6e3368a2added the click on Funkwhale's own confirmation ("Yes, log me out!"), which fires POST /api/v2/users/logout.c7e5b0a27then added the return to appBaseUrl so the assertion lands on Funkwhale rather than on the logout page. Each is right on its own; together the navigation aborts the request 9ms after it starts. Measured in all three traces of job 95883448598: the POST carries status -1 with send/wait/receive all -1 - never answered, aborted. Click at 59744ms, POST at 59876ms, goto at 59885ms. The WebSocket that follows still carries sessionid=bam0ska8z0qkbtegsp10iot028cbcsr9, and openresty logs the matching 499. Only swarm+tor is slow enough to lose the race; the other seven Funkwhale variants of that run are green. The catch keeps those seven safe: whether their confirm click fires the same XHR is unverified, and without it a missing POST would turn them red. Promise.all rather than sequential awaits because waitForResponse has to register before the click. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
130 lines
4.8 KiB
JavaScript
130 lines
4.8 KiB
JavaScript
const { test, expect } = require("@playwright/test");
|
|
const { resolveTimeout } = require("./timeouts");
|
|
|
|
const { decodeDotenvQuotedValue, isAuthChain, normalizeBaseUrl , gotoOnion } = require("./personas");
|
|
const { performKeycloakLoginForm } = require("./personas/utils/keycloak");
|
|
|
|
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
|
|
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
|
|
const ldapEnabled = (process.env.LDAP_SERVICE_ENABLED || "").toLowerCase() === "true";
|
|
const ssoEnabled = (process.env.SSO_SERVICE_ENABLED || "").toLowerCase() === "true";
|
|
|
|
const PERSONAS = [
|
|
{
|
|
label: "biber",
|
|
username: decodeDotenvQuotedValue(process.env.BIBER_USERNAME || ""),
|
|
password: decodeDotenvQuotedValue(process.env.BIBER_PASSWORD || ""),
|
|
},
|
|
{
|
|
label: "administrator",
|
|
username: decodeDotenvQuotedValue(process.env.ADMIN_USERNAME || ""),
|
|
password: decodeDotenvQuotedValue(process.env.ADMIN_PASSWORD || ""),
|
|
},
|
|
];
|
|
|
|
const signInAffordance = (page) => page.getByText(/^\s*(log\s?in|sign\s?in)\s*$/i).first();
|
|
|
|
const hostOf = (page) => {
|
|
try {
|
|
return new URL(page.url()).hostname;
|
|
} catch {
|
|
return "";
|
|
}
|
|
};
|
|
const onIdentityProvider = (page) => isAuthChain(page.url()) && hostOf(page) !== canonicalDomain;
|
|
const backOnApp = (page) => hostOf(page) === canonicalDomain && !isAuthChain(page.url());
|
|
|
|
for (const persona of PERSONAS) {
|
|
test(`${persona.label}: native sign-in → authenticated surface → sign-out`, async ({ page }) => {
|
|
test.skip(
|
|
!ldapEnabled,
|
|
"LDAP_SERVICE_ENABLED=false: tasks/main.yml provisions no local Funkwhale account, so no persona can sign in.",
|
|
);
|
|
test.setTimeout(resolveTimeout(180_000));
|
|
|
|
expect(persona.username, `${persona.label} username must be set`).toBeTruthy();
|
|
expect(persona.password, `${persona.label} password must be set`).toBeTruthy();
|
|
|
|
await page.context().clearCookies();
|
|
await gotoOnion(page, `${appBaseUrl}/login`, { waitUntil: "domcontentloaded" });
|
|
|
|
if (ssoEnabled) {
|
|
await expect
|
|
.poll(() => onIdentityProvider(page), {
|
|
timeout: resolveTimeout(60_000),
|
|
message: "the oauth2 ACL protects /login, so the proxy must hand the persona to Keycloak",
|
|
})
|
|
.toBe(true);
|
|
await performKeycloakLoginForm(page, persona.username, persona.password);
|
|
await expect
|
|
.poll(() => backOnApp(page), {
|
|
timeout: resolveTimeout(60_000),
|
|
message: `the oauth2 proxy gates /login, so Keycloak must hand back to ${canonicalDomain}`,
|
|
})
|
|
.toBe(true);
|
|
}
|
|
|
|
const password = page.locator("input[type='password']:visible").first();
|
|
await expect(
|
|
password,
|
|
"Funkwhale serves its own sign-in form on /login; the proxy only gates the route, it does not create an application session",
|
|
).toBeVisible({ timeout: resolveTimeout(60_000) });
|
|
|
|
await page
|
|
.locator(
|
|
"input[autocomplete='username']:visible, input[type='text']:visible, input[type='email']:visible",
|
|
)
|
|
.first()
|
|
.fill(persona.username);
|
|
await password.fill(persona.password);
|
|
await password.press("Enter");
|
|
|
|
await expect(
|
|
signInAffordance(page),
|
|
`the sidebar must stop offering sign-in once ${persona.label} holds a Funkwhale session`,
|
|
).toBeHidden({ timeout: resolveTimeout(60_000) });
|
|
|
|
await expect(page.locator("body")).toContainText(/library|playlist|artist|album|track|channel/i, {
|
|
timeout: resolveTimeout(60_000),
|
|
});
|
|
|
|
const signOut = page
|
|
.getByRole("link", { name: /log\s?out|sign\s?out/i })
|
|
.or(page.getByRole("button", { name: /log\s?out|sign\s?out/i }))
|
|
.first();
|
|
const signOutReachable = await signOut
|
|
.waitFor({ state: "visible", timeout: resolveTimeout(10_000) })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
if (signOutReachable) {
|
|
await signOut.click();
|
|
} else {
|
|
await gotoOnion(page, `${appBaseUrl}/logout`, { waitUntil: "domcontentloaded" });
|
|
}
|
|
|
|
const confirmSignOut = page.getByRole("button", { name: /(log|sign)\s?(me\s?)?out/i }).first();
|
|
const confirmReachable = await confirmSignOut
|
|
.waitFor({ state: "visible", timeout: resolveTimeout(10_000) })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
if (confirmReachable) {
|
|
await Promise.all([
|
|
page
|
|
.waitForResponse(
|
|
(r) => r.url().includes("/api/v2/users/logout") && r.request().method() === "POST",
|
|
{ timeout: resolveTimeout(15_000) },
|
|
)
|
|
.catch(() => null),
|
|
confirmSignOut.click(),
|
|
]);
|
|
}
|
|
|
|
await gotoOnion(page, appBaseUrl, { waitUntil: "domcontentloaded" });
|
|
|
|
await expect(
|
|
signInAffordance(page),
|
|
`after sign-out Funkwhale must offer sign-in to ${persona.label} again`,
|
|
).toBeVisible({ timeout: resolveTimeout(60_000) });
|
|
});
|
|
}
|