mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
151 lines
5.0 KiB
JavaScript
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,
|
|
};
|