0.2.1 Release (#32)
* feat(security): implement CSRF protection * chore: clean up CSRF implementation - Remove unused generateCsrfToken export from security.ts - Remove redundant /csrf-token path check (GET already exempt) - Restore defineConfig wrapper in vitest.config.ts for type safety * add K8S note in README, fix broken e2e * feat/upload-bar (#30) * feat/upload-bar: add a upload bar when user upload file, indicate the upload process * feat/save-loading-status: add save status when click back button from editor * fix: address PR review issues in upload and save features - Replace deprecated substr() with substring() in UploadContext - Fix broken error handling that checked stale task status - Fix missing useEffect dependency in UploadStatus - Fix CSS class conflict in progress bar styling - Add error recovery for save state in Editor (reset on failure) - Use .finally() instead of .then() to ensure refresh on upload failure - Fix inconsistent indentation in UploadContext * fix e2e tests --------- Co-authored-by: Zimeng Xiong <zxzimeng@gmail.com> * chore: pre-release v0.2.1-dev * Update backend/src/security.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix filename/math random UUID generation --------- Co-authored-by: AdrianAcala <adrianacala017@gmail.com> Co-authored-by: adamant368 <60790941+Yiheng-Liu@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { API_URL, createDrawing, deleteDrawing, getDrawing } from "./helpers/api";
|
||||
import {
|
||||
API_URL,
|
||||
createDrawing,
|
||||
deleteDrawing,
|
||||
getCsrfHeaders,
|
||||
getDrawing,
|
||||
} from "./helpers/api";
|
||||
|
||||
/**
|
||||
* E2E Browser Tests for Image Persistence - Issue #17 Regression
|
||||
@@ -28,13 +34,13 @@ function generateLargeImageDataUrl(sizeInBytes: number = 50000): string {
|
||||
|
||||
test.describe("Image Persistence - Browser E2E Tests", () => {
|
||||
let testDrawingIds: string[] = [];
|
||||
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
// Clean up any drawings created during tests
|
||||
for (const id of testDrawingIds) {
|
||||
try {
|
||||
await deleteDrawing(request, id);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
@@ -43,23 +49,23 @@ test.describe("Image Persistence - Browser E2E Tests", () => {
|
||||
|
||||
test("should navigate to dashboard and see drawing list", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
|
||||
// Wait for the page to load
|
||||
await expect(page).toHaveTitle(/ExcaliDash/i);
|
||||
|
||||
|
||||
// The dashboard should show some UI elements
|
||||
await expect(page.locator("body")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should create a new drawing via UI", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
|
||||
// Look for a "New Drawing" or similar button
|
||||
const newDrawingBtn = page.getByRole("button", { name: /new|create/i }).first();
|
||||
|
||||
|
||||
if (await newDrawingBtn.isVisible()) {
|
||||
await newDrawingBtn.click();
|
||||
|
||||
|
||||
// Should navigate to editor or show a modal
|
||||
await page.waitForURL(/\/(editor|drawing)/i, { timeout: 5000 }).catch(() => {
|
||||
// May stay on same page with modal
|
||||
@@ -71,7 +77,7 @@ test.describe("Image Persistence - Browser E2E Tests", () => {
|
||||
// This is the core regression test for issue #17
|
||||
const largeDataUrl = generateLargeImageDataUrl(50000);
|
||||
expect(largeDataUrl.length).toBeGreaterThan(10000);
|
||||
|
||||
|
||||
const files = {
|
||||
"test-image-1": {
|
||||
id: "test-image-1",
|
||||
@@ -80,23 +86,23 @@ test.describe("Image Persistence - Browser E2E Tests", () => {
|
||||
created: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// Create drawing with large image
|
||||
const createdDrawing = await createDrawing(request, {
|
||||
name: "E2E Test - Large Image",
|
||||
files,
|
||||
});
|
||||
testDrawingIds.push(createdDrawing.id);
|
||||
|
||||
|
||||
// Retrieve the drawing
|
||||
const drawing = await getDrawing(request, createdDrawing.id);
|
||||
const savedFiles = drawing.files || {}; // Already parsed by API
|
||||
|
||||
|
||||
// Verify the image data was preserved
|
||||
expect(savedFiles["test-image-1"]).toBeDefined();
|
||||
expect(savedFiles["test-image-1"].dataURL).toBe(largeDataUrl);
|
||||
expect(savedFiles["test-image-1"].dataURL.length).toBe(largeDataUrl.length);
|
||||
|
||||
|
||||
console.log("✓ Large image data preserved correctly through save/reload cycle");
|
||||
});
|
||||
|
||||
@@ -106,36 +112,36 @@ test.describe("Image Persistence - Browser E2E Tests", () => {
|
||||
name: "E2E Test - Editor View",
|
||||
});
|
||||
testDrawingIds.push(createdDrawing.id);
|
||||
|
||||
|
||||
// Navigate to the editor
|
||||
await page.goto(`/editor/${createdDrawing.id}`);
|
||||
|
||||
|
||||
// Wait for the page to load
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
|
||||
// The editor should be visible (Excalidraw canvas)
|
||||
// Look for the Excalidraw container or canvas
|
||||
const editorContainer = page.locator("[class*='excalidraw'], canvas").first();
|
||||
await expect(editorContainer).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("should import .excalidraw file with embedded image", async ({ page, request }) => {
|
||||
test("should import .excalidraw file with embedded image", async ({ request }) => {
|
||||
// Load the test fixture
|
||||
const fixturePath = path.join(__dirname, "..", "fixtures", "small-image.excalidraw");
|
||||
const fixtureContent = fs.readFileSync(fixturePath, "utf-8");
|
||||
const fixtureData = JSON.parse(fixtureContent);
|
||||
|
||||
// Create drawing via API with fixture data
|
||||
const createdDrawing = await createDrawing(request, {
|
||||
name: "E2E Test - Imported Image",
|
||||
files: fixtureData.files,
|
||||
});
|
||||
testDrawingIds.push(createdDrawing.id);
|
||||
|
||||
|
||||
// Create drawing via API with fixture data
|
||||
const createdDrawing = await createDrawing(request, {
|
||||
name: "E2E Test - Imported Image",
|
||||
files: fixtureData.files,
|
||||
});
|
||||
testDrawingIds.push(createdDrawing.id);
|
||||
|
||||
// Verify via API that image data was preserved
|
||||
const drawing = await getDrawing(request, createdDrawing.id);
|
||||
const drawing = await getDrawing(request, createdDrawing.id);
|
||||
const savedFiles = drawing.files || {}; // Already parsed by API
|
||||
|
||||
|
||||
expect(savedFiles["embedded-test-image"]).toBeDefined();
|
||||
expect(savedFiles["embedded-test-image"].dataURL).toBe(fixtureData.files["embedded-test-image"].dataURL);
|
||||
});
|
||||
@@ -161,23 +167,23 @@ test.describe("Image Persistence - Browser E2E Tests", () => {
|
||||
created: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const createdDrawing = await createDrawing(request, {
|
||||
name: "E2E Test - Multiple Images",
|
||||
files,
|
||||
});
|
||||
testDrawingIds.push(createdDrawing.id);
|
||||
|
||||
|
||||
const drawing = await getDrawing(request, createdDrawing.id);
|
||||
const savedFiles = drawing.files || {}; // Already parsed by API
|
||||
|
||||
|
||||
// Verify all images preserved correctly
|
||||
for (const [id, originalFile] of Object.entries(files)) {
|
||||
expect(savedFiles[id]).toBeDefined();
|
||||
expect(savedFiles[id].dataURL).toBe((originalFile as any).dataURL);
|
||||
expect(savedFiles[id].dataURL.length).toBe((originalFile as any).dataURL.length);
|
||||
}
|
||||
|
||||
|
||||
console.log("✓ Multiple images of varying sizes preserved correctly");
|
||||
});
|
||||
});
|
||||
@@ -192,10 +198,11 @@ test.describe("Security - Malicious Content Blocking", () => {
|
||||
created: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const response = await request.post(`${API_URL}/drawings`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(await getCsrfHeaders(request)),
|
||||
},
|
||||
data: {
|
||||
name: "Security Test - JS URL",
|
||||
@@ -205,7 +212,7 @@ test.describe("Security - Malicious Content Blocking", () => {
|
||||
preview: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok()) {
|
||||
const text = await response.text();
|
||||
console.error(`API Error: ${response.status()} - ${text}`);
|
||||
@@ -213,12 +220,14 @@ test.describe("Security - Malicious Content Blocking", () => {
|
||||
expect(response.ok()).toBe(true);
|
||||
const drawing = await response.json();
|
||||
const savedFiles = drawing.files; // Already parsed by API
|
||||
|
||||
|
||||
// The malicious URL should be blocked/cleared
|
||||
expect(savedFiles["malicious-image"].dataURL).not.toContain("javascript:");
|
||||
|
||||
|
||||
// Cleanup
|
||||
await request.delete(`${API_URL}/drawings/${drawing.id}`);
|
||||
await request.delete(`${API_URL}/drawings/${drawing.id}`, {
|
||||
headers: await getCsrfHeaders(request),
|
||||
});
|
||||
});
|
||||
|
||||
test("should block script tags in image data", async ({ request }) => {
|
||||
@@ -230,10 +239,11 @@ test.describe("Security - Malicious Content Blocking", () => {
|
||||
created: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const response = await request.post(`${API_URL}/drawings`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(await getCsrfHeaders(request)),
|
||||
},
|
||||
data: {
|
||||
name: "Security Test - Script Tag",
|
||||
@@ -243,7 +253,7 @@ test.describe("Security - Malicious Content Blocking", () => {
|
||||
preview: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok()) {
|
||||
const text = await response.text();
|
||||
console.error(`API Error: ${response.status()} - ${text}`);
|
||||
@@ -251,11 +261,13 @@ test.describe("Security - Malicious Content Blocking", () => {
|
||||
expect(response.ok()).toBe(true);
|
||||
const drawing = await response.json();
|
||||
const savedFiles = drawing.files; // Already parsed by API
|
||||
|
||||
|
||||
// The script tag should be blocked
|
||||
expect(savedFiles["malicious-image"].dataURL).not.toContain("<script>");
|
||||
|
||||
|
||||
// Cleanup
|
||||
await request.delete(`${API_URL}/drawings/${drawing.id}`);
|
||||
await request.delete(`${API_URL}/drawings/${drawing.id}`, {
|
||||
headers: await getCsrfHeaders(request),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user