import { randomBytes } from "node:crypto"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { parseModuleManifest } from "@ekklesia/contracts"; import { Client, type QueryResultRow } from "pg"; import { afterAll, describe, expect, it } from "vitest"; import { createValidatedPostgresPool } from "../src/database/index.js"; import { applyModuleMigrations, createPostgresMigrationDatabase, discoverAllMigrations, } from "../src/migrations/index.js"; import { createModuleConfigurationStore, createModuleConfigurationView, type ModuleConfigurationStore, ModuleConfigurationStoreError, resolveModuleConfigurationFor, } from "../src/modules/index.js"; /** * The module configuration store against real PostgreSQL. * * `core/tests/module-configuration.test.ts` proves the validation and defaulting rules with no * database at all; this proves the things only a database can: that migration `0005` really creates * the table with the composite primary key and value ceiling the schema declares, that a replace is * transactional and scoped to one module, that stored values survive a completely new connection, * and — the security-relevant one — that a module id or key is only ever *data* in a parameterized * column, never an SQL identifier. The in-memory double the API route tests use reproduces none of * that, which is why it lives here. */ const integrationDatabaseUrlEnvironment = "EKKLESIA_MIGRATION_TEST_DATABASE_URL"; const coreMigrationsDirectory = path.join( path.dirname(fileURLToPath(import.meta.url)), "..", "drizzle", ); const trackedDatabases = new Set(); let databaseCounter = 0; class IntegrationHarnessError extends Error { constructor(message: string) { super(message); this.name = "IntegrationHarnessError"; this.stack = `${this.name}: ${this.message}`; } } function harnessError(message: string): IntegrationHarnessError { return new IntegrationHarnessError(message); } function baseDatabaseUrl(): string { const value = process.env[integrationDatabaseUrlEnvironment]; if (typeof value !== "string" || value.trim() === "") { throw harnessError("The module configuration integration database is not configured."); } try { const parsed = new URL(value); if (parsed.protocol !== "postgres:" && parsed.protocol !== "postgresql:") { throw new TypeError("Unsupported protocol."); } } catch { throw harnessError("The module configuration integration database configuration is invalid."); } return value; } function quoteDatabaseName(databaseName: string): string { if (!/^[a-z][a-z0-9_]{0,62}$/u.test(databaseName)) { throw harnessError("The generated integration database name is invalid."); } return `"${databaseName}"`; } function makeDatabaseName(label: string): string { const safeLabel = label .toLowerCase() .replaceAll(/[^a-z0-9]+/gu, "_") .replaceAll(/^_+|_+$/gu, "") .slice(0, 10); databaseCounter += 1; return `ekk_modcfg_${process.pid}_${databaseCounter}_${safeLabel || "scenario"}_${randomBytes(4).toString("hex")}`; } function databaseUrl(databaseName: string): string { const parsed = new URL(baseDatabaseUrl()); parsed.pathname = `/${databaseName}`; parsed.searchParams.delete("application_name"); return parsed.toString(); } async function openAdministrativeClient(connectionString: string): Promise { const client = new Client({ connectionString }); client.on("error", () => undefined); try { await client.connect(); return client; } catch { try { await client.end(); } catch { // The public harness failure stays static when connection cleanup also fails. } throw harnessError("The module configuration integration database connection failed."); } } async function safeQuery( client: Client, text: string, values: unknown[] = [], ): Promise { try { const result = await client.query(text, values); return result.rows; } catch { throw harnessError("An integration database administration operation failed."); } } async function safeEnd(client: Client): Promise { try { await client.end(); } catch { throw harnessError("An integration database connection could not be closed."); } } async function dropDatabase(databaseName: string): Promise { const admin = await openAdministrativeClient(baseDatabaseUrl()); try { await safeQuery( admin, "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()", [databaseName], ); await safeQuery( admin, `DROP DATABASE IF EXISTS ${quoteDatabaseName(databaseName)} WITH (FORCE)`, ); trackedDatabases.delete(databaseName); } finally { await safeEnd(admin); } } async function createDisposableDatabase(label: string): Promise<{ name: string; url: string }> { const name = makeDatabaseName(label); const admin = await openAdministrativeClient(baseDatabaseUrl()); try { await safeQuery(admin, `CREATE DATABASE ${quoteDatabaseName(name)} TEMPLATE template0`); } finally { await safeEnd(admin); } trackedDatabases.add(name); return { name, url: databaseUrl(name) }; } async function applyCoreMigrations(url: string): Promise { const emptyModulesDirectory = await mkdtemp(path.join(tmpdir(), "ekklesia-modcfg-no-modules-")); try { const aggregate = await discoverAllMigrations({ coreMigrationsDirectory, moduleDiscovery: { moduleSdkVersion: "0.1.0", modulesDirectory: emptyModulesDirectory, platformVersion: "0.1.0", }, }); expect(aggregate.migrations.map((entry) => entry.identity)).toEqual([ "core:0000_identity_core_tables", "core:0001_authorization_core_tables", "core:0002_authorization_integrity", "core:0003_dashboard_navigation_tables", "core:0004_module_lifecycle_state", "core:0005_module_configuration", "core:0006_device_identity", "core:0007_verified_bootstrap_owners", ]); await applyModuleMigrations({ aggregate, database: createPostgresMigrationDatabase(url) }); } finally { await rm(emptyModulesDirectory, { force: true, recursive: true }); } } afterAll(async () => { for (const databaseName of [...trackedDatabases]) { await dropDatabase(databaseName).catch(() => undefined); } }); function manifestWith(configuration: readonly Record[], id = "announcements") { return parseModuleManifest({ manifestVersion: 6, id, name: `${id} module`, version: "1.0.0", compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0" }, configuration, entrypoints: { server: "./dist/does-not-exist/index.js" }, }); } describe.sequential("live Core module configuration store", () => { let database: { name: string; url: string }; let admin: Client; let store: ModuleConfigurationStore; let close: () => Promise; it("creates exactly the one configuration table, with its composite key and declared constraints", async () => { database = await createDisposableDatabase("fresh"); await applyCoreMigrations(database.url); admin = await openAdministrativeClient(database.url); const columns = await safeQuery<{ column_name: string; is_nullable: string }>( admin, `select column_name, is_nullable from information_schema.columns where table_name = 'ekklesia_core_module_configuration' order by column_name`, ); const primaryKey = await safeQuery<{ column_name: string }>( admin, `select kcu.column_name from information_schema.table_constraints tc join information_schema.key_column_usage kcu on kcu.constraint_name = tc.constraint_name where tc.table_name = 'ekklesia_core_module_configuration' and tc.constraint_type = 'PRIMARY KEY' order by kcu.ordinal_position`, ); // Only the explicitly declared CHECKs: PostgreSQL 17 also materializes a system `..._not_null` // CHECK per NOT NULL column, which is not something this schema declares. const checks = await safeQuery<{ constraint_name: string }>( admin, `select tc.constraint_name from information_schema.table_constraints tc where tc.table_name = 'ekklesia_core_module_configuration' and tc.constraint_type = 'CHECK' and tc.constraint_name not like '%_not_null' order by tc.constraint_name`, ); expect(columns.map((column) => column.column_name)).toEqual([ "created_at", "key", "module_id", "updated_at", "value", ]); // One row per (module, key): a module can never have two values for one key, enforced by the // database rather than by application care. expect(primaryKey.map((column) => column.column_name)).toEqual(["module_id", "key"]); expect(checks.map((check) => check.constraint_name)).toEqual([ "ekklesia_core_module_configuration_key_check", "ekklesia_core_module_configuration_module_check", "ekklesia_core_module_configuration_value_check", ]); // Deliberately absent: there is no column that could hold secret material, and none that // records where a value came from. Secrets are deployment-supplied and never persisted. expect(columns.map((column) => column.column_name)).not.toContain("secret"); expect(columns.map((column) => column.column_name)).not.toContain("encrypted"); }); it("reads an empty configuration from a freshly migrated database", async () => { const view = createValidatedPostgresPool(database.url, { connectionTimeoutMs: 10_000, operationTimeoutMs: 10_000, }); close = view.close; store = createModuleConfigurationStore(createModuleConfigurationView(view.pool)); expect(await store.readSettings()).toEqual({ records: [] }); }); it("persists values and reads them back over a completely new connection", async () => { await store.replace({ moduleId: "announcements", values: [ { key: "bucket", value: "slides" }, { key: "slideSeconds", value: "12" }, ], }); const separate = createValidatedPostgresPool(database.url, { connectionTimeoutMs: 10_000, operationTimeoutMs: 10_000, }); try { const reread = await createModuleConfigurationStore( createModuleConfigurationView(separate.pool), ).readSettings(); expect(reread.records).toEqual([ { key: "bucket", moduleId: "announcements", value: "slides" }, { key: "slideSeconds", moduleId: "announcements", value: "12" }, ]); } finally { await separate.close(); } }); it("replaces the whole document for one module, clearing keys the submission omits", async () => { await store.replace({ moduleId: "announcements", values: [{ key: "bucket", value: "slides" }], }); expect((await store.readSettings()).records).toEqual([ { key: "bucket", moduleId: "announcements", value: "slides" }, ]); }); it("never touches another module's rows during a replace", async () => { await store.replace({ moduleId: "calendar", values: [{ key: "bucket", value: "events" }] }); await store.replace({ moduleId: "announcements", values: [{ key: "bucket", value: "renamed" }], }); expect((await store.readSettings()).records).toEqual([ { key: "bucket", moduleId: "announcements", value: "renamed" }, { key: "bucket", moduleId: "calendar", value: "events" }, ]); }); it("clears a module's document entirely when given no values", async () => { await store.replace({ moduleId: "calendar", values: [] }); expect( (await store.readSettings()).records.filter((record) => record.moduleId === "calendar"), ).toEqual([]); }); it("treats a module id and a key as data, never as an SQL identifier", async () => { // The store validates identifiers before they reach PostgreSQL, so this is rejected by the // application. The row count afterwards proves nothing was executed either way. await expect( store.replace({ moduleId: 'announcements"; drop table ekklesia_core_module_configuration; --', values: [{ key: "bucket", value: "x" }], }), ).rejects.toMatchObject({ code: "MODULE_CONFIGURATION_MODULE_ID_INVALID" }); await expect( store.replace({ moduleId: "announcements", values: [{ key: 'bucket"; drop table ekklesia_core_module_configuration; --', value: "x" }], }), ).rejects.toMatchObject({ code: "MODULE_CONFIGURATION_KEY_INVALID" }); const tables = await safeQuery<{ count: string }>( admin, `select count(*)::text as count from information_schema.tables where table_name = 'ekklesia_core_module_configuration'`, ); expect(tables[0]?.count).toBe("1"); }); it("rejects a duplicate key in one submission before reaching the database", async () => { await expect( store.replace({ moduleId: "announcements", values: [ { key: "bucket", value: "a" }, { key: "bucket", value: "b" }, ], }), ).rejects.toBeInstanceOf(ModuleConfigurationStoreError); }); it("rejects an oversized value at the application boundary and at the database", async () => { await expect( store.replace({ moduleId: "announcements", values: [{ key: "bucket", value: "x".repeat(2049) }], }), ).rejects.toMatchObject({ code: "MODULE_CONFIGURATION_VALUE_INVALID" }); // Defense in depth: the same ceiling restated where the data lives, so no future caller can // store a value the resolver would then reject as invalid on every boot. await expect( safeQuery( admin, `insert into ekklesia_core_module_configuration (module_id, key, value) values ('announcements', 'oversized', repeat('x', 2049))`, ), ).rejects.toThrow(); }); it("never carries a configuration value in a store error", async () => { let captured: unknown; try { await store.replace({ moduleId: "announcements", values: [{ key: "bucket", value: "a-value-nobody-should-see".repeat(200) }], }); } catch (error) { captured = error; } const rendered = `${String(captured)} ${JSON.stringify(captured)}`; expect(rendered).not.toContain("a-value-nobody-should-see"); }); it("retains a value for a module that is no longer deployed, and resolution ignores it", async () => { await store.replace({ moduleId: "announcements", values: [{ key: "bucket", value: "slides" }], }); await store.replace({ moduleId: "retired", values: [{ key: "bucket", value: "old" }] }); const settings = await store.readSettings(); const resolved = resolveModuleConfigurationFor( manifestWith([{ key: "bucket", type: "string", required: true }]), { settings }, ); expect(resolved.configuration?.getString("bucket")).toBe("slides"); // Retained rather than deleted: redeploying `retired` restores its value with no new write. expect(settings.records.some((record) => record.moduleId === "retired")).toBe(true); }); it("drives real validation and defaulting from real stored rows", async () => { await store.replace({ moduleId: "announcements", values: [ { key: "bucket", value: "slides" }, { key: "slideSeconds", value: "900" }, ], }); const manifest = manifestWith([ { key: "bucket", type: "string", required: true }, { key: "slideSeconds", type: "integer", default: 7, minimum: 1, maximum: 600 }, { key: "pollIntervalSeconds", type: "integer", default: 30 }, ]); const resolved = resolveModuleConfigurationFor(manifest, { settings: await store.readSettings(), }); // 900 exceeds the module's own declared maximum: reported, never clamped and never silently // replaced by the default. expect(resolved.configuration).toBeUndefined(); expect(resolved.issue).toEqual({ key: "slideSeconds", reason: "configuration_invalid" }); await store.replace({ moduleId: "announcements", values: [ { key: "bucket", value: "slides" }, { key: "slideSeconds", value: "12" }, ], }); const fixed = resolveModuleConfigurationFor(manifest, { settings: await store.readSettings() }); expect(fixed.configuration?.getInteger("slideSeconds")).toBe(12); expect(fixed.configuration?.getInteger("pollIntervalSeconds")).toBe(30); await close(); }); });