concurrency
This commit is contained in:
@@ -3,6 +3,7 @@ import request from "supertest";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import JSZip from "jszip";
|
||||
import { getTestPrisma, setupTestDb, cleanupTestDb } from "./testUtils";
|
||||
|
||||
type LegacyDbOptions = {
|
||||
@@ -156,6 +157,111 @@ const createLegacySqliteDb = (opts: LegacyDbOptions): string => {
|
||||
return filePath;
|
||||
};
|
||||
|
||||
const createExcalidashArchiveWithDuplicateDrawingIds = async (): Promise<string> => {
|
||||
const dir = createTempDir();
|
||||
const filePath = path.join(dir, "duplicate-drawing-ids.excalidash");
|
||||
const zip = new JSZip();
|
||||
|
||||
const manifest = {
|
||||
format: "excalidash",
|
||||
formatVersion: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
unorganizedFolder: "Unorganized",
|
||||
collections: [] as any[],
|
||||
drawings: [
|
||||
{
|
||||
id: "duplicate-drawing-id",
|
||||
name: "Drawing One",
|
||||
filePath: "Unorganized/drawing-1.excalidraw",
|
||||
collectionId: null,
|
||||
},
|
||||
{
|
||||
id: "duplicate-drawing-id",
|
||||
name: "Drawing Two",
|
||||
filePath: "Unorganized/drawing-2.excalidraw",
|
||||
collectionId: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
zip.file("excalidash.manifest.json", JSON.stringify(manifest));
|
||||
zip.file(
|
||||
"Unorganized/drawing-1.excalidraw",
|
||||
JSON.stringify({ type: "excalidraw", version: 2, source: "test", elements: [], appState: {}, files: {} })
|
||||
);
|
||||
zip.file(
|
||||
"Unorganized/drawing-2.excalidraw",
|
||||
JSON.stringify({ type: "excalidraw", version: 2, source: "test", elements: [], appState: {}, files: {} })
|
||||
);
|
||||
|
||||
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
return filePath;
|
||||
};
|
||||
|
||||
const createLegacySqliteDbWithDuplicateDrawingIds = (): string => {
|
||||
const dir = createTempDir();
|
||||
const filePath = path.join(dir, "legacy-duplicate-ids.db");
|
||||
const db = openWritableDb(filePath);
|
||||
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE "Drawing" (
|
||||
id TEXT,
|
||||
name TEXT NOT NULL,
|
||||
elements TEXT NOT NULL,
|
||||
appState TEXT NOT NULL,
|
||||
files TEXT,
|
||||
preview TEXT,
|
||||
version INTEGER,
|
||||
collectionId TEXT,
|
||||
collectionName TEXT,
|
||||
createdAt TEXT,
|
||||
updatedAt TEXT
|
||||
);
|
||||
`);
|
||||
|
||||
const now = new Date("2024-01-03T00:00:00.000Z").toISOString();
|
||||
const insertDrawing = db.prepare(
|
||||
`INSERT INTO "Drawing"
|
||||
(id, name, elements, appState, files, preview, version, collectionId, collectionName, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
|
||||
insertDrawing.run(
|
||||
"legacy-duplicate-id",
|
||||
"Legacy Drawing A",
|
||||
JSON.stringify([]),
|
||||
JSON.stringify({}),
|
||||
JSON.stringify({}),
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
now,
|
||||
now,
|
||||
);
|
||||
|
||||
insertDrawing.run(
|
||||
"legacy-duplicate-id",
|
||||
"Legacy Drawing B",
|
||||
JSON.stringify([]),
|
||||
JSON.stringify({}),
|
||||
JSON.stringify({}),
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
now,
|
||||
now,
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
|
||||
return filePath;
|
||||
};
|
||||
|
||||
describe("Import compatibility (legacy exports)", () => {
|
||||
const uploadsDir = path.resolve(__dirname, "../../uploads");
|
||||
const userAgent = "vitest-import-compat";
|
||||
@@ -287,4 +393,52 @@ describe("Import compatibility (legacy exports)", () => {
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toBe("Invalid legacy DB");
|
||||
});
|
||||
|
||||
it("rejects .excalidash verify when manifest has duplicate drawing IDs", async () => {
|
||||
const archive = await createExcalidashArchiveWithDuplicateDrawingIds();
|
||||
const res = await request(app)
|
||||
.post("/import/excalidash/verify")
|
||||
.set("User-Agent", userAgent)
|
||||
.set(csrfHeaderName, csrfToken)
|
||||
.attach("archive", archive);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(String(res.body.message || "")).toContain("Duplicate drawing id");
|
||||
});
|
||||
|
||||
it("rejects .excalidash import when manifest has duplicate drawing IDs", async () => {
|
||||
const archive = await createExcalidashArchiveWithDuplicateDrawingIds();
|
||||
const res = await request(app)
|
||||
.post("/import/excalidash")
|
||||
.set("User-Agent", userAgent)
|
||||
.set(csrfHeaderName, csrfToken)
|
||||
.attach("archive", archive);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(String(res.body.message || "")).toContain("Duplicate drawing id");
|
||||
});
|
||||
|
||||
it("rejects legacy verify when DB has duplicate drawing IDs", async () => {
|
||||
const legacyDb = createLegacySqliteDbWithDuplicateDrawingIds();
|
||||
const res = await request(app)
|
||||
.post("/import/sqlite/legacy/verify")
|
||||
.set("User-Agent", userAgent)
|
||||
.set(csrfHeaderName, csrfToken)
|
||||
.attach("db", legacyDb);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(String(res.body.message || "")).toContain("Duplicate drawing id");
|
||||
});
|
||||
|
||||
it("rejects legacy import when DB has duplicate drawing IDs", async () => {
|
||||
const legacyDb = createLegacySqliteDbWithDuplicateDrawingIds();
|
||||
const res = await request(app)
|
||||
.post("/import/sqlite/legacy")
|
||||
.set("User-Agent", userAgent)
|
||||
.set(csrfHeaderName, csrfToken)
|
||||
.attach("db", legacyDb);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(String(res.body.message || "")).toContain("Duplicate drawing id");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -560,6 +560,7 @@ const drawingUpdateSchema = drawingBaseSchema
|
||||
elements: elementSchema.array().optional(),
|
||||
appState: appStateSchema.optional(),
|
||||
files: filesFieldSchema,
|
||||
version: z.number().int().positive().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
|
||||
@@ -310,7 +310,12 @@ export const registerDashboardRoutes = (
|
||||
appState?: Record<string, unknown>;
|
||||
preview?: string | null;
|
||||
files?: Record<string, unknown>;
|
||||
version?: number;
|
||||
};
|
||||
const isSceneUpdate =
|
||||
payload.elements !== undefined ||
|
||||
payload.appState !== undefined ||
|
||||
payload.files !== undefined;
|
||||
const data: Prisma.DrawingUpdateInput = { version: { increment: 1 } };
|
||||
|
||||
if (payload.name !== undefined) data.name = payload.name;
|
||||
@@ -334,11 +339,28 @@ export const registerDashboardRoutes = (
|
||||
}
|
||||
}
|
||||
|
||||
const updateWhere: Prisma.DrawingWhereInput = { id, userId: req.user.id };
|
||||
if (isSceneUpdate && payload.version !== undefined) {
|
||||
updateWhere.version = payload.version;
|
||||
}
|
||||
|
||||
const updateResult = await prisma.drawing.updateMany({
|
||||
where: { id, userId: req.user.id },
|
||||
where: updateWhere,
|
||||
data,
|
||||
});
|
||||
if (updateResult.count === 0) {
|
||||
if (isSceneUpdate && payload.version !== undefined) {
|
||||
const latestDrawing = await prisma.drawing.findFirst({
|
||||
where: { id, userId: req.user.id },
|
||||
select: { version: true },
|
||||
});
|
||||
return res.status(409).json({
|
||||
error: "Conflict",
|
||||
code: "VERSION_CONFLICT",
|
||||
message: "Drawing has changed since this editor state was loaded.",
|
||||
currentVersion: latestDrawing?.version ?? null,
|
||||
});
|
||||
}
|
||||
return res.status(404).json({ error: "Drawing not found" });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user