import { describe, expect, it } from "vitest"; import { generateOpenApiDocument } from "../../../src/api/openapi/document.js"; describe("OpenAPI document generation", () => { it("generates a valid OpenAPI 3.1 document", () => { const doc = generateOpenApiDocument(); expect(doc.openapi).toBe("3.1.0"); expect(doc.info.title).toBe("EmDash CMS API"); expect(doc.info.version).toBe("0.1.0"); }); it("includes content paths", () => { const doc = generateOpenApiDocument(); const paths = Object.keys(doc.paths ?? {}); expect(paths).toContain("/_emdash/api/content/{collection}"); expect(paths).toContain("/_emdash/api/content/{collection}/{id}"); expect(paths).toContain("/_emdash/api/content/{collection}/{id}/publish"); expect(paths).toContain("/_emdash/api/content/{collection}/{id}/schedule"); expect(paths).toContain("/_emdash/api/content/{collection}/{id}/duplicate"); expect(paths).toContain("/_emdash/api/content/{collection}/{id}/compare"); expect(paths).toContain("/_emdash/api/content/{collection}/{id}/translations"); expect(paths).toContain("/_emdash/api/content/{collection}/trash"); }); it("includes media paths", () => { const doc = generateOpenApiDocument(); const paths = Object.keys(doc.paths ?? {}); expect(paths).toContain("/_emdash/api/media"); expect(paths).toContain("/_emdash/api/media/{id}"); expect(paths).toContain("/_emdash/api/media/upload-url"); expect(paths).toContain("/_emdash/api/media/{id}/confirm"); }); it("includes schema paths", () => { const doc = generateOpenApiDocument(); const paths = Object.keys(doc.paths ?? {}); expect(paths).toContain("/_emdash/api/schema/collections"); expect(paths).toContain("/_emdash/api/schema/collections/{slug}"); expect(paths).toContain("/_emdash/api/schema/collections/{slug}/fields"); expect(paths).toContain("/_emdash/api/schema/collections/{slug}/fields/{fieldSlug}"); expect(paths).toContain("/_emdash/api/schema/orphans"); }); it("includes comments paths", () => { const doc = generateOpenApiDocument(); const paths = Object.keys(doc.paths ?? {}); expect(paths).toContain("/_emdash/api/comments/{collection}/{contentId}"); expect(paths).toContain("/_emdash/api/admin/comments"); expect(paths).toContain("/_emdash/api/admin/comments/counts"); expect(paths).toContain("/_emdash/api/admin/comments/bulk"); expect(paths).toContain("/_emdash/api/admin/comments/{id}"); }); it("includes taxonomy paths", () => { const doc = generateOpenApiDocument(); const paths = Object.keys(doc.paths ?? {}); expect(paths).toContain("/_emdash/api/taxonomies"); expect(paths).toContain("/_emdash/api/taxonomies/{name}/terms"); expect(paths).toContain("/_emdash/api/taxonomies/{name}/terms/{slug}"); }); it("includes menu paths", () => { const doc = generateOpenApiDocument(); const paths = Object.keys(doc.paths ?? {}); expect(paths).toContain("/_emdash/api/menus"); expect(paths).toContain("/_emdash/api/menus/{name}"); expect(paths).toContain("/_emdash/api/menus/{name}/items"); expect(paths).toContain("/_emdash/api/menus/{name}/reorder"); }); it("includes section paths", () => { const doc = generateOpenApiDocument(); const paths = Object.keys(doc.paths ?? {}); expect(paths).toContain("/_emdash/api/sections"); expect(paths).toContain("/_emdash/api/sections/{slug}"); }); it("includes widget paths", () => { const doc = generateOpenApiDocument(); const paths = Object.keys(doc.paths ?? {}); expect(paths).toContain("/_emdash/api/widget-areas"); expect(paths).toContain("/_emdash/api/widget-areas/{name}"); expect(paths).toContain("/_emdash/api/widget-areas/{name}/widgets"); expect(paths).toContain("/_emdash/api/widget-areas/{name}/widgets/{id}"); expect(paths).toContain("/_emdash/api/widget-areas/{name}/reorder"); }); it("includes settings paths", () => { const doc = generateOpenApiDocument(); const paths = Object.keys(doc.paths ?? {}); expect(paths).toContain("/_emdash/api/settings"); }); it("includes search paths", () => { const doc = generateOpenApiDocument(); const paths = Object.keys(doc.paths ?? {}); expect(paths).toContain("/_emdash/api/search"); expect(paths).toContain("/_emdash/api/search/suggest"); expect(paths).toContain("/_emdash/api/search/rebuild"); expect(paths).toContain("/_emdash/api/search/enable"); expect(paths).toContain("/_emdash/api/search/stats"); }); it("includes redirect paths", () => { const doc = generateOpenApiDocument(); const paths = Object.keys(doc.paths ?? {}); expect(paths).toContain("/_emdash/api/redirects"); expect(paths).toContain("/_emdash/api/redirects/{id}"); expect(paths).toContain("/_emdash/api/redirects/404s"); expect(paths).toContain("/_emdash/api/redirects/404s/summary"); }); it("includes user paths", () => { const doc = generateOpenApiDocument(); const paths = Object.keys(doc.paths ?? {}); expect(paths).toContain("/_emdash/api/admin/users"); expect(paths).toContain("/_emdash/api/admin/users/{id}"); expect(paths).toContain("/_emdash/api/admin/users/{id}/disable"); expect(paths).toContain("/_emdash/api/admin/users/{id}/enable"); expect(paths).toContain("/_emdash/api/admin/allowed-domains"); expect(paths).toContain("/_emdash/api/admin/allowed-domains/{domain}"); }); it("has correct HTTP methods on content collection endpoint", () => { const doc = generateOpenApiDocument(); const collectionPath = doc.paths?.["/_emdash/api/content/{collection}"]; expect(collectionPath).toBeDefined(); expect(collectionPath).toHaveProperty("get"); expect(collectionPath).toHaveProperty("post"); }); it("has correct HTTP methods on content item endpoint", () => { const doc = generateOpenApiDocument(); const itemPath = doc.paths?.["/_emdash/api/content/{collection}/{id}"]; expect(itemPath).toBeDefined(); expect(itemPath).toHaveProperty("get"); expect(itemPath).toHaveProperty("put"); expect(itemPath).toHaveProperty("delete"); }); it("generates unique operation IDs for all operations", () => { const doc = generateOpenApiDocument(); const operationIds: string[] = []; for (const pathItem of Object.values(doc.paths ?? {})) { for (const method of ["get", "post", "put", "delete", "patch"] as const) { const op = (pathItem as Record)?.[method] as | { operationId?: string } | undefined; if (op?.operationId) { operationIds.push(op.operationId); } } } // Content operations expect(operationIds).toContain("listContent"); expect(operationIds).toContain("createContent"); expect(operationIds).toContain("getContent"); expect(operationIds).toContain("updateContent"); expect(operationIds).toContain("deleteContent"); expect(operationIds).toContain("publishContent"); expect(operationIds).toContain("duplicateContent"); // Media operations expect(operationIds).toContain("listMedia"); expect(operationIds).toContain("getMedia"); expect(operationIds).toContain("deleteMedia"); expect(operationIds).toContain("getMediaUploadUrl"); // Schema operations expect(operationIds).toContain("listCollections"); expect(operationIds).toContain("createCollection"); expect(operationIds).toContain("listFields"); expect(operationIds).toContain("createField"); // Comments operations expect(operationIds).toContain("listPublicComments"); expect(operationIds).toContain("createComment"); expect(operationIds).toContain("listAdminComments"); expect(operationIds).toContain("bulkCommentAction"); // Taxonomy operations expect(operationIds).toContain("listTaxonomies"); expect(operationIds).toContain("listTerms"); expect(operationIds).toContain("createTerm"); // Menu operations expect(operationIds).toContain("listMenus"); expect(operationIds).toContain("createMenu"); expect(operationIds).toContain("createMenuItem"); // Section operations expect(operationIds).toContain("listSections"); expect(operationIds).toContain("createSection"); // Widget operations expect(operationIds).toContain("listWidgetAreas"); expect(operationIds).toContain("createWidget"); // Settings operations expect(operationIds).toContain("getSettings"); expect(operationIds).toContain("updateSettings"); // Search operations expect(operationIds).toContain("search"); expect(operationIds).toContain("rebuildSearchIndex"); // Redirect operations expect(operationIds).toContain("listRedirects"); expect(operationIds).toContain("createRedirect"); expect(operationIds).toContain("listNotFoundEntries"); // User operations expect(operationIds).toContain("listUsers"); expect(operationIds).toContain("getUser"); expect(operationIds).toContain("disableUser"); // No duplicate operation IDs const uniqueIds = new Set(operationIds); expect(uniqueIds.size).toBe(operationIds.length); }); it("includes reusable component schemas", () => { const doc = generateOpenApiDocument(); const schemas = doc.components?.schemas ?? {}; // Content schemas expect(schemas).toHaveProperty("ContentCreateBody"); expect(schemas).toHaveProperty("ContentUpdateBody"); expect(schemas).toHaveProperty("ContentItem"); expect(schemas).toHaveProperty("ContentResponse"); expect(schemas).toHaveProperty("ContentListResponse"); // Media schemas expect(schemas).toHaveProperty("MediaItem"); expect(schemas).toHaveProperty("MediaListResponse"); // Schema schemas expect(schemas).toHaveProperty("Collection"); expect(schemas).toHaveProperty("CollectionListResponse"); // Comment schemas expect(schemas).toHaveProperty("PublicComment"); expect(schemas).toHaveProperty("Comment"); expect(schemas).toHaveProperty("CommentBulkBody"); // Taxonomy schemas expect(schemas).toHaveProperty("Term"); expect(schemas).toHaveProperty("TermListResponse"); // Menu schemas expect(schemas).toHaveProperty("MenuWithItems"); // User schemas expect(schemas).toHaveProperty("User"); expect(schemas).toHaveProperty("UserListResponse"); }); it("wraps success responses in { data } envelope", () => { const doc = generateOpenApiDocument(); const listPath = doc.paths?.["/_emdash/api/content/{collection}"]; const getResponse = (listPath as Record)?.get as { responses: Record }> }>; }; const schema = getResponse?.responses?.["200"]?.content?.["application/json"]?.schema; expect(schema).toBeDefined(); // The envelope should have a "data" property expect(schema).toHaveProperty("properties"); const props = (schema as Record).properties as Record; expect(props).toHaveProperty("data"); }); it("includes error response schemas", () => { const doc = generateOpenApiDocument(); const listPath = doc.paths?.["/_emdash/api/content/{collection}"]; const getOp = (listPath as Record)?.get as { responses: Record; }; // Should have auth error responses expect(getOp?.responses).toHaveProperty("401"); expect(getOp?.responses).toHaveProperty("403"); }); it("includes security schemes", () => { const doc = generateOpenApiDocument(); const schemes = doc.components?.securitySchemes; expect(schemes).toHaveProperty("session"); expect(schemes).toHaveProperty("bearer"); }); it("tags all 12 domains", () => { const doc = generateOpenApiDocument(); const tagNames = (doc.tags ?? []).map((t: { name: string }) => t.name); expect(tagNames).toContain("Content"); expect(tagNames).toContain("Media"); expect(tagNames).toContain("Schema"); expect(tagNames).toContain("Comments"); expect(tagNames).toContain("Taxonomies"); expect(tagNames).toContain("Menus"); expect(tagNames).toContain("Sections"); expect(tagNames).toContain("Widgets"); expect(tagNames).toContain("Settings"); expect(tagNames).toContain("Search"); expect(tagNames).toContain("Redirects"); expect(tagNames).toContain("Users"); expect(tagNames).toHaveLength(12); }); it("produces valid JSON output", () => { const doc = generateOpenApiDocument(); const json = JSON.stringify(doc); // Should not throw const parsed = JSON.parse(json); expect(parsed.openapi).toBe("3.1.0"); }); });