chore(playwright): remove unnecessary global auth checks (#8341)

This commit is contained in:
Jamison Lahman authored and GitHub committed 2026-02-11 21:45:12 +00:00
1 parent faa97e92e8
commit 094d7a2d02
17 files changed
+379 -243

No files matched your search

@@ -17,7 +17,7 @@ test.describe("Default Assistant Admin Page", () => {
await page.context().clearCookies();
await loginAs(page, "admin");
const apiClient = new OnyxApiClient(page);
const apiClient = new OnyxApiClient(page.request);
// Create a connector so Internal Search tool becomes available
testCcPairId = await apiClient.createFileConnector(
@@ -83,7 +83,7 @@ test.describe("Default Assistant Admin Page", () => {
});
test.afterEach(async ({ page }) => {
const apiClient = new OnyxApiClient(page);
const apiClient = new OnyxApiClient(page.request);
// Clean up the test connector
if (testCcPairId !== null) {
+2 -2
View File
@@ -153,7 +153,7 @@ export const test = base.extend<{
// API client fixture - provides access to OnyxApiClient for backend operations
apiClient: async ({ page }, use) => {
await authenticateAdmin(page);
const client = new OnyxApiClient(page);
const client = new OnyxApiClient(page.request);
await use(client);
},
@@ -161,7 +161,7 @@ export const test = base.extend<{
seededGuild: async ({ page }, use) => {
await authenticateAdmin(page);
const apiClient = new OnyxApiClient(page);
const apiClient = new OnyxApiClient(page.request);
const guild = await apiClient.createDiscordGuild();
await use({
@@ -89,7 +89,7 @@ test.describe("Image Generation Provider Configuration", () => {
test.afterEach(async ({ page }) => {
// Clean up the image generation config created during the test
const apiClient = new OnyxApiClient(page);
const apiClient = new OnyxApiClient(page.request);
try {
await apiClient.deleteImageGenerationConfig("openai_dalle_3");
console.log("[image-gen-test] Cleaned up DALL-E 3 config");
@@ -16,6 +16,21 @@ test.describe("Appearance Theme Settings", () => {
await page.context().clearCookies();
await loginAs(page, "admin");
// Navigate first so localStorage is accessible (API-based login
// doesn't navigate, leaving the page on about:blank).
await page.goto("/admin/theme");
await page.waitForLoadState("networkidle");
// Skip the entire test when Enterprise features are not licensed.
// The /admin/theme page is gated behind ee_features_enabled and
// renders a license-required message instead of the settings form.
const eeLocked = page.getByText(
"This functionality requires an active Enterprise license."
);
if (await eeLocked.isVisible({ timeout: 1000 }).catch(() => false)) {
test.skip(true, "Enterprise license not active — skipping theme tests");
}
// Clear localStorage to ensure consent modal shows
await page.evaluate(() => {
localStorage.removeItem("allUsersInitialPopupFlowCompleted");
@@ -24,11 +39,17 @@ test.describe("Appearance Theme Settings", () => {
test.afterEach(async ({ page }) => {
// Reset settings to defaults
await page.goto("http://localhost:3000/admin/theme");
await page.goto("/admin/theme");
await page.waitForLoadState("networkidle");
// Clear form fields
// If the form isn't visible (e.g. EE license not active, or test failed
// before navigating here), skip cleanup — there's nothing to reset.
const appNameInput = page.locator('[data-label="application-name-input"]');
if (!(await appNameInput.isVisible({ timeout: 3000 }).catch(() => false))) {
return;
}
// Clear form fields
await appNameInput.clear();
const greetingInput = page.locator('[data-label="greeting-message-input"]');
@@ -70,11 +91,7 @@ test.describe("Appearance Theme Settings", () => {
test("admin configures branding and verifies across pages", async ({
page,
}) => {
// 1. Navigate to theme page
await page.goto("http://localhost:3000/admin/theme");
await page.waitForLoadState("networkidle");
// 2. Fill in Application Name
// 1. Fill in Application Name (page already navigated in beforeEach)
const appNameInput = page.locator('[data-label="application-name-input"]');
await appNameInput.fill(TEST_VALUES.applicationName);
@@ -154,7 +171,7 @@ test.describe("Appearance Theme Settings", () => {
await page.evaluate(() => {
localStorage.removeItem("allUsersInitialPopupFlowCompleted");
});
await page.goto("http://localhost:3000/app");
await page.goto("/app");
await page.waitForLoadState("networkidle");
// 16. Handle consent modal
@@ -180,7 +180,7 @@ test.describe("Assistant Creation and Edit Verification", () => {
storageState: "admin_auth.json",
});
const page = await context.newPage();
const cleanupClient = new OnyxApiClient(page);
const cleanupClient = new OnyxApiClient(page.request);
await cleanupClient.deleteDocumentSet(documentSetId);
await cleanupClient.deleteCCPair(ccPairId);
await context.close();
@@ -200,7 +200,7 @@ test.describe("Assistant Creation and Edit Verification", () => {
await loginAs(page, "admin");
// Create a connector and document set to enable the Knowledge toggle
const onyxApiClient = new OnyxApiClient(page);
const onyxApiClient = new OnyxApiClient(page.request);
ccPairId = await onyxApiClient.createFileConnector("Test Connector");
documentSetId = await onyxApiClient.createDocumentSet(
"Test Document Set",
@@ -50,7 +50,7 @@ test("Restricted LLM Provider should not appear for unauthorized users", async (
let groupId: number | null = null;
let providerId: number | null = null;
const client = new OnyxApiClient(page);
const client = new OnyxApiClient(page.request);
try {
groupId = await client.createUserGroup(restrictedGroupName);
+67
View File
@@ -0,0 +1,67 @@
import { test, expect } from "@playwright/test";
import { TEST_ADMIN_CREDENTIALS, TEST_USER_CREDENTIALS } from "../constants";
// These tests exercise the browser login UI.
// They clear cookies to start unauthenticated, then drive the login form.
test.describe("Login flow", () => {
test.beforeEach(async ({ page }) => {
await page.context().clearCookies();
});
test("Login page renders email and password fields", async ({ page }) => {
await page.goto("/auth/login");
await page.waitForLoadState("networkidle");
await expect(page.getByTestId("email")).toBeVisible();
await expect(page.getByTestId("password")).toBeVisible();
await expect(page.getByRole("button", { name: "Sign In" })).toBeVisible();
});
test("User can log in with valid credentials", async ({ page }) => {
const { email, password } = TEST_ADMIN_CREDENTIALS;
await page.goto("/auth/login");
await page.waitForLoadState("networkidle");
await page.getByTestId("email").fill(email);
await page.getByTestId("password").fill(password);
await page.getByRole("button", { name: "Sign In" }).click();
await expect(page).toHaveURL(/\/app/);
// Verify the session is valid
const me = await page.request.get("/api/me");
expect(me.ok()).toBe(true);
const body = await me.json();
expect(body.email).toBe(email);
});
test("Login fails with invalid password", async ({ page }) => {
await page.goto("/auth/login");
await page.waitForLoadState("networkidle");
await page.getByTestId("email").fill(TEST_USER_CREDENTIALS.email);
await page.getByTestId("password").fill("WrongPassword123!");
await page.getByRole("button", { name: "Sign In" }).click();
// Should stay on the login page
await expect(page).toHaveURL(/\/auth\/login/);
// Should not be authenticated
const me = await page.request.get("/api/me");
expect(me.ok()).toBe(false);
});
test("Login fails with non-existent user", async ({ page }) => {
await page.goto("/auth/login");
await page.waitForLoadState("networkidle");
await page.getByTestId("email").fill("nonexistent@example.com");
await page.getByTestId("password").fill("SomePassword123!");
await page.getByRole("button", { name: "Sign In" }).click();
// Should stay on the login page
await expect(page).toHaveURL(/\/auth\/login/);
});
});
@@ -42,7 +42,7 @@ test.describe("Chat Search Command Menu", () => {
storageState: "admin_auth.json",
});
const page = await context.newPage();
const client = new OnyxApiClient(page);
const client = new OnyxApiClient(page.request);
// Navigate to app to establish session
await page.goto("/app");
@@ -69,7 +69,7 @@ test.describe("Chat Search Command Menu", () => {
storageState: "admin_auth.json",
});
const page = await context.newPage();
const client = new OnyxApiClient(page);
const client = new OnyxApiClient(page.request);
// Navigate to app to establish session
await page.goto("/app");
+7 -7
View File
@@ -29,7 +29,7 @@ test.describe("Default Assistant Tests", () => {
await adminPage.goto("http://localhost:3000/app");
await adminPage.waitForLoadState("networkidle");
const apiClient = new OnyxApiClient(adminPage);
const apiClient = new OnyxApiClient(adminPage.request);
try {
imageGenConfigId = await apiClient.createImageGenerationConfig(
`test-default-assistant-${Date.now()}`
@@ -51,7 +51,7 @@ test.describe("Default Assistant Tests", () => {
await adminPage.goto("http://localhost:3000/app");
await adminPage.waitForLoadState("networkidle");
const apiClient = new OnyxApiClient(adminPage);
const apiClient = new OnyxApiClient(adminPage.request);
await apiClient.deleteImageGenerationConfig(imageGenConfigId);
await adminContext.close();
@@ -317,7 +317,7 @@ test.describe("Default Assistant Tests", () => {
await adminPage.goto("http://localhost:3000/app");
await adminPage.waitForLoadState("networkidle");
const apiClient = new OnyxApiClient(adminPage);
const apiClient = new OnyxApiClient(adminPage.request);
try {
imageGenConfigId = await apiClient.createImageGenerationConfig(
`test-action-toggle-${Date.now()}`
@@ -339,7 +339,7 @@ test.describe("Default Assistant Tests", () => {
await adminPage.goto("http://localhost:3000/app");
await adminPage.waitForLoadState("networkidle");
const apiClient = new OnyxApiClient(adminPage);
const apiClient = new OnyxApiClient(adminPage.request);
await apiClient.deleteImageGenerationConfig(imageGenConfigId);
await adminContext.close();
@@ -364,7 +364,7 @@ test.describe("Default Assistant Tests", () => {
await page.goto("/app");
await page.waitForLoadState("domcontentloaded");
const apiClient = new OnyxApiClient(page);
const apiClient = new OnyxApiClient(page.request);
let webSearchProviderId: number | null = null;
try {
@@ -555,7 +555,7 @@ test.describe("End-to-End Default Assistant Flow", () => {
await adminPage.goto("http://localhost:3000/app");
await adminPage.waitForLoadState("networkidle");
const apiClient = new OnyxApiClient(adminPage);
const apiClient = new OnyxApiClient(adminPage.request);
try {
imageGenConfigId = await apiClient.createImageGenerationConfig(
`test-e2e-journey-${Date.now()}`
@@ -577,7 +577,7 @@ test.describe("End-to-End Default Assistant Flow", () => {
await adminPage.goto("http://localhost:3000/app");
await adminPage.waitForLoadState("networkidle");
const apiClient = new OnyxApiClient(adminPage);
const apiClient = new OnyxApiClient(adminPage.request);
await apiClient.deleteImageGenerationConfig(imageGenConfigId);
await adminContext.close();
+2 -2
View File
@@ -11,7 +11,7 @@ test.describe("LLM Ordering", () => {
await page.context().clearCookies();
await loginAs(page, "admin");
const apiClient = new OnyxApiClient(page);
const apiClient = new OnyxApiClient(page.request);
// Create image generation config so the checkbox appears
try {
@@ -24,7 +24,7 @@ test.describe("LLM Ordering", () => {
});
test.afterEach(async ({ page }) => {
const apiClient = new OnyxApiClient(page);
const apiClient = new OnyxApiClient(page.request);
if (imageGenConfigId !== null) {
try {
@@ -47,14 +47,14 @@ test.describe("InlineFileManagement", () => {
await page.context().clearCookies();
await loginAs(page, "admin");
const apiClient = new OnyxApiClient(page);
const apiClient = new OnyxApiClient(page.request);
testCcPairId = await apiClient.createFileConnector(
`Test File Connector ${Date.now()}`
);
});
test.afterEach(async ({ page }) => {
const apiClient = new OnyxApiClient(page);
const apiClient = new OnyxApiClient(page.request);
if (testCcPairId !== null) {
try {
+52
View File
@@ -0,0 +1,52 @@
/**
* Playwright fixture that ensures a public LLM provider is available.
*
* Usage:
* ```ts
* // Import from this file instead of @playwright/test
* import { test, expect } from "@tests/e2e/fixtures/llmProvider";
*
* test("my test that needs an LLM provider", async ({ page, llmProviderId }) => {
* // llmProviderId is the ID of the provider that was created (or null if
* // one already existed). The fixture handles cleanup automatically.
* });
* ```
*
* The fixture:
* - Authenticates as admin
* - Creates a public LLM provider if none exists
* - Provides the created provider ID to the test
* - Cleans up the provider after all tests in the file complete
*/
import { test as base, expect } from "@playwright/test";
import { loginAs } from "@tests/e2e/utils/auth";
import { OnyxApiClient } from "@tests/e2e/utils/onyxApiClient";
export const test = base.extend<{
/**
* The ID of the public LLM provider created by this fixture, or `null`
* if a public provider already existed.
*/
llmProviderId: number | null;
}>({
llmProviderId: async ({ page }, use) => {
// Authenticate as admin to be able to create/list providers
await page.context().clearCookies();
await loginAs(page, "admin");
const client = new OnyxApiClient(page.request);
const createdId = await client.ensurePublicProvider();
await use(createdId);
// Cleanup: only delete if we created one
if (createdId !== null) {
// Re-authenticate in case the test changed the session
await page.context().clearCookies();
await loginAs(page, "admin");
await client.deleteProvider(createdId);
}
},
});
export { expect };
+62 -21
View File
@@ -1,10 +1,10 @@
import { chromium, FullConfig, request } from "@playwright/test";
import { loginAs } from "./utils/auth";
import { FullConfig, request } from "@playwright/test";
import {
TEST_ADMIN_CREDENTIALS,
TEST_ADMIN2_CREDENTIALS,
TEST_USER_CREDENTIALS,
} from "./constants";
import { OnyxApiClient } from "./utils/onyxApiClient";
const PREFLIGHT_TIMEOUT_MS = 60_000;
const PREFLIGHT_POLL_INTERVAL_MS = 2_000;
@@ -78,6 +78,34 @@ async function ensureUserExists(
}
}
/**
* Log in via the API and save the resulting cookies as a Playwright storage
* state file. No browser is needed — this uses Playwright's lightweight
* request context, which is much faster and produces no console noise.
*/
async function apiLoginAndSaveState(
baseURL: string,
email: string,
password: string,
storageStatePath: string
): Promise<void> {
const ctx = await request.newContext({ baseURL });
try {
const res = await ctx.post("/api/auth/login", {
form: { username: email, password },
});
if (!res.ok()) {
const body = await res.text();
throw new Error(
`[global-setup] Login failed for ${email}: ${res.status()} ${body}`
);
}
await ctx.storageState({ path: storageStatePath });
} finally {
await ctx.dispose();
}
}
/**
* Promote a user to admin via the manage API.
* Requires an authenticated context (admin storage state).
@@ -147,14 +175,13 @@ async function globalSetup(config: FullConfig) {
TEST_ADMIN2_CREDENTIALS.password
);
// ── Login and save storage state ─────────────────────────────────────
const browser = await chromium.launch();
const adminContext = await browser.newContext({ baseURL });
const adminPage = await adminContext.newPage();
await loginAs(adminPage, "admin");
await adminContext.storageState({ path: "admin_auth.json" });
await adminContext.close();
// ── Login via API and save storage state ───────────────────────────
await apiLoginAndSaveState(
baseURL,
TEST_ADMIN_CREDENTIALS.email,
TEST_ADMIN_CREDENTIALS.password,
"admin_auth.json"
);
// Promote admin2 now that we have an admin session
await promoteToAdmin(
@@ -163,19 +190,33 @@ async function globalSetup(config: FullConfig) {
TEST_ADMIN2_CREDENTIALS.email
);
const userContext = await browser.newContext({ baseURL });
const userPage = await userContext.newPage();
await loginAs(userPage, "user");
await userContext.storageState({ path: "user_auth.json" });
await userContext.close();
await apiLoginAndSaveState(
baseURL,
TEST_USER_CREDENTIALS.email,
TEST_USER_CREDENTIALS.password,
"user_auth.json"
);
const admin2Context = await browser.newContext({ baseURL });
const admin2Page = await admin2Context.newPage();
await loginAs(admin2Page, "admin2");
await admin2Context.storageState({ path: "admin2_auth.json" });
await admin2Context.close();
await apiLoginAndSaveState(
baseURL,
TEST_ADMIN2_CREDENTIALS.email,
TEST_ADMIN2_CREDENTIALS.password,
"admin2_auth.json"
);
await browser.close();
// ── Ensure a public LLM provider exists ───────────────────────────
// Many tests depend on a default LLM being configured (file uploads,
// assistant creation, etc.). Re-use the admin session we just saved.
const adminCtx = await request.newContext({
baseURL,
storageState: "admin_auth.json",
});
try {
const client = new OnyxApiClient(adminCtx, baseURL);
await client.ensurePublicProvider();
} finally {
await adminCtx.dispose();
}
}
export default globalSetup;
+10 -15
View File
@@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
import type { Page } from "@playwright/test";
import { loginAs, loginWithCredentials } from "../utils/auth";
import { loginAs, apiLogin } from "../utils/auth";
import { OnyxApiClient } from "../utils/onyxApiClient";
import { startMcpApiKeyServer, McpServerProcess } from "../utils/mcpServer";
@@ -90,15 +90,10 @@ test.describe("Default Assistant MCP Integration", () => {
storageState: "admin_auth.json",
});
const adminPage = await adminContext.newPage();
const adminClient = new OnyxApiClient(adminPage);
const adminClient = new OnyxApiClient(adminPage.request);
const providers = await adminClient.listLlmProviders();
const hasPublicProvider = providers.some((provider) => provider.is_public);
if (!hasPublicProvider) {
createdProviderId = await adminClient.createPublicProvider(
`PW Public Provider ${Date.now()}`
);
}
// Ensure a public LLM provider exists
createdProviderId = await adminClient.ensurePublicProvider();
// Clean up any existing servers with the same URL
try {
@@ -125,9 +120,9 @@ test.describe("Default Assistant MCP Integration", () => {
storageState: "admin_auth.json",
});
const adminPage = await adminContext.newPage();
const adminClient = new OnyxApiClient(adminPage);
const adminClient = new OnyxApiClient(adminPage.request);
if (createdProviderId) {
if (createdProviderId !== null) {
await adminClient.deleteProvider(createdProviderId);
}
@@ -361,7 +356,7 @@ test.describe("Default Assistant MCP Integration", () => {
test.skip(!basicUserEmail, "Basic user must be created first");
await page.context().clearCookies();
await loginWithCredentials(page, basicUserEmail, basicUserPassword);
await apiLogin(page, basicUserEmail, basicUserPassword);
console.log(`[test] Logged in as basic user: ${basicUserEmail}`);
// Navigate to chat (which uses default assistant for new users)
@@ -470,7 +465,7 @@ test.describe("Default Assistant MCP Integration", () => {
test.skip(!basicUserEmail, "Basic user must be created first");
await page.context().clearCookies();
await loginWithCredentials(page, basicUserEmail, basicUserPassword);
await apiLogin(page, basicUserEmail, basicUserPassword);
await page.goto("/app");
await ensureOnboardingComplete(page);
@@ -517,7 +512,7 @@ test.describe("Default Assistant MCP Integration", () => {
const assistantId = assistantIdMatch ? assistantIdMatch[1] : null;
expect(assistantId).not.toBeNull();
const client = new OnyxApiClient(page);
const client = new OnyxApiClient(page.request);
const assistant = await client.getAssistant(Number(assistantId));
const hasMcpTool = assistant.tools.some(
(tool) => tool.mcp_server_id === serverId
@@ -665,7 +660,7 @@ test.describe("Default Assistant MCP Integration", () => {
test.skip(!basicUserEmail, "Basic user must be created first");
await page.context().clearCookies();
await loginWithCredentials(page, basicUserEmail, basicUserPassword);
await apiLogin(page, basicUserEmail, basicUserPassword);
console.log(`[test] Logged in as basic user to verify tool visibility`);
// Navigate to chat
+7 -7
View File
@@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
import type { Page, Browser, Locator } from "@playwright/test";
import { loginAs, loginWithCredentials } from "../utils/auth";
import { loginAs, apiLogin } from "../utils/auth";
import { OnyxApiClient } from "../utils/onyxApiClient";
import { startMcpOauthServer, McpServerProcess } from "../utils/mcpServer";
import { TEST_ADMIN_CREDENTIALS } from "../constants";
@@ -1056,7 +1056,7 @@ test.describe("MCP OAuth flows", () => {
storageState: "admin_auth.json",
});
const adminPage = await adminContext.newPage();
const adminClient = new OnyxApiClient(adminPage);
const adminClient = new OnyxApiClient(adminPage.request);
try {
const existingServers = await adminClient.listMcpServers();
for (const server of existingServers) {
@@ -1128,7 +1128,7 @@ test.describe("MCP OAuth flows", () => {
storageState: "admin_auth.json",
});
const adminPage = await adminContext.newPage();
const adminClient = new OnyxApiClient(adminPage);
const adminClient = new OnyxApiClient(adminPage.request);
if (adminArtifacts?.assistantId) {
await adminClient.deleteAssistant(adminArtifacts.assistantId);
@@ -1180,7 +1180,7 @@ test.describe("MCP OAuth flows", () => {
{ email: TEST_ADMIN_CREDENTIALS.email, role: "admin" },
"AdminFlow primary login"
);
const adminApiClient = new OnyxApiClient(page);
const adminApiClient = new OnyxApiClient(page.request);
logStep("Logged in as admin");
const serverName = `PW MCP Admin ${Date.now()}`;
@@ -1432,7 +1432,7 @@ test.describe("MCP OAuth flows", () => {
await page.context().clearCookies();
logStep("Cleared cookies");
await loginWithCredentials(
await apiLogin(
page,
curatorCredentials!.email,
curatorCredentials!.password
@@ -1443,7 +1443,7 @@ test.describe("MCP OAuth flows", () => {
"CuratorFlow primary login"
);
logStep("Logged in as curator");
const curatorApiClient = new OnyxApiClient(page);
const curatorApiClient = new OnyxApiClient(page.request);
const serverName = `PW MCP Curator ${Date.now()}`;
const assistantName = `PW Curator Assistant ${Date.now()}`;
@@ -1654,7 +1654,7 @@ test.describe("MCP OAuth flows", () => {
curatorTwoPage,
"CuratorFlow secondary pre-login logout"
);
await loginWithCredentials(
await apiLogin(
curatorTwoPage,
curatorTwoCredentials!.email,
curatorTwoCredentials!.password
+43 -137
View File
@@ -1,17 +1,34 @@
import type { Locator, Page } from "@playwright/test";
import type { Page } from "@playwright/test";
import {
TEST_ADMIN2_CREDENTIALS,
TEST_ADMIN_CREDENTIALS,
TEST_USER_CREDENTIALS,
} from "../constants";
import { logPageState } from "./pageStateLogger";
// Logs in a user (admin, user, or admin2) to the application.
/**
* Log in via the API and set cookies on the page's browser context.
* Much faster than navigating through the login UI.
*/
export async function apiLogin(
page: Page,
email: string,
password: string
): Promise<void> {
const res = await page.request.post("/api/auth/login", {
form: { username: email, password },
});
if (!res.ok()) {
const body = await res.text();
throw new Error(`API login failed for ${email}: ${res.status()} ${body}`);
}
}
// Logs in a known test user (admin, user, or admin2) via the API.
// Users must already be provisioned (see global-setup.ts).
export async function loginAs(
page: Page,
userType: "admin" | "user" | "admin2"
) {
): Promise<void> {
const { email, password } =
userType === "admin"
? TEST_ADMIN_CREDENTIALS
@@ -19,92 +36,10 @@ export async function loginAs(
? TEST_ADMIN2_CREDENTIALS
: TEST_USER_CREDENTIALS;
const waitForVisible = async (
locator: Locator,
debugContext: string,
timeoutMs = 30000
) => {
try {
await locator.waitFor({ state: "visible", timeout: timeoutMs });
} catch (error) {
await logPageState(page, debugContext, "[login-debug]");
throw error;
}
};
const fillCredentials = async (contextLabel: string) => {
const emailInput = page.getByTestId("email");
const passwordInput = page.getByTestId("password");
await waitForVisible(emailInput, `${contextLabel}: email input`);
await waitForVisible(passwordInput, `${contextLabel}: password input`);
await emailInput.fill(email);
await passwordInput.fill(password);
};
console.log(`[loginAs] Navigating to /auth/login as ${userType}`);
await page.goto("/auth/login");
// Wait for navigation to settle (login page may redirect to signup if no users exist)
await page.waitForLoadState("networkidle");
const currentUrl = page.url();
const isOnSignup = currentUrl.includes("/auth/signup");
console.log(
`[loginAs] After navigation, landed on: ${currentUrl} (isOnSignup: ${isOnSignup})`
);
await fillCredentials(
isOnSignup ? "loginAs signup form" : "loginAs login form"
);
// Click the submit button
await page.click('button[type="submit"]');
// Log any console errors during login
page.on("console", (msg) => {
if (msg.type() === "error") {
console.log(`[page:console:error] ${msg.text()}`);
}
});
try {
await page.waitForURL(/\/app.*/, { timeout: 10000 });
console.log(
`[loginAs] Redirected to /app for ${userType}. URL: ${page.url()}`
);
} catch {
await logPageState(
page,
`[loginAs] Timeout waiting for /app redirect (${userType}). URL: ${page.url()}`,
"[login-debug]"
);
throw new Error(
`[loginAs] Failed to login as ${userType}. Current URL: ${page.url()}`
);
}
try {
// Try to fetch current user info from the page context
const me = await page.evaluate(async () => {
try {
const res = await fetch("/api/me", { credentials: "include" });
return {
ok: res.ok,
status: res.status,
url: res.url,
body: await res.text(),
};
} catch (e) {
return { ok: false, status: 0, url: "", body: `error: ${String(e)}` };
}
});
console.log(
`[loginAs] /api/me => ok=${me.ok} status=${me.status} url=${me.url}`
);
} catch (e) {
console.log(`[loginAs] Failed to query /api/me: ${String(e)}`);
}
await apiLogin(page, email, password);
}
// Function to generate a random email and password
// Generate a random email and password for throwaway test users.
const generateRandomCredentials = () => {
const randomString = Math.random().toString(36).substring(2, 10);
const specialChars = "!@#$%^&*()_+{}[]|:;<>,.?~";
@@ -121,57 +56,28 @@ const generateRandomCredentials = () => {
};
};
// Function to sign up a new random user
export async function loginAsRandomUser(page: Page) {
// Register and log in as a new random user via the API.
export async function loginAsRandomUser(page: Page): Promise<{
email: string;
password: string;
}> {
const { email, password } = generateRandomCredentials();
await page.goto("/auth/signup");
const emailInput = page.getByTestId("email");
const passwordInput = page.getByTestId("password");
await emailInput.waitFor({ state: "visible", timeout: 30000 });
await emailInput.fill(email);
await passwordInput.fill(password);
// Click the signup button
await page.click('button[type="submit"]');
try {
await page.waitForURL(/\/(app|chat)(\?.*)?$/, { timeout: 30000 });
await page.waitForLoadState("domcontentloaded");
} catch {
console.log(`Timeout occurred. Current URL: ${page.url()}`);
throw new Error("Failed to sign up and redirect to app page");
const registerRes = await page.request.post("/api/auth/register", {
data: { email, username: email, password },
});
if (!registerRes.ok()) {
const body = await registerRes.text();
throw new Error(
`Failed to register random user ${email}: ${registerRes.status()} ${body}`
);
}
await apiLogin(page, email, password);
// Navigate to the app so the page is ready for test interactions
await page.goto("/app?new_team=true");
await page.waitForLoadState("networkidle");
return { email, password };
}
export async function loginWithCredentials(
page: Page,
email: string,
password: string
) {
if (process.env.SKIP_AUTH === "true") {
console.log("[loginWithCredentials] Skipping authentication");
return;
}
await page.goto("/auth/login");
// Wait for navigation to settle (login page may redirect to signup if no users exist)
await page.waitForLoadState("networkidle");
const currentUrl = page.url();
const isOnSignup = currentUrl.includes("/auth/signup");
console.log(
`[loginWithCredentials] After navigation, landed on: ${currentUrl} (isOnSignup: ${isOnSignup})`
);
const emailInput = page.getByTestId("email");
const passwordInput = page.getByTestId("password");
await emailInput.waitFor({ state: "visible", timeout: 30000 });
await emailInput.fill(email);
await passwordInput.fill(password);
await page.click('button[type="submit"]');
await page.waitForURL(/\/app.*/, { timeout: 15000 });
}
+92 -34
View File
@@ -1,4 +1,4 @@
import { Page, expect, APIResponse } from "@playwright/test";
import { APIRequestContext, expect, APIResponse } from "@playwright/test";
/**
* API Client for Onyx backend operations in E2E tests.
@@ -18,7 +18,10 @@ import { Page, expect, APIResponse } from "@playwright/test";
* - `deleteDocumentSet(id)` - Deletes a document set (with polling until complete)
*
* **LLM Providers:**
* - `listLlmProviders()` - Lists LLM providers (admin endpoint, includes is_public)
* - `ensurePublicProvider(name?)` - Idempotently creates a public default LLM provider
* - `createRestrictedProvider(name, groupId)` - Creates a restricted LLM provider assigned to a group
* - `setProviderAsDefault(id)` - Sets an LLM provider as the default for chat
* - `deleteProvider(id)` - Deletes an LLM provider
*
* **User Groups:**
@@ -41,19 +44,33 @@ import { Page, expect, APIResponse } from "@playwright/test";
*
* **Usage Example:**
* ```typescript
* const client = new OnyxApiClient(page);
* const { ccPairId } = await client.createFileConnector("Test Connector");
* const docSetId = await client.createDocumentSet("Test Set", [ccPairId]);
* await client.deleteDocumentSet(docSetId);
* await client.deleteCCPair(ccPairId);
* // From a test with a Page:
* const client = new OnyxApiClient(page.request);
*
* // From global-setup with a standalone context (pass baseURL explicitly):
* const ctx = await request.newContext({ baseURL, storageState: "admin_auth.json" });
* const client = new OnyxApiClient(ctx, baseURL);
* ```
*
* @param page - Playwright Page instance with authenticated session
* @param request - Playwright APIRequestContext with authenticated session
* (e.g. `page.request`, `context.request`, or `request.newContext()`)
* @param baseUrl - Optional base URL override (e.g. `http://localhost:3000`).
* Defaults to `process.env.BASE_URL` or `http://localhost:3000`.
* Pass this when the Playwright-configured baseURL differs from
* the env var (e.g. in `global-setup.ts` where the config value
* is authoritative).
*/
export class OnyxApiClient {
private baseUrl = `${process.env.BASE_URL || "http://localhost:3000"}/api`;
private baseUrl: string;
constructor(private page: Page) {}
constructor(
private request: APIRequestContext,
baseUrl?: string
) {
this.baseUrl = `${
baseUrl ?? process.env.BASE_URL ?? "http://localhost:3000"
}/api`;
}
/**
* Generic GET request to the API.
@@ -62,7 +79,7 @@ export class OnyxApiClient {
* @returns The API response
*/
private async get(endpoint: string): Promise<APIResponse> {
return await this.page.request.get(`${this.baseUrl}${endpoint}`);
return await this.request.get(`${this.baseUrl}${endpoint}`);
}
/**
@@ -73,7 +90,7 @@ export class OnyxApiClient {
* @returns The API response
*/
private async post(endpoint: string, data?: any): Promise<APIResponse> {
return await this.page.request.post(`${this.baseUrl}${endpoint}`, {
return await this.request.post(`${this.baseUrl}${endpoint}`, {
data,
});
}
@@ -85,7 +102,7 @@ export class OnyxApiClient {
* @returns The API response
*/
private async delete(endpoint: string): Promise<APIResponse> {
return await this.page.request.delete(`${this.baseUrl}${endpoint}`);
return await this.request.delete(`${this.baseUrl}${endpoint}`);
}
/**
@@ -96,7 +113,7 @@ export class OnyxApiClient {
* @returns The API response
*/
private async put(endpoint: string, data?: any): Promise<APIResponse> {
return await this.page.request.put(`${this.baseUrl}${endpoint}`, {
return await this.request.put(`${this.baseUrl}${endpoint}`, {
data,
});
}
@@ -371,7 +388,7 @@ export class OnyxApiClient {
providerName: string,
groupId: number
): Promise<number> {
const response = await this.page.request.put(
const response = await this.request.put(
`${this.baseUrl}/admin/llm/provider?is_creation=true`,
{
data: {
@@ -397,18 +414,41 @@ export class OnyxApiClient {
return responseData.id;
}
/**
* Lists LLM providers visible to the admin (includes `is_public`).
*
* @returns Array of LLM providers with id and is_public fields
*/
async listLlmProviders(): Promise<
Array<{
id: number;
is_public?: boolean;
}>
> {
const response = await this.get("/llm/provider");
const response = await this.get("/admin/llm/provider");
return await this.handleResponse(response, "Failed to list LLM providers");
}
async createPublicProvider(providerName: string): Promise<number> {
const response = await this.page.request.put(
/**
* Ensure at least one public LLM provider exists and is set as default.
*
* Idempotent — returns `null` if a public provider already exists,
* or the new provider ID if one was created.
*
* @param providerName - Name for the provider (default: "PW Default Provider")
* @returns The provider ID if one was created, or `null` if already present
*/
async ensurePublicProvider(
providerName: string = "PW Default Provider"
): Promise<number | null> {
const providers = await this.listLlmProviders();
const hasPublic = providers.some((p) => p.is_public);
if (hasPublic) {
return null;
}
const response = await this.request.put(
`${this.baseUrl}/admin/llm/provider?is_creation=true`,
{
data: {
@@ -428,12 +468,33 @@ export class OnyxApiClient {
"Failed to create public provider"
);
// Set as default so get_default_llm() works (needed for tokenization, etc.)
await this.setProviderAsDefault(responseData.id);
this.log(
`Created public LLM provider: ${providerName} (ID: ${responseData.id})`
);
return responseData.id;
}
/**
* Sets an LLM provider as the default for chat.
*
* @param providerId - The provider ID to set as default
*/
async setProviderAsDefault(providerId: number): Promise<void> {
const response = await this.post(
`/admin/llm/provider/${providerId}/default`
);
await this.handleResponseSoft(
response,
`Failed to set provider ${providerId} as default`
);
this.log(`Set LLM provider ${providerId} as default`);
}
/**
* Deletes an LLM provider.
*
@@ -497,7 +558,7 @@ export class OnyxApiClient {
role: "admin" | "curator" | "global_curator" | "basic",
explicitOverride = false
): Promise<void> {
const response = await this.page.request.patch(
const response = await this.request.patch(
`${this.baseUrl}/manage/set-user-role`,
{
data: {
@@ -512,7 +573,7 @@ export class OnyxApiClient {
}
async deleteMcpServer(serverId: number): Promise<boolean> {
const response = await this.page.request.delete(
const response = await this.request.delete(
`${this.baseUrl}/admin/mcp/server/${serverId}`
);
const success = await this.handleResponseSoft(
@@ -526,7 +587,7 @@ export class OnyxApiClient {
}
async deleteAssistant(assistantId: number): Promise<boolean> {
const response = await this.page.request.delete(
const response = await this.request.delete(
`${this.baseUrl}/persona/${assistantId}`
);
const success = await this.handleResponseSoft(
@@ -589,16 +650,13 @@ export class OnyxApiClient {
}
async registerUser(email: string, password: string): Promise<{ id: string }> {
const response = await this.page.request.post(
`${this.baseUrl}/auth/register`,
{
data: {
email,
username: email,
password,
},
}
);
const response = await this.request.post(`${this.baseUrl}/auth/register`, {
data: {
email,
username: email,
password,
},
});
const data = await this.handleResponse<{ id: string }>(
response,
`Failed to register user ${email}`
@@ -611,7 +669,7 @@ export class OnyxApiClient {
email: string;
role: string;
} | null> {
const response = await this.page.request.get(
const response = await this.request.get(
`${this.baseUrl}/manage/users/accepted`,
{
params: {
@@ -639,7 +697,7 @@ export class OnyxApiClient {
userId: string,
isCurator: boolean = true
): Promise<void> {
const response = await this.page.request.post(
const response = await this.request.post(
`${this.baseUrl}/manage/admin/user-group/${userGroupId}/set-curator`,
{
data: {
@@ -851,7 +909,7 @@ export class OnyxApiClient {
guild_name: string | null;
enabled: boolean;
}> {
const response = await this.page.request.patch(
const response = await this.request.patch(
`${this.baseUrl}/manage/admin/discord-bot/guilds/${guildId}`,
{ data: updates }
);
@@ -926,7 +984,7 @@ export class OnyxApiClient {
channel_name: string;
enabled: boolean;
}> {
const response = await this.page.request.patch(
const response = await this.request.patch(
`${this.baseUrl}/manage/admin/discord-bot/guilds/${guildConfigId}/channels/${channelConfigId}`,
{ data: updates }
);