Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -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);
});
});