b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
1034 lines
25 KiB
TypeScript
1034 lines
25 KiB
TypeScript
import { Role } from "@emdash-cms/auth";
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
import { GET as schemaGET } from "../../../src/astro/routes/api/schema/index.js";
|
|
import { EmDashClient, EmDashApiError } from "../../../src/client/index.js";
|
|
import type { Interceptor } from "../../../src/client/transport.js";
|
|
import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../../utils/test-db.js";
|
|
|
|
// Regex patterns for route matching
|
|
const CONTENT_POSTS_ABC_REGEX = /\/content\/posts\/abc/;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mock backend
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface MockRoute {
|
|
method: string;
|
|
path: RegExp | string;
|
|
handler: (req: Request) => Response | Promise<Response>;
|
|
}
|
|
|
|
/**
|
|
* Creates a mock HTTP backend as an interceptor.
|
|
* Routes are matched in order. Unmatched requests return 404.
|
|
*/
|
|
function createMockBackend(routes: MockRoute[]): Interceptor {
|
|
return async (req) => {
|
|
const url = new URL(req.url);
|
|
const path = url.pathname + url.search;
|
|
|
|
for (const route of routes) {
|
|
if (req.method !== route.method) continue;
|
|
if (typeof route.path === "string") {
|
|
if (!path.includes(route.path)) continue;
|
|
} else {
|
|
if (!route.path.test(path)) continue;
|
|
}
|
|
return route.handler(req);
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({ error: { code: "NOT_FOUND", message: "No matching route" } }),
|
|
{ status: 404, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
};
|
|
}
|
|
|
|
/** Wraps body in `{ data: body }` to match the standard API response envelope. */
|
|
function jsonResponse(body: unknown, status: number = 200): Response {
|
|
// Error responses (4xx/5xx) are NOT wrapped in { data }
|
|
const payload = status >= 400 ? body : { data: body };
|
|
return new Response(JSON.stringify(payload), {
|
|
status,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
function mediaUsageRepairCollection(
|
|
collection: string,
|
|
status: "complete" | "partial" | "failed" | "stale",
|
|
) {
|
|
return {
|
|
collection,
|
|
status,
|
|
indexedSourceCount: status === "complete" ? 1 : 0,
|
|
failedSourceCount: status === "failed" ? 1 : 0,
|
|
skippedSourceCount: status === "partial" || status === "stale" ? 1 : 0,
|
|
deletedSourceCount: 0,
|
|
lastErrorCode: status === "complete" ? null : "CONTENT_USAGE_REPAIR_CONFLICT",
|
|
startedAt: "2026-07-07T00:00:00.000Z",
|
|
completedAt: status === "stale" ? null : "2026-07-07T00:00:01.000Z",
|
|
};
|
|
}
|
|
|
|
function mediaUsageRepairResponse(
|
|
collection: string,
|
|
status: "complete" | "partial" | "failed" | "stale",
|
|
) {
|
|
const summary = mediaUsageRepairCollection(collection, status);
|
|
return {
|
|
status,
|
|
indexedSourceCount: summary.indexedSourceCount,
|
|
failedSourceCount: summary.failedSourceCount,
|
|
skippedSourceCount: summary.skippedSourceCount,
|
|
deletedSourceCount: summary.deletedSourceCount,
|
|
collections: [summary],
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("EmDashClient", () => {
|
|
describe("_rev token flow", () => {
|
|
it("blind update (no _rev) succeeds", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/schema/collections/posts",
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: {
|
|
slug: "posts",
|
|
label: "Posts",
|
|
fields: [{ slug: "title", type: "string", label: "Title" }],
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
method: "PUT",
|
|
path: CONTENT_POSTS_ABC_REGEX,
|
|
handler: async (req) => {
|
|
const body = (await req.json()) as Record<string, unknown>;
|
|
// No _rev should be sent
|
|
expect(body._rev).toBeUndefined();
|
|
return jsonResponse({
|
|
item: { id: "abc", data: { title: "Blind" } },
|
|
_rev: "newrev",
|
|
});
|
|
},
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const updated = await client.update("posts", "abc", {
|
|
data: { title: "Blind" },
|
|
});
|
|
expect(updated.data.title).toBe("Blind");
|
|
});
|
|
|
|
it("get() returns _rev on the item", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/schema/collections/posts",
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: {
|
|
slug: "posts",
|
|
label: "Posts",
|
|
fields: [{ slug: "title", type: "string", label: "Title" }],
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
method: "GET",
|
|
path: CONTENT_POSTS_ABC_REGEX,
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: {
|
|
id: "abc",
|
|
type: "posts",
|
|
slug: "hello",
|
|
status: "draft",
|
|
data: { title: "Hello" },
|
|
authorId: null,
|
|
createdAt: "2026-01-01",
|
|
updatedAt: "2026-01-01",
|
|
publishedAt: null,
|
|
scheduledAt: null,
|
|
liveRevisionId: null,
|
|
draftRevisionId: null,
|
|
},
|
|
_rev: "dGVzdHJldg",
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const post = await client.get("posts", "abc");
|
|
expect(post.id).toBe("abc");
|
|
expect(post._rev).toBe("dGVzdHJldg");
|
|
});
|
|
|
|
it("update() sends _rev when provided", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/schema/collections/posts",
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: {
|
|
slug: "posts",
|
|
label: "Posts",
|
|
fields: [{ slug: "title", type: "string", label: "Title" }],
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
method: "PUT",
|
|
path: CONTENT_POSTS_ABC_REGEX,
|
|
handler: async (req) => {
|
|
const body = await req.json();
|
|
expect((body as Record<string, unknown>)._rev).toBe("dGVzdHJldg");
|
|
return jsonResponse({
|
|
item: { id: "abc", data: { title: "Updated" } },
|
|
_rev: "bmV3cmV2",
|
|
});
|
|
},
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const updated = await client.update("posts", "abc", {
|
|
data: { title: "Updated" },
|
|
_rev: "dGVzdHJldg",
|
|
});
|
|
expect(updated.data.title).toBe("Updated");
|
|
expect(updated._rev).toBe("bmV3cmV2");
|
|
});
|
|
|
|
it("update() appends locale for slug resolution", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/schema/collections/posts",
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: {
|
|
slug: "posts",
|
|
label: "Posts",
|
|
fields: [{ slug: "title", type: "string", label: "Title" }],
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
method: "PUT",
|
|
path: "/content/posts/shared?locale=fr",
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: { id: "fr-id", locale: "fr", data: { title: "French Updated" } },
|
|
_rev: "bmV3cmV2",
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const updated = await client.update("posts", "shared", {
|
|
data: { title: "French Updated" },
|
|
locale: "fr",
|
|
});
|
|
|
|
expect(updated.id).toBe("fr-id");
|
|
expect(updated.locale).toBe("fr");
|
|
});
|
|
});
|
|
|
|
describe("create()", () => {
|
|
it("does not require a prior get()", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/schema/collections/posts",
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: {
|
|
slug: "posts",
|
|
label: "Posts",
|
|
fields: [{ slug: "title", type: "string", label: "Title" }],
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
method: "POST",
|
|
path: "/content/posts",
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: {
|
|
id: "new1",
|
|
type: "posts",
|
|
slug: "hello",
|
|
status: "draft",
|
|
data: { title: "Hello" },
|
|
authorId: null,
|
|
createdAt: "2026-01-01",
|
|
updatedAt: "2026-01-01",
|
|
publishedAt: null,
|
|
scheduledAt: null,
|
|
liveRevisionId: null,
|
|
draftRevisionId: null,
|
|
},
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const item = await client.create("posts", {
|
|
data: { title: "Hello" },
|
|
slug: "hello",
|
|
});
|
|
expect(item.id).toBe("new1");
|
|
});
|
|
});
|
|
|
|
describe("API error handling", () => {
|
|
it("throws EmDashApiError on 4xx responses", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/schema/collections",
|
|
handler: () => jsonResponse({ error: { code: "FORBIDDEN", message: "No access" } }, 403),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
try {
|
|
await client.collections();
|
|
expect.fail("Should have thrown");
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(EmDashApiError);
|
|
const apiErr = error as EmDashApiError;
|
|
expect(apiErr.status).toBe(403);
|
|
expect(apiErr.code).toBe("FORBIDDEN");
|
|
expect(apiErr.message).toBe("No access");
|
|
}
|
|
});
|
|
|
|
it("throws EmDashApiError on 500 responses", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/manifest",
|
|
handler: () =>
|
|
jsonResponse(
|
|
{
|
|
error: {
|
|
code: "INTERNAL_ERROR",
|
|
message: "Something broke",
|
|
},
|
|
},
|
|
500,
|
|
),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
try {
|
|
await client.manifest();
|
|
expect.fail("Should have thrown");
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(EmDashApiError);
|
|
expect((error as EmDashApiError).status).toBe(500);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("list()", () => {
|
|
it("returns items and nextCursor", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/content/posts",
|
|
handler: () =>
|
|
jsonResponse({
|
|
items: [
|
|
{
|
|
id: "1",
|
|
type: "posts",
|
|
slug: "a",
|
|
status: "published",
|
|
data: {},
|
|
},
|
|
{
|
|
id: "2",
|
|
type: "posts",
|
|
slug: "b",
|
|
status: "published",
|
|
data: {},
|
|
},
|
|
],
|
|
nextCursor: "cursor123",
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const result = await client.list("posts", { status: "published" });
|
|
expect(result.items).toHaveLength(2);
|
|
expect(result.nextCursor).toBe("cursor123");
|
|
});
|
|
});
|
|
|
|
describe("listAll()", () => {
|
|
it("follows cursors until exhaustion", async () => {
|
|
let page = 0;
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/content/posts",
|
|
handler: () => {
|
|
page++;
|
|
if (page === 1) {
|
|
return jsonResponse({
|
|
items: [{ id: "1", data: {} }],
|
|
nextCursor: "page2",
|
|
});
|
|
}
|
|
return jsonResponse({
|
|
items: [{ id: "2", data: {} }],
|
|
});
|
|
},
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const all = [];
|
|
for await (const item of client.listAll("posts")) {
|
|
all.push(item);
|
|
}
|
|
expect(all).toHaveLength(2);
|
|
expect(all[0]?.id).toBe("1");
|
|
expect(all[1]?.id).toBe("2");
|
|
});
|
|
});
|
|
|
|
describe("delete/publish/unpublish/schedule/restore", () => {
|
|
it("calls the correct endpoints", async () => {
|
|
const calledPaths: string[] = [];
|
|
|
|
const backend: Interceptor = async (req) => {
|
|
calledPaths.push(`${req.method} ${new URL(req.url).pathname}`);
|
|
return jsonResponse({});
|
|
};
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
await client.delete("posts", "abc");
|
|
await client.publish("posts", "abc");
|
|
await client.unpublish("posts", "abc");
|
|
await client.schedule("posts", "abc", { at: "2026-03-01T00:00:00Z" });
|
|
await client.restore("posts", "abc");
|
|
|
|
expect(calledPaths).toEqual([
|
|
"DELETE /_emdash/api/content/posts/abc",
|
|
"POST /_emdash/api/content/posts/abc/publish",
|
|
"POST /_emdash/api/content/posts/abc/unpublish",
|
|
"POST /_emdash/api/content/posts/abc/schedule",
|
|
"POST /_emdash/api/content/posts/abc/restore",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("schema methods", () => {
|
|
it("schema route wraps JSON exports in the API data envelope", async () => {
|
|
const db = await setupTestDatabaseWithCollections();
|
|
|
|
try {
|
|
const response = await schemaGET({
|
|
request: new Request("http://localhost:4321/_emdash/api/schema"),
|
|
locals: {
|
|
emdash: { db },
|
|
user: { id: "user_1", role: Role.EDITOR },
|
|
},
|
|
} as never);
|
|
|
|
expect(response.status).toBe(200);
|
|
const version = response.headers.get("X-Schema-Version");
|
|
expect(version).toBeTruthy();
|
|
|
|
const body = await response.json();
|
|
expect(body).toEqual({
|
|
data: expect.objectContaining({
|
|
collections: expect.arrayContaining([
|
|
expect.objectContaining({ slug: "page" }),
|
|
expect.objectContaining({ slug: "post" }),
|
|
]),
|
|
version,
|
|
}),
|
|
});
|
|
} finally {
|
|
await teardownTestDatabase(db);
|
|
}
|
|
});
|
|
|
|
it("collections() returns list", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/schema/collections",
|
|
handler: () =>
|
|
jsonResponse({
|
|
items: [
|
|
{ slug: "posts", label: "Posts", supports: [] },
|
|
{ slug: "pages", label: "Pages", supports: [] },
|
|
],
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const cols = await client.collections();
|
|
expect(cols).toHaveLength(2);
|
|
expect(cols[0]?.slug).toBe("posts");
|
|
});
|
|
|
|
it("schemaExport() returns collections and version", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/schema",
|
|
handler: () =>
|
|
jsonResponse({
|
|
collections: [
|
|
{
|
|
slug: "posts",
|
|
label: "Posts",
|
|
labelSingular: "Post",
|
|
supports: ["drafts"],
|
|
fields: [
|
|
{
|
|
slug: "title",
|
|
label: "Title",
|
|
type: "string",
|
|
required: true,
|
|
unique: false,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
version: "schema-v1",
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const schema = await client.schemaExport();
|
|
expect(schema.collections).toHaveLength(1);
|
|
expect(schema.collections[0]?.slug).toBe("posts");
|
|
expect(schema.version).toBe("schema-v1");
|
|
});
|
|
|
|
it("schemaTypes() returns raw TypeScript", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: /\/schema\?format=typescript$/,
|
|
handler: () =>
|
|
new Response("export interface Post { title: string }\n", {
|
|
status: 200,
|
|
headers: { "Content-Type": "text/typescript" },
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
await expect(client.schemaTypes()).resolves.toBe("export interface Post { title: string }\n");
|
|
});
|
|
|
|
it("schemaExport() throws EmDashApiError on non-2xx responses", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/schema",
|
|
handler: () =>
|
|
jsonResponse(
|
|
{
|
|
error: {
|
|
code: "SCHEMA_EXPORT_ERROR",
|
|
message: "Schema export failed",
|
|
},
|
|
},
|
|
500,
|
|
),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
await expect(client.schemaExport()).rejects.toMatchObject({
|
|
status: 500,
|
|
code: "SCHEMA_EXPORT_ERROR",
|
|
message: "Schema export failed",
|
|
});
|
|
});
|
|
|
|
it("createCollection() sends correct payload", async () => {
|
|
let capturedBody: unknown;
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "POST",
|
|
path: "/schema/collections",
|
|
handler: async (req) => {
|
|
capturedBody = await req.json();
|
|
return jsonResponse({
|
|
item: {
|
|
slug: "events",
|
|
label: "Events",
|
|
labelSingular: "Event",
|
|
},
|
|
});
|
|
},
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
await client.createCollection({
|
|
slug: "events",
|
|
label: "Events",
|
|
labelSingular: "Event",
|
|
});
|
|
|
|
expect(capturedBody).toEqual({
|
|
slug: "events",
|
|
label: "Events",
|
|
labelSingular: "Event",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("mediaRepairUsage()", () => {
|
|
it("sends collection repair requests with the caller-provided body", async () => {
|
|
let capturedPath = "";
|
|
let capturedBody: unknown;
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "POST",
|
|
path: "/admin/media-usage/repair",
|
|
handler: async (req) => {
|
|
capturedPath = new URL(req.url).pathname;
|
|
capturedBody = await req.json();
|
|
return jsonResponse(mediaUsageRepairResponse("posts", "complete"));
|
|
},
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const result = await client.mediaRepairUsage({ scope: "collection", collection: "posts" });
|
|
|
|
expect(capturedPath).toBe("/_emdash/api/admin/media-usage/repair");
|
|
expect(capturedBody).toEqual({ scope: "collection", collection: "posts" });
|
|
expect(result).toMatchObject({
|
|
status: "complete",
|
|
collections: [expect.objectContaining({ collection: "posts", status: "complete" })],
|
|
});
|
|
});
|
|
|
|
it("sends all-content repair requests with the explicit all scope", async () => {
|
|
let capturedBody: unknown;
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "POST",
|
|
path: "/admin/media-usage/repair",
|
|
handler: async (req) => {
|
|
capturedBody = await req.json();
|
|
return jsonResponse({
|
|
...mediaUsageRepairResponse("posts", "complete"),
|
|
collections: [
|
|
mediaUsageRepairCollection("pages", "complete"),
|
|
mediaUsageRepairCollection("posts", "complete"),
|
|
],
|
|
});
|
|
},
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const result = await client.mediaRepairUsage({ scope: "all" });
|
|
|
|
expect(capturedBody).toEqual({ scope: "all" });
|
|
expect(result.collections.map((collection) => collection.collection)).toEqual([
|
|
"pages",
|
|
"posts",
|
|
]);
|
|
});
|
|
|
|
it("returns structured failed and stale 200 responses without throwing", async () => {
|
|
const statuses = ["failed", "stale"] as const;
|
|
let call = 0;
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "POST",
|
|
path: "/admin/media-usage/repair",
|
|
handler: () => jsonResponse(mediaUsageRepairResponse("posts", statuses[call++]!)),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
await expect(
|
|
client.mediaRepairUsage({ scope: "collection", collection: "posts" }),
|
|
).resolves.toMatchObject({ status: "failed" });
|
|
await expect(client.mediaRepairUsage({ scope: "all" })).resolves.toMatchObject({
|
|
status: "stale",
|
|
collections: [expect.objectContaining({ completedAt: null })],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("PT <-> Markdown auto-conversion", () => {
|
|
it("converts PT fields to markdown on get()", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/schema/collections/posts",
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: {
|
|
slug: "posts",
|
|
label: "Posts",
|
|
fields: [
|
|
{ slug: "title", type: "string", label: "Title" },
|
|
{ slug: "body", type: "portableText", label: "Body" },
|
|
],
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
method: "GET",
|
|
path: CONTENT_POSTS_ABC_REGEX,
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: {
|
|
id: "abc",
|
|
type: "posts",
|
|
data: {
|
|
title: "Hello",
|
|
body: [
|
|
{
|
|
_type: "block",
|
|
style: "normal",
|
|
markDefs: [],
|
|
children: [
|
|
{
|
|
_type: "span",
|
|
text: "World",
|
|
marks: [],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
_rev: "rev1",
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const item = await client.get("posts", "abc");
|
|
expect(item.data.title).toBe("Hello");
|
|
expect(typeof item.data.body).toBe("string");
|
|
expect(item.data.body).toContain("World");
|
|
});
|
|
|
|
it("returns raw PT when raw: true", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/schema/collections/posts",
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: {
|
|
slug: "posts",
|
|
fields: [{ slug: "body", type: "portableText", label: "Body" }],
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
method: "GET",
|
|
path: CONTENT_POSTS_ABC_REGEX,
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: {
|
|
id: "abc",
|
|
data: {
|
|
body: [
|
|
{
|
|
_type: "block",
|
|
children: [{ _type: "span", text: "Raw" }],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
_rev: "rev1",
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const item = await client.get("posts", "abc", { raw: true });
|
|
expect(Array.isArray(item.data.body)).toBe(true);
|
|
});
|
|
|
|
it("converts markdown to PT on create()", async () => {
|
|
let capturedData: Record<string, unknown> | undefined;
|
|
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/schema/collections/posts",
|
|
handler: () =>
|
|
jsonResponse({
|
|
item: {
|
|
slug: "posts",
|
|
fields: [
|
|
{ slug: "title", type: "string", label: "Title" },
|
|
{ slug: "body", type: "portableText", label: "Body" },
|
|
],
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
method: "POST",
|
|
path: "/content/posts",
|
|
handler: async (req) => {
|
|
const body = (await req.json()) as Record<string, unknown>;
|
|
capturedData = body.data as Record<string, unknown>;
|
|
return jsonResponse({
|
|
item: {
|
|
id: "new1",
|
|
data: capturedData,
|
|
},
|
|
});
|
|
},
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
await client.create("posts", {
|
|
data: {
|
|
title: "Hello",
|
|
body: "Some **bold** text",
|
|
},
|
|
});
|
|
|
|
expect(capturedData).toBeDefined();
|
|
expect(capturedData!.title).toBe("Hello");
|
|
expect(Array.isArray(capturedData!.body)).toBe(true);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Taxonomy & menu envelope bugs
|
|
// -----------------------------------------------------------------------
|
|
|
|
describe("taxonomies()", () => {
|
|
it("returns taxonomy array from { taxonomies } envelope", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/taxonomies",
|
|
handler: () =>
|
|
jsonResponse({
|
|
taxonomies: [
|
|
{
|
|
id: "t1",
|
|
name: "categories",
|
|
label: "Categories",
|
|
hierarchical: true,
|
|
collections: ["posts"],
|
|
},
|
|
],
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const result = await client.taxonomies();
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result.length).toBe(1);
|
|
expect(result[0]!.name).toBe("categories");
|
|
});
|
|
});
|
|
|
|
describe("terms()", () => {
|
|
it("returns ListResult with items mapped from { terms } envelope", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/taxonomies/categories/terms",
|
|
handler: () =>
|
|
jsonResponse({
|
|
terms: [
|
|
{ id: "t1", slug: "uncategorized", label: "Uncategorized", count: 3 },
|
|
{ id: "t2", slug: "news", label: "News", count: 1 },
|
|
],
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const result = await client.terms("categories");
|
|
expect(result.items).toHaveLength(2);
|
|
expect(result.items[0]!.slug).toBe("uncategorized");
|
|
expect(result.items[1]!.slug).toBe("news");
|
|
expect(result.nextCursor).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("menus()", () => {
|
|
it("returns menu array from bare-array envelope", async () => {
|
|
const backend = createMockBackend([
|
|
{
|
|
method: "GET",
|
|
path: "/menus",
|
|
handler: () =>
|
|
jsonResponse([
|
|
{
|
|
id: "m1",
|
|
name: "primary",
|
|
label: "Primary",
|
|
itemCount: 3,
|
|
},
|
|
]),
|
|
},
|
|
]);
|
|
|
|
const client = new EmDashClient({
|
|
baseUrl: "http://localhost:4321",
|
|
token: "test",
|
|
interceptors: [backend],
|
|
});
|
|
|
|
const result = await client.menus();
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result.length).toBe(1);
|
|
expect(result[0]!.name).toBe("primary");
|
|
});
|
|
});
|
|
});
|