Files

151 lines
5.0 KiB
JavaScript

const { expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, performKeycloakLoginForm, gotoOnion } = require("./personas");
const { isServiceEnabled, skipUnlessServiceEnabled } = require("./service-gating");
const ssoEnabled = isServiceEnabled("sso");
const ldapEnabled = isServiceEnabled("ldap");
const oidcIssuerUrl = normalizeBaseUrl(process.env.OIDC_ISSUER_URL || "");
const jellyfinBaseUrl = normalizeBaseUrl(process.env.JELLYFIN_BASE_URL || "");
const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME);
const adminNativePassword = decodeDotenvQuotedValue(process.env.ADMIN_NATIVE_PASSWORD);
const biberUsername = decodeDotenvQuotedValue(process.env.BIBER_USERNAME);
const biberPassword = decodeDotenvQuotedValue(process.env.BIBER_PASSWORD);
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN);
async function gotoLogin(page) {
await gotoOnion(page, `${jellyfinBaseUrl}/web/`, { waitUntil: "domcontentloaded" });
}
function onLoginSurface(page) {
return /login\.html|\/web\/?($|#\/?$)/.test(page.url());
}
async function assertAuthenticated(page, label) {
await expect(
page
.getByRole("button", { name: "Menu" })
.or(page.getByRole("button", { name: "Search" }))
.first(),
`${label}: authenticated Jellyfin chrome must be visible`,
).toBeVisible({ timeout: resolveTimeout(60_000) });
}
async function fillManualLogin(page, username, password) {
const userField = page.locator("#txtManualName, input[name='username'], input[type='text']").first();
await userField.waitFor({ state: "visible", timeout: resolveTimeout(30_000) });
await userField.fill(username);
await page.locator("#txtManualPassword, input[type='password']").first().fill(password);
await page
.locator("button[type='submit'], .btnSubmit")
.first()
.click({ timeout: resolveTimeout(30_000) })
.catch(() => page.keyboard.press("Enter"));
}
async function signInViaLocal(page, username, password, label) {
await gotoLogin(page);
await fillManualLogin(page, username, password);
await assertAuthenticated(page, label);
}
async function signInViaLdap(page, username, password, label) {
await signInViaLocal(page, username, password, label);
}
async function signInViaOidc(page, username, password, label) {
await gotoLogin(page);
const oidcButton = page
.locator("a, button")
.filter({ hasText: /sign\s*in\s*with|keycloak|single\s*sign|sso|openid/i })
.first();
await expect(
oidcButton,
`${label}: the SSO provider button must render on the Jellyfin login page`,
).toBeVisible({ timeout: resolveTimeout(30_000) });
await oidcButton.click({ timeout: resolveTimeout(30_000) });
await expect
.poll(() => page.url(), {
timeout: resolveTimeout(60_000),
message: `${label}: expected redirect to Keycloak (${oidcIssuerUrl}/protocol/openid-connect/auth)`,
})
.toContain(`${oidcIssuerUrl}/protocol/openid-connect/auth`);
await performKeycloakLoginForm(page, username, password);
await assertAuthenticated(page, label);
}
async function logout(page, label = "session") {
try {
const menu = page
.getByRole("button", { name: "Menu" })
.or(page.locator(".headerUserButton, .mainDrawerButton"))
.first();
if (await menu.count()) {
await menu.click({ timeout: resolveTimeout(5_000) }).catch(() => {});
}
const signOut = page
.getByRole("link", { name: /sign\s*out|log\s*out|logout|abmelden/i })
.or(page.locator("a, button, .listItem").filter({ hasText: /sign\s*out|log\s*out|logout|abmelden/i }))
.first();
if (await signOut.count()) {
await signOut.click({ timeout: resolveTimeout(5_000) }).catch(() => {});
await page.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(8_000) }).catch(() => {});
}
} catch {
/* best-effort */
}
await page.context().clearCookies();
await gotoLogin(page).catch(() => {});
await page
.evaluate(() => {
try {
window.localStorage.clear();
window.sessionStorage.clear();
} catch {
/* noop */
}
})
.catch(() => {});
await gotoLogin(page);
await expect
.poll(() => onLoginSurface(page), {
timeout: resolveTimeout(30_000),
message: `${label}: expected logged-out landing on the Jellyfin login surface`,
})
.toBe(true);
}
async function beforeEach({ page }) {
await page.setViewportSize({ width: 1440, height: 1100 });
expect(jellyfinBaseUrl, "JELLYFIN_BASE_URL must be set").toBeTruthy();
expect(canonicalDomain, "CANONICAL_DOMAIN must be set").toBeTruthy();
await page.context().clearCookies();
}
module.exports = {
env: {
ssoEnabled,
ldapEnabled,
oidcIssuerUrl,
jellyfinBaseUrl,
adminUsername,
adminNativePassword,
biberUsername,
biberPassword,
canonicalDomain,
},
gotoLogin,
onLoginSurface,
assertAuthenticated,
signInViaLocal,
signInViaLdap,
signInViaOidc,
logout,
beforeEach,
skipUnlessServiceEnabled,
};