test: stabilize e2e auth and rate limits
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
import type { Page } from "@playwright/test";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
@@ -18,6 +19,21 @@ import {
|
||||
* - Drag multiple selected drawings
|
||||
*/
|
||||
|
||||
const waitForDrawingImports = async (page: Page, count: number) => {
|
||||
const waiters = Array.from({ length: count }, () =>
|
||||
page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes("/api/drawings") &&
|
||||
response.request().method() === "POST"
|
||||
)
|
||||
);
|
||||
|
||||
const responses = await Promise.all(waiters);
|
||||
for (const response of responses) {
|
||||
expect(response.ok()).toBeTruthy();
|
||||
}
|
||||
};
|
||||
|
||||
test.describe("Drag and Drop - Collections", () => {
|
||||
let createdDrawingIds: string[] = [];
|
||||
let createdCollectionIds: string[] = [];
|
||||
@@ -272,17 +288,15 @@ test.describe("Drag and Drop - File Import", () => {
|
||||
|
||||
// Find the hidden file input and upload the file
|
||||
const fileInput = page.locator("#dashboard-import");
|
||||
const importResponses = waitForDrawingImports(page, 1);
|
||||
await fileInput.setInputFiles(fixturePath);
|
||||
|
||||
// Wait for upload to complete - the UploadStatus component shows "Done" when finished
|
||||
await expect(page.getByText("Uploads (Done)")).toBeVisible({ timeout: 10000 });
|
||||
await importResponses;
|
||||
|
||||
// Search for the imported drawing (it uses the filename as name)
|
||||
await page.getByPlaceholder("Search drawings...").fill("small-image");
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify at least one drawing was imported
|
||||
const importedCards = page.locator("[id^='drawing-card-']");
|
||||
await expect(importedCards.first()).toBeVisible();
|
||||
await expect(importedCards.first()).toBeVisible({ timeout: 30000 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -388,9 +388,16 @@ test.describe("Drawing Deletion", () => {
|
||||
// Card should be gone
|
||||
await expect(card).not.toBeVisible();
|
||||
|
||||
// Verify via API that drawing is deleted
|
||||
const response = await request.get(`${API_URL}/drawings/${drawing.id}`);
|
||||
expect(response.status()).toBe(404);
|
||||
// Verify via API that drawing is deleted (allow a short window for backend completion)
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const response = await request.get(`${API_URL}/drawings/${drawing.id}`);
|
||||
return response.status();
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
.toBe(404);
|
||||
|
||||
// Remove from cleanup list since it's already deleted
|
||||
createdDrawingIds = createdDrawingIds.filter(id => id !== drawing.id);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
import type { Page } from "@playwright/test";
|
||||
import {
|
||||
API_URL,
|
||||
createDrawing,
|
||||
@@ -20,6 +21,21 @@ import {
|
||||
* - Import database backup
|
||||
*/
|
||||
|
||||
const waitForDrawingImports = async (page: Page, count: number) => {
|
||||
const waiters = Array.from({ length: count }, () =>
|
||||
page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes("/api/drawings") &&
|
||||
response.request().method() === "POST"
|
||||
)
|
||||
);
|
||||
|
||||
const responses = await Promise.all(waiters);
|
||||
for (const response of responses) {
|
||||
expect(response.ok()).toBeTruthy();
|
||||
}
|
||||
};
|
||||
|
||||
test.describe("Export Functionality", () => {
|
||||
let createdDrawingIds: string[] = [];
|
||||
let createdCollectionIds: string[] = [];
|
||||
@@ -214,24 +230,18 @@ test.describe.serial("Import Functionality", () => {
|
||||
const fileInput = page.locator("#dashboard-import");
|
||||
|
||||
// Create a buffer from the fixture content
|
||||
const importResponses = waitForDrawingImports(page, 1);
|
||||
await fileInput.setInputFiles({
|
||||
name: `Import_ExcalidrawTest_${Date.now()}.excalidraw`,
|
||||
mimeType: "application/json",
|
||||
buffer: Buffer.from(fixtureContent),
|
||||
});
|
||||
|
||||
// Wait for upload to complete - the UploadStatus component shows "Done" when finished
|
||||
await expect(page.getByText("Uploads (Done)")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Reload to ensure dashboard state reflects the newly imported drawing
|
||||
await page.reload({ waitUntil: "networkidle" });
|
||||
await importResponses;
|
||||
|
||||
// Verify the drawing was imported - the drawing name is the filename without extension
|
||||
await page.getByPlaceholder("Search drawings...").fill("Import_ExcalidrawTest");
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const importedCards = page.locator("[id^='drawing-card-']");
|
||||
await expect(importedCards.first()).toBeVisible({ timeout: 10000 });
|
||||
await expect(importedCards.first()).toBeVisible({ timeout: 30000 });
|
||||
});
|
||||
|
||||
test("should import JSON drawing file from Dashboard", async ({ page }) => {
|
||||
@@ -281,31 +291,18 @@ test.describe.serial("Import Functionality", () => {
|
||||
|
||||
const fileInput = page.locator("#dashboard-import");
|
||||
|
||||
const importResponses = waitForDrawingImports(page, 1);
|
||||
await fileInput.setInputFiles({
|
||||
name: `${testName}.json`,
|
||||
mimeType: "application/json",
|
||||
buffer: Buffer.from(jsonContent),
|
||||
});
|
||||
|
||||
// Wait for upload to complete - the UploadStatus component shows "Done" when finished
|
||||
await expect(page.getByText("Uploads (Done)")).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Check if upload failed (shows "Failed" text in the upload status)
|
||||
const failedIndicator = page.getByText("Failed");
|
||||
if (await failedIndicator.isVisible()) {
|
||||
console.log("Import failed - skipping rest of test");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reload to force a fresh fetch of drawings after import
|
||||
await page.reload({ waitUntil: "networkidle" });
|
||||
await importResponses;
|
||||
|
||||
// Clear any existing search and search for the imported drawing
|
||||
const searchInput = page.getByPlaceholder("Search drawings...");
|
||||
await searchInput.clear();
|
||||
await searchInput.fill(testName);
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Wait for the card to appear - the drawing should be visible in the UI
|
||||
const importedCards = page.locator("[id^='drawing-card-']");
|
||||
await expect(importedCards.first()).toBeVisible({ timeout: 15000 });
|
||||
@@ -326,10 +323,8 @@ test.describe.serial("Import Functionality", () => {
|
||||
buffer: Buffer.from(invalidContent),
|
||||
});
|
||||
|
||||
// Wait for upload to complete and check for failure indicator
|
||||
await expect(page.getByText("Uploads (Done)")).toBeVisible({ timeout: 10000 });
|
||||
// Should show "Failed" status in the upload status component
|
||||
await expect(page.getByText("Failed")).toBeVisible();
|
||||
// Should show error modal for invalid file
|
||||
await expect(page.getByText("Import Failed")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("should import multiple drawings at once", async ({ page }) => {
|
||||
@@ -364,17 +359,15 @@ test.describe.serial("Import Functionality", () => {
|
||||
];
|
||||
|
||||
const fileInput = page.locator("#dashboard-import");
|
||||
const importResponses = waitForDrawingImports(page, files.length);
|
||||
await fileInput.setInputFiles(files);
|
||||
|
||||
// Wait for upload to complete - the UploadStatus component shows "Done" when finished
|
||||
await expect(page.getByText("Uploads (Done)")).toBeVisible({ timeout: 10000 });
|
||||
await importResponses;
|
||||
|
||||
// Verify both were imported by searching for the unique prefix
|
||||
await page.getByPlaceholder("Search drawings...").fill(searchPrefix);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const importedCards = page.locator("[id^='drawing-card-']");
|
||||
await expect(importedCards).toHaveCount(2);
|
||||
await expect(importedCards).toHaveCount(2, { timeout: 30000 });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { PrismaClient } from "../../backend/src/generated/client";
|
||||
import { test, expect } from "./fixtures";
|
||||
|
||||
const BASE_URL = process.env.BASE_URL || "http://localhost:5173";
|
||||
const AUTH_USERNAME = process.env.AUTH_USERNAME || "admin";
|
||||
const AUTH_PASSWORD = process.env.AUTH_PASSWORD || "admin123";
|
||||
const DATABASE_URL = process.env.DATABASE_URL;
|
||||
|
||||
const ensureLoggedOut = async (page: Page) => {
|
||||
await page.context().clearCookies();
|
||||
@@ -57,33 +56,62 @@ const ensureDashboard = async (page: Page) => {
|
||||
await expect(page.getByPlaceholder("Search drawings...")).toBeVisible({ timeout: 30000 });
|
||||
};
|
||||
|
||||
const setMustResetPassword = async (enabled: boolean) => {
|
||||
if (!DATABASE_URL) {
|
||||
throw new Error("DATABASE_URL is not set for e2e test.");
|
||||
}
|
||||
type CsrfInfo = {
|
||||
token: string;
|
||||
headerName: string;
|
||||
};
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: { url: DATABASE_URL },
|
||||
},
|
||||
const fetchCsrfInfo = async (page: Page): Promise<CsrfInfo> => {
|
||||
const response = await page.request.get(`${BASE_URL}/api/csrf-token`, {
|
||||
headers: { origin: BASE_URL },
|
||||
});
|
||||
|
||||
try {
|
||||
const admin = await prisma.user.findFirst({
|
||||
where: { username: AUTH_USERNAME },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!response.ok()) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`Failed to fetch CSRF token: ${response.status()} ${text || "(empty response)"}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!admin) {
|
||||
throw new Error(`Admin user ${AUTH_USERNAME} not found.`);
|
||||
}
|
||||
const data = (await response.json()) as { token: string; header?: string };
|
||||
if (!data || typeof data.token !== "string" || data.token.trim().length === 0) {
|
||||
throw new Error("Failed to fetch CSRF token: missing token in response");
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: admin.id },
|
||||
data: { mustResetPassword: enabled },
|
||||
const headerName =
|
||||
typeof data.header === "string" && data.header.trim().length > 0
|
||||
? data.header
|
||||
: "x-csrf-token";
|
||||
|
||||
return { token: data.token, headerName };
|
||||
};
|
||||
|
||||
const setMustResetPassword = async (page: Page, enabled: boolean) => {
|
||||
const csrfInfo = await fetchCsrfInfo(page);
|
||||
let response = await page.request.post(`${BASE_URL}/api/auth/test/must-reset`, {
|
||||
headers: {
|
||||
origin: BASE_URL,
|
||||
"Content-Type": "application/json",
|
||||
[csrfInfo.headerName]: csrfInfo.token,
|
||||
},
|
||||
data: { enabled },
|
||||
});
|
||||
|
||||
if (!response.ok() && response.status() === 403) {
|
||||
const refreshed = await fetchCsrfInfo(page);
|
||||
response = await page.request.post(`${BASE_URL}/api/auth/test/must-reset`, {
|
||||
headers: {
|
||||
origin: BASE_URL,
|
||||
"Content-Type": "application/json",
|
||||
[refreshed.headerName]: refreshed.token,
|
||||
},
|
||||
data: { enabled },
|
||||
});
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
if (!response.ok()) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Failed to toggle mustResetPassword: ${response.status()} ${text}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,7 +135,7 @@ test.describe("Admin password reset", () => {
|
||||
await expect(page.getByPlaceholder("Search drawings...")).toBeVisible({ timeout: 30000 });
|
||||
}
|
||||
|
||||
await setMustResetPassword(true);
|
||||
await setMustResetPassword(page, true);
|
||||
await ensureLoggedOut(page);
|
||||
|
||||
await login(page, AUTH_PASSWORD);
|
||||
|
||||
Reference in New Issue
Block a user