96 lines
5.1 KiB
TypeScript
96 lines
5.1 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("@toolctrl/db/entities.server", () => ({
|
|
getAllLegacyEntities: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("~/locale/i18next.server", () => ({
|
|
getTranslations: vi.fn(async () => ({ t: (value: string) => value, translations: {}, locale: "en" })),
|
|
}));
|
|
|
|
vi.mock("~/utils/url.server", () => ({
|
|
getAbsoluteApiUrl: vi.fn((_request: Request, path: string) => `https://api.example.com${path.startsWith("/") ? path : `/${path}`}`),
|
|
}));
|
|
|
|
import { getAllLegacyEntities } from "@toolctrl/db/entities.server";
|
|
import ApiSpecsService from "./ApiSpecsService";
|
|
|
|
const entity = {
|
|
id: "entity_1",
|
|
slug: "contacts",
|
|
title: "Contact",
|
|
titlePlural: "Contacts",
|
|
hasApi: true,
|
|
active: true,
|
|
properties: [],
|
|
childEntities: [{ id: "child_relation" }],
|
|
parentEntities: [],
|
|
};
|
|
|
|
describe("ApiSpecsService", () => {
|
|
it("documents canonical API auth and idempotency for generated specs and Postman requests", async () => {
|
|
vi.mocked(getAllLegacyEntities).mockResolvedValue([entity] as never);
|
|
|
|
const specs = await ApiSpecsService.generateSpecs({ request: new Request("https://admin.example.com/api/docs") });
|
|
const openApi = specs.openApi;
|
|
|
|
expect(openApi.servers).toEqual([{ url: "https://api.example.com", description: "API host" }]);
|
|
expect(openApi.components.securitySchemes.bearerAuth).toMatchObject({
|
|
type: "http",
|
|
scheme: "bearer",
|
|
bearerFormat: "service-token",
|
|
});
|
|
expect(openApi.paths["/v1/entities/{entity}/rows"].post.parameters).toContainEqual(expect.objectContaining({ name: "Idempotency-Key", required: true }));
|
|
expect(openApi.paths["/v1/entities/{entity}/rows/{rowId}"].patch.parameters).toContainEqual(expect.objectContaining({ name: "Idempotency-Key", required: true }));
|
|
expect(openApi.paths["/v1/entities/{entity}/rows/{rowId}"].delete.parameters).toContainEqual(expect.objectContaining({ name: "Idempotency-Key", required: true }));
|
|
expect(openApi.paths["/v1/relationships"].post.parameters).toContainEqual(expect.objectContaining({ name: "Idempotency-Key", required: true }));
|
|
expect(openApi.paths["/v1/relationships/{id}"].delete.parameters).toContainEqual(expect.objectContaining({ name: "Idempotency-Key", required: true }));
|
|
|
|
const postmanItems = specs.postmanCollection.item as Array<{
|
|
name: string;
|
|
request: { header: Array<{ key: string; value?: string }>; url: { raw: string; query?: Array<{ key: string; value: string; disabled?: boolean }> } };
|
|
}>;
|
|
expect(postmanItems.find((item) => item.name === "GET /v1/session/current-user")?.request.header).toEqual([
|
|
expect.objectContaining({ key: "Authorization", value: "Bearer <token>" }),
|
|
]);
|
|
expect(postmanItems.find((item) => item.name === "POST /v1/api-keys/validate")?.request.header).toEqual([
|
|
expect.objectContaining({ key: "x-api-key", value: "<api-key-or-service-token>" }),
|
|
]);
|
|
expect(postmanItems.some((item) => item.name.startsWith("POST /v1/workflows") || item.name.startsWith("GET /v1/workflows"))).toBe(false);
|
|
expect(postmanItems.find((item) => item.name === "GET /v1/entities/:entity/rows")?.request.header).toEqual([
|
|
expect.objectContaining({ key: "x-api-key", value: "<api-key>" }),
|
|
]);
|
|
expect(postmanItems.find((item) => item.name === "GET /v1/entities/:entity/rows")?.request.url.query).toContainEqual(
|
|
expect.objectContaining({ key: "tenantId", value: "<tenantId>" })
|
|
);
|
|
expect(postmanItems.find((item) => item.name === "GET /v1/entities/:entity/rows")?.request.url.raw).toContain("tenantId=%3CtenantId%3E");
|
|
expect(postmanItems.find((item) => item.name === "POST /v1/entities/:entity/rows")?.request.header).toEqual([
|
|
expect.objectContaining({ key: "x-api-key", value: "<api-key>" }),
|
|
expect.objectContaining({ key: "Idempotency-Key" }),
|
|
]);
|
|
expect(postmanItems.find((item) => item.name === "POST /v1/ai/upload")?.request.header).toContainEqual(
|
|
expect.objectContaining({ key: "x-tenant-id", value: "<x-tenant-id>" })
|
|
);
|
|
expect(postmanItems.find((item) => item.name === "POST /v1/analytics/events")?.request.header).toEqual([
|
|
expect.objectContaining({ key: "x-analytics-token" }),
|
|
]);
|
|
expect(postmanItems.find((item) => item.name === "GET /v1/exports/:exportJobId/download-url")?.request.header).toEqual([
|
|
expect.objectContaining({ key: "x-api-key", value: "<api-key>" }),
|
|
]);
|
|
expect(postmanItems.find((item) => item.name === "POST /v1/billing/stripe/webhooks")?.request.header).toEqual([
|
|
expect.objectContaining({ key: "stripe-signature" }),
|
|
]);
|
|
expect(postmanItems.find((item) => item.name === "POST /v1/events/webhooks/attempts/:id")?.request.header).toEqual([
|
|
expect.objectContaining({ key: "Authorization", value: "Basic <base64(mailjet:webhook-secret)>" }),
|
|
]);
|
|
|
|
expect((specs.endpoints ?? []).map((endpoint) => endpoint.route)).toEqual([
|
|
"https://api.example.com/v1/entities/contacts/rows",
|
|
"https://api.example.com/v1/entities/contacts/rows/:rowId",
|
|
"https://api.example.com/v1/entities/contacts/rows",
|
|
"https://api.example.com/v1/entities/contacts/rows/:rowId",
|
|
"https://api.example.com/v1/entities/contacts/rows/:rowId",
|
|
]);
|
|
});
|
|
});
|