文件
hyb-oa/client/tests/e2e/helpers/auth.ts
T

222 行
6.2 KiB
TypeScript

import { expect, request as playwrightRequest, type APIRequestContext, type Page } from "@playwright/test";
import { TEST_IDS } from "./testIds";
export const E2E_BASE_URL = process.env.E2E_BASE_URL ?? "http://127.0.0.1:3000";
export type E2EUser = {
identifier: string;
password: string;
name: string;
employeeNo?: string;
};
export const EMPLOYEE: E2EUser = {
identifier: "[email protected]",
password: "EmployeePassword1234",
name: "陈工程",
employeeNo: "HYD000003",
};
export const MANAGER: E2EUser = {
identifier: "[email protected]",
password: "ManagerPassword1234",
name: "李研发",
employeeNo: "HYD000002",
};
export const FINANCE: E2EUser = {
identifier: "[email protected]",
password: "FinancePassword1234",
name: "赵财务",
employeeNo: "HYD000004",
};
export const FINANCE_MGR: E2EUser = {
identifier: "[email protected]",
password: "FinanceMgrPassword1234",
name: "钱主管",
employeeNo: "HYD000005",
};
export const ADMIN: E2EUser = {
identifier: "[email protected]",
password: "AdminPassword1234",
name: "王顺利",
employeeNo: "HYD000006",
};
export type MeResponse = {
id: string;
employeeNo: string;
name: string;
email: string;
roleCodes: string[];
permissions: string[];
profileCompleteness?: {
hasEmergencyContact: boolean;
hasBankAccount: boolean;
hasAvatar: boolean;
hasIdNumber: boolean;
score: number;
};
};
export type AuthenticatedApiClient = {
api: APIRequestContext;
accessToken: string;
me: MeResponse;
dispose: () => Promise<void>;
};
type Envelope<T> = {
data: T;
error?: {
code?: string;
message?: string;
details?: unknown;
};
};
function buildDeviceFingerprint(user: E2EUser) {
return `pw-e2e-${user.identifier.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`;
}
async function readJson<T>(response: { ok(): boolean; status(): number; text(): Promise<string> }) {
const rawText = await response.text();
const parsed = rawText ? (JSON.parse(rawText) as Envelope<T>) : ({ data: null } as Envelope<T>);
if (!response.ok()) {
const errorCode = parsed.error?.code ?? `HTTP_${response.status()}`;
const errorMessage = parsed.error?.message ?? rawText;
throw new Error(`${errorCode}: ${errorMessage}`);
}
return parsed.data;
}
export async function login(page: Page, user: E2EUser) {
await page.goto("/login");
await page.getByTestId(TEST_IDS.login.identifier).fill(user.identifier);
await page.getByTestId(TEST_IDS.login.password).fill(user.password);
await page.getByTestId(TEST_IDS.login.submit).click();
await expect(page).toHaveURL(/\/portal/);
await expect(page.getByTestId(TEST_IDS.topbar.profileTrigger).first()).toBeVisible();
}
export async function logout(page: Page, userDisplayName?: string) {
try {
const profileTrigger = page.getByTestId(TEST_IDS.topbar.profileTrigger).first();
if (await profileTrigger.isVisible().catch(() => false)) {
await profileTrigger.click({ force: true });
} else if (userDisplayName) {
await page.getByRole("button", { name: userDisplayName, exact: true }).first().click({ force: true });
} else {
throw new Error("profile trigger not visible");
}
await expect(page.getByRole("menuitem", { name: "退出登录" })).toBeVisible({ timeout: 5_000 });
await page.getByRole("menuitem", { name: "退出登录" }).click();
} catch {
await page.context().clearCookies();
await page.evaluate(() => {
window.localStorage.clear();
window.sessionStorage.clear();
});
await page.goto("/login");
}
await expect(page).toHaveURL(/\/login/);
}
export async function createAuthenticatedApiClient(user: E2EUser, baseURL = E2E_BASE_URL): Promise<AuthenticatedApiClient> {
const loginContext = await playwrightRequest.newContext({
baseURL,
extraHTTPHeaders: {
"x-device-fingerprint": buildDeviceFingerprint(user),
},
});
const loginResponse = await loginContext.post("/api/auth/login", {
data: {
identifier: user.identifier,
password: user.password,
},
});
const loginData = await readJson<{ accessToken: string }>(loginResponse);
const accessToken = loginData.accessToken;
const api = await playwrightRequest.newContext({
baseURL,
extraHTTPHeaders: {
authorization: `Bearer ${accessToken}`,
},
});
const meResponse = await api.get("/api/me");
const me = await readJson<MeResponse>(meResponse);
return {
api,
accessToken,
me,
dispose: async () => {
await Promise.all([api.dispose(), loginContext.dispose()]);
},
};
}
export async function createAuthenticatedBrowserStorageState(user: E2EUser, baseURL = E2E_BASE_URL) {
const deviceFingerprint = buildDeviceFingerprint(user);
const loginContext = await playwrightRequest.newContext({
baseURL,
extraHTTPHeaders: {
"x-device-fingerprint": deviceFingerprint,
},
});
try {
const loginResponse = await loginContext.post("/api/auth/login", {
data: {
identifier: user.identifier,
password: user.password,
},
});
await readJson<{ accessToken: string }>(loginResponse);
const state = await loginContext.storageState();
const origin = new URL(baseURL).origin;
const originState = state.origins.find((item) => item.origin === origin);
const deviceFingerprintEntry = {
name: "hyb_oa_device_fingerprint",
value: deviceFingerprint,
};
if (originState) {
originState.localStorage = originState.localStorage.filter((item) => item.name !== deviceFingerprintEntry.name);
originState.localStorage.push(deviceFingerprintEntry);
} else {
state.origins.push({
origin,
localStorage: [deviceFingerprintEntry],
});
}
return state;
} finally {
await loginContext.dispose();
}
}
export async function apiGetData<T>(api: APIRequestContext, path: string) {
const response = await api.get(path);
return readJson<T>(response);
}
export async function apiPostData<T>(api: APIRequestContext, path: string, data: unknown) {
const response = await api.post(path, { data });
return readJson<T>(response);
}
export async function apiPatchData<T>(api: APIRequestContext, path: string, data: unknown) {
const response = await api.patch(path, { data });
return readJson<T>(response);
}