174 lines
5.2 KiB
TypeScript
174 lines
5.2 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
type DoctorDependencies,
|
|
diagnoseEkklesia,
|
|
formatDoctorReport,
|
|
} from "../src/diagnostics/index.js";
|
|
import type { MigrationDatabase } from "../src/migrations/index.js";
|
|
import type { ModuleMigrationAggregate, ModuleSchemaAggregate } from "../src/modules/index.js";
|
|
|
|
const originalNoColor = process.env.NO_COLOR;
|
|
|
|
afterEach(() => {
|
|
if (originalNoColor === undefined) {
|
|
delete process.env.NO_COLOR;
|
|
} else {
|
|
process.env.NO_COLOR = originalNoColor;
|
|
}
|
|
});
|
|
|
|
function zeroSchemaAggregate(): ModuleSchemaAggregate {
|
|
return Object.freeze({
|
|
discoveredModules: Object.freeze([]),
|
|
drizzleSchema: Object.freeze({}),
|
|
modules: Object.freeze([]),
|
|
tables: Object.freeze([]),
|
|
});
|
|
}
|
|
|
|
function zeroMigrationAggregate(): ModuleMigrationAggregate {
|
|
return Object.freeze({
|
|
discoveredModules: Object.freeze([]),
|
|
migrations: Object.freeze([]),
|
|
modules: Object.freeze([]),
|
|
});
|
|
}
|
|
|
|
function noConfigurationDependencies(): DoctorDependencies {
|
|
return {
|
|
createMigrationDatabase: () => {
|
|
throw new Error("database must not be constructed");
|
|
},
|
|
discoverMigrations: async () => zeroMigrationAggregate(),
|
|
discoverModules: async () => Object.freeze([]),
|
|
discoverSchemas: async () => zeroSchemaAggregate(),
|
|
planMigrations: async () => {
|
|
throw new Error("database must not be planned");
|
|
},
|
|
runtimeSatisfies: () => true,
|
|
};
|
|
}
|
|
|
|
describe("Ekklesia Doctor terminal report", () => {
|
|
it("renders the real zero-module and no-database shape exactly", async () => {
|
|
const report = await diagnoseEkklesia(
|
|
{
|
|
moduleDiscovery: {
|
|
moduleSdkVersion: "0.1.0",
|
|
modulesDirectory: "/not-rendered/modules",
|
|
platformVersion: "0.1.0",
|
|
},
|
|
runtimeRange: ">=24.11.0 <25",
|
|
runtimeVersion: "24.15.0",
|
|
},
|
|
noConfigurationDependencies(),
|
|
);
|
|
|
|
expect(formatDoctorReport(report).join("\n")).toBe(`Ekklesia Doctor
|
|
|
|
Platform
|
|
✓ Runtime version supported
|
|
Active Node.js satisfies the declared supported range.
|
|
|
|
Modules
|
|
✓ Module discovery complete
|
|
0 modules installed
|
|
✓ Dependency graph valid
|
|
Dependency order is empty.
|
|
|
|
Data definitions
|
|
✓ Schemas validated
|
|
0 modules · 0 tables
|
|
✓ Migrations validated
|
|
0 migrations · checksums valid
|
|
|
|
PostgreSQL
|
|
○ Connection check skipped
|
|
DATABASE_URL is not configured.
|
|
○ Migration plan skipped
|
|
A validated migration inventory and configured PostgreSQL connection are required.
|
|
|
|
Object storage
|
|
○ Check unavailable
|
|
Core storage integration has not been implemented yet.
|
|
|
|
Result: READY WITH WARNINGS`);
|
|
});
|
|
|
|
it("does not depend on NO_COLOR or terminal interactivity", async () => {
|
|
const report = await diagnoseEkklesia(
|
|
{
|
|
moduleDiscovery: {
|
|
moduleSdkVersion: "0.1.0",
|
|
modulesDirectory: "/not-rendered/modules",
|
|
platformVersion: "0.1.0",
|
|
},
|
|
runtimeRange: ">=24.11.0 <25",
|
|
runtimeVersion: "24.15.0",
|
|
},
|
|
noConfigurationDependencies(),
|
|
);
|
|
const baseline = formatDoctorReport(report);
|
|
const originalIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
|
|
|
try {
|
|
process.env.NO_COLOR = "1";
|
|
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true });
|
|
const interactiveWithoutColor = formatDoctorReport(report);
|
|
process.env.NO_COLOR = "";
|
|
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: false });
|
|
const nonInteractive = formatDoctorReport(report);
|
|
|
|
expect(interactiveWithoutColor).toEqual(baseline);
|
|
expect(nonInteractive).toEqual(baseline);
|
|
expect(interactiveWithoutColor.some((line) => line.includes("\u001B"))).toBe(false);
|
|
expect(Object.isFrozen(interactiveWithoutColor)).toBe(true);
|
|
} finally {
|
|
if (originalIsTtyDescriptor) {
|
|
Object.defineProperty(process.stdout, "isTTY", originalIsTtyDescriptor);
|
|
} else {
|
|
delete (process.stdout as { isTTY?: boolean }).isTTY;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("never renders secrets from a hostile database exception", async () => {
|
|
const hostile =
|
|
"postgresql://admin:[email protected]/db SELECT password FROM users /home/me/.env";
|
|
const database = Object.freeze({}) as MigrationDatabase;
|
|
const dependencies: DoctorDependencies = {
|
|
...noConfigurationDependencies(),
|
|
createMigrationDatabase: () => database,
|
|
planMigrations: async () => {
|
|
throw new Error(hostile);
|
|
},
|
|
};
|
|
const report = await diagnoseEkklesia(
|
|
{
|
|
databaseUrl: "postgresql://another:[email protected]/db",
|
|
moduleDiscovery: {
|
|
moduleSdkVersion: "0.1.0",
|
|
modulesDirectory: "/not-rendered/modules",
|
|
platformVersion: "0.1.0",
|
|
},
|
|
runtimeRange: ">=24.11.0 <25",
|
|
runtimeVersion: "24.15.0",
|
|
},
|
|
dependencies,
|
|
);
|
|
const output = formatDoctorReport(report).join("\n");
|
|
|
|
for (const forbidden of [
|
|
"admin",
|
|
"secret",
|
|
"host.invalid",
|
|
"SELECT",
|
|
"password",
|
|
"/home/me",
|
|
"postgresql://",
|
|
]) {
|
|
expect(output).not.toContain(forbidden);
|
|
}
|
|
});
|
|
});
|