Adds Core-owned identity contracts, a Core-owned Better Auth database schema (ekklesia_core_user/session/account/verification), a first-class Core-owned migration source that coexists with module-owned migrations under the exact existing migration system, and an explicit Better Auth construction boundary. No authentication endpoint or sign-in UI is exposed yet (ADR 0010). Co-Authored-By: Claude Sonnet 5 <[email protected]>
124 lines
4.5 KiB
TypeScript
124 lines
4.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const betterAuthMock = vi.hoisted(() => vi.fn());
|
|
const drizzleAdapterMock = vi.hoisted(() =>
|
|
vi.fn((db: unknown, options: unknown) => ({ __adapterOptions: options, db })),
|
|
);
|
|
|
|
vi.mock("better-auth", () => ({ betterAuth: betterAuthMock }));
|
|
vi.mock("better-auth/adapters/drizzle", () => ({ drizzleAdapter: drizzleAdapterMock }));
|
|
|
|
import type { IdentityConfig } from "../src/identity/config.js";
|
|
import { createIdentityAuth, IdentityConstructionError } from "../src/identity/factory.js";
|
|
import { identityAdapterSchema } from "../src/identity/schema.js";
|
|
|
|
function validConfig(): IdentityConfig {
|
|
return Object.freeze({
|
|
baseUrl: "https://identity.example.test",
|
|
secret: "correct-horse-battery-staple-0123456789",
|
|
trustedOrigins: Object.freeze(["https://identity.example.test"]),
|
|
});
|
|
}
|
|
|
|
describe("Core identity Better Auth construction boundary", () => {
|
|
beforeEach(() => {
|
|
betterAuthMock.mockReset();
|
|
drizzleAdapterMock.mockClear();
|
|
});
|
|
|
|
it("constructs explicitly with narrow injected dependencies and returns only a success marker", () => {
|
|
betterAuthMock.mockReturnValue({ api: {}, handler: () => undefined });
|
|
const fakeDb = { __fakeDb: true };
|
|
|
|
const result = createIdentityAuth({ config: validConfig(), db: fakeDb as never });
|
|
|
|
expect(result).toEqual({ constructed: true });
|
|
expect(Object.keys(result)).toEqual(["constructed"]);
|
|
expect(Object.isFrozen(result)).toBe(true);
|
|
});
|
|
|
|
it("does not expose the raw database, adapter, or Better Auth instance on the returned value", () => {
|
|
betterAuthMock.mockReturnValue({ api: {}, handler: () => undefined });
|
|
const fakeDb = { __fakeDb: true };
|
|
|
|
const result = createIdentityAuth({ config: validConfig(), db: fakeDb as never });
|
|
|
|
expect(JSON.stringify(result)).not.toContain("__fakeDb");
|
|
expect(JSON.stringify(result)).not.toContain("handler");
|
|
});
|
|
|
|
it("passes the explicit ekklesia_core_* schema mapping to the Drizzle adapter", () => {
|
|
betterAuthMock.mockReturnValue({});
|
|
const fakeDb = { __fakeDb: true };
|
|
|
|
createIdentityAuth({ config: validConfig(), db: fakeDb as never });
|
|
|
|
expect(drizzleAdapterMock).toHaveBeenCalledTimes(1);
|
|
const [db, options] = drizzleAdapterMock.mock.calls[0] as [unknown, unknown];
|
|
expect(db).toBe(fakeDb);
|
|
expect(options).toEqual({ provider: "pg", schema: identityAdapterSchema });
|
|
});
|
|
|
|
it("enables no provider or plugin beyond the four core models", () => {
|
|
betterAuthMock.mockReturnValue({});
|
|
|
|
createIdentityAuth({ config: validConfig(), db: {} as never });
|
|
|
|
expect(betterAuthMock).toHaveBeenCalledTimes(1);
|
|
const [options] = betterAuthMock.mock.calls[0] as [Record<string, unknown>];
|
|
expect(Object.keys(options).sort()).toEqual([
|
|
"baseURL",
|
|
"database",
|
|
"secret",
|
|
"trustedOrigins",
|
|
]);
|
|
expect(options.baseURL).toBe(validConfig().baseUrl);
|
|
expect(options.secret).toBe(validConfig().secret);
|
|
expect(options.trustedOrigins).toEqual([...validConfig().trustedOrigins]);
|
|
});
|
|
|
|
it("sanitizes a construction failure without leaking the underlying error, secret, or URL", () => {
|
|
const hostileSecret = validConfig().secret;
|
|
betterAuthMock.mockImplementation(() => {
|
|
throw new Error(
|
|
`Better Auth failed for secret ${hostileSecret} at https://internal.example.test`,
|
|
);
|
|
});
|
|
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityAuth({ config: validConfig(), db: {} as never });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
|
|
expect(captured).toBeInstanceOf(IdentityConstructionError);
|
|
expect(captured).toMatchObject({ code: "IDENTITY_CONSTRUCTION_FAILED" });
|
|
const publicText = `${String(captured)}\n${
|
|
captured instanceof Error ? (captured.stack ?? "") : ""
|
|
}`;
|
|
expect(publicText).not.toContain(hostileSecret);
|
|
expect(publicText).not.toContain("internal.example.test");
|
|
});
|
|
|
|
it("sanitizes an adapter construction failure the same way", () => {
|
|
drizzleAdapterMock.mockImplementation(() => {
|
|
throw new Error("adapter exploded with postgresql://admin:[email protected]/ekklesia");
|
|
});
|
|
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityAuth({ config: validConfig(), db: {} as never });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
|
|
expect(captured).toBeInstanceOf(IdentityConstructionError);
|
|
const publicText = `${String(captured)}\n${
|
|
captured instanceof Error ? (captured.stack ?? "") : ""
|
|
}`;
|
|
expect(publicText).not.toContain("db.internal");
|
|
expect(publicText).not.toContain("admin");
|
|
});
|
|
});
|