Wires Core's identity and authorization domains into apps/api over one shared, validated PostgreSQL pool; adds an explicit, single-use, concurrency-safe platform-owner bootstrap CLI; adds a smallest-possible immutable permission registry with one Core-owned restricted permission (core.authorization.grants.manage); and adds reusable Hono authorization enforcement plus a narrowly scoped /api/v1/grants HTTP API for managing explicit grants only, gated by an exact trusted Origin on every mutation.
165 lines
5.8 KiB
TypeScript
165 lines
5.8 KiB
TypeScript
import type { Pool } from "pg";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
const poolConstructor = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("pg", () => ({ Pool: poolConstructor }));
|
|
|
|
import {
|
|
createIdentityDrizzleDatabase,
|
|
createIdentityDrizzleView,
|
|
IdentityDatabaseError,
|
|
} from "../src/identity/database.js";
|
|
|
|
describe("Core identity Drizzle database composition", () => {
|
|
it("does not construct a pool merely because the module is imported", () => {
|
|
expect(poolConstructor).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
"",
|
|
" ",
|
|
"not-a-database-url",
|
|
"https://admin:[email protected]/database",
|
|
])("rejects an invalid connection string without exposing it (%s)", (connectionString) => {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityDrizzleDatabase(connectionString);
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
|
|
expect(captured).toBeInstanceOf(IdentityDatabaseError);
|
|
expect(captured).toMatchObject({ code: "IDENTITY_DATABASE_CONFIGURATION_INVALID" });
|
|
const publicText = `${String(captured)}\n${
|
|
captured instanceof Error ? (captured.stack ?? "") : ""
|
|
}`;
|
|
if (connectionString) {
|
|
expect(publicText).not.toContain(connectionString);
|
|
}
|
|
expect(poolConstructor).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("explicitly constructs the pool only when called, with the identity schema attached", () => {
|
|
poolConstructor.mockImplementation(function fakePool() {
|
|
return { end: async () => undefined, on: vi.fn() };
|
|
});
|
|
|
|
const handle = createIdentityDrizzleDatabase(
|
|
"postgresql://operator:[email protected]/ekklesia",
|
|
);
|
|
|
|
expect(poolConstructor).toHaveBeenCalledTimes(1);
|
|
expect(handle.db).toBeDefined();
|
|
expect(typeof handle.close).toBe("function");
|
|
expect(Object.isFrozen(handle)).toBe(true);
|
|
});
|
|
|
|
it("passes the connection timeout through to the pool configuration", () => {
|
|
poolConstructor.mockImplementation(function fakePool() {
|
|
return { end: async () => undefined, on: vi.fn() };
|
|
});
|
|
poolConstructor.mockClear();
|
|
|
|
createIdentityDrizzleDatabase("postgresql://example.test/ekklesia", {
|
|
connectionTimeoutMs: 5_000,
|
|
});
|
|
|
|
expect(poolConstructor).toHaveBeenCalledWith(
|
|
expect.objectContaining({ connectionTimeoutMillis: 5_000 }),
|
|
);
|
|
});
|
|
|
|
it("passes the operation timeout through to the pool configuration as both query_timeout and statement_timeout", () => {
|
|
poolConstructor.mockImplementation(function fakePool() {
|
|
return { end: async () => undefined, on: vi.fn() };
|
|
});
|
|
poolConstructor.mockClear();
|
|
|
|
createIdentityDrizzleDatabase("postgresql://example.test/ekklesia", {
|
|
operationTimeoutMs: 7_500,
|
|
});
|
|
|
|
expect(poolConstructor).toHaveBeenCalledWith(
|
|
expect.objectContaining({ query_timeout: 7_500, statement_timeout: 7_500 }),
|
|
);
|
|
});
|
|
|
|
it("does not set query_timeout, statement_timeout, or connectionTimeoutMillis when no timeout is configured", () => {
|
|
poolConstructor.mockImplementation(function fakePool() {
|
|
return { end: async () => undefined, on: vi.fn() };
|
|
});
|
|
poolConstructor.mockClear();
|
|
|
|
createIdentityDrizzleDatabase("postgresql://example.test/ekklesia");
|
|
|
|
const [poolConfig] = poolConstructor.mock.calls[0] as [Record<string, unknown>];
|
|
expect(poolConfig).not.toHaveProperty("query_timeout");
|
|
expect(poolConfig).not.toHaveProperty("statement_timeout");
|
|
expect(poolConfig).not.toHaveProperty("connectionTimeoutMillis");
|
|
});
|
|
|
|
it.each([
|
|
["operationTimeoutMs", 0],
|
|
["operationTimeoutMs", -1],
|
|
["operationTimeoutMs", Number.NaN],
|
|
["operationTimeoutMs", 1.5],
|
|
["operationTimeoutMs", 2_147_483_648],
|
|
["connectionTimeoutMs", 0],
|
|
["connectionTimeoutMs", -1],
|
|
["connectionTimeoutMs", Number.NaN],
|
|
] as const)("rejects an invalid %s (%s) without constructing a pool and without leaking the value", (optionName, value) => {
|
|
poolConstructor.mockClear();
|
|
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityDrizzleDatabase("postgresql://example.test/ekklesia", {
|
|
[optionName]: value,
|
|
});
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
|
|
expect(captured).toBeInstanceOf(IdentityDatabaseError);
|
|
expect(captured).toMatchObject({ code: "IDENTITY_DATABASE_CONFIGURATION_INVALID" });
|
|
const publicText = `${String(captured)}\n${
|
|
captured instanceof Error ? (captured.stack ?? "") : ""
|
|
}`;
|
|
expect(publicText).not.toContain(String(value));
|
|
expect(poolConstructor).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("registers a pool-level error listener so an idle client failure never becomes an uncaught exception", () => {
|
|
const on = vi.fn();
|
|
poolConstructor.mockImplementation(function fakePool() {
|
|
return { end: async () => undefined, on };
|
|
});
|
|
|
|
createIdentityDrizzleDatabase("postgresql://example.test/ekklesia");
|
|
|
|
expect(on).toHaveBeenCalledWith("error", expect.any(Function));
|
|
const handler = on.mock.calls.find((call) => call[0] === "error")?.[1] as
|
|
| ((error: unknown) => void)
|
|
| undefined;
|
|
expect(() =>
|
|
handler?.(new Error("terminating connection due to administrator command")),
|
|
).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("createIdentityDrizzleView", () => {
|
|
it("wraps an externally owned pool without constructing, registering an error listener on, or closing one itself", () => {
|
|
poolConstructor.mockClear();
|
|
const fakePool = { end: vi.fn(), on: vi.fn(), query: vi.fn() };
|
|
|
|
// A minimal fake pg.Pool double is sufficient — this test proves createIdentityDrizzleView
|
|
// never touches the pool beyond wrapping it.
|
|
const db = createIdentityDrizzleView(fakePool as unknown as Pool);
|
|
|
|
expect(db).toBeDefined();
|
|
expect(poolConstructor).not.toHaveBeenCalled();
|
|
expect(fakePool.on).not.toHaveBeenCalled();
|
|
expect(fakePool.end).not.toHaveBeenCalled();
|
|
});
|
|
});
|