Initial Commit
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
import { OpenAPIHono } from "@hono/zod-openapi";
|
||||
import { createOpenAPIRegistry } from "@suaveplan/api-openapi";
|
||||
import { createPluginContext } from "@suaveplan/api-plugin";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
import type { SurrealAdapter } from "@suaveplan/surrealdb";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
CONTACTS_API_CODES,
|
||||
ContactsApiError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import { createContactsPlugin, runOnInit } from "./plugin.js";
|
||||
|
||||
const noopLogger = {
|
||||
debug: () => undefined,
|
||||
info: () => undefined,
|
||||
warn: () => undefined,
|
||||
error: () => undefined,
|
||||
};
|
||||
|
||||
function makeFakeDb(): SurrealAdapter {
|
||||
return {
|
||||
async execute() {},
|
||||
async query() {
|
||||
return [[]];
|
||||
},
|
||||
} as unknown as SurrealAdapter;
|
||||
}
|
||||
|
||||
describe("createContactsPlugin", () => {
|
||||
it("createContactsPlugin returns a typed descriptor with default config", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const desc = createContactsPlugin();
|
||||
expect(desc.name).toBe("contacts");
|
||||
expect(desc.version).toBe("0.1.0");
|
||||
expect(desc.dependencies).toEqual(["database", "auth"]);
|
||||
expect(desc.defaults.routePrefix).toBe("/");
|
||||
expect(desc.defaults.maxImportBatchSize).toBe(1000);
|
||||
expect(desc.defaults.maxGroupDepth).toBe(10);
|
||||
expect(desc.defaults.enforceGroupDepth).toBe(true);
|
||||
expect(desc.defaults.applySchemaOnInit).toBe(true);
|
||||
});
|
||||
|
||||
it("createContactsPlugin throws CONTACTS_API.INVALID_INPUT on invalid options", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() =>
|
||||
createContactsPlugin({ maxGroupDepth: -1 } as never)
|
||||
).toThrowError(ContactsApiError);
|
||||
});
|
||||
|
||||
it("createContactsPlugin honours a custom routePrefix", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const desc = createContactsPlugin({ routePrefix: "/api" });
|
||||
expect(desc.defaults.routePrefix).toBe("/api");
|
||||
});
|
||||
|
||||
it("descriptor advertises name, dependencies, version, tags", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const desc = createContactsPlugin();
|
||||
expect(desc.tags).toEqual(["contacts", "groups", "audience"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin onInit", () => {
|
||||
function makeContext(
|
||||
overrides: {
|
||||
app?: OpenAPIHono | undefined;
|
||||
db?: SurrealAdapter | undefined;
|
||||
registry?: ReturnType<typeof createOpenAPIRegistry> | undefined;
|
||||
} = {}
|
||||
) {
|
||||
const app = overrides.app ?? new OpenAPIHono();
|
||||
const services = new Map<string, unknown>();
|
||||
if (app !== undefined) services.set("app", app);
|
||||
if (overrides.db !== undefined) services.set("db", overrides.db);
|
||||
if (overrides.registry !== undefined)
|
||||
services.set("openAPIRegistry", overrides.registry);
|
||||
const ctx = createPluginContext({
|
||||
logger: noopLogger,
|
||||
eventBus: new EventBus(),
|
||||
signal: new AbortController().signal,
|
||||
config: {} as never,
|
||||
services,
|
||||
});
|
||||
return { ctx, app, services };
|
||||
}
|
||||
|
||||
it("throws SERVICE_NOT_AVAILABLE when app missing", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const services = new Map<string, unknown>();
|
||||
// Note: no `app` slot.
|
||||
services.set("db", makeFakeDb());
|
||||
const ctx = createPluginContext({
|
||||
logger: noopLogger,
|
||||
eventBus: new EventBus(),
|
||||
signal: new AbortController().signal,
|
||||
config: {} as never,
|
||||
services,
|
||||
});
|
||||
const desc = createContactsPlugin({ applySchemaOnInit: false });
|
||||
await expect(
|
||||
runOnInit(ctx as never, desc.defaults)
|
||||
).rejects.toMatchObject({
|
||||
code: CONTACTS_API_CODES.SERVICE_NOT_AVAILABLE,
|
||||
});
|
||||
});
|
||||
|
||||
it("throws SERVICE_NOT_AVAILABLE when db missing", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { ctx } = makeContext({});
|
||||
const desc = createContactsPlugin({ applySchemaOnInit: false });
|
||||
await expect(
|
||||
runOnInit(ctx as never, desc.defaults)
|
||||
).rejects.toMatchObject({
|
||||
code: CONTACTS_API_CODES.SERVICE_NOT_AVAILABLE,
|
||||
});
|
||||
});
|
||||
|
||||
it("registers the three repositories in the ServiceRegistry", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const db = makeFakeDb();
|
||||
const { ctx } = makeContext({ db });
|
||||
const desc = createContactsPlugin({ applySchemaOnInit: false });
|
||||
await runOnInit(ctx as never, desc.defaults);
|
||||
expect(ctx.getService("contactRepository")).toBeDefined();
|
||||
expect(ctx.getService("groupRepository")).toBeDefined();
|
||||
expect(ctx.getService("membershipRepository")).toBeDefined();
|
||||
});
|
||||
|
||||
it("logs a warning when openAPIRegistry is absent and still mounts handlers", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const db = makeFakeDb();
|
||||
const messages: string[] = [];
|
||||
const logger = {
|
||||
debug: () => undefined,
|
||||
info: () => undefined,
|
||||
warn: (msg: string) => messages.push(msg),
|
||||
error: () => undefined,
|
||||
};
|
||||
const app = new OpenAPIHono();
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", app);
|
||||
services.set("db", db);
|
||||
const ctx = createPluginContext({
|
||||
logger,
|
||||
eventBus: new EventBus(),
|
||||
signal: new AbortController().signal,
|
||||
config: {} as never,
|
||||
services,
|
||||
});
|
||||
const desc = createContactsPlugin({ applySchemaOnInit: false });
|
||||
await runOnInit(ctx as never, desc.defaults);
|
||||
expect(messages.some((m) => m.includes("openAPIRegistry"))).toBe(true);
|
||||
});
|
||||
|
||||
it("registers routes when openAPIRegistry is supplied", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const db = makeFakeDb();
|
||||
const registry = createOpenAPIRegistry({
|
||||
info: { title: "t", version: "0.0.0" },
|
||||
});
|
||||
const { ctx } = makeContext({ db, registry });
|
||||
const desc = createContactsPlugin({ applySchemaOnInit: false });
|
||||
await runOnInit(ctx as never, desc.defaults);
|
||||
const paths = registry.getRoutes().map((r) => r.path);
|
||||
expect(paths).toContain("/contacts");
|
||||
expect(paths).toContain("/groups");
|
||||
});
|
||||
|
||||
it("raises ROUTE_PREFIX_COLLISION when /contacts already mounted", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const db = makeFakeDb();
|
||||
const registry = createOpenAPIRegistry({
|
||||
info: { title: "t", version: "0.0.0" },
|
||||
});
|
||||
const { ctx } = makeContext({ db, registry });
|
||||
const desc = createContactsPlugin({ applySchemaOnInit: false });
|
||||
await runOnInit(ctx as never, desc.defaults);
|
||||
// Second call should detect collision.
|
||||
await expect(
|
||||
runOnInit(ctx as never, desc.defaults)
|
||||
).rejects.toMatchObject({
|
||||
code: CONTACTS_API_CODES.ROUTE_PREFIX_COLLISION,
|
||||
});
|
||||
});
|
||||
|
||||
it("applies SurrealQL schema when applySchemaOnInit is true", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const executed: string[] = [];
|
||||
const db = {
|
||||
async execute(sql: string) {
|
||||
executed.push(sql);
|
||||
},
|
||||
async query() {
|
||||
return [[]];
|
||||
},
|
||||
} as unknown as SurrealAdapter;
|
||||
const { ctx } = makeContext({ db });
|
||||
const desc = createContactsPlugin({ applySchemaOnInit: true });
|
||||
await runOnInit(ctx as never, desc.defaults);
|
||||
expect(executed.length).toBeGreaterThan(0);
|
||||
expect(executed[0]).toContain("DEFINE TABLE OVERWRITE contact");
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin route prefix overrides", () => {
|
||||
it("mounts under /api when routePrefix is /api", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const db = makeFakeDb();
|
||||
const app = new OpenAPIHono();
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", app);
|
||||
services.set("db", db);
|
||||
const ctx = createPluginContext({
|
||||
logger: noopLogger,
|
||||
eventBus: new EventBus(),
|
||||
signal: new AbortController().signal,
|
||||
config: {} as never,
|
||||
services,
|
||||
});
|
||||
const desc = createContactsPlugin({
|
||||
routePrefix: "/api",
|
||||
applySchemaOnInit: false,
|
||||
});
|
||||
await runOnInit(ctx as never, desc.defaults);
|
||||
// The route was registered onto the bare Hono app (no registry path).
|
||||
// Probe the app for a known route under /api.
|
||||
const res = await app.request("/api/contacts");
|
||||
// Will be 401 (no auth) but indicates handler is mounted.
|
||||
expect([401, 500]).toContain(res.status);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user