Files
ExcaliDash/e2e/tests/helpers/auth.ts
T
Adrian Acala 15ac634d15 feat(auth): add password reset functionality and user model update
- Introduced a `mustResetPassword` field in the User model to manage password reset requirements.
- Enhanced authentication flow to support password changes, including validation and error handling.
- Updated frontend components to handle password reset scenarios and integrate with the new API endpoints.
- Modified authentication context and hooks to accommodate the new password reset logic.
- Adjusted E2E tests to ensure proper coverage for the password reset functionality.
2026-01-18 13:02:18 -08:00

221 lines
6.4 KiB
TypeScript

import { APIRequestContext, Page } from "@playwright/test";
import { API_URL, getCsrfHeaders, refreshCsrfToken } from "./api";
const BASE_URL = process.env.API_URL || API_URL;
type AuthStatus = {
enabled: boolean;
authenticated: boolean;
bootstrapRequired?: boolean;
user?: { mustResetPassword?: boolean } | null;
};
const AUTH_USERNAME = process.env.AUTH_USERNAME || "admin";
const AUTH_PASSWORD = process.env.AUTH_PASSWORD || "admin123";
const fetchAuthStatus = async (request: APIRequestContext): Promise<AuthStatus> => {
const maxAttempts = 3;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
try {
const response = await request.get(`${BASE_URL}/auth/status`);
if (response.ok()) {
return (await response.json()) as AuthStatus;
}
const text = await response.text();
if (response.status() === 429 && attempt < maxAttempts - 1) {
await new Promise((resolve) => setTimeout(resolve, 1000 + attempt * 500));
continue;
}
throw new Error(`Failed to fetch auth status: ${response.status()} ${text}`);
} catch (error) {
if (attempt < maxAttempts - 1) {
await new Promise((resolve) => setTimeout(resolve, 500 + attempt * 250));
continue;
}
throw error;
}
}
throw new Error("Failed to fetch auth status");
};
export const ensureApiAuthenticated = async (request: APIRequestContext) => {
const status = await fetchAuthStatus(request);
if (!status.enabled || (status.authenticated && !status.user?.mustResetPassword)) {
return;
}
const resetIfRequired = async () => {
if (!status.user?.mustResetPassword) return;
let resetResponse = await request.post(`${BASE_URL}/auth/password`, {
headers: {
"Content-Type": "application/json",
...(await getCsrfHeaders(request)),
},
data: {
currentPassword: AUTH_PASSWORD,
newPassword: AUTH_PASSWORD,
},
});
if (!resetResponse.ok() && resetResponse.status() === 403) {
await refreshCsrfToken(request);
resetResponse = await request.post(`${BASE_URL}/auth/password`, {
headers: {
"Content-Type": "application/json",
...(await getCsrfHeaders(request)),
},
data: {
currentPassword: AUTH_PASSWORD,
newPassword: AUTH_PASSWORD,
},
});
}
if (!resetResponse.ok()) {
const text = await resetResponse.text();
throw new Error(`Failed to reset admin password: ${resetResponse.status()} ${text}`);
}
};
if (status.bootstrapRequired) {
let response = await request.post(`${BASE_URL}/auth/bootstrap`, {
headers: {
"Content-Type": "application/json",
...(await getCsrfHeaders(request)),
},
data: {
username: AUTH_USERNAME,
password: AUTH_PASSWORD,
},
});
if (!response.ok() && response.status() === 403) {
await refreshCsrfToken(request);
response = await request.post(`${BASE_URL}/auth/bootstrap`, {
headers: {
"Content-Type": "application/json",
...(await getCsrfHeaders(request)),
},
data: {
username: AUTH_USERNAME,
password: AUTH_PASSWORD,
},
});
}
if (!response.ok()) {
const text = await response.text();
throw new Error(`Failed to bootstrap test session: ${response.status()} ${text}`);
}
await resetIfRequired();
return;
}
let response = await request.post(`${BASE_URL}/auth/login`, {
headers: {
"Content-Type": "application/json",
...(await getCsrfHeaders(request)),
},
data: {
username: AUTH_USERNAME,
password: AUTH_PASSWORD,
},
});
if (!response.ok() && response.status() === 403) {
await refreshCsrfToken(request);
const freshHeaders = {
"Content-Type": "application/json",
...(await getCsrfHeaders(request)),
};
response = await request.post(`${BASE_URL}/auth/login`, {
headers: freshHeaders,
data: {
username: AUTH_USERNAME,
password: AUTH_PASSWORD,
},
});
}
if (!response.ok()) {
const text = await response.text();
throw new Error(`Failed to authenticate test session: ${response.status()} ${text}`);
}
};
type EnsureAuthOptions = {
skipNavigation?: boolean;
};
export const ensurePageAuthenticated = async (
page: Page,
{ skipNavigation = false }: EnsureAuthOptions = {}
) => {
await ensureApiAuthenticated(page.request);
const storageState = await page.request.storageState();
if (storageState.cookies.length > 0) {
await page.context().addCookies(
storageState.cookies.filter((cookie) => cookie.name && cookie.value)
);
}
if (!skipNavigation) {
await page.goto("/", { waitUntil: "domcontentloaded" });
}
const dashboardReady = page.getByPlaceholder("Search drawings...");
const identifierField = page.getByLabel("Username or Email");
const passwordField = page.getByLabel("Password");
if (skipNavigation) {
if (await identifierField.isVisible({ timeout: 1000 }).catch(() => false)) {
await identifierField.fill(AUTH_USERNAME);
await passwordField.fill(AUTH_PASSWORD);
const confirmPasswordField = page.getByLabel("Confirm Password");
if (await confirmPasswordField.isVisible().catch(() => false)) {
await confirmPasswordField.fill(AUTH_PASSWORD);
}
await page
.getByRole("button", { name: /sign in|create admin|create account/i })
.click();
await dashboardReady.waitFor({ state: "visible", timeout: 30000 });
}
return;
}
await Promise.race([
dashboardReady.waitFor({ state: "visible", timeout: 15000 }),
identifierField.waitFor({ state: "visible", timeout: 15000 }),
]);
if (await dashboardReady.isVisible().catch(() => false)) {
return;
}
if (await identifierField.isVisible().catch(() => false)) {
await identifierField.fill(AUTH_USERNAME);
await passwordField.fill(AUTH_PASSWORD);
const confirmPasswordField = page.getByLabel("Confirm Password");
if (await confirmPasswordField.isVisible().catch(() => false)) {
await confirmPasswordField.fill(AUTH_PASSWORD);
}
await page
.getByRole("button", { name: /sign in|create admin|create account/i })
.click();
await dashboardReady.waitFor({ state: "visible", timeout: 30000 });
return;
}
await dashboardReady.waitFor({ state: "visible", timeout: 15000 });
};