mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-17 18:35:19 +02:00
chore: implement test-only token mint endpoint and update E2E test authentication flow
This commit is contained in:
parent
741d6e7eea
commit
b247ff37df
8 changed files with 344 additions and 170 deletions
|
|
@ -28,7 +28,7 @@ export default defineConfig({
|
|||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
workers: 1,
|
||||
reporter: process.env.CI
|
||||
? [["html", { open: "never" }], ["github"], ["list"]]
|
||||
: [["html", { open: "on-failure" }], ["list"]],
|
||||
|
|
|
|||
|
|
@ -1,46 +1,21 @@
|
|||
import path from "node:path";
|
||||
import { expect, test as setup } from "@playwright/test";
|
||||
import { acquireTestToken } from "./helpers/api/auth";
|
||||
|
||||
/**
|
||||
* One-time authentication setup. Logs in via the FastAPI backend directly
|
||||
* (skipping the UI) and persists the resulting localStorage token so every
|
||||
* test in the chromium project starts already authenticated.
|
||||
*
|
||||
* Mirrors the real auth flow in `lib/apis/auth-api.service.ts`:
|
||||
* POST /auth/jwt/login -> { access_token }
|
||||
* localStorage.setItem("surfsense_bearer_token", access_token)
|
||||
*
|
||||
* Requires a seeded test user in the dev/test DB. Defaults match the
|
||||
* docker/docker-compose.e2e.yml local stack and can be overridden via env.
|
||||
* One-time authentication setup. Acquires a bearer token for the seeded
|
||||
* e2e user (rate-limit-free /__e2e__/auth/token first, /auth/jwt/login
|
||||
* fallback) and persists it via localStorage so every test in the
|
||||
* chromium project starts already authenticated.
|
||||
*/
|
||||
|
||||
const authFile = path.join(__dirname, "..", "playwright", ".auth", "user.json");
|
||||
|
||||
const TEST_USER_EMAIL = process.env.PLAYWRIGHT_TEST_EMAIL || "e2e-test@surfsense.net";
|
||||
const TEST_USER_PASSWORD = process.env.PLAYWRIGHT_TEST_PASSWORD || "E2eTestPassword123!";
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
|
||||
const STORAGE_KEY = "surfsense_bearer_token";
|
||||
|
||||
setup("authenticate", async ({ page, request }) => {
|
||||
const response = await request.post(`${BACKEND_URL}/auth/jwt/login`, {
|
||||
form: {
|
||||
username: TEST_USER_EMAIL,
|
||||
password: TEST_USER_PASSWORD,
|
||||
grant_type: "password",
|
||||
},
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
});
|
||||
|
||||
expect(
|
||||
response.ok(),
|
||||
`Login to ${BACKEND_URL}/auth/jwt/login failed (${response.status()}). ` +
|
||||
`Check that the backend is running and that PLAYWRIGHT_TEST_EMAIL ` +
|
||||
`(${TEST_USER_EMAIL}) is seeded with PLAYWRIGHT_TEST_PASSWORD. ` +
|
||||
`Body: ${await response.text()}`
|
||||
).toBeTruthy();
|
||||
|
||||
const { access_token } = (await response.json()) as { access_token: string };
|
||||
expect(access_token, "Backend response missing access_token").toBeTruthy();
|
||||
const access_token = await acquireTestToken(request);
|
||||
expect(access_token, "Failed to acquire e2e bearer token").toBeTruthy();
|
||||
|
||||
await page.addInitScript(
|
||||
({ key, token }) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { test as base } from "@playwright/test";
|
||||
import { loginAsTestUser } from "../helpers/api/auth";
|
||||
import { acquireTestToken } from "../helpers/api/auth";
|
||||
import {
|
||||
createSearchSpace,
|
||||
deleteSearchSpace,
|
||||
|
|
@ -20,12 +22,45 @@ export type SearchSpaceFixtures = {
|
|||
searchSpace: SearchSpaceRow;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "surfsense_bearer_token";
|
||||
|
||||
// Reuse the token written by tests/auth.setup.ts; on cache miss we
|
||||
// mint a fresh one via /__e2e__/auth/token (rate-limit-free).
|
||||
const AUTH_STATE_PATH = path.join(__dirname, "..", "..", "playwright", ".auth", "user.json");
|
||||
|
||||
function loadCachedBearerToken(): string | null {
|
||||
try {
|
||||
const raw = fs.readFileSync(AUTH_STATE_PATH, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
origins?: Array<{
|
||||
origin?: string;
|
||||
localStorage?: Array<{ name?: string; value?: string }>;
|
||||
}>;
|
||||
};
|
||||
for (const origin of parsed.origins ?? []) {
|
||||
for (const entry of origin.localStorage ?? []) {
|
||||
if (entry.name === STORAGE_KEY && entry.value) {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall back to a fresh login.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const searchSpaceFixtures = base.extend<SearchSpaceFixtures, { apiTokenWorker: string }>({
|
||||
apiTokenWorker: [
|
||||
async ({ playwright }, use) => {
|
||||
const cached = loadCachedBearerToken();
|
||||
if (cached) {
|
||||
await use(cached);
|
||||
return;
|
||||
}
|
||||
const ctx = await playwright.request.newContext();
|
||||
try {
|
||||
const token = await loginAsTestUser(ctx);
|
||||
const token = await acquireTestToken(ctx);
|
||||
await use(token);
|
||||
} finally {
|
||||
await ctx.dispose();
|
||||
|
|
|
|||
|
|
@ -13,6 +13,38 @@ export const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http:
|
|||
|
||||
const TEST_USER_EMAIL = process.env.PLAYWRIGHT_TEST_EMAIL || "e2e-test@surfsense.net";
|
||||
const TEST_USER_PASSWORD = process.env.PLAYWRIGHT_TEST_PASSWORD || "E2eTestPassword123!";
|
||||
const E2E_MINT_SECRET =
|
||||
process.env.E2E_MINT_SECRET || "local-e2e-mint-secret-not-for-production";
|
||||
|
||||
/**
|
||||
* Mints a JWT for the seeded e2e user via the test-only endpoint mounted
|
||||
* by surfsense_backend/tests/e2e/run_backend.py. Bypasses the production
|
||||
* /auth/jwt/login rate limit (5/min/IP), so it's safe to call from any
|
||||
* worker / retry. Returns 404 from the backend when the endpoint isn't
|
||||
* mounted (i.e. someone is pointing the suite at a non-e2e backend).
|
||||
*/
|
||||
export async function mintTestToken(
|
||||
request: APIRequestContext,
|
||||
email: string = TEST_USER_EMAIL
|
||||
): Promise<string> {
|
||||
const response = await request.post(`${BACKEND_URL}/__e2e__/auth/token`, {
|
||||
data: { email },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-E2E-Mint-Secret": E2E_MINT_SECRET,
|
||||
},
|
||||
});
|
||||
if (!response.ok()) {
|
||||
throw new Error(
|
||||
`Mint token at ${BACKEND_URL}/__e2e__/auth/token failed (${response.status()}): ${await response.text()}`
|
||||
);
|
||||
}
|
||||
const { access_token } = (await response.json()) as { access_token: string };
|
||||
if (!access_token) {
|
||||
throw new Error("Mint response missing access_token");
|
||||
}
|
||||
return access_token;
|
||||
}
|
||||
|
||||
export async function loginAsTestUser(request: APIRequestContext): Promise<string> {
|
||||
const response = await request.post(`${BACKEND_URL}/auth/jwt/login`, {
|
||||
|
|
@ -37,6 +69,23 @@ export async function loginAsTestUser(request: APIRequestContext): Promise<strin
|
|||
return access_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bearer token by trying the rate-limit-free mint endpoint first
|
||||
* and falling back to /auth/jwt/login if the e2e endpoint isn't mounted
|
||||
* (e.g. running against a non-e2e backend in local dev).
|
||||
*/
|
||||
export async function acquireTestToken(request: APIRequestContext): Promise<string> {
|
||||
try {
|
||||
return await mintTestToken(request);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes("(404)") || msg.includes("(405)")) {
|
||||
return loginAsTestUser(request);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard auth headers for backend API calls. Optionally injects an
|
||||
* X-E2E-Scenario header that the test-only ScenarioMiddleware in
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue