Initial Commit
This commit is contained in:
134
packages/api-stack/api/api-plugin-auth/tests/dep-graph.test.ts
Normal file
134
packages/api-stack/api/api-plugin-auth/tests/dep-graph.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
|
||||
const pkgRoot = resolve(__dirname, "..");
|
||||
const srcRoot = join(pkgRoot, "src");
|
||||
|
||||
function walk(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = join(dir, entry);
|
||||
const st = statSync(full);
|
||||
if (st.isDirectory()) out.push(...walk(full));
|
||||
else out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
describe("@suaveplan/api-plugin-auth dep graph + layout", () => {
|
||||
it("package.json carries required dependencies", { timeout: 5000 }, () => {
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(join(pkgRoot, "package.json"), "utf8")
|
||||
) as {
|
||||
dependencies: Record<string, string>;
|
||||
devDependencies: Record<string, string>;
|
||||
};
|
||||
const deps = pkg.dependencies;
|
||||
expect(deps["@suaveplan/api-plugin"]).toBe("workspace:^");
|
||||
expect(deps["@suaveplan/api-openapi"]).toBe("workspace:^");
|
||||
expect(deps["@suaveplan/api-migrations"]).toBe("workspace:^");
|
||||
expect(deps["@suaveplan/better-auth-surrealdb"]).toBe("workspace:^");
|
||||
expect(deps["@suaveplan/surrealdb"]).toBe("workspace:^");
|
||||
expect(deps["@suaveplan/error"]).toBe("workspace:^");
|
||||
expect(deps["@suaveplan/types"]).toBe("workspace:^");
|
||||
expect(deps["hono"]).toBe("catalog:");
|
||||
expect(deps["@hono/zod-openapi"]).toBe("catalog:server");
|
||||
expect(deps["zod"]).toBe("catalog:forms");
|
||||
expect(deps["better-auth"]).toBe("catalog:auth");
|
||||
expect(deps["@opentelemetry/api"]).toBe("catalog:observability");
|
||||
});
|
||||
|
||||
it("package.json exposes 15 subpath exports", { timeout: 5000 }, () => {
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(join(pkgRoot, "package.json"), "utf8")
|
||||
) as { exports: Record<string, unknown> };
|
||||
const subs = Object.keys(pkg.exports).filter(
|
||||
(k) => k.startsWith("./") && k !== "./package.json"
|
||||
);
|
||||
// 15 named feature subpaths (root "." is excluded by filter).
|
||||
expect(subs.length).toBe(15);
|
||||
});
|
||||
|
||||
it("every impl .ts has a sibling .md", { timeout: 5000 }, () => {
|
||||
const missing: string[] = [];
|
||||
for (const file of walk(srcRoot)) {
|
||||
if (!file.endsWith(".ts")) continue;
|
||||
if (
|
||||
file.endsWith(".test.ts") ||
|
||||
file.endsWith(".spec.ts") ||
|
||||
file.endsWith(".types.ts") ||
|
||||
file.endsWith("/index.ts")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const md = file.replace(/\.ts$/, ".md");
|
||||
try {
|
||||
statSync(md);
|
||||
} catch {
|
||||
missing.push(md);
|
||||
}
|
||||
}
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it("every schemas/routes/<name>.ts has a sibling .md", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const routesDir = join(srcRoot, "schemas", "routes");
|
||||
const missing: string[] = [];
|
||||
for (const entry of readdirSync(routesDir)) {
|
||||
if (!entry.endsWith(".ts")) continue;
|
||||
if (
|
||||
entry === "index.ts" ||
|
||||
entry.endsWith(".test.ts") ||
|
||||
entry.endsWith(".types.ts")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const md = join(routesDir, entry.replace(/\.ts$/, ".md"));
|
||||
try {
|
||||
statSync(md);
|
||||
} catch {
|
||||
missing.push(md);
|
||||
}
|
||||
}
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it("README.md is present at package root", { timeout: 5000 }, () => {
|
||||
const readme = readFileSync(join(pkgRoot, "README.md"), "utf8");
|
||||
expect(readme.length).toBeGreaterThan(200);
|
||||
expect(readme).toContain("@suaveplan/api-plugin-auth");
|
||||
});
|
||||
|
||||
it("every subpath export resolves to a file within src/", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(join(pkgRoot, "package.json"), "utf8")
|
||||
) as {
|
||||
exports: Record<string, { import?: { default?: string } } | string>;
|
||||
};
|
||||
const missing: string[] = [];
|
||||
for (const [sub, val] of Object.entries(pkg.exports)) {
|
||||
if (sub === "./package.json") continue;
|
||||
const defaultPath =
|
||||
typeof val === "string"
|
||||
? val
|
||||
: (val?.import?.default ?? undefined);
|
||||
if (defaultPath === undefined) continue;
|
||||
// dist/<rel>.js → src/<rel>.ts
|
||||
const rel = defaultPath
|
||||
.replace(/^\.\/dist\//, "")
|
||||
.replace(/\.js$/, ".ts");
|
||||
const srcPath = join(srcRoot, rel);
|
||||
try {
|
||||
statSync(srcPath);
|
||||
} catch {
|
||||
missing.push(`${sub} → ${srcPath}`);
|
||||
}
|
||||
}
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
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 { 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("openapi-doc", () => {
|
||||
it("served /openapi.json includes auth routes with Zod-typed schemas", {
|
||||
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);
|
||||
await plugin.onInit?.(
|
||||
createPluginContext<AuthPluginConfig>({
|
||||
logger: noopLogger,
|
||||
eventBus: new EventBus(),
|
||||
signal: new AbortController().signal,
|
||||
config: plugin.defaults as AuthPluginConfig,
|
||||
services,
|
||||
})
|
||||
);
|
||||
registry.applyTo(app, "/openapi.json");
|
||||
const res = await app.request("/openapi.json");
|
||||
const doc = (await res.json()) as {
|
||||
paths: Record<string, Record<string, unknown>>;
|
||||
};
|
||||
for (const path of [
|
||||
"/auth/sign-up",
|
||||
"/auth/sign-in",
|
||||
"/auth/sign-out",
|
||||
"/auth/session",
|
||||
"/auth/forgot-password",
|
||||
"/auth/reset-password",
|
||||
"/auth/verify-email",
|
||||
]) {
|
||||
expect(doc.paths[path]).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("served doc lists all 9 auth routes", { 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);
|
||||
await plugin.onInit?.(
|
||||
createPluginContext<AuthPluginConfig>({
|
||||
logger: noopLogger,
|
||||
eventBus: new EventBus(),
|
||||
signal: new AbortController().signal,
|
||||
config: plugin.defaults as AuthPluginConfig,
|
||||
services,
|
||||
})
|
||||
);
|
||||
const routes = registry.getRoutesByPlugin("auth");
|
||||
expect(routes.length).toBe(9);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
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 { 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);
|
||||
|
||||
async function bootOnce(c: SurrealAdapter): Promise<void> {
|
||||
const app = new OpenAPIHono();
|
||||
const registry = createOpenAPIRegistry({
|
||||
info: { title: "auth-test", version: "0.1.0" },
|
||||
});
|
||||
const plugin = createAuthPlugin({
|
||||
betterAuth: {
|
||||
database: betterAuthSurrealAdapter({ client: c }),
|
||||
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", c);
|
||||
await plugin.onInit?.(
|
||||
createPluginContext<AuthPluginConfig>({
|
||||
logger: noopLogger,
|
||||
eventBus: new EventBus(),
|
||||
signal: new AbortController().signal,
|
||||
config: plugin.defaults as AuthPluginConfig,
|
||||
services,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
describe("schema apply idempotency", () => {
|
||||
it("re-apply succeeds after a prior boot wrote rows", {
|
||||
timeout: 180_000,
|
||||
}, async () => {
|
||||
if (client === null) throw new Error("client not ready");
|
||||
await bootOnce(client);
|
||||
// Write a row so the second apply has data in scope.
|
||||
await client.execute(
|
||||
"CREATE user:integration_idem SET email = 'idem@example.test';"
|
||||
);
|
||||
// Re-apply.
|
||||
await bootOnce(client);
|
||||
const rows = await client.query<{ email: string }>(
|
||||
"SELECT * FROM user WHERE email = 'idem@example.test';"
|
||||
);
|
||||
expect(rows.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import fc from "fast-check";
|
||||
import { PrincipalSchema } from "../src/schemas/principal.js";
|
||||
|
||||
const principalArb = fc.record(
|
||||
{
|
||||
id: fc.string({ minLength: 1, maxLength: 64 }),
|
||||
email: fc.option(
|
||||
fc
|
||||
.tuple(
|
||||
fc.string({ minLength: 1, maxLength: 16 }),
|
||||
fc.string({ minLength: 1, maxLength: 16 })
|
||||
)
|
||||
.map(
|
||||
([l, r]) =>
|
||||
`${l.replace(/[^A-Za-z0-9]/g, "a")}@${r.replace(
|
||||
/[^A-Za-z0-9]/g,
|
||||
"a"
|
||||
)}.test`
|
||||
),
|
||||
{ nil: undefined }
|
||||
),
|
||||
emailVerified: fc.boolean(),
|
||||
roles: fc.array(fc.string({ minLength: 1, maxLength: 16 }), {
|
||||
maxLength: 5,
|
||||
}),
|
||||
tenantId: fc.option(fc.string({ minLength: 1, maxLength: 32 }), {
|
||||
nil: undefined,
|
||||
}),
|
||||
metadata: fc.option(
|
||||
fc.dictionary(
|
||||
fc.string({ minLength: 1, maxLength: 8 }),
|
||||
fc.oneof(fc.string(), fc.integer(), fc.boolean())
|
||||
),
|
||||
{ nil: undefined }
|
||||
),
|
||||
},
|
||||
{ requiredKeys: ["id", "emailVerified", "roles"] }
|
||||
);
|
||||
|
||||
describe("PrincipalSchema fast-check round-trip", () => {
|
||||
it("accepts arbitrary valid principals (200 runs)", {
|
||||
timeout: 30_000,
|
||||
}, () => {
|
||||
fc.assert(
|
||||
fc.property(principalArb, (sample) => {
|
||||
const result = PrincipalSchema.safeParse(sample);
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`unexpected reject: ${JSON.stringify(result.error.issues)}`
|
||||
);
|
||||
}
|
||||
expect(result.data.id).toBe(sample.id);
|
||||
expect(result.data.emailVerified).toBe(sample.emailVerified);
|
||||
}),
|
||||
{ numRuns: 200 }
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Test setup for @suaveplan/api-plugin-auth.
|
||||
*
|
||||
* Installs a real in-memory OpenTelemetry MeterProvider BEFORE any source
|
||||
* module imports so the meter resolved on first call binds to it. Node 18+
|
||||
* + Bun expose `crypto.subtle` / `crypto.getRandomValues` globally so no
|
||||
* crypto polyfill is required.
|
||||
*/
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import {
|
||||
AggregationTemporality,
|
||||
InMemoryMetricExporter,
|
||||
MeterProvider,
|
||||
PeriodicExportingMetricReader,
|
||||
} from "@opentelemetry/sdk-metrics";
|
||||
|
||||
export const testInMemoryMetricExporter = new InMemoryMetricExporter(
|
||||
AggregationTemporality.CUMULATIVE
|
||||
);
|
||||
|
||||
const reader = new PeriodicExportingMetricReader({
|
||||
exporter: testInMemoryMetricExporter,
|
||||
exportIntervalMillis: 60_000,
|
||||
});
|
||||
|
||||
export const testMeterProvider = new MeterProvider({ readers: [reader] });
|
||||
metrics.setGlobalMeterProvider(testMeterProvider);
|
||||
|
||||
export async function flushTestMetrics() {
|
||||
await testMeterProvider.forceFlush();
|
||||
const collected = testInMemoryMetricExporter.getMetrics();
|
||||
testInMemoryMetricExporter.reset();
|
||||
return collected;
|
||||
}
|
||||
Reference in New Issue
Block a user