194 lines
7.3 KiB
TypeScript
194 lines
7.3 KiB
TypeScript
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
import { createOpenAPIRegistry } from "@suaveplan/api-openapi";
|
|
import { createPluginContext } from "@suaveplan/api-plugin";
|
|
import { betterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb";
|
|
import { EventBus } from "@suaveplan/events";
|
|
import { SurrealAdapter } from "@suaveplan/surrealdb";
|
|
import {
|
|
afterAll,
|
|
beforeAll,
|
|
describe,
|
|
expect,
|
|
it,
|
|
} from "@suaveplan/testing/runner";
|
|
import { SurrealDBFixture } from "@suaveplan/testing-integration-surrealdb";
|
|
import { createAuthPlugin } from "../../src/plugin/plugin.js";
|
|
import type { AuthService } from "../../src/services/auth-service.js";
|
|
import type { AuthPluginConfig } from "../../src/types/types.js";
|
|
|
|
const noopLogger = {
|
|
debug: () => undefined,
|
|
info: () => undefined,
|
|
warn: () => undefined,
|
|
error: () => undefined,
|
|
};
|
|
|
|
let fixture: SurrealDBFixture | null = null;
|
|
let client: SurrealAdapter | null = null;
|
|
|
|
beforeAll(async () => {
|
|
fixture = new SurrealDBFixture();
|
|
const cfg = await fixture.start();
|
|
client = new SurrealAdapter({
|
|
url: cfg.url,
|
|
namespace: cfg.namespace,
|
|
database: cfg.database,
|
|
username: cfg.username,
|
|
password: cfg.password,
|
|
});
|
|
await client.connect();
|
|
await client.execute("DEFINE TABLE OVERWRITE user SCHEMAFULL;");
|
|
await client.execute(
|
|
"DEFINE FIELD OVERWRITE email ON user TYPE option<string>;"
|
|
);
|
|
await client.execute(
|
|
"DEFINE FIELD OVERWRITE phoneNumber ON user TYPE option<string>;"
|
|
);
|
|
await client.execute(
|
|
"DEFINE FIELD OVERWRITE createdAt ON user TYPE option<datetime> DEFAULT time::now() VALUE $before OR time::now();"
|
|
);
|
|
await client.execute(
|
|
"DEFINE FIELD OVERWRITE updatedAt ON user TYPE option<datetime> DEFAULT time::now() VALUE time::now();"
|
|
);
|
|
}, 180_000);
|
|
|
|
afterAll(async () => {
|
|
if (client) {
|
|
await client.disconnect();
|
|
client = null;
|
|
}
|
|
if (fixture) {
|
|
await fixture.stop();
|
|
fixture = null;
|
|
}
|
|
}, 60_000);
|
|
|
|
describe("full integration boot against real SurrealDB", () => {
|
|
it("full sign-up/sign-in/session/sign-out roundtrip against real SurrealDB", {
|
|
timeout: 120_000,
|
|
}, async () => {
|
|
if (client === null) throw new Error("client not ready");
|
|
const app = new OpenAPIHono();
|
|
const registry = createOpenAPIRegistry({
|
|
info: { title: "auth-test", version: "0.1.0" },
|
|
});
|
|
const plugin = createAuthPlugin({
|
|
betterAuth: {
|
|
database: betterAuthSurrealAdapter({ client }),
|
|
secret: "test-secret-at-least-32-chars-long-for-better-auth",
|
|
baseURL: "http://localhost:3000/auth",
|
|
emailAndPassword: { enabled: true },
|
|
},
|
|
});
|
|
const services = new Map<string, unknown>();
|
|
services.set("app", app);
|
|
services.set("openAPIRegistry", registry);
|
|
services.set("db", client);
|
|
const ctx = createPluginContext<AuthPluginConfig>({
|
|
logger: noopLogger,
|
|
eventBus: new EventBus(),
|
|
signal: new AbortController().signal,
|
|
config: plugin.defaults as AuthPluginConfig,
|
|
services,
|
|
});
|
|
await plugin.onInit?.(ctx);
|
|
registry.applyTo(app, "/openapi.json");
|
|
|
|
// The Better-Auth instance is reachable via the registered
|
|
// authService slot. We drive the lifecycle directly through
|
|
// its API (which is what HTTP routes delegate to anyway) so
|
|
// the test does not depend on Better-Auth's URL-routing
|
|
// configuration which lives outside this plugin.
|
|
const authService = services.get("authService") as AuthService;
|
|
expect(authService).toBeDefined();
|
|
const auth = authService.getBetterAuthInstance();
|
|
const signUp = (await auth.api.signUpEmail({
|
|
body: {
|
|
email: "integration@example.test",
|
|
password: "Passw0rd!2026",
|
|
name: "Integration User",
|
|
},
|
|
})) as { user: { email: string } };
|
|
expect(signUp.user.email).toBe("integration@example.test");
|
|
|
|
const users = await client.query<{ email: string }>(
|
|
"SELECT * FROM user WHERE email = 'integration@example.test';"
|
|
);
|
|
expect(users.length).toBeGreaterThan(0);
|
|
|
|
// OpenAPI document includes the 9 routes (prefixed).
|
|
const openapiRes = await app.request("/openapi.json");
|
|
const doc = (await openapiRes.json()) as {
|
|
paths: Record<string, Record<string, unknown>>;
|
|
};
|
|
const paths = Object.keys(doc.paths ?? {});
|
|
const authPaths = paths.filter((p) => p.startsWith("/auth/"));
|
|
expect(authPaths.length).toBe(9);
|
|
|
|
// Hit the registered Hono routes — the OpenAPI route
|
|
// handler delegates to auth.handler so any 2xx/3xx/4xx
|
|
// response (rather than 404) confirms routing reached
|
|
// Better-Auth. We use /auth/get-session which is the
|
|
// canonical anonymous endpoint.
|
|
const sessionRes = await app.request(
|
|
"http://localhost:3000/auth/get-session"
|
|
);
|
|
expect(sessionRes.status).not.toBe(404);
|
|
});
|
|
|
|
it("schema apply is idempotent across restarts", {
|
|
timeout: 60_000,
|
|
}, async () => {
|
|
if (client === null) throw new Error("client not ready");
|
|
const app = new OpenAPIHono();
|
|
const registry = createOpenAPIRegistry({
|
|
info: { title: "auth-test", version: "0.1.0" },
|
|
});
|
|
const plugin = createAuthPlugin({
|
|
betterAuth: {
|
|
database: betterAuthSurrealAdapter({ client }),
|
|
secret: "test-secret-at-least-32-chars-long-for-better-auth",
|
|
baseURL: "http://localhost:3000",
|
|
emailAndPassword: { enabled: true },
|
|
},
|
|
});
|
|
const services = new Map<string, unknown>();
|
|
services.set("app", app);
|
|
services.set("openAPIRegistry", registry);
|
|
services.set("db", client);
|
|
const ctx = createPluginContext<AuthPluginConfig>({
|
|
logger: noopLogger,
|
|
eventBus: new EventBus(),
|
|
signal: new AbortController().signal,
|
|
config: plugin.defaults as AuthPluginConfig,
|
|
services,
|
|
});
|
|
await plugin.onInit?.(ctx);
|
|
const app2 = new OpenAPIHono();
|
|
const registry2 = createOpenAPIRegistry({
|
|
info: { title: "auth-test", version: "0.1.0" },
|
|
});
|
|
const plugin2 = createAuthPlugin({
|
|
betterAuth: {
|
|
database: betterAuthSurrealAdapter({ client }),
|
|
secret: "test-secret-at-least-32-chars-long-for-better-auth",
|
|
baseURL: "http://localhost:3000",
|
|
emailAndPassword: { enabled: true },
|
|
},
|
|
});
|
|
const services2 = new Map<string, unknown>();
|
|
services2.set("app", app2);
|
|
services2.set("openAPIRegistry", registry2);
|
|
services2.set("db", client);
|
|
const ctx2 = createPluginContext<AuthPluginConfig>({
|
|
logger: noopLogger,
|
|
eventBus: new EventBus(),
|
|
signal: new AbortController().signal,
|
|
config: plugin2.defaults as AuthPluginConfig,
|
|
services: services2,
|
|
});
|
|
await plugin2.onInit?.(ctx2);
|
|
expect(registry2.getRoutesByPlugin("auth")).toHaveLength(9);
|
|
});
|
|
});
|