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 type { EkklesiaUserId } from "@ekklesia/contracts"; import { Client, type QueryResultRow } from "pg"; import { afterAll, describe, expect, it } from "vitest"; import { type AccountStore, AccountStoreError, createAccountDrizzleView, createAccountStore, } from "../src/account/index.js"; import { createValidatedPostgresPool } from "../src/database/index.js"; import { applyModuleMigrations, createPostgresMigrationDatabase, discoverAllMigrations, } from "../src/migrations/index.js"; /** * A person's own account summary and preferences against real PostgreSQL (ADR 0062). * * The unit suite proves the model and parser; this proves what only a database can: that the * migration creates the table with its constraints, that a choice survives a completely new * connection (which is what "follows you to another device" rests on), that one person's change * never touches another's row, that the database itself refuses a value outside the model, and * that deleting a person deletes their preferences. */ 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 account 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 account 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, 14); databaseCounter += 1; return `ekk_acct_${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 account 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-acct-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", "core:0008_user_preferences", ]); 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); } }); const ada = "user-ada" as EkklesiaUserId; const grace = "user-grace" as EkklesiaUserId; const nobody = "user-nobody" as EkklesiaUserId; async function insertPerson( client: Client, id: string, name: string, email: string, ): Promise { await safeQuery( client, "insert into ekklesia_core_user (id, name, email, email_verified) values ($1, $2, $3, true)", [id, name, email], ); } describe.sequential("live Core account store", () => { let database: { name: string; url: string }; let admin: Client; let store: AccountStore; let close: () => Promise; it("creates the preference table with its constraints", async () => { database = await createDisposableDatabase("fresh"); await applyCoreMigrations(database.url); admin = await openAdministrativeClient(database.url); const constraints = await safeQuery<{ constraint_name: string; constraint_type: string }>( admin, `select constraint_name, constraint_type from information_schema.table_constraints where table_name = 'ekklesia_core_user_preference' and constraint_type <> 'CHECK' or constraint_name like 'ekklesia_core_user_preference_%_check' order by constraint_name`, ); const deleteRules = await safeQuery<{ delete_rule: string }>( admin, `select rc.delete_rule from information_schema.referential_constraints rc join information_schema.table_constraints tc on tc.constraint_name = rc.constraint_name where tc.table_name = 'ekklesia_core_user_preference'`, ); expect(constraints.map((constraint) => constraint.constraint_name)).toEqual([ "ekklesia_core_user_preference_appearance_check", "ekklesia_core_user_preference_motion_check", "ekklesia_core_user_preference_pkey", "ekklesia_core_user_preference_user_id_ekklesia_core_user_id_fk", ]); expect(deleteRules.map((rule) => rule.delete_rule)).toEqual(["CASCADE"]); await insertPerson(admin, ada, "Ada Lovelace", "ada@example.test"); await insertPerson(admin, grace, "Grace Hopper", "grace@example.test"); const handle = createValidatedPostgresPool(database.url); close = handle.close; store = createAccountStore(createAccountDrizzleView(handle.pool)); }); it("reads a person who never chose anything with their name, email, and the defaults", async () => { expect(await store.readAccount(ada)).toEqual({ email: "ada@example.test", name: "Ada Lovelace", preferences: { appearance: "system", motion: "system" }, }); const rows = await safeQuery(admin, "select * from ekklesia_core_user_preference"); // Reading never materializes a row nobody chose. expect(rows).toHaveLength(0); }); it("answers undefined for a person who does not exist", async () => { expect(await store.readAccount(nobody)).toBeUndefined(); }); it.each([ ["appearance", "light"], ["appearance", "dark"], ["appearance", "system"], ["motion", "reduce"], ["motion", "allow"], ["motion", "system"], ] as const)("saves %s %s and reads it back", async (name, value) => { const saved = await store.updatePreferences(ada, { [name]: value }); expect(saved[name]).toBe(value); expect((await store.readAccount(ada))?.preferences[name]).toBe(value); }); it("changes only the preference named, keeping the other", async () => { await store.updatePreferences(ada, { appearance: "dark", motion: "reduce" }); expect(await store.updatePreferences(ada, { motion: "allow" })).toEqual({ appearance: "dark", motion: "allow", }); expect(await store.updatePreferences(ada, { appearance: "light" })).toEqual({ appearance: "light", motion: "allow", }); }); it("stores a first partial choice with the other preference still never chosen", async () => { expect(await store.updatePreferences(grace, { motion: "reduce" })).toEqual({ appearance: "system", motion: "reduce", }); const rows = await safeQuery<{ appearance: string | null; motion: string | null }>( admin, "select appearance, motion from ekklesia_core_user_preference where user_id = $1", [grace], ); expect(rows).toEqual([{ appearance: null, motion: "reduce" }]); }); it("keeps each person's preferences to themselves", async () => { await store.updatePreferences(ada, { appearance: "dark", motion: "allow" }); await store.updatePreferences(grace, { appearance: "light", motion: "reduce" }); expect((await store.readAccount(ada))?.preferences).toEqual({ appearance: "dark", motion: "allow", }); expect(await store.readAccount(grace)).toEqual({ email: "grace@example.test", name: "Grace Hopper", preferences: { appearance: "light", motion: "reduce" }, }); }); it("survives a completely new connection, as another device signing in would see", async () => { const reopened = createValidatedPostgresPool(database.url); try { const other = createAccountStore(createAccountDrizzleView(reopened.pool)); expect((await other.readAccount(ada))?.preferences).toEqual({ appearance: "dark", motion: "allow", }); } finally { await reopened.close(); } }); it("refuses to store preferences for a person who does not exist", async () => { const failure = await store .updatePreferences(nobody, { appearance: "dark" }) .catch((error: unknown) => error); expect(failure).toBeInstanceOf(AccountStoreError); expect(failure).toMatchObject({ code: "ACCOUNT_NOT_FOUND" }); const rows = await safeQuery( admin, "select 1 from ekklesia_core_user_preference where user_id = $1", [nobody], ); expect(rows).toHaveLength(0); }); it("refuses an invalid update with nothing written", async () => { await expect( store.updatePreferences(ada, { appearance: "sepia" } as never), ).rejects.toMatchObject({ code: "ACCOUNT_PREFERENCES_INVALID" }); expect((await store.readAccount(ada))?.preferences.appearance).toBe("dark"); }); it("has the database itself refuse a value outside the model", async () => { await expect( admin.query( "update ekklesia_core_user_preference set appearance = 'sepia' where user_id = $1", [ada], ), ).rejects.toMatchObject({ code: "23514" }); await expect( admin.query("update ekklesia_core_user_preference set motion = 'none' where user_id = $1", [ ada, ]), ).rejects.toMatchObject({ code: "23514" }); }); it("deletes a person's preferences with the person", async () => { await safeQuery(admin, "delete from ekklesia_core_user where id = $1", [grace]); const rows = await safeQuery( admin, "select 1 from ekklesia_core_user_preference where user_id = $1", [grace], ); expect(rows).toHaveLength(0); expect(await store.readAccount(grace)).toBeUndefined(); await safeEnd(admin); await close(); }); });