432 lines
12 KiB
TypeScript
432 lines
12 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createApi } from "./api";
|
|
import { createDrawingStore } from "./db";
|
|
|
|
function withApi() {
|
|
const dir = mkdtempSync(join(tmpdir(), "excali-api-"));
|
|
const store = createDrawingStore(join(dir, "test.sqlite"));
|
|
const api = createApi(store);
|
|
|
|
return {
|
|
api,
|
|
cleanup() {
|
|
store.close();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("api", () => {
|
|
test("creates a drawing with dark theme by default", async () => {
|
|
const { api, cleanup } = withApi();
|
|
|
|
const created = await api.createDrawing().json();
|
|
|
|
expect(created.appState.theme).toBe("dark");
|
|
expect(created.revision).toBe(0);
|
|
cleanup();
|
|
});
|
|
|
|
test("returns 400 for invalid JSON", async () => {
|
|
const { api, cleanup } = withApi();
|
|
const drawing = await api.createDrawing().json();
|
|
|
|
const response = await api.updateDrawing(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${drawing.id}`, {
|
|
method: "PUT",
|
|
body: "{",
|
|
}),
|
|
{ params: { id: drawing.id } },
|
|
),
|
|
);
|
|
|
|
expect(response.status).toBe(400);
|
|
cleanup();
|
|
});
|
|
|
|
test("returns 404 for missing drawing", () => {
|
|
const { api, cleanup } = withApi();
|
|
|
|
const response = api.getDrawing(
|
|
Object.assign(new Request("http://local/api/drawings/missing"), {
|
|
params: { id: "missing" },
|
|
}),
|
|
);
|
|
|
|
expect(response.status).toBe(404);
|
|
cleanup();
|
|
});
|
|
|
|
test("updates a drawing scene", async () => {
|
|
const { api, cleanup } = withApi();
|
|
const created = await api.createDrawing().json();
|
|
|
|
const response = await api.updateDrawing(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
elements: [{ id: "shape" }],
|
|
appState: {},
|
|
files: {},
|
|
expectedRevision: created.revision,
|
|
}),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
expect(response.status).toBe(200);
|
|
const body = await response.json();
|
|
expect(body.drawing.revision).toBe(1);
|
|
cleanup();
|
|
});
|
|
|
|
test("does not increment revisions for equivalent normalized scenes", async () => {
|
|
const { api, cleanup } = withApi();
|
|
const created = await api.createDrawing().json();
|
|
|
|
await api.updateDrawing(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
elements: [{ id: "shape" }],
|
|
appState: { gridSize: 20 },
|
|
files: {},
|
|
expectedRevision: 0,
|
|
}),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
const response = await api.updateDrawing(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
elements: [{ id: "shape" }],
|
|
appState: { gridSize: 20, selectedElementIds: { shape: true } },
|
|
files: {},
|
|
expectedRevision: 0,
|
|
}),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
expect(response.status).toBe(200);
|
|
const body = await response.json();
|
|
expect(body.drawing.revision).toBe(1);
|
|
cleanup();
|
|
});
|
|
|
|
test("updates scene and title with a matching expected revision", async () => {
|
|
const { api, cleanup } = withApi();
|
|
const created = await api.createDrawing().json();
|
|
|
|
const response = await api.updateDrawing(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
title: "Synced",
|
|
elements: [{ id: "shape" }],
|
|
appState: {},
|
|
files: {},
|
|
expectedRevision: 0,
|
|
}),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
expect(response.status).toBe(200);
|
|
const body = await response.json();
|
|
expect(body.drawing.title).toBe("Synced");
|
|
expect(body.drawing.revision).toBe(1);
|
|
cleanup();
|
|
});
|
|
|
|
test("returns 409 for stale expected revisions without overwriting", async () => {
|
|
const { api, cleanup } = withApi();
|
|
const created = await api.createDrawing().json();
|
|
|
|
await api.updateDrawing(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
elements: [{ id: "server" }],
|
|
appState: {},
|
|
files: {},
|
|
expectedRevision: 0,
|
|
}),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
const conflict = await api.updateDrawing(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
elements: [{ id: "stale" }],
|
|
appState: {},
|
|
files: {},
|
|
expectedRevision: 0,
|
|
}),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
expect(conflict.status).toBe(409);
|
|
const body = await conflict.json();
|
|
expect(body).toEqual({
|
|
ok: false,
|
|
error: "Drawing changed externally",
|
|
drawing: {
|
|
id: created.id,
|
|
title: created.title,
|
|
createdAt: body.drawing.createdAt,
|
|
updatedAt: body.drawing.updatedAt,
|
|
revision: 1,
|
|
},
|
|
});
|
|
|
|
const stored = await api
|
|
.getDrawing(
|
|
Object.assign(new Request(`http://local/api/drawings/${created.id}`), {
|
|
params: { id: created.id },
|
|
}),
|
|
)
|
|
.json();
|
|
expect(stored.elements).toEqual([{ id: "server" }]);
|
|
cleanup();
|
|
});
|
|
|
|
test("returns disabled and enabled publication shapes", async () => {
|
|
const { api, cleanup } = withApi();
|
|
const created = await api.createDrawing().json();
|
|
|
|
const disabled = await api.getDrawingPublication(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}/publication`),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
expect(disabled.status).toBe(200);
|
|
expect(await disabled.json()).toEqual({ enabled: false });
|
|
|
|
const publish = await api.updateDrawingPublication(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}/publication`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
enabled: true,
|
|
slug: "Flow Chart",
|
|
}),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
expect(publish.status).toBe(200);
|
|
expect(await publish.json()).toEqual({
|
|
enabled: true,
|
|
slug: "flow-chart",
|
|
});
|
|
|
|
const enabled = await api.getDrawingPublication(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}/publication`),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
expect(await enabled.json()).toEqual({
|
|
enabled: true,
|
|
slug: "flow-chart",
|
|
});
|
|
cleanup();
|
|
});
|
|
|
|
test("publishes, renames, and disables publications", async () => {
|
|
const { api, cleanup } = withApi();
|
|
const created = await api.createDrawing().json();
|
|
|
|
await api.updateDrawingPublication(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}/publication`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
enabled: true,
|
|
slug: "first",
|
|
}),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
const renamed = await api.updateDrawingPublication(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}/publication`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
enabled: true,
|
|
slug: "second",
|
|
}),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
expect(renamed.status).toBe(200);
|
|
expect(await renamed.json()).toEqual({
|
|
enabled: true,
|
|
slug: "second",
|
|
});
|
|
|
|
const disable = await api.updateDrawingPublication(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}/publication`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ enabled: false }),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
expect(disable.status).toBe(200);
|
|
expect(await disable.json()).toEqual({ enabled: false });
|
|
cleanup();
|
|
});
|
|
|
|
test("returns 400 for invalid publication slugs", async () => {
|
|
const { api, cleanup } = withApi();
|
|
const created = await api.createDrawing().json();
|
|
|
|
const response = await api.updateDrawingPublication(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}/publication`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
enabled: true,
|
|
slug: "API",
|
|
}),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(await response.json()).toEqual({
|
|
ok: false,
|
|
error: "slug is reserved",
|
|
});
|
|
cleanup();
|
|
});
|
|
|
|
test("returns 409 for publication slug collisions", async () => {
|
|
const { api, cleanup } = withApi();
|
|
const first = await api.createDrawing().json();
|
|
const second = await api.createDrawing().json();
|
|
|
|
await api.updateDrawingPublication(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${first.id}/publication`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
enabled: true,
|
|
slug: "shared",
|
|
}),
|
|
}),
|
|
{ params: { id: first.id } },
|
|
),
|
|
);
|
|
|
|
const response = await api.updateDrawingPublication(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${second.id}/publication`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
enabled: true,
|
|
slug: "shared",
|
|
}),
|
|
}),
|
|
{ params: { id: second.id } },
|
|
),
|
|
);
|
|
|
|
expect(response.status).toBe(409);
|
|
expect(await response.json()).toEqual({
|
|
ok: false,
|
|
error: "Published slug already exists",
|
|
});
|
|
cleanup();
|
|
});
|
|
|
|
test("returns public scene data for enabled slugs and 404 otherwise", async () => {
|
|
const { api, cleanup } = withApi();
|
|
const created = await api.createDrawing().json();
|
|
|
|
await api.updateDrawing(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
title: "Public Flow",
|
|
elements: [{ id: "shape" }],
|
|
appState: { theme: "dark" },
|
|
files: {},
|
|
expectedRevision: 0,
|
|
}),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
await api.updateDrawingPublication(
|
|
Object.assign(
|
|
new Request(`http://local/api/drawings/${created.id}/publication`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
enabled: true,
|
|
slug: "public-flow",
|
|
}),
|
|
}),
|
|
{ params: { id: created.id } },
|
|
),
|
|
);
|
|
|
|
const enabled = await api.getPublicDrawing(
|
|
Object.assign(
|
|
new Request("http://local/api/public/drawings/public-flow"),
|
|
{ params: { slug: "public-flow" } },
|
|
),
|
|
);
|
|
|
|
expect(enabled.status).toBe(200);
|
|
expect(await enabled.json()).toEqual({
|
|
title: "Public Flow",
|
|
elements: [{ id: "shape" }],
|
|
appState: { theme: "dark" },
|
|
files: {},
|
|
});
|
|
|
|
const missing = await api.getPublicDrawing(
|
|
Object.assign(new Request("http://local/api/public/drawings/missing"), {
|
|
params: { slug: "missing" },
|
|
}),
|
|
);
|
|
expect(missing.status).toBe(404);
|
|
cleanup();
|
|
});
|
|
});
|