Files
Ekklesia/core/tests/doctor.test.ts
T
ekklesia edd7148aa8 feat: integrate module authorization
Wires composePlatformPermissionRegistry into apps/api's production
startup: startApi() discovers every installed module, composes the one
immutable platform permission registry (Core's own declarations plus
every module's), and only then constructs the shared PostgreSQL pool,
Better Auth, and the HTTP server. A discovery or composition failure —
an invalid manifest, a reserved core.* namespace claim, or a duplicate
declaration — propagates before anything exists to close; there is no
Core-only registry fallback. apps/api/src/platform-permissions.ts
resolves the repository root from its own file location, never
process.cwd(), so package-scoped test runs discover the right modules/
directory.

createApiApp/createGrantsRoutes now require the composed registry as
an injected authorization option, replacing the previous hard-coded
CORE_PERMISSION_REGISTRY; requirePermission continues to work
identically for Core-owned and module-owned permissions, since it has
no concept of the distinction.

Extends pnpm doctor with a modules.permissions check (reusing the
already-discovered module list, distinguishing reserved-namespace from
generic-duplicate failures, never re-discovering modules) and pnpm
modules:validate to compose the same registry before a module is
considered installable.

Adds focused coverage: startup-ordering and failure-isolation tests in
apps/api/tests/lifecycle.test.ts, module-owned-declaration enforcement
tests in apps/api/tests/authorization-middleware.test.ts, Doctor
composition tests in core/tests/doctor.test.ts, and a live PostgreSQL
end-to-end scenario proving a real installed module's permission is
discoverable, grantable, and deniable once the module is absent again
after a restart, without ever executing its server entrypoint.
2026-07-24 14:24:50 -05:00

633 lines
23 KiB
TypeScript

import { parsePermissionId, type ModuleManifest } from "@ekklesia/contracts";
import { describe, expect, it, vi } from "vitest";
import { createPermissionRegistry, PermissionRegistryError } from "../src/authorization/index.js";
import {
type DoctorCheck,
type DoctorDependencies,
type DoctorOptions,
deriveDoctorResult,
diagnoseEkklesia,
doctorExitCode,
} from "../src/diagnostics/index.js";
import {
type MigrationDatabase,
type MigrationDatabaseSession,
type MigrationPlanResult,
MigrationRunnerError,
planModuleMigrations,
} from "../src/migrations/index.js";
import type {
DiscoveredModule,
DiscoveredModuleMigration,
ModuleMigrationAggregate,
ModuleSchemaAggregate,
} from "../src/modules/index.js";
import { ModulePermissionCompositionError } from "../src/modules/permissions.js";
const checksumA = "a".repeat(64);
const checksumB = "b".repeat(64);
const checksumC = "c".repeat(64);
function discoveredModule(id: string, version = "0.1.0"): DiscoveredModule {
return {
directory: `/unpublished/${id}`,
entrypoints: { server: `/unpublished/${id}/server.js` },
manifest: {
capabilities: [],
compatibility: { moduleSdk: "^0.1.0", platform: "^0.1.0" },
dependencies: {
capabilities: { optional: {}, required: {} },
modules: { optional: {}, required: {} },
},
entrypoints: { server: "./dist/server.js" },
id,
manifestVersion: 3,
name: `${id} module`,
permissions: [],
storage: { buckets: [] },
version,
} as unknown as ModuleManifest,
manifestPath: `/unpublished/${id}/ekklesia.module.json`,
packageName: `@test/${id}`,
};
}
function migration(
moduleId: string,
tag: string,
checksum: string,
approvalRequired = false,
): DiscoveredModuleMigration {
const index = Number(tag.slice(0, tag.indexOf("_")));
return Object.freeze({
approvalRequired,
breakpoints: true,
checksum,
destructive: Object.freeze(
approvalRequired
? { mode: "approval-required" as const, reason: "Operator review is required." }
: { mode: "none" as const },
),
file: `${tag}.sql`,
identity: `${moduleId}:${tag}`,
index,
moduleId,
sql: "SELECT 1;",
tag,
timestamp: index + 1,
});
}
function migrationAggregate(
migrations: readonly DiscoveredModuleMigration[] = [],
modules: readonly DiscoveredModule[] = [],
): ModuleMigrationAggregate {
const moduleIds = [...new Set(migrations.map((entry) => entry.moduleId))];
return Object.freeze({
discoveredModules: Object.freeze([...modules]),
migrations: Object.freeze([...migrations]),
modules: Object.freeze(
moduleIds.map((moduleId) =>
Object.freeze({
migrations: Object.freeze(migrations.filter((entry) => entry.moduleId === moduleId)),
moduleId,
}),
),
),
});
}
function schemaAggregate(modules: readonly DiscoveredModule[] = []): ModuleSchemaAggregate {
return Object.freeze({
discoveredModules: Object.freeze([...modules]),
drizzleSchema: Object.freeze({}),
modules: Object.freeze([]),
tables: Object.freeze([]),
});
}
function emptyDatabase(): MigrationDatabase {
return Object.freeze({
openSession: vi.fn().mockRejectedValue(new Error("unused database")),
});
}
function plan(entries: MigrationPlanResult["entries"] = []): MigrationPlanResult {
const appliedCount = entries.filter((entry) => entry.status === "applied").length;
const approvalRequiredCount = entries.filter(
(entry) => entry.status === "approval-blocked",
).length;
return Object.freeze({
appliedCount,
approvalRequiredCount,
entries: Object.freeze([...entries]),
pendingCount: entries.length - appliedCount,
});
}
function dependencies(overrides: Partial<DoctorDependencies> = {}): DoctorDependencies {
return {
composePermissions: vi.fn((modules: readonly DiscoveredModule[]) =>
createPermissionRegistry(
modules.flatMap((discovered) => [...discovered.manifest.permissions]),
),
),
createMigrationDatabase: vi.fn(() => emptyDatabase()),
discoverMigrations: vi.fn(async () => migrationAggregate()),
discoverModules: vi.fn(async () => Object.freeze([])),
discoverSchemas: vi.fn(async () => schemaAggregate()),
planMigrations: vi.fn(async () => plan()),
runtimeSatisfies: vi.fn(() => true),
...overrides,
};
}
function options(databaseUrl?: string): DoctorOptions {
return {
...(databaseUrl === undefined ? {} : { databaseUrl }),
moduleDiscovery: {
moduleSdkVersion: "0.1.0",
modulesDirectory: "/not-rendered/modules",
platformVersion: "0.1.0",
},
runtimeRange: ">=24.11.0 <25",
runtimeVersion: "24.15.0",
};
}
function check(
report: Awaited<ReturnType<typeof diagnoseEkklesia>>,
id: DoctorCheck["id"],
): DoctorCheck {
const found = report.checks.find((candidate) => candidate.id === id);
if (!found) {
throw new Error(`Missing test check ${id}.`);
}
return found;
}
function planEntry(
identity: string,
checksum: string,
status: "applied" | "approval-blocked" | "pending",
): MigrationPlanResult["entries"][number] {
const [moduleId = "", tag = ""] = identity.split(":");
return Object.freeze({
approvalRequired: status === "approval-blocked",
checksum,
identity,
index: Number(tag.slice(0, tag.indexOf("_"))),
moduleId,
status,
tag,
});
}
describe("Ekklesia Doctor diagnostics", () => {
it("publishes a healthy deterministic static report without database configuration", async () => {
const people = discoveredModule("people");
const calendar = discoveredModule("calendar", "0.2.0");
const inventory = migrationAggregate(
[migration("people", "0000_initial", checksumA)],
[people, calendar],
);
const services = dependencies({
discoverMigrations: vi.fn(async () => inventory),
discoverModules: vi.fn(async () => Object.freeze([people, calendar])),
discoverSchemas: vi.fn(async () => schemaAggregate([people, calendar])),
});
const report = await diagnoseEkklesia(options(), services);
expect(report.checks.map((entry) => entry.id)).toEqual([
"platform.runtime",
"modules.discovery",
"modules.dependencies",
"modules.permissions",
"schemas.discovery",
"migrations.discovery",
"postgresql.connection",
"postgresql.plan",
"identity.configuration",
"object-storage.integration",
]);
expect(check(report, "modules.discovery").details).toEqual(["people 0.1.0", "calendar 0.2.0"]);
expect(check(report, "modules.dependencies").details).toEqual(["Order: people → calendar"]);
expect(check(report, "modules.permissions")).toMatchObject({
details: ["0 permissions registered"],
status: "pass",
});
expect(check(report, "migrations.discovery").details).toEqual([
"1 migration · checksums valid",
"people:0000_initial · aaaaaaaaaaaa",
]);
expect(JSON.stringify(report)).not.toContain(checksumA);
expect(report.result).toBe("ready-with-warnings");
expect(doctorExitCode(report)).toBe(0);
expect(services.createMigrationDatabase).not.toHaveBeenCalled();
expect(Object.isFrozen(report)).toBe(true);
expect(Object.isFrozen(report.checks)).toBe(true);
expect(report.checks.every(Object.isFrozen)).toBe(true);
expect(report.checks.every((entry) => Object.isFrozen(entry.details))).toBe(true);
});
it("derives aggregate results and exit codes with failure precedence", () => {
const pass = { details: [], id: "platform.runtime", status: "pass", summary: "pass" } as const;
const warning = { ...pass, status: "warning" } as const;
const skipped = { ...pass, status: "skipped" } as const;
const failure = { ...pass, status: "failure" } as const;
expect(deriveDoctorResult([pass])).toBe("ready");
expect(deriveDoctorResult([pass, warning])).toBe("ready-with-warnings");
expect(deriveDoctorResult([pass, skipped])).toBe("ready-with-warnings");
expect(deriveDoctorResult([warning, failure])).toBe("not-ready");
expect(doctorExitCode({ checks: [pass], result: "ready" })).toBe(0);
expect(doctorExitCode({ checks: [warning], result: "ready-with-warnings" })).toBe(0);
expect(doctorExitCode({ checks: [failure], result: "not-ready" })).toBe(1);
});
it("uses the configured production planner seam with finite timeouts", async () => {
const createMigrationDatabase = vi.fn(() => emptyDatabase());
const planMigrations = vi.fn(async () => plan());
const report = await diagnoseEkklesia(
options("postgresql://not-rendered.invalid/ekklesia"),
dependencies({ createMigrationDatabase, planMigrations }),
);
expect(createMigrationDatabase).toHaveBeenCalledWith(
"postgresql://not-rendered.invalid/ekklesia",
{ connectionTimeoutMs: 10_000, operationTimeoutMs: 10_000 },
);
expect(planMigrations).toHaveBeenCalledOnce();
expect(check(report, "postgresql.connection")).toMatchObject({
status: "pass",
summary: "Connection established",
});
expect(check(report, "postgresql.plan")).toMatchObject({
details: ["0 applied · 0 pending · 0 approval-blocked"],
status: "pass",
});
});
it("uses the read-only planner for a fresh database and closes without mutation", async () => {
const pending = migration("people", "0000_initial", checksumA);
const aggregate = migrationAggregate([pending], [discoveredModule("people")]);
const session: MigrationDatabaseSession = {
acquireMigrationLock: vi.fn(),
close: vi.fn().mockResolvedValue(undefined),
readAppliedMigrations: vi.fn().mockResolvedValue(Object.freeze([])),
releaseMigrationLock: vi.fn(),
transaction: vi.fn(),
};
const database: MigrationDatabase = Object.freeze({
openSession: vi.fn().mockResolvedValue(session),
});
const report = await diagnoseEkklesia(
options("postgresql://not-rendered.invalid/ekklesia"),
dependencies({
createMigrationDatabase: vi.fn(() => database),
discoverMigrations: vi.fn(async () => aggregate),
planMigrations: planModuleMigrations,
}),
);
expect(session.readAppliedMigrations).toHaveBeenCalledOnce();
expect(session.close).toHaveBeenCalledOnce();
expect(session.acquireMigrationLock).not.toHaveBeenCalled();
expect(session.releaseMigrationLock).not.toHaveBeenCalled();
expect(session.transaction).not.toHaveBeenCalled();
expect(check(report, "postgresql.plan")).toMatchObject({
status: "warning",
summary: "1 migration requires attention",
});
});
it("reports applied, pending, and approval-blocked entries in plan order", async () => {
const entries = Object.freeze([
planEntry("people:0000_initial", checksumA, "applied"),
planEntry("people:0001_add_email", checksumB, "pending"),
planEntry("people:0002_remove_old", checksumC, "approval-blocked"),
]);
const report = await diagnoseEkklesia(
options("postgresql://not-rendered.invalid/ekklesia"),
dependencies({ planMigrations: vi.fn(async () => plan(entries)) }),
);
expect(check(report, "postgresql.plan")).toEqual({
details: [
"1 applied · 1 pending · 1 approval-blocked",
"applied · people:0000_initial · aaaaaaaaaaaa",
"pending · people:0001_add_email · bbbbbbbbbbbb",
"approval-blocked · people:0002_remove_old · cccccccccccc",
],
id: "postgresql.plan",
status: "warning",
summary: "2 migrations require attention",
});
});
it("fails a configured connection without attempting a plan result", async () => {
const report = await diagnoseEkklesia(
options("postgresql://not-rendered.invalid/ekklesia"),
dependencies({
planMigrations: vi
.fn()
.mockRejectedValue(new MigrationRunnerError("MIGRATION_CONNECTION_FAILED")),
}),
);
expect(check(report, "postgresql.connection").status).toBe("failure");
expect(check(report, "postgresql.plan").status).toBe("skipped");
expect(report.result).toBe("not-ready");
expect(doctorExitCode(report)).toBe(1);
});
it("reports invalid history as a planning failure after connectivity succeeds", async () => {
const report = await diagnoseEkklesia(
options("postgresql://not-rendered.invalid/ekklesia"),
dependencies({
planMigrations: vi
.fn()
.mockRejectedValue(
new MigrationRunnerError("MIGRATION_HISTORY_INVALID", "never-render-this"),
),
}),
);
expect(check(report, "postgresql.connection").status).toBe("pass");
expect(check(report, "postgresql.plan")).toMatchObject({
details: ["Applied migration history is invalid."],
status: "failure",
});
expect(JSON.stringify(report)).not.toContain("never-render-this");
});
it("skips every dependent check after atomic module discovery fails", async () => {
const hostile =
"postgresql://admin:[email protected]/db SELECT secret FROM x /home/operator/.env";
const discoverSchemas = vi.fn();
const discoverMigrations = vi.fn();
const createMigrationDatabase = vi.fn();
const report = await diagnoseEkklesia(
options("postgresql://not-rendered.invalid/ekklesia"),
dependencies({
createMigrationDatabase,
discoverMigrations,
discoverModules: vi.fn().mockRejectedValue(new Error(hostile)),
discoverSchemas,
}),
);
expect(check(report, "modules.discovery").status).toBe("failure");
for (const id of [
"modules.dependencies",
"modules.permissions",
"schemas.discovery",
"migrations.discovery",
"postgresql.connection",
"postgresql.plan",
] as const) {
expect(check(report, id).status).toBe("skipped");
}
expect(check(report, "platform.runtime").status).toBe("pass");
expect(check(report, "object-storage.integration").status).toBe("skipped");
expect(discoverSchemas).not.toHaveBeenCalled();
expect(discoverMigrations).not.toHaveBeenCalled();
expect(createMigrationDatabase).not.toHaveBeenCalled();
expect(JSON.stringify(report)).not.toContain(hostile);
expect(JSON.stringify(report)).not.toContain("password");
});
it("passes permission composition with a safe declaration count when modules are installed", async () => {
const calendar = discoveredModule("calendar");
const report = await diagnoseEkklesia(
options(),
dependencies({
composePermissions: vi.fn(() =>
createPermissionRegistry([
{
baseline: "restricted",
description: "Read calendar events.",
id: parsePermissionId("calendar.events.read"),
},
]),
),
discoverModules: vi.fn(async () => Object.freeze([calendar])),
}),
);
expect(check(report, "modules.permissions")).toMatchObject({
details: ["1 permission registered"],
status: "pass",
});
});
it("reuses the already-discovered module list rather than rediscovering modules for permission composition", async () => {
const calendar = discoveredModule("calendar");
const composePermissions = vi.fn(() => createPermissionRegistry([]));
const discoverModules = vi.fn(async () => Object.freeze([calendar]));
await diagnoseEkklesia(options(), dependencies({ composePermissions, discoverModules }));
expect(discoverModules).toHaveBeenCalledOnce();
expect(composePermissions).toHaveBeenCalledOnce();
expect(composePermissions).toHaveBeenCalledWith([calendar]);
});
it("fails permission composition when a module claims the reserved core.* namespace, distinctly from a generic duplicate", async () => {
const report = await diagnoseEkklesia(
options(),
dependencies({
composePermissions: vi.fn(() => {
throw new ModulePermissionCompositionError("core", "core.rogue.permission");
}),
}),
);
expect(check(report, "modules.permissions")).toMatchObject({
details: [
'Module "core" declared permission "core.rogue.permission" in the reserved core.* namespace.',
],
status: "failure",
});
expect(report.result).toBe("not-ready");
});
it("fails permission composition on a generic duplicate declaration, distinctly from a reserved-namespace violation", async () => {
const report = await diagnoseEkklesia(
options(),
dependencies({
composePermissions: vi.fn(() => {
throw new PermissionRegistryError("calendar.events.read");
}),
}),
);
expect(check(report, "modules.permissions")).toMatchObject({
details: ['Permission "calendar.events.read" is declared more than once.'],
status: "failure",
});
expect(report.result).toBe("not-ready");
});
it("sanitizes an unexpected permission-composition exception to a fixed generic detail", async () => {
const hostile = "postgresql://admin:[email protected]/db /home/operator/.env";
const report = await diagnoseEkklesia(
options(),
dependencies({
composePermissions: vi.fn(() => {
throw new Error(hostile);
}),
}),
);
expect(check(report, "modules.permissions")).toMatchObject({
details: ["The installed permission declarations could not be composed safely."],
status: "failure",
});
expect(JSON.stringify(report)).not.toContain(hostile);
});
it("continues independent schema, migration, and PostgreSQL checks after permission composition fails", async () => {
const planMigrations = vi.fn(async () => plan());
const report = await diagnoseEkklesia(
options("postgresql://not-rendered.invalid/ekklesia"),
dependencies({
composePermissions: vi.fn(() => {
throw new PermissionRegistryError("calendar.events.read");
}),
planMigrations,
}),
);
expect(check(report, "modules.permissions").status).toBe("failure");
expect(check(report, "schemas.discovery").status).toBe("pass");
expect(check(report, "migrations.discovery").status).toBe("pass");
expect(check(report, "postgresql.connection").status).toBe("pass");
expect(planMigrations).toHaveBeenCalledOnce();
});
it("continues independent migration and PostgreSQL checks after schema failure", async () => {
const planMigrations = vi.fn(async () => plan());
const report = await diagnoseEkklesia(
options("postgresql://not-rendered.invalid/ekklesia"),
dependencies({
discoverSchemas: vi.fn().mockRejectedValue(new Error("hostile schema detail")),
planMigrations,
}),
);
expect(check(report, "schemas.discovery").status).toBe("failure");
expect(check(report, "migrations.discovery").status).toBe("pass");
expect(check(report, "postgresql.connection").status).toBe("pass");
expect(planMigrations).toHaveBeenCalledOnce();
expect(JSON.stringify(report)).not.toContain("hostile schema detail");
});
it("sanitizes an unexpected configured-database exception", async () => {
const hostile =
"postgresql://root:[email protected]:5432/db -- DROP TABLE x; /srv/secret/.env";
const report = await diagnoseEkklesia(
options("postgresql://also-secret.invalid/ekklesia"),
dependencies({ planMigrations: vi.fn().mockRejectedValue(new Error(hostile)) }),
);
const serialized = JSON.stringify(report);
expect(check(report, "postgresql.connection").status).toBe("failure");
expect(serialized).not.toContain("hunter2");
expect(serialized).not.toContain("private.invalid");
expect(serialized).not.toContain("DROP TABLE");
expect(serialized).not.toContain("/srv/secret");
expect(serialized).not.toContain("also-secret.invalid");
});
it("always reports object storage as unavailable without probing configuration", async () => {
const report = await diagnoseEkklesia(options(), dependencies());
expect(check(report, "object-storage.integration")).toEqual({
details: ["Core storage integration has not been implemented yet."],
id: "object-storage.integration",
status: "skipped",
summary: "Check unavailable",
});
});
it("continues static discovery when runtime validation fails", async () => {
const services = dependencies({ runtimeSatisfies: vi.fn(() => false) });
const report = await diagnoseEkklesia(options(), services);
expect(check(report, "platform.runtime").status).toBe("failure");
expect(check(report, "modules.discovery").status).toBe("pass");
expect(services.discoverSchemas).toHaveBeenCalledOnce();
expect(services.discoverMigrations).toHaveBeenCalledOnce();
});
it("skips identity configuration (not a failure) when none of it is set", async () => {
const report = await diagnoseEkklesia(options(), dependencies());
const identity = check(report, "identity.configuration");
expect(identity.status).toBe("skipped");
expect(identity.details[0]).toContain("Core identity schema defines 4 tables");
expect(report.result).toBe("ready-with-warnings");
expect(doctorExitCode(report)).toBe(0);
});
it("passes identity configuration when a valid secret, base URL, and trusted origins are provided", async () => {
const report = await diagnoseEkklesia(
{
...options(),
identityConfiguration: {
baseUrl: "https://identity.example.test",
environment: "production",
secret: "correct-horse-battery-staple-0123456789",
trustedOrigins: ["https://identity.example.test"],
},
},
dependencies(),
);
const identity = check(report, "identity.configuration");
expect(identity.status).toBe("pass");
expect(report.result).toBe("ready-with-warnings"); // PostgreSQL is still skipped in this fixture
});
it("fails identity configuration for an invalid secret without leaking it", async () => {
const hostileSecret = "s3cr3t-do-not-log";
const report = await diagnoseEkklesia(
{
...options(),
identityConfiguration: {
baseUrl: "https://identity.example.test",
environment: "production",
secret: hostileSecret,
trustedOrigins: ["https://identity.example.test"],
},
},
dependencies(),
);
const identity = check(report, "identity.configuration");
expect(identity.status).toBe("failure");
expect(identity.details.some((detail) => detail.includes("SECRET_TOO_SHORT"))).toBe(true);
expect(report.result).toBe("not-ready");
expect(doctorExitCode(report)).toBe(1);
expect(JSON.stringify(report)).not.toContain(hostileSecret);
});
it("never constructs a migration database or Better Auth merely to check identity configuration", async () => {
const services = dependencies();
await diagnoseEkklesia(
{
...options(),
identityConfiguration: {
baseUrl: "https://identity.example.test",
secret: "correct-horse-battery-staple-0123456789",
trustedOrigins: ["https://identity.example.test"],
},
},
services,
);
expect(services.createMigrationDatabase).not.toHaveBeenCalled();
});
});