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>
153 lines
5.1 KiB
JavaScript
153 lines
5.1 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 oidcEnabled = isServiceEnabled("sso");
|
|
const ldapEnabled = isServiceEnabled("ldap");
|
|
const oidcIssuerUrl = normalizeBaseUrl(process.env.OIDC_ISSUER_URL || "");
|
|
const semaphoreBaseUrl = normalizeBaseUrl(process.env.SEMAPHORE_BASE_URL || "");
|
|
const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME);
|
|
const adminPassword = decodeDotenvQuotedValue(process.env.ADMIN_PASSWORD);
|
|
const breakglassUsername = decodeDotenvQuotedValue(process.env.BREAKGLASS_USERNAME);
|
|
const breakglassPassword = decodeDotenvQuotedValue(process.env.BREAKGLASS_PASSWORD);
|
|
const biberUsername = decodeDotenvQuotedValue(process.env.BIBER_USERNAME);
|
|
const biberPassword = decodeDotenvQuotedValue(process.env.BIBER_PASSWORD);
|
|
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN);
|
|
|
|
const LOGIN_PATH = "/auth/login";
|
|
|
|
function onLoginPage(page) {
|
|
return page.url().includes(LOGIN_PATH);
|
|
}
|
|
|
|
async function gotoLogin(page) {
|
|
await gotoOnion(page, `${semaphoreBaseUrl}${LOGIN_PATH}`, { waitUntil: "domcontentloaded" });
|
|
}
|
|
|
|
async function assertAuthenticated(page, label) {
|
|
await expect
|
|
.poll(() => page.url(), {
|
|
timeout: resolveTimeout(60_000),
|
|
message: `${label}: expected to leave the Semaphore login page`,
|
|
})
|
|
.not.toContain(LOGIN_PATH);
|
|
await expect(
|
|
page.locator("header, nav, .v-navigation-drawer, .v-app-bar").first(),
|
|
`${label}: authenticated Semaphore chrome must be visible`,
|
|
).toBeVisible({ timeout: resolveTimeout(60_000) });
|
|
}
|
|
|
|
async function fillLocalLogin(page, username, password) {
|
|
const userField = page.locator("#auth-username").first();
|
|
await userField.waitFor({ state: "visible", timeout: resolveTimeout(30_000) });
|
|
await userField.fill(username);
|
|
await page.locator("#auth-password").first().fill(password);
|
|
await page
|
|
.locator('[data-testid="auth-signin"]')
|
|
.first()
|
|
.click({ timeout: resolveTimeout(30_000) })
|
|
.catch(() => page.keyboard.press("Enter"));
|
|
}
|
|
|
|
async function signInViaLocal(page, username, password, label) {
|
|
await gotoLogin(page);
|
|
await fillLocalLogin(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/i })
|
|
.first();
|
|
await expect(
|
|
oidcButton,
|
|
`${label}: the "Sign in with Keycloak" button must render on the Semaphore 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 OIDC auth (${oidcIssuerUrl}/protocol/openid-connect/auth)`,
|
|
})
|
|
.toContain(`${oidcIssuerUrl}/protocol/openid-connect/auth`);
|
|
|
|
await performKeycloakLoginForm(page, username, password);
|
|
await assertAuthenticated(page, label);
|
|
}
|
|
|
|
async function openAccountMenu(page) {
|
|
const trigger = page
|
|
.locator(
|
|
'header button:has(.mdi-account), header button:has(.mdi-account-circle), header .v-avatar, button[aria-label*="account" i], header [role="button"]:has(.mdi-account)',
|
|
)
|
|
.first();
|
|
if (await trigger.count()) {
|
|
await trigger.click().catch(() => {});
|
|
}
|
|
}
|
|
|
|
async function logout(page, label = "session") {
|
|
await openAccountMenu(page);
|
|
const signOut = page
|
|
.locator('a, button, .v-list-item, [role="menuitem"]')
|
|
.filter({ hasText: /sign\s*out|log\s*out|logout|abmelden/i })
|
|
.first();
|
|
if (await signOut.count()) {
|
|
await signOut.click({ timeout: resolveTimeout(30_000) }).catch(() => {});
|
|
}
|
|
await page.waitForTimeout(resolveTimeout(1500));
|
|
if (!onLoginPage(page)) {
|
|
await page.context().clearCookies();
|
|
await gotoLogin(page);
|
|
}
|
|
await expect
|
|
.poll(() => page.url(), {
|
|
timeout: resolveTimeout(30_000),
|
|
message: `${label}: expected logged-out landing on the Semaphore login page`,
|
|
})
|
|
.toContain(LOGIN_PATH);
|
|
}
|
|
|
|
async function beforeEach({ page }) {
|
|
await page.setViewportSize({ width: 1440, height: 1100 });
|
|
expect(semaphoreBaseUrl, "SEMAPHORE_BASE_URL must be set").toBeTruthy();
|
|
expect(canonicalDomain, "CANONICAL_DOMAIN must be set").toBeTruthy();
|
|
await page.context().clearCookies();
|
|
}
|
|
|
|
module.exports = {
|
|
env: {
|
|
oidcEnabled,
|
|
ldapEnabled,
|
|
oidcIssuerUrl,
|
|
semaphoreBaseUrl,
|
|
adminUsername,
|
|
adminPassword,
|
|
breakglassUsername,
|
|
breakglassPassword,
|
|
biberUsername,
|
|
biberPassword,
|
|
canonicalDomain,
|
|
},
|
|
LOGIN_PATH,
|
|
gotoLogin,
|
|
onLoginPage,
|
|
assertAuthenticated,
|
|
signInViaLocal,
|
|
signInViaLdap,
|
|
signInViaOidc,
|
|
logout,
|
|
beforeEach,
|
|
skipUnlessServiceEnabled,
|
|
};
|